How to perform background operations in Android that update the UI properly using Fragments?
-
02-01-2020 - |
Question
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.
Solution
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):
- User clicks a button to start an image upload
- Activity starts a service to upload the image to a remote service
- Activity registers a
LocalBroadcastManager
for some broadcast event that you define (e.g.com.mypackage.ACTION_PHOTO_UPlOADED
) - Service persists some information about the task (e.g. SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", false)
- Activity shows some sort of loading UI while the service processes the upload task
- User presses home and sends the app into stopped state
- Activity unregisters the local broadcast manager
- The service finishes uploading the image
- The service sends a local broadcast (
com.mypackage.ACTION_PHOTO_UPlOADED
) which is unused because the Activity is no longer listening - The service updates the persisted information (
SharedPreferences.putBoolean("TASK_PHOTO_UPLOADED", true)
) - Android kills off the Activity, saving its state
- User later returns to the Activity
- 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. - 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
OTHER TIPS
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 problemsI have written an explanation essay below that will clear all your doubts
In android Life-Cycle and dealing with Fragments
Activity
has thecontainer
for thefragments
Mount the first fragment from the
oncreate
ofActivity
When we are using fragments to add replace etc. we need to use android
fragment
reference instead ofclassreference
Keep the fragments in the
backstack
every time we mount the fragment to the containerIn 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 intoonSaveInstance()
event access these tags inonActivityCreatd()
are set to the view objects. We use this process because localvariables are holded in the class but view objects arenull
inonSaveInstance
. This perticular observation is very important observed inActivity
-FragmentOne
-FragmentTwo
-FragmentOne
-OrientationChange
-OrientationChange
Android
static objects
are able to retain thae stateonOrientation change
butdynamic Objects
has to be resettedonOrientation
change with the value using localvariables as explained aboveIf we are using the dynamic fragments we have the create the dynamic objects in
OnActivityCreated()
event before using thesaveInstanceState
to restore the dynamic objectsThe fragment is added to the backstack on the
onPause
eventIf the screen navigation is
Activity
-FragmentOne
-FragmentTwo
-FragmentOne
-FragmentTwo
then on press of the back button navigation works asFragmentTwo
-FragmentOne
-FragmentTwo
-FragmentOne
-Activity
, thus we can clearly observe our path is being kept in track by the android backstackIf the path is
Activity
-FragmentOne
and change the orientation for the first time then the events fired are as followsMainActivity-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 preserveonOrientation
, whenphonecall
comes, any other scenariosNext
onSaveInstanceState
will be executed so use those local variables to set here to set data inbundle
Next the activity will be pushed to
backstack
your data will not be lost on clicking home buttonSo when
onActivityCreated
get the data frombundle' and store it back to your
views`Also remember android always completes the full
lifecycle
when destroying the fragment, if it is sent tobackstack
fragment wont be destroyed(when you press home button) android destroys only when it needs more space frombackstack
and it happens automatically
Let me know if you need any more information