Android has this Loader framework that offers a powerful (yet simple) way to asynchronously load data from content providers
or other data sources like an SQLite database, network operation, et al. in an Activity or a Fragment (clients).
Loaders have already been well explained by the documentation and some other guys, hence I’d highly recommend you
to go through these articles:
- http://developer.android.com/guide/components/loaders.html
- http://www.grokkingandroid.com/using-loaders-in-android/
- http://www.androiddesignpatterns.com/2012/07/understanding-loadermanager.html (4 part series)
Although these articles are enough, I’ll still try to summarize (and simplify) the entire concept and include certain points
that might not be mentioned in those links.
Introduction
So what you basically do is, connect to the Loader framework from a client which can be either an Activity or a Fragment.
This happens by requesting a loader from the LoaderManager
class. The loader manages a connection with the data source.
Android has a CursorLoader
class for when the data source is a ContentProvider
, so it queries the ContentResolver
and returns a Cursor
. For any other type of data source, you’ll have to code up a custom loader which ain’t too hard,
but requires a deeper understanding of the Loader framework.
Some of the characteristics or features offered by Loaders are:
- They interact with the data source in a background thread to provider asynchronous loading of data. once data is available,
the loader triggers a callback (generally in the UI thread) with the data that can be used to modify the user interface for instance. - They’re available to every Activity and Fragment and plays well with their respective lifecycles. This means if the Activity or Fragment is stopped (or destroyed), the loaders will also stop.
- Loaders can monitor (observe) the data source to deliver/update new results when the underlying data/content changes.
- On a configuration change, for eg. an orientation change, loaders that were running in the background continue to do their work. Then they retain their state over a configuration change so that they don’t have to re-query their data and delivers their cached
data to the recreated Activity. They operate on `Application` context, hence the Activity `Context` object undergoing
a configuration change is not preserved causing memory leak.
The entire framework consists of these 4-5 important classes/interfaces:
LoaderManager
LoaderManager
is an abstract class that manages all the loaders used by an Activity or a Fragment (clients).
A LoaderManager
can have multiple loaders but there’s only one LoaderManager
per client.LoaderManager
is like a mediator between a client and its loaders.
To get a LoaderManager
object, you don’t instantiate it but
call Activity.getLoaderManager()
or Fragment.getLoaderManager()
.
Now there are two important methods that the LoaderManager
API exposes:
Loader<D> initLoader(int id, Bundle args, LoaderCallbacks<D> callback)
Loader<D> restartLoader(int id, Bundle args, LoaderCallbacks<D> callback)
These two methods basically creates new loaders to use and has certain differences.
But before diving deep into them, we must first go through the LoaderManager.LoaderCallbacks
interface
(which is also the third argument passed to these methods).
LoaderManager.LoaderCallbacks
For the clients to interact with the LoaderManager
to create loaders and handle the data loaded by the loaders, they need
to implement the LoaderManager.LoaderCallbacks
interface. Here’s a basic Activity that doesn’t do anything but shows
how to implement LoaderCallbacks
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public class LoadersActivity extends Activity implements LoaderManager.LoaderCallbacks<D> { @Override public Loader<D> onCreateLoader( int id, Bundle args) { return null ; } @Override public void onLoadFinished(Loader<D> loader, D data) { } @Override public void onLoaderReset(Loader<D> loader) { } } |
The loader is defined as Loader<D>
where <D>
is the data type returned by the loader and also passed to onLoadFinished()
.
Now when LoaderManager.initLoader()
is called with a unique identifier, if there is no loader available with that ID then onCreateLoader()
callback is invoked so that a new loader can be created and returned to the LoaderManager
.
The LoaderManager
can now start dealing with the loader’s lifecycle and loading data from the data source via the loader.
On the other hand, is a loader with that ID already exists, then there is no need to create a new loader as the old one is cached.
Also when the initialization is done through initLoader()
, the background data loading is initiated.
Once the loader is initialized and the background data loading is completed, the result is delivered to the LoaderManager
which passes it to the client’s onLoadFinished()
callback on the UI thread so that the UI components can be updated.
This callback is also called if initLoader()
is called when a loader already exists or the data source has updated its content
and the loader is observing it for changes. The client can also force an asynchronous data load by calling Loader.forceLoad()
.
Clients can cancel initiated background loads with Loader.cancelLoad()
. If the load has already started (background execution) then the results will be discarded and not delivered to the client.
Once a previously created loader is no longer available because maybe the client was destroyed or LoaderManager.destroyLoader(id)
was called, then the client is notified in the onLoaderReset()
callback where it should free up resources by removing all
old references.
initLoader()
This method initializes a loader with the given ID (first argument).
This identifier must be unique for all loaders within the same client.
Note: An Activity and a Fragment can have loaders with the same numbers (identifiers) without any interference.
Now if no loader exists with the specific ID, then a new loader is fetched from onCreateLoader()
after which background
data load is initiated and the returned result is delivered to onLoadFinished()
.
If a loader already exists, then onLoadFinished()
is called with the latest loaded data (cached version).
So with initLoader()
we create a loader and get data or just retrieve the cached result from an existing loader.
This same loader is reused to retrieve cached results after a configuration change too (screen rotation for instance).
restartLoader()
The restartLoader()
will never reuse existing loaders. They always destroy the existing loader (along with its data) with the
specified ID and creates a new loader by invoking the onCreateLoader()
callback. This also initiates a completely new data load.
There’s no question of data caching in this case either. You must be wondering why one would use this.
This method is useful if you’ve a search bar where the user types a keyword to search.
In such a case, the data source will need to be queried and a new result set will be returned for every search term.
Both initLoader()
and restartLoader()
accepts a second argument as a Bundle
that is passed toLoaderCallbacks.onCreateLoader()
.
Loader and AsyncTaskLoader
The Loader framework has a Loader
class that basically represents a loader performing asynchronous loading of data.
This class is not very interesting as you wouldn’t use it nor derive another class from it really. Instead it has a subclass called AsyncTaskLoader
which uses an AsyncTask
to perform the data loading work in a background thread.
It relies on AsyncTask.executeOnExecutor()
to perform its background execution.
This is the class that should be extended to write your own custom loaders.
The AsyncTaskLoader
tries to keep the number of active threads to a minimum. So several consecutive forceLoad()
calls from
the client might delay the result’s delivery to onLoadFinished()
. Infact AsyncTaskLoader
will cancel all the previous loads
to invoke only the newest load. This means not only will you not get all the results in your callback but also the last one’s might
delivery might get delayed.
A Loader with an observer that’s observing the underlying data set will trigger multiple background loads for multiple changes,
which’ll also call onLoadFinished()
multiple times that updates the UI components (redraw).
This might lead to an unresponsive laggy user interface.
This can be prevented by throttling the data load to occur after a delay using setUpdateThrottle()
.
CursorLoader
The Loader framework has the CursorLoader
class that subclasses AsyncTaskLoader
.
This class can be used to load Cursor
objects from ContentProvider
data sources only.
It’s actually a loader with the Cursor
data type (extends AsyncTaskLoader<Cursor>
). CursorLoader
itself manages the lifecycle of the Cursor
object too, so for example no need to close it in your application code.
Note: It cannot be used with SQLite databases, for that you’ll need to code your own custom loader class.
CursorLoader
registers Loader.ForceLoadContentObserver
which is actually a ContentObserver
, on the Cursor
object
to detect changes in the underlying data set (content provider). Internally this is done by creating an instance of ForceLoadContentObserver
and passing it to cursor.registerContentObserver
.
The content provider will also need to register the cursor to watch for any data changes pointed by the content URI
using cursor.setNotificationUri()
.
Instead of writing a class that shows how to use CursorLoader
and hence shows the usage of loaders,
I’d just point you to the last huge piece of code in the documentation which is fairly simple to understand.
Notice how that piece of code uses swapCursor()
but not changeCursor()
– the latter also closes the old cursor object.
One thing you might still be wondering is that, from that code sample you learnt how to query/retrieve data from the
Content Provider, but what about changing data, i.e., insert, update and delete (other CRUD operations).
For those operations, I don’t know if there’s a recommended solution but you could possibly use an AsyncTask
inside your
Activity or even better make use of AsyncQueryHandler
. Your AsyncQueryHandler
code will remain separate from anything
related to loaders/CursorLoader – no mix up.
Implementing Custom Loaders
Most of the Loaders examples you’ll come across will be in conjunction with content providers because that is supported by
Android out of the box (via CursorLoader
). If you want your data source to be something other than a content provider,
like a file or a SQLite database directly, you’ll have to write a custom loader by generally extending AsyncTaskLoader
.
This task is not simple and requires quite a bit of understanding of how the Loader framework works internally,
or just the CursorLoader
and AsyncTaskLoader
. Let’s cover various concepts that’ll help us with the process.
States of a Loader
Loader
is the base class for any loader. A loader can be in four different states that is held (instance variables) by Loader
.
- Reset – In this state, the loader gives up any data associated with it for garbage collection. This state is achieved by calling the
reset()
method that calls theonReset()
callback that you must implement in your subclass and free up resources.
Called by theLoaderManager
when destroying the loader.
Started – This is the started state achieved by a call tostartLoading()
that’ll invoke theonStartLoading()
callback that you
must override where you shall start loading the data by making a call toforceLoad()
that invokes theonForceLoad()
callback
on theAsyncTaskLoader
that in turn will callloadInBackground()
on a worker thread where you do your background loading
operations. Monitoring for changes and performing new loads based on the changes will also be done here.
This is the only state in whichonLoadFinished()
is called (generally in the UI thread).- Stopped – In this data, no data can be delivered to the client (that can only happen in the Started state).
It may observe/monitor for changes and load content in the background for the purpose of caching that can be used later
if the loader is started again. From this state the loader can be started or reset. - Abandoned – An intermediary state between stopped and reset where it holds the data until a new loader is connected to the
data source, so that the data is available until the restart is completed.
The client (Activity/Fragment) need not call any of the methods to change the loader’s state, it’s all handled by the LoaderManager
.
All the client has to care about is calling initLoader()
and restartLoader()
.
Of course it can call forceLoad()
to just trigger a new data load.
This should be done in the started state else no data will be delivered to onLoadFinished()
.
Loading Data in Background
As mentioned earlier, your custom loader should extend AsyncTaskLoader
which has a method called loadInBackground()
which is what you must override. This method is invoked on a worker thread where all your long task should be executed.
Here’s a simple custom loader demonstrating this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class CustomLoader extends AsyncTaskLoader<String> { public CustomLoader(Context context) { super (context); } @Override protected void onStartLoading() { super .onStartLoading(); forceLoad(); } @Override public String loadInBackground() { return loadData(); } private String loadData() { SystemClock.sleep( 2000 ); return "Hello World" ; } } |
Once the loadInBackground()
method returns the data, the AsyncTaskLoader
calls deliverResult()
(with that data) that you
should override and from it should call super.deliverResult()
which basically calls the Loader.deliverResult()
which
forwards the results to registered OnLoadCompleteListener<D>
. The LoaderManager
registers an OnLoadCompleteListener<D>
for each of its loaders which forwards the results received from that Loader.deliverResult()
call to the client’s onLoadFinished()
implementation.
Observe Underlying Data Set
Your loader should observe the underlying data set just like CursorLoader
does, so that on data change it can initiate
a new data load request in the background. In the case of CursorLoader
, ContentObserver
has been used internally.
But apparently that’s not possible with say direct SQLite cursors without dirty hacks. Here are some helpful SO answers on it:
So as mentioned in the first answer make use of a LocalBroadcastManager
or EventBus library.
Event based approaches are lovely, so feel free to use that library. This article even explains how to use it.
Some other solutions are using Observer
and Observable
. Here’s an example. You can use BroadcastReceiver
that works
across processes or just use LocalBroadcastManager
locally within the application. There’s the FileObserver
class to monitor
filesystem changes.
When your observer is updated with a change notification, you’ll want to load new data asynchronously again.
This can now be done in two ways:
- Call
forceLoad()
regardless of the loader’s state, hence it’s up to you whether you want to check for the started state or not
(isStarted
returnstrue
). - Trigger
Loader.onContentChanged()
which is generally called whenLoader.ForceLoadContentObserver
detects a change, i.e.,
itsonChange()
method is called. You callonContentChanged()
directly that initiates data loading (by callingforceLoad()
)
if the loader’s state is started. If not started, it sets a flag so thattakeContentChanged()
returns true and reset the flag tofalse
. SotakeContentChanged()
basically indicates whether the loader’s content had changed while it was stopped.
So based on what we learnt about takeContentChanged()
, your custom loader’s onStartLoading()
should look more like this:
1 2 3 4 5 6 7 8 | @Override protected void onStartLoading() { super .onStartLoading(); if (data == null || takeContentChanged()) { forceLoad(); } } |
Caching Results
It’s always a good idea to cache results fetched from loadInBackground()
so that further calls to onStartLoading()
when initLoader()
is called doesn’t lead to loading data again in a background thread, hence saving resources.
This can be done by caching the results in deliverResult()
and then the new onStartLoading()
would look like this:
1 2 3 4 5 6 7 8 9 10 11 12 | @Override protected void onStartLoading() { super .onStartLoading(); if (data != null ) { deliverResult(data); } if (data == null || takeContentChanged()) { forceLoad(); } } |
That was a really long explanation of the intricacies of the Loader framework. Now that you know how it works, and what you
have to do, why not try building a custom loader as a challenge and share in the comments below ?
If you struggle with it or need further help, feel free to check out the CursorLoader
source code here.
Loaders for Long Running Background Tasks
For super long running background tasks like a network operation, don’t use Loaders as they’re strictly attached to their
Activity/Fragment context. If the Activity or Fragment is destroyed, they’ll also be destroyed. It’s better to make use of
an IntentService
in such a case which also executes on a background thread and is not tied to its client component.
It can be also started with various flags to restart, if killed by the system.
Conclusion
We covered almost everything about Loaders. In case of confusion you should always go through the source code of various
classes like AsyncTaskLoader
, Loader
, CursorLoader
, etc. If you’re working with content providers or similar data sources,
then do consider using them as they solve various problems really efficiently with a neat API. Got questions ?
Ask in the comments below.
'IT_Programming > Android_Java' 카테고리의 다른 글
TextView에 ScrollView로 감싸지 않고 스크롤되게 하기 (0) | 2015.01.14 |
---|---|
[펌] GCM Architecture (0) | 2015.01.13 |
[펌] Using AsyncQueryHandler to Access Content Providers Asynchronously in Android (0) | 2015.01.08 |
[펌] HTML5 Canvas base64 데이타를 Android Bitmap으로 사용하기 (0) | 2015.01.02 |
Branding the EdgeEffect aka Hit the Wall with Your Own Color (0) | 2015.01.01 |