One perpetual problem in Android development is getting work to run outside the main application thread. Every millisecond we spend on the main application thread is a millisecond that our UI is frozen and unresponsive. Disk I/O, in particular, is a common source of such slowdowns, particularly since this is one place where the emulator typically out-performs actual devices. While disk operations rarely get to the level of causing an “application not responding” (ANR) dialog to appear, they can make a UI “janky”.
Android 3.0 introduced a new framework to help deal with loading bulk data off of disk, called “loaders”. The hope is that developers can use loaders to move database queries and similar operations into the background and off the main application thread. That being said, loaders themselves have issues, not the least of which is the fact that it is new to Android 3.0 and therefore presents some surmountable challenges for use in older Android devices.
This chapter will outline the programming pattern loaders are designed to solve, how to use loaders (both built-in and third-party ones) in your activities, and how to create your own loaders for scenarios not already covered.
Understanding this chapter requires that you have read the chapters on:
Android had the concept of “managed cursors” in Android 1.x/2.x.
A managed Cursor
was one that an
Activity
… well… manages. More specifically:
Cursor
was deactivated,
freeing up all of the memory associated with the result set, and
thereby reducing the activity’s heap footprint while it was not in the
foregroundCursor
was requeried,
to bring back the deactivated data, along the way incorporating any
changes in that data that may have occurred while the activity was
off-screenCursor
was closed.This is a delightful set of functionality. Cursor
objects obtained
from a ContentProvider
via managedQuery()
were automatically
managed; a Cursor
from SQLiteDatabase
could be managed by
startManagingCursor()
.
The problem is that the requery()
operation that was performed when
the activity is restarted is executed on the main application thread.
As has been noted elsewhere in the book, you really do not want
to do disk I/O on the main application thread, as it freezes the UI
and causes jank. This is particularly true for database I/O, where
you may not know in advance exactly how much data you will get back
or how long the query will take.
The Loader
framework was designed to solve three issues with the
old managed Cursor
implementation:
requery()
(or the equivalent) to be performed on
a background thread)Cursor
solution did not address at allCursor
, in case you have
data from other sources (e.g., XML files, JSON files, Web service
calls) that might be able to take advantage of the same capabilities
as you can get from a Cursor
via the loadersThere are three major pieces to the Loader
framework:
LoaderManager
, LoaderCallbacks
, and the Loader
itself.
LoaderManager
is your gateway to the Loader
framework. You obtain
one by calling getLoaderManager()
(or getSupportLoaderManager()
,
as is described later in this chapter). Via the
LoaderManager
you can initialize a Loader
, restart that Loader
(e.g., if you have a different query to use for loading the data),
etc.
Much of your interaction with the Loader
, though, comes from your
LoaderCallbacks
object, such as your activity if that is where you
elect to implement the LoaderCallbacks
interface. Here, you will
implement three “lifecycle” methods for consuming a Loader
:
onCreateLoader()
is called when your activity requests that a
LoaderManager
initialize a Loader
. Here, you will create the
instance of the Loader itself, teaching it whatever it needs to know
to go load your dataonLoadFinished()
is called when the Loader
has actually loaded
the data — you can take those results and pour them into your
UI, such as calling swapCursor()
on a CursorAdapter
to supply the
fresh Cursor
’s worth of dataonLoaderReset()
is called when you should stop using the data
supplied to you in the last onLoadFinished()
call (e.g., the
Cursor
is going to be closed), so you can arrange to make that
happen (e.g., call swapCursor(null)
on a CursorAdapter
)When you implement the LoaderCallbacks
interface, you will need to
provide the data type of whatever it is that your Loader
is loading
(e.g., LoaderCallbacks<Cursor>
). If you have several loaders
returning different data types, you may wish to consider implementing
LoaderCallbacks
on multiple objects (e.g., instances of anonymous
inner classes), so you can take advantage of the type safety offered
by Java generics, rather than implementing LoaderCallbacks<Object>
or something to that effect.
Then, of course, there is Loader
itself.
Consumers of the Loader framework will use some concrete
implementation of the abstract Loader
class in their
LoaderCallbacks
onCreateLoader()
method. API Level 11 introduced
only one concrete implementation: CursorLoader
, designed to perform
queries on a ContentProvider
, and described in
a later section.
Loader
and its related classes were introduced in API
Level 11 (Android 3.0). If your minSdkVersion
is 11 or higher,
you can use loaders “naturally” via the standard implementation.
If your minSdkVersion
is below 11, the Android Support package offers its
own implementation of Loader
and the other classes. However, to use
it, you will need to work within four constraints:
support-v4
from the Android Support package
as a dependency, directly or indirectly. For example, if you are using
appcompat-v7
, you are already pulling in support-v4
.FragmentActivity
, not the OS base
Activity
class or other refinements (e.g., MapActivity
), or from
other classes that inherit from FragmentActivity
(e.g., AppCompatActivity
).support.v4
versions of various
classes (e.g., android.support.v4.app.LoaderManager
instead of
android.app.LoaderManager
)LoaderManager
by calling
getSupportLoaderManager()
, instead of getLoaderManager()
, on your
FragmentActivity
These limitations are the same ones that you will encounter when using fragments on older devices. Hence, while loaders and fragments are not really related, you may find yourself adopting both of them at the same time, as part of incorporating the Android Support package into your project.
Let’s start off by examining the simplest case: using a
CursorLoader
to asynchronously populate and update a Cursor
retrieved from a ContentProvider
. This is illustrated in the
Loaders/ConstantsLoader
sample project, which is the same
show-the-list-of-gravity-constants sample application that
we examined previously, updated to use the
Loader
framework. Note that this project does not use the Android
Support package and therefore only supports API Level 11 and higher.
In onCreate()
, rather than executing a managedQuery()
to retrieve
our constants, we ask our LoaderManager
to initialize a loader,
after setting up our SimpleCursorAdapter
on a null Cursor
:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
adapter=new SimpleCursorAdapter(this,
R.layout.row, null,
new String[] {Provider.Constants.TITLE,
Provider.Constants.VALUE},
new int[] {R.id.title, R.id.value});
setListAdapter(adapter);
registerForContextMenu(getListView());
getLoaderManager().initLoader(0, null, this);
}
Using a null
Cursor
means we will have an empty list at the
outset, a problem we will rectify shortly.
The initLoader()
call on LoaderManager
(retrieved via
getLoaderManager()
) takes three parameters:
Bundle
of data to supply to the loaderLoaderCallbacks
implementation to use for the results from this
loader (here set to be the activity itself, as it implements the
LoaderManager.LoaderCallbacks<Cursor>
interface)The first time you call this for a given identifier, your
onCreateLoader()
method of the LoaderCallbacks
will be called.
Here, you need to initialize the Loader
to use for this identifier.
You are passed the identifier plus the Bundle
(if any was
supplied). In our case, we want to use a CursorLoader
:
public Loader<Cursor> onCreateLoader(int loaderId, Bundle args) {
return(new CursorLoader(this, Provider.Constants.CONTENT_URI,
PROJECTION, null, null, null));
}
CursorLoader
takes a Context
plus all of the parameters you would
ordinarily use with managedQuery()
, such as the content provider
Uri
. Hence, converting existing code to use CursorLoader
means
converting your managedQuery()
call into an invocation of the
CursorLoader
constructor inside of your onCreateLoader()
method.
At this point, the CursorLoader
will query the content provider,
but do so on a background thread, so the main application thread is
not tied up. When the Cursor
has been retrieved, it is supplied to
your onLoadFinished()
method of your LoaderCallbacks
:
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor);
}
Here, we call the new swapCursor()
available on CursorAdapter
, to
replace the original null
Cursor
with the newly-loaded Cursor
.
Your onLoadFinished()
method will also be called whenever the data
represented by your Uri
changes. That is because the CursorLoader
is registering a ContentObserver
, so it will find out about data
changes and will automatically requery the Cursor
and supply you
with the updated data.
Eventually, onLoaderReset()
will be called. You are passed a
Cursor
object that you were supplied previously in
onLoadFinished()
. You need to make sure that you are no longer
using that Cursor
at this point — in our case, we swap null
back into our CursorAdapter
:
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
And that’s pretty much it, at least for using CursorLoader
. Of
course, you need a content provider to make this work, and creating a
content provider involves a bit of work.
The Loader
framework does an excellent job of handling queries in
the background. What it does not do is help us with anything else
that is supposed to be in the background, such as inserts, updates,
deletes, or creating/upgrading the database. It is all too easy to
put those on the main application thread and therefore possibly
encounter issues. Moreover, since the thread(s) used by the Loader
framework are an implementation detail, we cannot use those threads
ourselves necessarily for the other CRUD operations.
Here are some common development scenarios and how the Loader
framework addresses them.
According to
the Loader
documentation,
“They monitor the source of their data and deliver new
results when the content changes”.
The documentation is incorrect.
A Loader
can “monitor the source of their data and deliver new
results when the content changes”. There is nothing in the framework
that requires this behavior. Moreover, there are some cases where it is
clearly a bad idea to do this — imagine a Loader
loading data off
of the Internet, needing to constantly poll some server to look for
changes.
The documentation for a Loader implementation should tell you the
rules. Android’s built-in CursorLoader
does deliver new results, by
means of a behind-the-scenes ContentObserver
. However, it is not
automatic that a Loader
deliver new results, and it may be impractical
for a Loader
to deliver new results.
The managed Cursor
system that the Loader
framework replaces
would automatically requery()
any managed Cursor
objects when an
activity was restarted. This would update the Cursor
in place with
fresh data after a configuration change. Of course, it would do that
on the main application thread, which was not ideal.
Your Loader objects are retained across the configuration change
automatically. Barring bugs in a specific Loader
implementation,
your Loader should then hand the new activity instance the data that
was retrieved on behalf of the old activity instance (e.g., the
Cursor
).
Hence, you do not have to do anything special for configuration changes.
Another thing the managed Cursor
system gave you was the automatic
closing of your Cursor
when the activity was destroyed. The
Loader
framework does this as well, by triggering a reset of the
Loader
, which obligates the Loader
to release any loaded data.
The final major feature of the managed Cursor
system was that it
would deactivate()
a managed Cursor
when the activity was
stopped. This would release all of the heap space held by that
Cursor
while it was not on the screen. Since the Cursor
was
refreshed as part of restarting the activity, this usually worked
fairly well and would help minimize pressure on the heap.
Alas, this does not appear to be supported by the Loader
framework.
The Loader
is reset when an activity is destroyed, not stopped.
Hence, the Loader
data will continue to tie up heap space even
while the activity is not in the foreground.
For many activities, this should not pose a problem, as the heap
space consumed by their Cursor
objects is modest. If you have an
activity with a massive Cursor
, though, you may wish to consider
what steps you can take on your own, outside of the Loader
framework, to help with this.
Perhaps, despite the above issues, and despite the author’s assertion
that the Loader
framework is a failed abstraction,
you want to implement a custom Loader
.
You have two main choices for doing that:
Loader
AsyncTaskLoader
, which manages an AsyncTask
for you, giving you
a background thread for loading the contentIf your API can work either way — synchronously or asynchronously –
either option works. In terms of getting the Loader
implementation right,
you may want to use AsyncTaskLoader
, as it will ensure that everything
is delivered on the right thread. On the other hand, you may have greater
control over the nature of the asynchronous work using the API’s native
asynchronous capability, such as configuring a thread pool.
The
HTTP/RetroLoader
sample project is a clone of the HTTP/Retrofit
sample app from
the chapter on Internet access. However, this time, the
Retrofit work to load the most recent android
Stack Overflow questions
will be mediated by a QuestionsLoader
.
Retrofit offers both synchronous and asynchronous APIs. For the purposes
of this sample, we will use the synchronous API, to see how one might
implement an AsyncTaskLoader
. That, in turn, requires us to modify
StackOverflowInterface
, having questions()
return the SOQuestions
directly, rather than by using a callback:
package com.commonsware.android.retrofit;
import retrofit.http.GET;
import retrofit.http.Query;
public interface StackOverflowInterface {
@GET("/2.1/questions?order=desc&sort=creation&site=stackoverflow")
SOQuestions questions(@Query("tagged") String tags);
}
Otherwise, this is unchanged from the original edition of this sample.
QuestionsLoader
is an implementation of AsyncTaskLoader
. The documentation
for Loader
and AsyncTaskLoader
leave a lot to be desired. QuestionsLoader
is based on “triangulation” between the installed-applications loader
included in the AsyncTaskLoader
documentation
and the source code to CursorLoader
.
Loader
uses Java generics. Its declaration requires the type of
content being loaded by the Loader
. So, QuestionsFragment
is a Loader
of SOQuestions
:
public class QuestionsLoader extends AsyncTaskLoader<SOQuestions> {
final private StackOverflowInterface so;
private SOQuestions lastResult;
public QuestionsLoader(Context context) {
super(context);
RestAdapter restAdapter=
new RestAdapter.Builder().setEndpoint("https://api.stackexchange.com")
.build();
so=restAdapter.create(StackOverflowInterface.class);
}
@Override
protected void onStartLoading() {
super.onStartLoading();
if (lastResult!=null) {
deliverResult(lastResult);
}
else {
forceLoad();
}
}
@Override
synchronized public SOQuestions loadInBackground() {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
}
return(so.questions("android"));
}
@Override
public void deliverResult(SOQuestions data) {
if (isReset()) {
// actual cleanup, if any
}
lastResult=data;
if (isStarted()) {
super.deliverResult(data);
}
}
@Override
protected void onStopLoading() {
super.onStopLoading();
cancelLoad();
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
// plus any actual cleanup
}
}
A Loader
needs to have a constructor that takes a Context
as a
parameter. So, QuestionsLoader
has one, that chains to the superclass
constructor, plus sets up the StackOverflowInterface
using Retrofit:
public QuestionsLoader(Context context) {
super(context);
RestAdapter restAdapter=
new RestAdapter.Builder().setEndpoint("https://api.stackexchange.com")
.build();
so=restAdapter.create(StackOverflowInterface.class);
}
The real work for loading the questions comes in loadInBackground()
.
Subclasses of AsyncTaskLoader
need to implement this to return the
content being loaded. As the name suggests, loadInBackground()
is
called on a background thread, so you can take time here.
The key piece of the loadInBackground()
of QuestionsFragment
is the
call to questions()
on the StackOverflowInterface
, to retrieve the
desired questions:
@Override
synchronized public SOQuestions loadInBackground() {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
}
return(so.questions("android"));
}
However, it is possible that before loadInBackground()
is called,
that something cancels the AsyncTask
in the AsyncTaskLoader
.
A subclass of AsyncTaskLoader
needs to check isLoadInBackgroundCanceled()
in loadInBackground()
, and throw an OperationCanceledException
if
isLoadInBackgroundCanceled()
returns true
.
Not only do subclasses of AsyncTaskLoader
have to load the content,
they also have to cache the results of the previous load. So, QuestionsLoader
has a lastResult
field, holding onto an SOQuestions
object.
In deliverResult()
, we need to do three things:
isReset()
returns true
, indicating that the Loader
was
reset, we need to clean up anything associated with the previous load
resultsdeliverResult()
as a parameterisStarted()
returns true
), we need
to chain to the superclass implementation of deliverResult()
to actually
deliver the data to the LoaderCallbacks
implementation:
@Override
public void deliverResult(SOQuestions data) {
if (isReset()) {
// actual cleanup, if any
}
lastResult=data;
if (isStarted()) {
super.deliverResult(data);
}
}
Here, lastResult
is just a POJO holding onto other POJOs, so there is
nothing specific for us to do in case the loader was reset. If our results
were a Cursor
, a Bitmap
, or other objects with clear “close” or
“release” semantics, you might do that work if onReset()
returned true
.
You also need to implement three additional methods.
First is onStartLoading()
. Here is where we use the cached result,
delivering it via deliverResults()
. If we do not have a cached result,
we need to call forceLoad()
to trigger the AsyncTask
which, in turn,
triggers loadInBackground()
and the rest of the work to actually
retrieve the results:
@Override
protected void onStartLoading() {
super.onStartLoading();
if (lastResult!=null) {
deliverResult(lastResult);
}
else {
forceLoad();
}
}
There is a corresponding onStopLoading()
, where we need to call
cancelLoad()
, to cancel the AsyncTask
:
@Override
protected void onStopLoading() {
super.onStopLoading();
cancelLoad();
}
Finally, there is onReset()
, where we need to call onStopLoading()
(as loading should stop if the Loader
is reset), plus do any cleanup
of our cached results as needed:
@Override
protected void onReset() {
super.onReset();
onStopLoading();
// plus any actual cleanup
}
You might think that with 70-odd lines of QuestionsLoader
code that
we would have a corresponding savings in QuestionsFragment
.
Alas, no, though it is a bit shorter.
QuestionsFragment
now implements LoaderCallbacks
instead of
Retrofit’s Callback
interface:
public class QuestionsFragment extends ListFragment implements
LoaderManager.LoaderCallbacks<SOQuestions> {
In the original QuestionsFragment
, we fired off the asynchronous
Retrofit work in onCreateView()
, and we processed the results in the
success()
and failure()
methods.
Now, we just need to call initLoader()
, in this case from onViewCreated()
:
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getLoaderManager().initLoader(0, null, this);
}
Then, our LoaderCallbacks
methods create the QuestionsLoader
and
apply the results:
@Override
public Loader<SOQuestions> onCreateLoader(int id, Bundle args) {
return(new QuestionsLoader(getActivity()));
}
@Override
public void onLoadFinished(Loader<SOQuestions> loader,
SOQuestions data) {
setListAdapter(new ItemsAdapter(data.items));
}
@Override
public void onLoaderReset(Loader<SOQuestions> loader) {
setListAdapter(null);
}
As noted earlier in this chapter, there are three key pieces of the
Loader
contract: asynchronicity, data retention, and automatic
delivery of updates on content changes.
So, how does QuestionsLoader
stack up?
AsyncTaskLoader
provides the asynchronous operation for us, so we
inherit thatLoaderManager
, so there
is nothing specific that we need to do for thatQuestionsLoader
This is a common gap with loader implementations. Occasionally, we may
be in position to find out when the content changes. More often, we
are not, or the work to find out about content changes has to be handled
by a much larger subsystem, beyond a simple Loader
subclass.
For example, if we really wanted to have QuestionsLoader
automatically
deliver a fresh SOQuestions
object when new Stack Overflow android
questions were asked, we could:
use some sort of timing mechanism (e.g., ScheduledExecutorService
)
to poll the Stack Exchange API every so often. However, then we would
need to implement some sort of “diff” algorithm to determine if relevant
data changed in the JSON response, so we knew to deliver a fresh
SOQuestions
to clients. However, our polling period would be somewhat
arbitrary. Frequent polling would consume a fair amount of battery and
bandwidth as well.