Android Loaders : Reloaded

Post on 06-May-2015

12,037 views 5 download

description

All you ever wanted to know about Android Loaders and never dared to ask. Important: I no longer recommend to use a Loader for "one-shot" actions because it's complicated and has a few side-effects. So I recommend to still use AsyncTasks in that case. You can create an AsyncTask inside a Fragment with setRetainInstance(true) to keep the same AsyncTask instance accross configuration changes, but beware not to update the view or interact with the Activity if the result arrives while the fragment is stopped. If you don't need the result, a static AsyncTask will do the job.

transcript

Android LoadersR E L O A D E D

Christophe Beyls

Brussels GTUG13th march 2013 READY.

LOAD "*",8,1

SEARCHING FOR *LOADINGREADY.RUN▀

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

About the speaker

(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

A bit of History

1. Plain Threadsfinal Handler handler = new Handler(new Handler.Callback() {

@Overridepublic boolean handleMessage(Message msg) {

switch(msg.what) {case RESULT_WHAT:

handleResult((Result) msg.obj);return true;

}return false;

}});

Thread thread = new Thread(new Runnable() {

@Overridepublic void run() {

Result result = doStuff();if (isResumed()) {

handler.sendMessage(handler.obtainMessage(RESULT_WHAT, result));}

}});thread.start();

A bit of History

1. Plain ThreadsDifficulties:● 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.

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.

A bit of History

2. AsyncTaskprivate class DownloadFilesTask extends AsyncTask<Void, Integer, Result> {

@Overrideprotected void onPreExecute() {

// Something like showing a progress bar}

@Overrideprotected Result doInBackground(Void... params) {

Result result = new Result();for (int i = 0; i < STEPS; i++) {

result.add(doStuff());publishProgress(100 * i / STEPS);

}return result;

}

@Overrideprotected void onProgressUpdate(Integer... progress) {

setProgressPercent(progress[0]);}

@Overrideprotected void onPostExecute(Result result) {

handleResult(result);}

}

A bit of History2. AsyncTaskProblems:● 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.

A bit of History

2. AsyncTaskA less known but big problem.

Demo

A bit of History2. AsyncTaskAsyncTask 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.

A bit of History2. AsyncTaskA 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.

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).

Loaders to the rescueIf 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.

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)

Using the LoaderManagerprivate final LoaderCallbacks<Result> loaderCallbacks = new LoaderCallbacks<Result>() {

@Overridepublic Loader<Result> onCreateLoader(int id, Bundle args) {

return new MyLoader(getActivity(), args.getLong("id"));}

@Overridepublic void onLoadFinished(Loader<Result> loader, Result result) {

handleResult(result);}

@Overridepublic void onLoaderReset(Loader<Result> loader) {}

};

Never call a standard Loader method yourself directly on the Loader. Always use the LoaderManager.

Using the LoaderManagerWhen to init Loaders at Activity/Fragment startupActivities

@Overrideprotected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);...getSupportLoaderManager().initLoader(LOADER_ID, null, callbacks);

}

Fragments

@Overridepublic void onActivityCreated(Bundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);...getLoaderManager().initLoader(LOADER_ID, null, callbacks);

}

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.

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 destroyedor restartLoader() is calledor destroyLoader() is called→ Loader resets: onReset()

Passing argumentsUsing args Bundle

private void onNewQuery(String query) {Bundle args = new Bundle();args.putString("query", query);getLoaderManager().restartLoader(LOADER_ID, args,

loaderCallbacks);}

@Overridepublic Loader<Result> onCreateLoader(int id, Bundle args) {

return new QueryLoader(getActivity(), args.getString("query"));}

Passing argumentsUsing 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.@Overridepublic void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);this.newsId = getArguments().getLong("newsId");

}

...@Overridepublic Loader<News> onCreateLoader(int id, Bundle args) {

return new NewsLoader(getActivity(), NewsFragment.this.newsId);}

A LoaderManager bugWhen 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().

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.

One-shot LoadersIn your LoaderCallbacks

@Overridepublic void onLoadFinished(Loader<Integer> loader, Result result) {

getLoaderManager().destroyLoader(LOADER_ID);... // Process the result

}

On Activity/Fragment creation@Overridepublic void onActivityCreated(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);...// Reconnect to the loader only if presentif (getLoaderManager().getLoader(LOADER_ID) != null) {

getLoaderManager().initLoader(LOADER_ID, null, this);}

}

Common mistakes

1. Don't go crazy with loader idsYou 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.

Common mistakes

1. Don't go crazy with loader idsYou 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.

Common mistakes

2. Avoid FragmentManager ExceptionsYou 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.

Common mistakes

2. Avoid FragmentManager Exceptionspublic class LinesFragment extends ContextMenuSherlockListFragment implements LoaderCallbacks<List<LineInfo>>, Callback {

...

@Overridepublic void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);handler = new Handler(this);adapter = new LinesAdapter(getActivity());

}

@Overridepublic void onActivityCreated(Bundle savedInstanceState) {

super.onActivityCreated(savedInstanceState);

setListAdapter(adapter);setListShown(false);

getLoaderManager().initLoader(LINES_LOADER_ID, null, this);}

Common mistakes

2. Avoid FragmentManager Exceptions@Overridepublic Loader<List<LineInfo>> onCreateLoader(int id, Bundle args) {

return new LinesLoader(getActivity());}

@Overridepublic 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);}

}

Common mistakes

2. Avoid FragmentManager Exceptions@Overridepublic void onLoaderReset(Loader<List<LineInfo>> loader) {

adapter.setLinesList(null);}

@Overridepublic 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;

}

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.

AsyncTaskLoader

Does it suffer from AsyncTask's limitations?

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;

Implementing a basic Loader

IMPORTANT: Avoid memory leaksBy 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!

Implementing a basic Loader

We need to extend AsyncTaskLoader and implement its behavior.

Implementing a basic Loader

The callbacks to implementMandatory● onStartLoading()● onStopLoading()● onReset()● onForceLoad() from Loader OR

loadInBackground() from AsyncTaskLoader

Optional● deliverResult() [override]

Implementing a basic Loaderpublic abstract class BasicLoader<T> extends AsyncTaskLoader<T> {

public BasicLoader(Context context) {super(context);

}

@Overrideprotected void onStartLoading() {

forceLoad(); // Launch the background task}

@Overrideprotected void onStopLoading() {

cancelLoad(); // Attempt to cancel the current load task if possible}

@Overrideprotected void onReset() {

super.onReset();onStopLoading();

}}

Implementing a basic Loaderpublic abstract class LocalCacheLoader<T> extends AsyncTaskLoader<T> {

private T mResult;

public AbstractAsyncTaskLoader(Context context) {super(context);

}

@Overrideprotected 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();

}}

...

Implementing a basic Loader@Overrideprotected void onStopLoading() {

// Attempt to cancel the current load task if possible.cancelLoad();

}

@Overrideprotected void onReset() {

super.onReset();

onStopLoading();mResult = null;

}

@Overridepublic void deliverResult(T data) {

mResult = data;

if (isStarted()) {// If the Loader is currently started, we can immediately// deliver its results.super.deliverResult(data);

}}

}

Implementing a basic Loader

What about a global cache instead?

public abstract class GlobalCacheLoader<T> extends AsyncTaskLoader<T> {...

@Overrideprotected 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();

}

Monitoring dataTwo 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. @Overrideprotected void onStartLoading() {

if (mResult != null) {deliverResult(mResult);

}

if (takeContentChanged() || mResult == null) {forceLoad();

}}

AutoRefreshLoaderpublic abstract class AutoRefreshLoader<T> extends LocalCacheLoader<T> {

private long interval;private Handler handler;private final Runnable timeoutRunnable = new Runnable() {

@Overridepublic void run() {

onContentChanged();}

};

public AutoRefreshLoader(Context context, long interval) {super(context);this.interval = interval;this.handler = new Handler();

}

...

AutoRefreshLoader...

@Overrideprotected void onForceLoad() {

super.onForceLoad();handler.removeCallbacks(timeoutRunnable);handler.postDelayed(timeoutRunnable, interval);

}

@Overridepublic void onCanceled(T data) {

super.onCanceled(data);// Retry a refresh the next time the loader is startedonContentChanged();

}

@Overrideprotected void onReset() {

super.onReset();handler.removeCallbacks(timeoutRunnable);

}}

CursorLoaderCursorLoader 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()

CursorLoaderUsage with a CursorAdapter in a ListFragment

@Overridepublic Loader<Cursor> onCreateLoader(int id, Bundle args) {

return new BookmarksLoader(getActivity(),args.getDouble("latitude"), args.getDouble("longitude"));

}

@Overridepublic void onLoadFinished(Loader<Cursor> loader, Cursor data) {

adapter.swapCursor(data);

// The list should now be shown.if (isResumed()) {

setListShown(true);} else {

setListShownNoAnimation(true);}

}

@Overridepublic void onLoaderReset(Loader<Cursor> loader) {

adapter.swapCursor(null);}

CursorLoader

SimpleCursorLoaderIf 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.

SimpleCursorLoaderExample usage - bookmarksprivate static class BookmarksLoader extends SimpleCursorLoader {

private double latitude;private double longitude;

public BookmarksLoader(Context context, double latitude, double longitude) {super(context);this.latitude = latitude;this.longitude = longitude;

}

@Overrideprotected Cursor getCursor() {

return DatabaseManager.getInstance().getBookmarks(latitude, longitude);}

}

SimpleCursorLoaderExample usage - bookmarkspublic 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;

}

...

SimpleCursorLoaderExample 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 presentreturn result != -1L;

} finally {db.endTransaction();context.getContentResolver().notifyChange(URI_BOOKMARKS, null);

}}

}

Loaders limitations

Loaders limitations1. No built-in progress updates supportWorkaround: use LocalBroadcastManager.In the Activity:@Overrideprotected void onStart() {

// Receive loading status broadcasts in order to update the progress barLocalBroadcastManager.getInstance(this).registerReceiver(loadingStatusReceiver,

new IntentFilter(MyLoader.LOADING_ACTION));super.onStart();

}

@Overrideprotected void onStop() {

super.onStop();LocalBroadcastManager.getInstance(this)

.unregisterReceiver(loadingStatusReceiver);}

Loaders limitations1. No built-in progress updates supportWorkaround: use LocalBroadcastManager.In the Loader:

@Overridepublic Result loadInBackground() {

// Show progress barIntent intent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA, true);LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);

try {return doStuff();

} finally {// Hide progress barintent = new Intent(LOADING_ACTION).putExtra(LOADING_EXTRA, false);LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);

}}

Loaders limitations2. 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.

Loaders limitationspublic abstract class ExceptionSupportLoader<T> extends LocalCacheLoader<T> {

private Exception lastException;

public ExceptionSupportLoader(Context context) {super(context);

}

public Exception getLastException() {return lastException;

}

@Overridepublic T loadInBackground() {

try {return tryLoadInBackground();

} catch (Exception e) {this.lastException = e;return null;

}}

protected abstract T tryLoadInBackground() throws Exception;}

Loaders limitations2. No error handling in LoaderCallbacks.Workaround #2 (end)Then in your LoaderCallbacks:

@Overridepublic void onLoadFinished(Loader<Result> loader, Result result) {

if (result == null) {Exception exception = ((ExceptionSupportLoader<Result>) loader)

.getLastException();// Error handling

} else {// Result handling

}}

The End

We made it!Thank you for watching.

Questions?