The document discusses Android Loaders, which provide a way for Activities and Fragments to asynchronously load data from a data source and deliver it back without having to manage threads or handle configuration changes. Loaders allow data to persist across configuration changes like orientation changes. The document covers the history of loading data in Android including threads and AsyncTask, introduces Loaders and the LoaderManager API, discusses implementing basic Loaders including CursorLoaders, and covers common mistakes to avoid.
1. Android Loaders
R E L O A D E D
Christophe Beyls
Brussels GTUG
13th march 2013 READY.
LOAD "*",8,1
SEARCHING FOR *
LOADING
READY.
RUN▀
2. About the speaker
● Developer living in Brussels.
● Likes coding, hacking devices,
travelling, movies, music, (LOL)cats.
● Programs mainly in Java and C#.
● Uses the Android SDK nearly every
day at work.
@BladeCoder
4. (Big) Agenda
● A bit of History: from Threads to Loaders
● Introduction to Loaders
● Using the LoaderManager
● Avoiding common mistakes
● Implementing a basic Loader
● More Loader examples
● Databases and CursorLoaders
● Overcoming Loaders limitations
5. A bit of History
1. Plain Threads
final Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch(msg.what) {
case RESULT_WHAT:
handleResult((Result) msg.obj);
return true;
}
return false;
}
});
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Result result = doStuff();
if (isResumed()) {
handler.sendMessage(handler.obtainMessage(RESULT_WHAT, result));
}
}
});
thread.start();
6. A bit of History
1. Plain Threads
Difficulties:
● Requires you to post the result back on the
main thread;
● Cancellation must be handled manually;
● Want a thread pool?
You need to implement it yourself.
7. A bit of History
2. AsyncTask (Android's SwingWorker)
● Handles thread switching for you : result is
posted to the main thread.
● Manages scheduling for you.
● Handles cancellation: if you call cancel(),
onPostExecute() will not be called.
● Allows to report progress.
8. A bit of History
2. AsyncTask
private class DownloadFilesTask extends AsyncTask<Void, Integer, Result> {
@Override
protected void onPreExecute() {
// Something like showing a progress bar
}
@Override
protected Result doInBackground(Void... params) {
Result result = new Result();
for (int i = 0; i < STEPS; i++) {
result.add(doStuff());
publishProgress(100 * i / STEPS);
}
return result;
}
@Override
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
@Override
protected void onPostExecute(Result result) {
handleResult(result);
}
}
9. A bit of History
2. AsyncTask
Problems:
● You need to keep a reference to each running
AsyncTask to be able to cancel it when your
Activity is destroyed.
● Memory leaks: as long as the AsyncTask runs, it
keeps a reference to its enclosing Activity even if
the Activity has already been destroyed.
● Results arriving after the Activity has been
recreated (orientation change) are lost.
10. A bit of History
2. AsyncTask
A less known but big problem.
Demo
11. A bit of History
2. AsyncTask
AsyncTask scheduling varies between Android versions:
● Before 1.6, they run in sequence on a single thread.
● From 1.6 to 2.3, they run in parallel on a thread pool.
● Since 3.0, back to the old behaviour by default! They
run in sequence, unless you execute them with
executeOnExecutor() with a
ThreadPoolExecutor.
→ No parallelization by default on modern phones.
12. A bit of History
2. AsyncTask
A workaround:
1. public class ConcurrentAsyncTask {
2. public static void execute(AsyncTask as) {
3. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
4. as.execute();
5. } else {
6. as.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
7. }
8. }
9. }
... but you should really use Loaders instead.
13. Loaders to the rescue
● Allows an Activity or Fragment to reconnect to the
same Loader after recreation and retrieve the last
result.
● If the result comes after a Loader has been
disconnected from an Activity/Fragment, it can keep it
in cache to deliver it when reconnected to the recreated
Activity/Fragment.
● A Loader monitors its data source and delivers new
results when the content changes.
● Loaders handle allocation/disallocation of resources
associated with the result (example: Cursors).
14. Loaders to the rescue
If you need to perform any
kind of asynchronous load
in an Activity or Fragment,
you must never use
AsyncTask again.
And don't do like this man
because Loaders are much
more than just
CursorLoaders.
15. Using the LoaderManager
● Simple API to allow Activities and Fragments to
interact with Loaders.
● One instance of LoaderManager for each Activity
and each Fragment. They don't share Loaders.
● Main methods:
○ initLoader(int id, Bundle args,
LoaderCallbacks<D> callbacks)
○ restartLoader(int id, Bundle args,
LoaderCallbacks<D> callbacks)
○ destroyLoader(int id)
○ getLoader(int id)
16. Using the LoaderManager
private final LoaderCallbacks<Result> loaderCallbacks
= new LoaderCallbacks<Result>() {
@Override
public Loader<Result> onCreateLoader(int id, Bundle args) {
return new MyLoader(getActivity(), args.getLong("id"));
}
@Override
public void onLoadFinished(Loader<Result> loader, Result result) {
handleResult(result);
}
@Override
public void onLoaderReset(Loader<Result> loader) {
}
};
Never call a standard Loader method yourself directly on
the Loader. Always use the LoaderManager.
17. Using the LoaderManager
When to init Loaders at Activity/Fragment startup
Activities
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
getSupportLoaderManager().initLoader(LOADER_ID, null, callbacks);
}
Fragments
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...
getLoaderManager().initLoader(LOADER_ID, null, callbacks);
}
18. Loaders lifecycle
A loader has 3 states:
● Started
● Stopped
● Reset
The LoaderManager automatically changes
the state of the Loaders according to the
Activity or Fragment state.
19. Loaders lifecycle
● Activity/Fragment starts
→ Loader starts: onStartLoading()
● Activity becomes invisible or Fragment is detached
→ Loader stops: onStopLoading()
● Activity/Fragment is recreated → no callback.
The LoaderManager will continue to receive the
results and keep them in a local cache.
● Activity/Fragment is destroyed
or restartLoader() is called
or destroyLoader() is called
→ Loader resets: onReset()
20. Passing arguments
Using args Bundle
private void onNewQuery(String query) {
Bundle args = new Bundle();
args.putString("query", query);
getLoaderManager().restartLoader(LOADER_ID, args,
loaderCallbacks);
}
@Override
public Loader<Result> onCreateLoader(int id, Bundle args) {
return new QueryLoader(getActivity(), args.getString("query"));
}
21. Passing arguments
Using args Bundle
● You don't need to use the args param most of the time.
Pass null.
● The loaderCallBacks is part of your Fragment/Activity.
You can access your Fragment/Activity instance
variables too.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.newsId = getArguments().getLong("newsId");
}
...
@Override
public Loader<News> onCreateLoader(int id, Bundle args) {
return new NewsLoader(getActivity(), NewsFragment.this.newsId);
}
22. A LoaderManager bug
When a Fragment is recreated on configuration
change, its LoaderManager calls the
onLoadFinished() callback twice to send back the
last result of the Loader when you call
initLoader() in onActivityCreated().
3 possible workarounds:
1. Don't do anything. If your code permits it.
2. Save the previous result and check if it's different.
3. Call setRetainInstance(true) in onCreate().
23. One-shot Loaders
Sometimes you only want to perform a loader
action once.
Example: submitting a form.
● You call initLoader() in response to an action.
● You need to reconnect to the loader on
orientation change to get the result.
24. One-shot Loaders
In your LoaderCallbacks
@Override
public void onLoadFinished(Loader<Integer> loader, Result result) {
getLoaderManager().destroyLoader(LOADER_ID);
... // Process the result
}
On Activity/Fragment creation
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Reconnect to the loader only if present
if (getLoaderManager().getLoader(LOADER_ID) != null) {
getLoaderManager().initLoader(LOADER_ID, null, this);
}
}
25. Common mistakes
1. Don't go crazy with loader ids
You don't need to:
● Increment the loader id or choose a random loader id
each time you initialize or restart a Loader.
This will prevent your Loaders from being reused and
will create a complete mess!
Use a single unique id for each kind of Loader.
26. Common mistakes
1. Don't go crazy with loader ids
You don't need to:
● Create a loader id constant for each and
every kind of Loader accross your entire app.
Each LoaderManager is independent.
Just create private constants in your Activity
or Fragment for each kind of loader in it.
27. Common mistakes
2. Avoid FragmentManager Exceptions
You can not create a FragmentTransaction
directly in LoaderCallbacks.
This includes any dialog you create as a
DialogFragment.
Solution: Use a Handler to dispatch the
FragmentTransaction.
28. Common mistakes
2. Avoid FragmentManager Exceptions
public class LinesFragment extends ContextMenuSherlockListFragment implements
LoaderCallbacks<List<LineInfo>>, Callback {
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handler = new Handler(this);
adapter = new LinesAdapter(getActivity());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setListAdapter(adapter);
setListShown(false);
getLoaderManager().initLoader(LINES_LOADER_ID, null, this);
}
29. Common mistakes
2. Avoid FragmentManager Exceptions
@Override
public Loader<List<LineInfo>> onCreateLoader(int id, Bundle args) {
return new LinesLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<List<LineInfo>> loader, List<LineInfo> data) {
if (data != null) {
adapter.setLinesList(data);
} else if (isResumed()) {
handler.sendEmptyMessage(LINES_LOADING_ERROR_WHAT);
}
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
30. Common mistakes
2. Avoid FragmentManager Exceptions
@Override
public void onLoaderReset(Loader<List<LineInfo>> loader) {
adapter.setLinesList(null);
}
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case LINES_LOADING_ERROR_WHAT:
MessageDialogFragment
.newInstance(R.string.error_title, R.string.lines_loading_error)
.show(getFragmentManager());
return true;
}
return false;
}
31. Implementing a basic Loader
3 classes provided by the support library:
● Loader
Base abstract class.
● AsyncTaskLoader
Abstract class, extends Loader.
● CursorLoader
Extends AsyncTaskLoader.
Particular implementation dedicated to
querying ContentProviders.
33. AsyncTaskLoader
Does it suffer from AsyncTask's limitations?
No, because it uses ModernAsyncTask
internally, which has the same implementation
on each Android version.
private static final int CORE_POOL_SIZE = 5;
private static final int MAXIMUM_POOL_SIZE = 128;
private static final int KEEP_ALIVE = 1;
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR;
34. Implementing a basic Loader
IMPORTANT: Avoid memory leaks
By design, Loaders only keep a reference to the
Application context so there is no leak:
/**
* Stores away the application context associated with context. Since Loaders can be
* used across multiple activities it's dangerous to store the context directly.
*
* @param context used to retrieve the application context.
*/
public Loader(Context context) {
mContext = context.getApplicationContext();
}
But each of your Loader inner classes must be
declared static or they will keep an implicit
reference to their parent!
35. Implementing a basic Loader
We need to extend AsyncTaskLoader and
implement its behavior.
36. Implementing a basic Loader
The callbacks to implement
Mandatory
● onStartLoading()
● onStopLoading()
● onReset()
● onForceLoad() from Loader OR
loadInBackground() from AsyncTaskLoader
Optional
● deliverResult() [override]
37. Implementing a basic Loader
public abstract class BasicLoader<T> extends AsyncTaskLoader<T> {
public BasicLoader(Context context) {
super(context);
}
@Override
protected void onStartLoading() {
forceLoad(); // Launch the background task
}
@Override
protected void onStopLoading() {
cancelLoad(); // Attempt to cancel the current load task if possible
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
}
}
38. Implementing a basic Loader
public abstract class LocalCacheLoader<T> extends AsyncTaskLoader<T> {
private T mResult;
public AbstractAsyncTaskLoader(Context context) {
super(context);
}
@Override
protected void onStartLoading() {
if (mResult != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
...
39. Implementing a basic Loader
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
protected void onReset() {
super.onReset();
onStopLoading();
mResult = null;
}
@Override
public void deliverResult(T data) {
mResult = data;
if (isStarted()) {
// If the Loader is currently started, we can immediately
// deliver its results.
super.deliverResult(data);
}
}
}
40. Implementing a basic Loader
What about a global cache instead?
public abstract class GlobalCacheLoader<T> extends AsyncTaskLoader<T> {
...
@Override
protected void onStartLoading() {
T cachedResult = getCachedResult();
if (cachedResult != null) {
// If we currently have a result available, deliver it
// immediately.
deliverResult(cachedResult);
}
if (takeContentChanged() || cachedResult == null) {
// If the data has changed since the last time it was loaded
// or is not currently available, start a load.
forceLoad();
}
}
...
protected abstract T getCachedResult();
}
41. Monitoring data
Two Loader methods to help
● onContentChanged()
If the Loader is started: will call forceLoad().
If the Loader is stopped: will set a flag.
● takeContentChanged()
Returns the flag value and clears the flag.
@Override
protected void onStartLoading() {
if (mResult != null) {
deliverResult(mResult);
}
if (takeContentChanged() || mResult == null) {
forceLoad();
}
}
42. AutoRefreshLoader
public abstract class AutoRefreshLoader<T> extends LocalCacheLoader<T> {
private long interval;
private Handler handler;
private final Runnable timeoutRunnable = new Runnable() {
@Override
public void run() {
onContentChanged();
}
};
public AutoRefreshLoader(Context context, long interval) {
super(context);
this.interval = interval;
this.handler = new Handler();
}
...
43. AutoRefreshLoader
...
@Override
protected void onForceLoad() {
super.onForceLoad();
handler.removeCallbacks(timeoutRunnable);
handler.postDelayed(timeoutRunnable, interval);
}
@Override
public void onCanceled(T data) {
super.onCanceled(data);
// Retry a refresh the next time the loader is started
onContentChanged();
}
@Override
protected void onReset() {
super.onReset();
handler.removeCallbacks(timeoutRunnable);
}
}
44. CursorLoader
CursorLoader is a Loader dedicated to querying
ContentProviders
● It returns a database Cursor as result.
● It performs the database query on a background
thread (it inherits from AsyncTaskLoader).
● It replaces Activity.startManagingCursor(Cursor c)
It manages the Cursor lifecycle according to the
Activity Lifecycle. → Never call close()
● It monitors the database and returns a new cursor
when data has changed. → Never call requery()
45. CursorLoader
Usage with a CursorAdapter in a ListFragment
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new BookmarksLoader(getActivity(),
args.getDouble("latitude"), args.getDouble("longitude"));
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.swapCursor(data);
// The list should now be shown.
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
47. SimpleCursorLoader
If you don't need the complexity of a ContentProvider
... but want to access a local database anyway
● SimpleCursorLoader is an abstract class based on
CursorLoader with all the ContentProvider-specific
stuff removed.
● You just need to override one method which
performs the actual database query.
49. SimpleCursorLoader
Example usage - bookmarks
public class DatabaseManager {
private static final Uri URI_BOOKMARKS =
Uri.parse("sqlite://your.package.name/bookmarks");
...
public Cursor getBookmarks(double latitude, double longitude) {
// A big database query you don't want to see
...
cursor.setNotificationUri(context.getContentResolver(), URI_BOOKMARKS);
return cursor;
}
...
50. SimpleCursorLoader
Example usage - bookmarks
...
public boolean addBookmark(Bookmark bookmark) {
SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();
try {
// Other database stuff you don't want to see
...
long result = db.insert(DatabaseHelper.BOOKMARKS_TABLE_NAME, null,
values);
db.setTransactionSuccessful();
// Will return -1 if the bookmark was already present
return result != -1L;
} finally {
db.endTransaction();
context.getContentResolver().notifyChange(URI_BOOKMARKS, null);
}
}
}
52. Loaders limitations
1. No built-in progress updates support
Workaround: use LocalBroadcastManager.
In the Activity:
@Override
protected void onStart() {
// Receive loading status broadcasts in order to update the progress bar
LocalBroadcastManager.getInstance(this).registerReceiver(loadingStatusReceiver,
new IntentFilter(MyLoader.LOADING_ACTION));
super.onStart();
}
@Override
protected void onStop() {
super.onStop();
LocalBroadcastManager.getInstance(this)
.unregisterReceiver(loadingStatusReceiver);
}
53. Loaders limitations
1. No built-in progress updates support
Workaround: use LocalBroadcastManager.
In the Loader:
@Override
public Result loadInBackground() {
// Show progress bar
Intent intent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA, true);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
try {
return doStuff();
} finally {
// Hide progress bar
intent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA, false);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
}
54. Loaders limitations
2. No error handling in LoaderCallbacks.
You usually simply return null in case of error.
Possible Workarounds:
1) Encapsulate the result along with an Exception in a
composite Object like Pair<T, Exception>.
Warning: your Loader's cache must be smarter and
check if the object is null or contains an error.
2) Add a property to your Loader to expose a catched
Exception.
55. Loaders limitations
public abstract class ExceptionSupportLoader<T> extends LocalCacheLoader<T> {
private Exception lastException;
public ExceptionSupportLoader(Context context) {
super(context);
}
public Exception getLastException() {
return lastException;
}
@Override
public T loadInBackground() {
try {
return tryLoadInBackground();
} catch (Exception e) {
this.lastException = e;
return null;
}
}
protected abstract T tryLoadInBackground() throws Exception;
}
56. Loaders limitations
2. No error handling in LoaderCallbacks.
Workaround #2 (end)
Then in your LoaderCallbacks:
@Override
public void onLoadFinished(Loader<Result> loader, Result result) {
if (result == null) {
Exception exception = ((ExceptionSupportLoader<Result>) loader)
.getLastException();
// Error handling
} else {
// Result handling
}
}