3

I couldn't find any solution to this problem that satisfies all my requirements.

In my application I use AsyncTasks to perform some operations like saving data to a memory or reading data from a database. I create a progress dialog in onPreExecute, update a progress value in onProgressUpdate and dismiss the dialog in onPostExecute.

Recently I switched to the Fragment API (I use the Support Library to target older versions of Android), meaning that my activities subclass FragmentActivity and dialogs subclass DialogFragment.

Switching to the Fragment API caused a well-known problem - sometimes I get the following exception:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

This happens, for instance, when a user starts a background operation (a progress dialog appears), uses the home button to minimize the application and the operation finishes when the activity is in background. The application then tries to dismiss the dialog and this fails since the state of the activity was saved.

I understand the issue. It can be fixed by ensuring that changes to the UI are postponed until the activity is resumed as described in this post: How to handle Handler messages when activity/fragment is paused.

However, this solution leads to another problem. What if the operation finishes when the activity is in background and later the activity is killed by Android? When a user navigates back to the application, it restores its state that was saved in onSaveInstanceState. Therefore, the progress dialog is still visible with the same progress value as when the activity was put into background. A message that should dismiss it, was never processed and it was lost when the activity was killed.

What is the correct solution that handles all the described issues properly? How to allow for changing the UI when the activity is in background or, at least, allow for postponing UI changes and ensuring they won't be lost in case the activity is killed by Android? The solution must allow for tracking progress of a background task.

Community
  • 1
  • 1
maral
  • 1,431
  • 1
  • 10
  • 15
  • If the `DialogFragment` is restored upon `Fragment` reinstantiation, then just perform a check on whether the operation is still running, and dismiss the `DialogFragment` if not. If you aren't persisting your tasks, then it won't be running in any case after a process restart. – corsair992 Aug 02 '14 at 03:02

2 Answers2

2

The only sure-fire way to avoid the situation you've described is to persist the results to some sort of storage. You could start a service to do the long-running task, and have it write the results to a database, and also send a local broadcast when complete.

If the activity happens to be in the foreground, it will receive the broadcast and update its state to know that the process has finished, and could remove the information about the task from whatever persistent storage mechanism you use.

If the activity is in the background, the service will finish, send the broadcast, and persist the results. The next time the activity comes to the foreground, it should check this persistent storage and see if there's an unresolved task status, and update its state to match.

So, an example rundown might be (let's use "Uploading a Photo" as the task):

  1. User clicks a button to start an image upload

  2. Activity starts a service to upload the image to a remote service

  3. Activity registers a LocalBroadcastManager for some broadcast event that you define (e.g. com.mypackage.ACTION_PHOTO_UPlOADED)

  4. Service persists some information about the task (e.g. SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", false)

  5. Activity shows some sort of loading UI while the service processes the upload task

  6. User presses home and sends the app into stopped state

  7. Activity unregisters the local broadcast manager

  8. The service finishes uploading the image

  9. The service sends a local broadcast (com.mypackage.ACTION_PHOTO_UPlOADED) which is unused because the Activity is no longer listening

  10. The service updates the persisted information (SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", true))

  11. Android kills off the Activity, saving its state

  12. User later returns to the Activity

  13. Activity checks in onResume() with the service to see if there's an unhandled task result (if (SharedPreferences.has("TASK_PHOTO_UPLOADED"))), then checks to see if it should updated the state.

  14. If the photo upload key is there, and is true, Activity removes the dialog fragment and clears the TASK_PHOTO_UPLOADED key from SharedPreferences.

    If the photo upload key is there, and is false, Activity registers a local broadcast manager to wait for the event to complete

Kevin Coppock
  • 133,643
  • 45
  • 263
  • 274
  • 1
    Although you describe it in terms of `Service` and `LocalBroadcastManager` instead of `AsyncTask`, the same idea can be used here. However, using `SharedPreferences` to keep an information whether a dialog should be dismissed next time an activity is created/resumed seems to be using a cannon to kill a fly. I wonder there's any simpler and more elegant solution. – maral Aug 01 '14 at 22:51
  • 1
    SharedPreferences (like Service and LocalBroadcastManager) were just suggested implementations. You will need *some* sort of persistent storage -- SharedPreferences was the easiest to show, although you could use a database (now *that's* cannon to fly). Service can be substituted with an AsyncTask (although I'd highly recommend to use a Service instead as you don't have to worry about managing it with the Activity lifecycle) and LocalBroadcastManager could be replaced with an Event Bus or similar pattern. – Kevin Coppock Aug 01 '14 at 22:54
  • 2
    +1 A `Service` is the way to go if you actually want to have the operation completed, even in the case of it being terminated because of the process being killed. Otherwise, you might as well dismiss the `ProgressDialog` whenever the application is restored after being killed, as the operation would be not be running in any case, @maral. Alternatively, you might be interested in using some third-party library that will handle persistence of your tasks, such as [Tape](http://square.github.io/tape/) or [Priority Job Queue](https://github.com/path/android-priority-jobqueue). – corsair992 Aug 02 '14 at 01:12
  • Note that the important part here is persisting the _task_ (which would be accomplished by using a `Service`), not it's status or result, as otherwise the issue in the question doesn't apply. @maral, if you can provide a method to query the state of the operation, then that would be sufficient to determine if the `DialogFragment` needs to be dismissed upon `Fragment` reinstantiation. If you use the task queuing libraries I mentioned, they should provide this functionality inherently. – corsair992 Aug 02 '14 at 03:33
-1
  • Your Question was lengthy without any code sample, ill try to come up with my best answer, I have made a project just for you on how you should use onSaveinstance state in android which will solve all the possible problems

  • I have written an explanation essay below that will clear all your doubts


In android Life-Cycle and dealing with Fragments

  • Activity has the container for the fragments

  • Mount the first fragment from the oncreate of Activity

  • When we are using fragments to add replace etc. we need to use android fragment reference instead of classreference

  • Keep the fragments in the backstack every time we mount the fragment to the container

  • In the project we can observe that we are collecting all the states of widgets on onpause() and stored in a local variable and using this local variable, then pass this local variable into onSaveInstance() event access these tags in onActivityCreatd() are set to the view objects. We use this process because localvariables are holded in the class but view objects are null in onSaveInstance. This perticular observation is very important observed in Activity-FragmentOne-FragmentTwo-FragmentOne-OrientationChange-OrientationChange

  • Android static objects are able to retain thae state onOrientation change but dynamic Objects has to be resetted onOrientation change with the value using localvariables as explained above

  • If we are using the dynamic fragments we have the create the dynamic objects in OnActivityCreated() event before using the saveInstanceState to restore the dynamic objects

  • The fragment is added to the backstack on the onPause event

  • If the screen navigation is Activity-FragmentOne-FragmentTwo-FragmentOne-FragmentTwo then on press of the back button navigation works as FragmentTwo-FragmentOne-FragmentTwo-FragmentOne-Activity, thus we can clearly observe our path is being kept in track by the android backstack

  • If the path is Activity-FragmentOne and change the orientation for the first time then the events fired are as follows MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • If the path is Activity-FragmentOne-orientationchange and change the orientation for the first time then the events fired are as follows

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • If the path is Activity-FragmentOne-orientationchange-orientationchange and change the orientation for the first time then the events fired are as follows

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • If the path is Activity-FragmentOne-orientationchange-orientationchange-FragmentTwo and change the orientation for the first time then the events fired are as follows

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onStop- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume

  • If the path is Activity-FragmentOne-orientationchange-orientationchange-FragmentTwo-orientationchange and change the orientation for the first time then the events fired are as follows

MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentOne-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onStop- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume- FragmentOne-onPause- FragmentOne-onSaveInstanceState- FragmentOne-onSaveInstanceState- FragmentOne-onStop- FragmentOne-onDestroy- FragmentOne-onDetach- FragmentOne-onDestroy- FragmentOne-onDetach- MainActivity-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onAttach- FragmentOne-onCreate- FragmentOne-onCreateView- FragmentTwo-onActivityCreated- FragmentOne-onStart- FragmentOne-onResume


Summarising::

  • Now remember onPause Event is always is fired just before closing a fragment store the values that you want to preserve onOrientation, when phonecall comes, any other scenarios

  • Next onSaveInstanceState will be executed so use those local variables to set here to set data in bundle

  • Next the activity will be pushed to backstack your data will not be lost on clicking home button

  • So when onActivityCreated get the data from bundle' and store it back to yourviews`

  • Also remember android always completes the full lifecycle when destroying the fragment, if it is sent to backstack fragment wont be destroyed(when you press home button) android destroys only when it needs more space from backstack and it happens automatically


Let me know if you need any more information

Devrath
  • 42,072
  • 54
  • 195
  • 297
  • 1
    This answer doesn't address my question. I don't have a problem with orientation change, using `onSaveInstanceState()` or activity lifecycle. Look at the last paragraph: I ask how to ensure that a state (UI) of a `FragmentActivity` is updated (or will be updated next time a user navigates to it) if `AsyncTask` finishes when the activity is in background. – maral Aug 01 '14 at 22:45
  • @ maral .... Ok .... i think i couldn't analyze your question.... Still the way i have mentioned how to handle the events is the right way to do this! ......`Scenario-1:` ... Navigating to next activity to be placed in `onPostExecute` so that navigation wont happen until `AsyncTask` things are completed .....`Scenario-2:` ...If you are dealing with Database, you can add it in a `Transaction`..... in one of the answers `kcoppock` also describes a way to handle this scenario in a right way. ! .....Hope that it helps – Devrath Aug 02 '14 at 04:16