I have an app, fetchnig data from API and I use AsyncTaskLoader to update adapter and update UI with new data.
Loader works fine, when rotating device (data is kept unchanged).
Problem occurs, when I leave an app - using 'Home button' or turn off the screen and turn it back on. Then, data adapter and view are automatically updated with new, fetched data.
I have found out that when device is rotated - only onLoadFinished() method is executed.
When I turn off/on screen - onStartLoading() and loadInBackground() Loader methods are executed before onLoadFinished()! I cannot understand why there is such a difference between device rotation and turning off/on screen. I believe that Loader should behave the same in these two situations - ONLY onLoadFinished() should be called.
EDIT: I managed to fix issue:
1. create global 'cache variable' in AsyncTaskLoader implementation
2. store fetched data in deliverResult method in global 'cache variable'
3. in onStartLoading() method check for cache variable, and if it isn't empty => deliverResult(cache variable)
More info:
What does AsyncTaskLoader.deliverResult() actually do?
Yes loaders behavior got changed in 27.1
The underlying implementation of Loaders has been rewritten to use Lifecycle. While the API remains unchanged, there are a number of behavior changes
initLoader(), restartLoader(), and destroyLoader() can now only be
called on the main thread.
A Loader's onStartLoading() and onStopLoading() are now called when
the containing FragmentActivity/Fragment is started and stopped,
respectively.
onLoadFinished() will only be called between onStart() and onStop. As
a result, Fragment transactions can now safely be done in
onLoadFinished().
The FragmentController methods related to Loaders are now deprecated.
https://developer.android.com/topic/libraries/support-library/revisions
Check important changes in it
Related
I am an experienced Android developer and it is a shame that I am still ineffectively fighting against fundamental issues like persisting UI changes across configuration changes.
I know, this is an all too well known problem and there exists a lot of literature on it. The recommended solution is to setRetainInstance(true) a UI-less fragment:
https://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
Best practice: AsyncTask during orientation change
For the sake of completeness, let me draw up the scenario. You have an AsyncTask that performs a Network request and is supposed to update the UI in onPostExecute()
I see some issues with the proposed solution:
The proposed solution says that onPostExecute() is guaranteed to not be called during between the calls to onDetach() and the next onAttach() but what if the AsyncTask returns early(after Activity's onPause() and before the call to onDetach()). If onPostExecute() executes during the period, since onSaveInstanceState() will have been called, any update to the Views will not persist across Activity restart.
Second if the AsyncTask returns such that onPostExecute() executes before the call to Activity's onCreate() then the views will not have been initialized for any view updates to be pushed yet.
Is there any effective solution against this problem that addresses memory leaks and state save across config changes?
I am trying to understand some finer points of AsyncTaskLoaders. This may be obvious, to others but I can't find an unambiguous example or definition that demonstrates and exmplains what happens when you override the deliverResult() method. What actually gets delivered ? How does this interact with the calling object ? I can see use of super.deliverResult, which passes a private object from the class. So, does the loader automatically know what to associate with the "delivered result". I am totally confused.
Seems I'm a bit late to the party, but anyway...
One of the main advantages of this intermediary step between the background loading and the UI thread's callback onLoadFinished() getting called
loadInBackground()
deliverResult() and
the callback onLoadFinished()
is that it gives us a means of shortcutting the whole loading process from within the AsyncTaskLoader class.
And this can be put to good use for caching the loading result within your AsyncTaskLoader and preventing the background loading from happening if there is cached data.
And why would we want to do this? Isn't the whole point of loaders dealing with those dreaded activity lifecycle issues (e.g. rotating the device), maintaining state (like, caching data) and having a means to get updated when underlying data changes (CursorLoader)?
Well, yes, but this isn't the whole story.
Consider this use case:
You've got your app (the one with the AsynTaskLoader) up-and-running and it already has loaded data into your UI.
Then, you switch over to your Twitter app to check on some news and return to you app.
Without caching, upon returning to your app, the loader would do its reloading.
This behavior is different from the one after configuration changes, e.g. rotating your device, in which case no reloading would take place.
So, how would we then prevent the loader from re-fetching data in case we're just sending our app to the background and, later, return to it again?
Solution
Create a cache member variable in your AsyncTaskLoader implementation.
Override deliverResult() so that you save your fetched data in your cache first, before you call the superclass's implementation of deliverResult().
In onStartLoading() check if there's cached data, and if so, let your AsyncTaskLoader just deliver that. Otherwise, start loading.
Here's a link to a sample app which implements this behaviour.
It's just a "Toy app" and as such part of Udacity's current version of the "Developing Android Apps" fundamentals course. And here is the link to the respective video within that course that deals with this issue. (The course is free, but you'll still have to sign-up w/ Udacity).
In short, what this app demonstrates, is a UI in which the user can input a search query for searching GitHub's repos (via the GitHub API), showing the resulting search URL in a TextView and also the raw JSON fetched from GitHub in another TextView.
The whole action happens in just MainActivity.java and the relevant part here is within the AsyncTaskLoader that's implemented as an anonymous inner class:
For step 1, just introduce a member variable in your AsyncTaskLoader implementation that's meant to serve as your data cache.
/* This String will contain the raw JSON
from the results of our Github search */
String mGithubJson;
For step 2, override deliverResult() as to cache the loading result.
When loadInBackground() has finished, it passes its return value to deliverResult().
It does so anyway, but now that we've overridden deliverResult() we can step right in and store our fetched data into the cache member variable which we've created with just so good foresight.
And finally, we chain up to the super class implementation of deliverResult() with super.deliverResult() which will pass-on the result to the callback method onLoadFinished(), running on the UI thread.
#Override
public void deliverResult(String githubJson) {
mGithubJson = githubJson;
super.deliverResult(githubJson);
}
For step 3, check in onStartLoading() whether or not we've got cached data.
If we don't have cached data (yet), just force the loading to begin with a call to forceLoad().
But if we do have cached data, just call deliverResult(yourCachedDataGoesHere) and pass-in the cached data as argument.
if (mGithubJson != null) {
deliverResult(mGithubJson);
} else {
forceLoad();
}
So, if you now switch back and forth between your app and some other app(s), you'll notice that no reloading takes place, as the loader will just use your cached data.
suppose when data are loading in the background, at this time, user press HOME button and exist the app, when user comes back to the app, loading has been finished. So we have already have the data, then AsyncTaskLoader will call the deliverResult() method, deliver the data to the onLoadFinished() method for displaying.
When the user come back to app, onStartLoading() is being called before loadInBackground(). In this method, we could check if our data if empty or not, if not empty, we call deliverResult() and send the result to onLoaderFinished(), so it could prevent to reload data.
When we press HOME exist the app and then come back, it will not create a new Loader, instead the old loader will try to load data.
The only answer I can find that makes any sense is based on a decription in this link.
"A registered listener to receive the Loader's results when it
completes a load. For each of its Loaders, the LoaderManager
registers an OnLoadCompleteListener which will forward the Loader’s
delivered results to the client with a call to
onLoadFinished(Loader loader, D result). Loaders should deliver
results to these registered listeners with a call to
Loader#deliverResult(D result)."
deliverResult appears to be used when you have listeners to the AsyncTask and want to send the results back to them. I would say it's uncommon. The Android documentation is even less descriptive:
"Sends the result of the load to the registered listener. Should only
be called by subclasses. Must be called from the process's main
thread.
Parameters
data : the result of the load"
deliverResult works after doInbackground completes. It sends the result D (returned by doInBackground) to the calling thread. You may wish to override it for cleaning data, but you can do clean-up in doInBackground instead without overriding deliverResult.
I'm developing an android tablet application and i need to implement screen orientation (landscape and portrait). when screen orientation changes the fragment asyntactask restarting again. (sometimes application crashing) I need to stop restarting the asynctask and request data again. want to load the existing json data in to the new screen.
any help appreciate.
approch: android:configChanges="keyboardHidden|orientation|screenSize" is not working because of im using a fragment class.
You may find some hack (including muting the configuration changes or retaining the instance) to somehow get it work, but really AsyncTasks are evil and you should stay as far as you can from it most of the time.
If your current AsyncTask's job is to load some data from the network before showing it inside your Fragment, I'd recommend to use an AsyncTaskLoader. The examples in the javadoc should give you a good idea of how to implement it.
This will allow the network request to keep going while you rotate, to notify the UI (i.e. the LoaderManager.LoaderCallbacks) with the result only when it is ready to process it (so not while rotating for example), and will also cache the result so that you don't re-issue the network request each time you need the data.
There are a bunch of 3rd-party libraries trying to address this very common problem too, and Loaders also have their intricacies, but if I understand your use case correctly it should be just what you need.
Don't forget that whatever solution you choose it will have to account for the rotation (activity destroy / recreate) happening before, during and after the load of the data.
It seems that this approach is the less recommended. If you're initializing and executing your AsyncTask within your onCreate() method, I'd suggest using this approach. Basically it consists on saving the data you don't need to restart on a runtime configuration change, so you can handle them afterwards in the onCreate() method and decide which information you want to keep from the previous landscape and which should be restarted.
You can easily avoid the "re-creation" of a fragment, just using the setRetainInstance method on it.
public void setRetainInstance (boolean retain)
Control whether a fragment instance is retained across Activity
re-creation (such as from a configuration change). This can only be
used with fragments not in the back stack. If set, the fragment
lifecycle will be slightly different when an activity is recreated:
•
onDestroy() will not be called (but onDetach() still will be, because
the fragment is being detached from its current activity).
•
onCreate(Bundle) will not be called since the fragment is not being
re-created.
• onAttach(Activity) and onActivityCreated(Bundle) will
still be called.
Reference link
You can use this for example in you onCreateView method, like this:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
setRetainInstance(true);
// ... more of your code
}
While the Activity that contains the fragment still re-creating, the fragment instance will be the same, and will be reattached to the new activity.
Keep in mind that even with setRetainInstance the onCreateView method will be called always (in orientation change) because the fragment need to inflate the view according to the new space/dimensions, so if you are executing your asynctask from that method, you should consider moving it to the onCreate method (which will only be executed once) to avoid the "re-launching" every orientation change.
Hope it helps.
In the click handler for a button, I'm loading some data from a content provider (using getContentResolver().query(...)), then sending that data off in a network request. Since the query happens on the main thread with this approach, I want to move this off the main UI thread.
I think I can use a LoaderManager, and fire off the network request in onLoadFinished(), but the problem is that I don't want onLoadFinished() called ever again (for that Loader id), because I don't want to fire the network request again, during a screen orientation for example.
So, how do I use a LoaderManager for a query that I only want to happen only once?
Calling LoaderManager#initLoader() in your Activity#onCreate() method will either create a new Loader and force a new load, or reuse an existing Loader and deliver the most recently queried data if any exists. So as long as you are using the LoaderManager correctly (i.e. the way the developer's site recommends in the documentation), you shouldn't have any problems.
In your onLoadFinished(), you can call getLoaderManager().destroyLoader(loaderId) (or getSupportLoaderManager() as applicable). That will stop the Loader from automatically reloading. You may also need to ensure that where you are calling initLoader is guarded from executing again (via saving a boolean variable in onSaveInstanceState for example).
Using android-support-v4.jar and FragmentActivity (no fragments at this point)
I have an AsyncTaskLoader which I start loading and then change the orientation while the background thread is still running. In my logs I see the responses come through to the background requests. The responses complete and I expect onLoadFinished() to be called, but it never is.
As a means of troubleshooting, in the Manifest, if I set android:configChanges="orientation" onLoadFinished() gets called as expected.
My Activity implements the loader callbacks. In the source for LoaderManager.initLoader() I see that if the loader already exists, the new callback is set to the LoaderInfo inner object class but I don't see where Loader.registerListener() is called again. registerListener only seems to be called when LoaderManagerImpl.createAndInstallLoader() is called.
I suspect that since the activity is destroyed and recreated on orientation change and since it is the listener for callbacks, the new activity is not registered to be notified.
Can anyone confirm my understanding and what the solution so that onLoadFinished is called after orientation change?
Nikolay identified the issue - Thank you.
I was calling initLoader fron onResume(). The Android documentation states:
"You typically initialize a Loader within the activity's onCreate()
method, or within the fragment's onActivityCreated() method."
Read "typically" as a bit more emphatic than I did when it comes to dealing with configuration change life cycle.
I moved my initLoader call to onCreate() and that solved my problem.
I think the reason is that in FragmentActivity.onCreate() a collection of LoaderManagers is pulled from LastNonConfigurationInstance and in FragmentActivity.onStart() there is some start up work regarding Loaders and LoaderManagers. Things are already in process by the time onResume() is called. When the Loader needs instantiated for the first time, calling initLoader from outside onCreate() still works.
It's actually not the call to initLoader() in onCreate() that's fixing it. It's the call to getLoaderManager(). In summary, what happens is that when an activity is restarted, it already knows about the loaders. It tries to restart them when your activity hits onStart(), but then it hits this code in FragmentHostCallback.doLoaderStart()*:
void doLoaderStart() {
if (mLoadersStarted) {
return;
}
mLoadersStarted = true;
if (mLoaderManager != null) {
mLoaderManager.doStart();
} else if (!mCheckedForLoaderManager) {
mLoaderManager = getLoaderManager("(root)", mLoadersStarted, false);
// WTF: Why aren't we calling doStart() here?!
}
mCheckedForLoaderManager = true;
}
Since getLoaderManager() wasn't called yet, mLoaderManager is null. It therefore skips the first condition and the call to mLoaderManager.doStart().
You can test this by simply putting a call to getLoaderManager() in onCreate(). You don't need to call init / restart loaders there.
This really seems like a bug to me.
* This is the code path even if you aren't using fragments, so don't get confused by that.