Guide to background work in Android. Part 2: Loaders
This is the second of a series of articles on Android background tools and techniques. AsyncTask has already been reviewed earlier, in the following releases - ThreadPools with EventBus, RxJava 2 and Kortelins in Kotlin.
In the previous text, we mentioned that AsyncTasks have several problems. Let's recall two of them:
AsyncTasks know nothing about the Activity Life Cycle. If mishandled, you will at best get a memory leak, and at worst a failure.
AsyncTask does not support the preservation of state of progress and the reuse of download results.
The meaning of the first problem is this: in order to update the UI in the onPostExecute method, we need a link to a specific view or to the whole Activity it belongs to. A naive approach is to store this link inside AsyncTask itself: ')
The problem is that as soon as the user rotates the device, the Activity is destroyed and the link becomes outdated. This leads to a memory leak. Why? Recall that our doInBackground method is called inside the Future, executed on an executor - a static member of the AsyncTask class. This makes our AsyncTask object, as well as an Activity, strictly achievable (because statics is one of the roots of GC), and therefore unsuitable for garbage collection. This in turn means that several screen rotations can cause OutOfMemoryError, because the Activity takes up a decent amount of memory.
Well, we got rid of OOM, but the result of AsyncTask is lost in any case, and we are doomed to re-launch it, discharging the phone and expending traffic.
In order to fix this, the Android team several years ago proposed the Loaders API (“Boot Loaders”). Let's see how to use this API . We need to implement the Loader.Callbacks interface:
Please note: WeatherForecastLoaderCallbacks is a nested class of our Activity;LoaderManager stores a link to this Callbacks object - and this means to the Activity itself.
A lot of code, right? But we get an important advantage here. First, the Loader is reused when the screen is rotated (or other configuration changes). If the screen is rotated, onLoadFinished will be called with the transfer of the result we loaded earlier.
Another advantage is that we don’t have a memory leak, although we still have access to the Activity, allowing us to update the interface.
Cool, AsyncTask did not have both of these advantages! Let's now figure out how it all works.
This is where the main fun begins. I thought that LoaderManager is stored somewhere inside Application, but the actual implementation was much more interesting.
LoaderManager is created when creating an instance of an Activity:
publicclassActivity{ final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); public LoaderManager getLoaderManager(){ return mFragments.getLoaderManager(); } }
FragmentController.createController is simply the named constructor for the FragmentController class. FragmentController delegates the creation of LoaderManager to HostCallbacks (a nested class of our Activity), the implementation looks like this:
As you can see, LoaderManager for the Activity itself is lazily initialized; An instance of LoaderManager is not created until it is first needed. LoaderManager access to our Activity occurs via the '(root)' key in Map LoadersManagers. Access to this Map is implemented as follows:
The restoreLoaderNonConfig method ultimately simply updates the host controller, which is now a member of the class of the new Activity instance created after the configuration change.
When calling the initLoader () method, LoaderManager already has all the information about Loaders that was created in the destroyed Activity. So he can publish the downloaded result immediately:
publicabstractclassLoaderManager { public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) { ... LoaderInfo info = mLoaders.get(id); ... if (info == null) { info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks<Object>)callback); } else { // override old callbacks reference here to new one info.mCallbacks = (LoaderManager.LoaderCallbacks<Object>) callback; } if (info.mHaveData && mStarted) { // deliver the result we already have info.callOnLoadFinished(info.mLoader, info.mData); } return (Loader<D>)info.mLoader; }
It's great that we dealt with two things at once: how we avoid memory leaks (replacing the LoaderCallback instance) and how we deliver the result to a new Activity!
Perhaps you are wondering what else this beast is for mLastNonConfigurationInstances. This is an instance of the NonConfigurationInstances class, defined inside the Activity class:
The object is created using the retainNonConfigurationInstance () method, and then it is directly accessed by the Android OS. And it becomes available for Activity in the Activity # attach () method (and this is the internal Activity API):
So, unfortunately, the main magic remains inside the Android OS. But no one will forbid us to learn from her example!
Let's summarize what we found:
Loaders have a non-obvious API, but they allow you to save the download result when the configuration changes
Loaders do not cause memory leaks: just don't make them nested classes. Activity
Loaders allow you to reuse AsyncTask in a background job, but you can also implement your own Loader
LoaderManager is reused between deleted and newly created Activity due to saving to a special object.
In the next article we will talk about how to organize background work on Executors, and mix in a bit of EventBus. Stay tuned!
Minute advertising. From the author of the text:
As you noticed, this is a translation of my English-language article . If the article seemed valuable to you, pay attention - in April there will be a Mobius conference, in whose program committee I enter, and I can promise that this year the program will be particularly rich. Only a part of the program has been published on the conference site so far, because it is extremely difficult for us to choose the best ones - the competition is more acute than ever. You can already study the existing descriptions of the reports, and soon new ones will be added to them!