android loaders : reloaded

57
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

Upload: cbeyls

Post on 06-May-2015

12.045 views

Category:

Technology


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

Page 1: Android Loaders : Reloaded

Android LoadersR E L O A D E D

Christophe Beyls

Brussels GTUG13th march 2013 READY.

LOAD "*",8,1

SEARCHING FOR *LOADINGREADY.RUN▀

Page 2: Android Loaders : Reloaded

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

Page 3: Android Loaders : Reloaded

About the speaker

Page 4: Android Loaders : Reloaded

(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

Page 5: Android Loaders : Reloaded

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

Page 6: Android Loaders : Reloaded

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.

Page 7: Android Loaders : Reloaded

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.

Page 8: Android Loaders : Reloaded

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

}

Page 9: Android Loaders : Reloaded

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.

Page 10: Android Loaders : Reloaded

A bit of History

2. AsyncTaskA less known but big problem.

Demo

Page 11: Android Loaders : Reloaded

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.

Page 12: Android Loaders : Reloaded

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.

Page 13: Android Loaders : Reloaded

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

Page 14: Android Loaders : Reloaded

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.

Page 15: Android Loaders : Reloaded

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)

Page 16: Android Loaders : Reloaded

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.

Page 17: Android Loaders : Reloaded

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

}

Page 18: Android Loaders : Reloaded

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.

Page 19: Android Loaders : Reloaded

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

Page 20: Android Loaders : Reloaded

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"));}

Page 21: Android Loaders : Reloaded

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

Page 22: Android Loaders : Reloaded

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

Page 23: Android Loaders : Reloaded

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.

Page 24: Android Loaders : Reloaded

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

}

Page 25: Android Loaders : Reloaded

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.

Page 26: Android Loaders : Reloaded

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.

Page 27: Android Loaders : Reloaded

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.

Page 28: Android Loaders : Reloaded

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

Page 29: Android Loaders : Reloaded

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

}

Page 30: Android Loaders : Reloaded

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;

}

Page 31: Android Loaders : Reloaded

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.

Page 32: Android Loaders : Reloaded

AsyncTaskLoader

Does it suffer from AsyncTask's limitations?

Page 33: Android Loaders : Reloaded

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;

Page 34: Android Loaders : Reloaded

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!

Page 35: Android Loaders : Reloaded

Implementing a basic Loader

We need to extend AsyncTaskLoader and implement its behavior.

Page 36: Android Loaders : Reloaded

Implementing a basic Loader

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

loadInBackground() from AsyncTaskLoader

Optional● deliverResult() [override]

Page 37: Android Loaders : Reloaded

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

}}

Page 38: Android Loaders : Reloaded

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

}}

...

Page 39: Android Loaders : Reloaded

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

}}

}

Page 40: Android Loaders : Reloaded

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

}

Page 41: Android Loaders : Reloaded

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

}}

Page 42: Android Loaders : Reloaded

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

}

...

Page 43: Android Loaders : Reloaded

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

}}

Page 44: Android Loaders : Reloaded

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

Page 45: Android Loaders : Reloaded

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

Page 46: Android Loaders : Reloaded

CursorLoader

Page 47: Android Loaders : Reloaded

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.

Page 48: Android Loaders : Reloaded

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

}

Page 49: Android Loaders : Reloaded

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;

}

...

Page 50: Android Loaders : Reloaded

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

}}

}

Page 51: Android Loaders : Reloaded

Loaders limitations

Page 52: Android Loaders : Reloaded

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

Page 53: Android Loaders : Reloaded

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

}}

Page 54: Android Loaders : Reloaded

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.

Page 55: Android Loaders : Reloaded

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;}

Page 56: Android Loaders : Reloaded

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

}}

Page 57: Android Loaders : Reloaded

The End

We made it!Thank you for watching.

Questions?