If you fetch the data from any ContentProvider
in your application, the most likely scenarios are:
- You’re absolutely clueless and fetch the data on UI thread.
- You’re using an
AsyncTask
and:- Your app crashes on screen orientation change.
- You block the screen orientation, because you googled this “solution” on StackOverflow.
- You wrote error prone boilerplate code to detach and reattach the
AsyncTask
from the Activity.
- You are using
CursorLoader
.
But accessing ContentProvider
is probably not the only type of asynchronous operations you perform. You might want to access SharedPreferences
, read a file or query a web API. In that case you need Loader<SomeOtherDataThanCursor>
, but implementing one correctly is a bit tricky.
I’ll walk you through the entire process of understanding how the Loaders work, implementing a sane base class for your Loaders, implementing CursorLoader
with all issues fixed and extending it to allow multiple notification Uris. It’ll be a long post so grab a cup of your favourite beverage.
Loaders 101
Loader should do three things:
- Load data in background thread.
- Cache the loaded data, so you won’t reload it after screen orientation change.
- If applicable, monitor the data source and reload the data when necessary.
The Loader class itself doesn’t provide any mechanism for loading the data in background thread. You either have to implement this yourself, or you can subclass the AsyncTaskLoader
. This covers the first point on our requirements list.
The 2nd point is not handled by AsyncTaskLoader
. In fact the AsyncTaskLoader
is far from being fully functional, for example this perfectly reasonable looking implementation won’t work:
1 2 3 4 5 6 7 8 9 10 |
|
AbstractLoader v1
A good starting point for creating either CursorLoader
implementation or LoaderCustom.java from SDK samples. Here’s the common part of these two implementations, which provides all necessary boilerplate for loading and caching the data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
|
Why this class is not provided by the framework is a mystery to me, but hey, that’s just one more thing you have to know when coding for Android platform. Now you can write your custom Loader like this:
1 2 3 4 5 6 7 8 9 10 |
|
But why do we need all that code? The key to understanding the Loaders is understanding the expected Loader’s behaviour in different states: started, stopped, abandoned and reset. Upon entering each state, the appropriate callback is executed:
onStartLoading
: the Loader was created and should either load the data or return cached data.onStopLoading
: the Loader should keep the cached data and monitor the data source for changes, but it shouldn’t load the data. This happens for example when users presses home button from your app.onAbandoned
: someone restarted the Loader. New instance of this Loader was created inonCreateLoader
callback in yourFragment
/Activity
/whatever and loads new data. The abandoned Loader should keep the data until the new Loader loads and delivers it’s data. There is no point of monitoring data source or reloading the data in abandoned Loader – the data will be loaded by the new instance. When new Loader delivers it’s data this Loader will be reset.onReset
: the data previously loaded by this Loader are no longer used and should be cleaned up. This Loader might be started again, so make sure you clean up also any old state in your Loader implementation.
The AsyncTaskLoader
provides additional callback:
onCancelled
: called after data loading when it turns out that this data is no longer needed, for example when theAsyncTask
executing youronLoadInBackground
was cancelled. In this callback you should take care of releasing resources.
Since the releasing resources should be also performed in onReset
callback and in our deliverResults implementation, our AbstractLoader class provides handy releaseResources()
callback for closing your Cursor
s, file handles, etc.
Now let’s walk through our AbstractLoader
implementation. When someone starts our Loader using LoaderManager.initLoader()
, the onStartLoading
is called:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
We keep the loaded data in mResult
member of our AbstractLoader
. If we have already loaded the data, we can just deliver the results to Loader clients. If the cache is empty or the Loader was notified about new data available for fetching, we force data reload by calling forceLoad()
method. It starts AsyncTask
which executes loadInBackground
in background thread and the result is passed to deliverResults
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
A lot of interesting things happen here. First, we check if the loader was put into reset
state. In this state all the previous resources were already released, so we just need to take care of newly loaded data. Then we swap the data in cache, call deliverResults
in Loader and then release the resources for previously cached data.
When the Fragment or Activity with active Loader is stopped, the Loaders are also put in the stopped state. It means that they should keep the cached data, monitor if this data is still valid, but they should not actively load the data or deliver the results to UI thread. In terms of AsyncTaskLoader
it means that any running AsyncTasks
should be cancelled:
1 2 3 4 |
|
Current implementation of AsyncTaskLoader
do not interrupt the active tasks, it only marks that the results of these tasks should not be delivered to the UI thread. However, the results might require some resource releasing, so the onCancelled
callback is called:
1 2 3 4 5 |
|
The last callback we have to implement is onReset
:
1 2 3 4 5 6 7 8 9 10 |
|
There are two important things here. First, the Loader can be moved to reset state from started state, which means it can still have active AsyncTasks
executing loadInBackground
. We need to stop them first. Then, as per the specified contract, we have to release the resources and clear the cache.
What about onAbandoned
callback? AbstractLoader doesn’t monitor any data source by itself, so this callback doesn’t have to be implemented.
CursorLoader
So how would we implement observing a data source and automatic reloading? Let’s see how would the CursorLoader
implementation look like had we used our AbstractLoader as a base class (literally; if you merge MyCursorLoader and AbstractLoader code samples from this post, you’ll get exactly the CursorLoader implementation from support-v4):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
This implementation has two bugs and one kind-of-feature. Let’s start with the last one.
onStartLoading
contract specifies, that you should start monitoring the data source upon entering this state. But let’s think what would happen if you had a query that takes 200ms to run and your data source would change every 150ms. The loader would never deliver any data, because every load request would be cancelled in middle of loadInBackground
execution by our content observer.
I guess that’s why the Android implementation of CursorLoader
registers the observer after the data is loaded. This way the first results are delivered as soon as possible, but for subsequent loads the data is delivered only when data didn’t change during loading. I’m not sure if it was intentional, or this behavior was implemented by accident, but it makes sense to me, so let’s adjust our Loaders contract and implement this behavior in our AbstractLoader.
But if you look closely, the CursorLoader
implementation violates even this updated contract. Remember that loadInBackground
and deliverResults
are executed on separate threads. So what would happen if the data observer is triggered after registerContentObserver
call, but before the deliverResults
? We’d get exactly the same behavior we’d get had we registered the ContentObserver in onStartLoading
– the loader would never deliver it’s data. That’s the first bug.
The second issue with CursorLoader
implementation is violation of onAbandon
callback contract. If someone calls restartLoader and the content observer is triggered, the abandoned Loader instance will happily reload it’s data just to throw it away.
You can dismiss it as something that would happen only 1% of the time and has negligible impact, and if we were talking about application code, I’d agree with you, but IMO library code that will be used by thousands of developers should be held to a higher standard.
Fixing CursorLoader
Here’s the wrap up of changes in behavior:
1. Register ContentObserver
after the first data is delivered, not after the first data is loaded.
2. Unregister ContentObserver
in onAbandon
.
The first point requires changes to deliverResult
method, so it makes sense to modify our AbstractLoader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Our CursorLoader
implementation would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
The second part – unregistering observer in onAbandon
– is tricky. It’s illegal to call Cursor.unregisterContentObserver
with observer that wasn’t registered and the onAbandon
can be called when the deliverResults
wasn’t called (see AsyncTaskLoader.dispatchOnLoadComplete()
implementation). One solution would be keeping the set of Cursors that were registered, but it’s not optimal. Instead, we can create a proxy ContentObserver that can be enabled or disabled:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
AbstractObservingLoader
The CursorLoader
is a bit special case, because the Cursor
itself contains ContentObservable
. In most cases however the content observers and loaded data would be completely separated. For these cases it would be useful to have a base class for Loader which registers some ContentObservers
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
We need to keep the registered state in our Loader, because the default Observable
implementation doesn’t like registering the same observer twice or unregistering not registered observer.
Now we can use this class as a base for a Loader which should be reloaded when one of specified Uri
s is triggered:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
Conclusions
I think the Loaders
aren’t as bad as some people think and say. Four Loader states might seem complicated at first, but if you think about Android Activity
lifecycle they make perfect sense and they are something you’d have to implement yourself, with a high probability of mucking things up. The only thing lacking is documentation and sane base classes for extensions, something I hope I delivered through this blog post.
+CommonsWare wrote few weeks ago that he considers Loader to be a failed abstraction, mostly because the interface assumes there is a single object which notifies the Loader about new data. He concluded his post with the following sentence:
In my case, if I am going to have some singleton manager object, with distinct data objects per operation, I am going to use something more flexible than Loader, such as an event bus.
Extending AbstractObservingLoader
to load some data from SQLiteDatabase
and subscribe to some event bus for model change events should be trivial, and you’d get a lot of things for free – performing loads in background, cancelling loads, caching results, invalidating cache, and so on.
Having said that, Loaders
are not solution to every single problem. They are coupled with activity lifecycle, so they are not suitable for long running tasks that should not be interrupted when user navigates away. In these cases the IntentService
, or some other Service
implementation is a better choice.