avatarIan Lake

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

13063

Abstract

the JSON using the library of your choice</span> <span class="hljs-keyword">return</span> data; }</pre></div><div id="bd81"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">deliverResult</span>(<span class="hljs-params">List<<span class="hljs-built_in">String</span>> data</span>) { <span class="hljs-comment">// We’ll save the data for later retrieval</span> mData = data; <span class="hljs-comment">// We can do any pre-processing we want here</span> <span class="hljs-comment">// Just remember this is on the UI thread so nothing lengthy!</span> <span class="hljs-variable language_">super</span>.<span class="hljs-title function_">deliverResult</span>(data); }</pre></div><div id="9077"><pre> <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onReset</span>()</span> { <span class="hljs-comment">// Stop watching for file changes</span> <span class="hljs-keyword">if</span> (mFileObserver != <span class="hljs-literal">null</span>) { mFileObserver.stopWatching(); mFileObserver = <span class="hljs-literal">null</span>; } } }</pre></div><p id="2d3a">So by hooking into the <a href="http://developer.android.com/reference/android/support/v4/content/Loader.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onStartLoading()"><i>onStartLoading()</i></a> callback to start our processing and the final <a href="http://developer.android.com/reference/android/support/v4/content/Loader.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onReset()"><i>onReset()</i></a>, we can stay perfectly in sync with the underlying data. We could have used <a href="http://developer.android.com/reference/android/support/v4/content/Loader.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onStopLoading()"><i>onStopLoading()</i></a> as the ending callback, but <i>onReset()</i> ensures that we have continuous coverage (even mid-configuration change).</p><p id="e31e">You’ll note the usage of <a href="http://developer.android.com/reference/android/support/v4/content/Loader.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#takeContentChanged()"><i>takeContentChanged()</i></a> in <i>onStartLoading()</i> — this is how your <i>Loader</i> knows that something has changed (i.e.,someone called <a href="http://developer.android.com/reference/android/support/v4/content/Loader.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onContentChanged()"><i>onContentChanged()</i></a>) while the <i>Loader</i> was stopped so even if there were cached results, a data load still needs to be done.</p><blockquote id="a207"><p><b>Note</b>: we still deliver the old, cached data before loading the new — make sure that’s the right behavior for your app and change <i>onStartLoading()</i> as necessary. For example, you might check for <i>takeContentChanged()</i> and immediately throw away cached results, rather than redeliver them.</p></blockquote><h1 id="ef30">Working with the rest of your app: the LoaderManager</h1><p id="d333">Of course, even the best Loader would be nothing if it wasn’t connected to something. That connection point for activities and fragments comes in the form of <a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog"><b><i>LoaderManager</i></b></a>. You’ll call <a href="http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog"><i>FragmentActivity</i></a>’s <a href="http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#getSupportLoaderManager()"><i>getSupportLoaderManager()</i></a> or a <a href="http://developer.android.com/reference/android/support/v4/app/Fragment.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog"><i>Fragment</i></a>’s <a href="http://developer.android.com/reference/android/support/v4/app/Fragment.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#getLoaderManager()"><i>getLoaderManager()</i></a> to get your instance.</p><p id="b887">In almost every case, there’s only one method you’ll need to call: <a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#initLoader(int, android.os.Bundle, android.support.v4.app.LoaderManager.LoaderCallbacks<D>)"><b><i>initLoader()</i></b></a><b>. This is generally called in <a href="http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onCreate(android.os.Bundle)"><i>onCreate()</i></a> or <a href="http://developer.android.com/reference/android/support/v4/app/Fragment.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onActivityCreated(android.os.Bundle)"><i>onActivityCreated()</i></a></b> — basically as soon as you know you’ll need to load some data. You’ll pass in a unique id (only within that <i>Activity</i>/<i>Fragment</i> though — not globally unique), pass an optional Bundle, and an instance of <a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog"><i>LoaderCallbacks</i></a>.</p><blockquote id="6ff1"><p>Note: make sure to upgrade to version 24.0.0 or higher of the Android Support Library if you want to call <i>initLoader()</i> within a Fragment’s <i>onCreate()</i> — there were issues in previous versions of the Support Library (and all framework fragments <api 24)="" where="" loaders="" would="" be="" shared="" across="" fragments="" as="" noted="" in="" <a="" href="https://code.google.com/p/android/issues/detail?id=94081&amp;utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">this Lint request and <a href="https://plus.google.com/+JonFHancock/posts/bgXh4XEAeui?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">this related Google+ post</a>.</api></p></blockquote><p id="e902">You might notice there’s a <a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#restartLoader(int, android.os.Bundle, android.support.v4.app.LoaderManager.LoaderCallbacks<D>)"><i>restartLoader()</i></a> method in <i>LoaderManager</i> which gives you the ability to force a reload. In most cases, this shouldn’t be necessary if the Loader is managing its own listeners, but it is useful in cases where you want to pass in a different <i>Bundle</i> — you’ll find your existing <i>Loader</i> is destroyed and a new call to <i>onCreateLoader()</i> is done.</p><p id="bf37">We mentioned using <i>onReset()</i> instead of <i>onStopLoading()</i> for our <i>FileObserver</i> example above — here we can see where this interacts with the normal lifecycle. Just by calling <i>initLoader()</i>, we’ve hooked into the <i>Activity</i>/<i>Fragment</i> lifecycle and <i>onStopLoading()</i> will be called when the corresponding <i>onStop()</i> is called. However, <i>onReset()</i> is only called when you specifically call <a href="http://developer.android.com/reference/android/app/LoaderManager.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#destroyLoader(int)"><i>destroyLoader()</i></a> or automatically when the <i>Activity</i>/<i>Fragment</i> is completely destroyed.</p><h2 id="c8ae">LoaderCallbacks</h2><p id="a696"><i>LoaderCallbacks</i> is where everything <i>actually</i> happens. And by ‘everything’, we mean three callbacks:</p><ul><li><a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onCreateLoader(int,%20android.os.Bundle)"><i>onCreateLoader()</i></a> — here’s where you construct the actual Loader instance</li><li><a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onLoadFinished(android.support.v4.content.Loader<D>, D)"><b><i>onLoadFinished()</i></b></a><b> — this is where the results you deliver appear</b></li><li><a href="http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#onLoaderReset(android.support.v4.content.Loader<D>)"><i>onLoaderReset()</i></a> — your chance to clean up any references to the now reset Loader data</li></ul><p id="0347">So our previous example might look like:</p><div id="585a"><pre><span class="hljs-comment">// A RecyclerView.Adapter which will display the data</span> <span class="hljs-keyword">private</span> MyAdapter mAdapter;</pre></div><div id="5309"><pre><span class="hljs-comment">// Our Callbacks. Could also have the Activity/Fragment implement</span> <span class="hljs-comment">// LoaderManager.LoaderCallbacks<List<String>></span> <span class="hljs-keyword">private</span> LoaderManager.LoaderCallbacks<List<<span class="hljs-keyword">String</span>>> mLoaderCallbacks = <span class="hljs-keyword">new</span> <span class="hljs-type">LoaderManager</span>.LoaderCallbacks<List<<span class="hljs-keyword">String</span>>>() { <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> Loader<List<<span class="hljs-keyword">String</span>>> onCreateLoader( int id, Bundle args) { <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-type">JsonAsyncTaskLoader</span>(MainActivity.<span class="hljs-built_in">this</span>); }</pre></div><div id="a509"><pre> @Override <span class="hljs-keyword"> public</span> void onLoadFinished( Loader<List<String>> loader, List<String> data) { // Display our data, for<span class="hljs-built_in"> instance </span>updating our adapter mAdapter.setData(data); }</pre></div><div id="7f0a"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onLoaderReset</span>(<span class="hljs-params">Loader<List<<span class="hljs-built_in">String</span>>> loader</span>) { <span class="hljs-comment">// Loader reset, throw away our data,</span> <span class="hljs-comment">// unregister any listeners, etc.</span> mAdapter.<span class="hljs-title function_">setData</span>(<span class="hljs-literal">null</span>); <span class="hljs-comment">// Of course, unless you use destroyLoader(),</span> <span class="hljs-comment">// this is called when everything is already dying</span> <span class="hljs-comment">// so a completely empty onLoaderReset() is</span> <span class="hljs-comment">// totally acceptable</span> } };</pre></div><div id="3074"><pre><span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onCreate</span>(<span class="hljs-params">Bundle savedInstanceState</span>) { <span class="hljs-variable language_">super</span>.<span class="hljs-title function_">onCreate</span>(savedInstanceState); <span class="hljs-comment">// The usual onCreate() — setContentView(), etc.</span></pre></div><div id="f605"><pre> <span class="hljs-built_in">getSupportLoaderManager</span>()<span class="hljs-selector-class">.initLoader</span>(<span class="hljs-number">0</span>, null, mLoaderCallbacks); }</pre></div><p id="73d0">Of course, there’s no hard requirement to use <i>LoaderManager</i>, although you’ll find life much easier if you do. Feel free to look at the <a href="https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/app/FragmentActivity.java?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog"><i>FragmentActivity</i> source</a> and the <a href="https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/app/LoaderManager.java?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&am

Options

p;utm_medium=blog"><i>LoaderManager</i> source</a> for a detailed look into everything it is giving you.</p><h1 id="85b1">Cool, but I don’t need a background thread</h1><p id="f7ea"><i>AsyncTaskLoader</i> tries to make it easy to get off the background thread, but if you’ve already done your own background threading or rely on event bus / subscription models, <i>AsyncTaskLoader</i> is overkill. Let’s take an example of loading location changes without throwing all that code into your <i>Activity</i>/<i>Fragment</i>:</p><div id="46c7"><pre>public static <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LocationLoader</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Loader<Location></span></span> implements <span class="hljs-type">GoogleApiClient</span>.<span class="hljs-type">ConnectionCallbacks</span>, <span class="hljs-type">GoogleApiClient</span>.<span class="hljs-type">OnConnectionFailedListener</span>, <span class="hljs-type">LocationListener</span> {</pre></div><div id="2289"><pre> private GoogleApiClient mGoogleApiClient<span class="hljs-comment">;</span> private Location mLastLocation<span class="hljs-comment">;</span> private ConnectionResult mConnectionResult<span class="hljs-comment">;</span></pre></div><div id="70d2"><pre> public <span class="hljs-built_in">LocationLoader</span>(Context context) { super(context); }</pre></div><div id="ca2f"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onStartLoading</span>(<span class="hljs-params"></span>) { <span class="hljs-keyword">if</span> (mLastLocation != <span class="hljs-literal">null</span>) { <span class="hljs-title function_">deliverResult</span>(mLastLocation); }</pre></div><div id="d5a3"><pre> <span class="hljs-keyword">if</span> (mGoogleApiClient == <span class="hljs-literal">null</span>) { mGoogleApiClient = <span class="hljs-keyword">new</span> <span class="hljs-type">GoogleApiClient</span>.Builder(getContext(), <span class="hljs-built_in">this</span>, <span class="hljs-built_in">this</span>) .addApi(LocationServices.API) .build(); mGoogleApiClient.connect(); } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (mGoogleApiClient.isConnected()) { <span class="hljs-comment">// Request updates</span> LocationServices.FusedLocationApi.requestLocationUpdates( mGoogleApiClient, <span class="hljs-keyword">new</span> <span class="hljs-type">LocationRequest</span>(), <span class="hljs-built_in">this</span>); } }</pre></div><div id="5658"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onStopLoading</span>(<span class="hljs-params"></span>) { <span class="hljs-comment">// Reduce battery usage when the activity is stopped</span> <span class="hljs-comment">// This helps us handle if the home button is pressed</span> <span class="hljs-comment">// And the loader is stopped but not yet destroyed </span> <span class="hljs-keyword">if</span> (mGoogleApiClient.<span class="hljs-title function_">isConnected</span>()) { <span class="hljs-title class_">LocationServices</span>.<span class="hljs-property">FusedLocationApi</span>.<span class="hljs-title function_">requestLocationUpdates</span>( mGoogleApiClient, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocationRequest</span>() .<span class="hljs-title function_">setPriority</span>(<span class="hljs-title class_">LocationRequest</span>.<span class="hljs-property">PRIORITY_NO_POWER</span>), <span class="hljs-variable language_">this</span>); } }</pre></div><div id="c58a"><pre> <span class="hljs-keyword">@Override</span> protected void onForceLoad() { <span class="hljs-comment">// Resend the last known location if we have one</span> if (mLastLocation != null) { <span class="hljs-built_in">deliverResult</span>(mLastLocation); } <span class="hljs-comment">// Try to reconnect if we aren’t connected</span> if (!mGoogleApiClient.isConnected()) { mGoogleApiClient<span class="hljs-selector-class">.connect</span>(); } }</pre></div><div id="d316"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onConnected</span>(<span class="hljs-params">Bundle connectionHint</span>) { mConnectionResult = <span class="hljs-literal">null</span>; <span class="hljs-comment">// Try to immediately return a result</span> mLastLocation = <span class="hljs-title class_">LocationServices</span>.<span class="hljs-property">FusedLocationApi</span> .<span class="hljs-title function_">getLastLocation</span>(mGoogleApiClient); <span class="hljs-keyword">if</span> (mLastLocation != <span class="hljs-literal">null</span>) { <span class="hljs-title function_">deliverResult</span>(mLastLocation); } <span class="hljs-comment">// Request updates</span> <span class="hljs-title class_">LocationServices</span>.<span class="hljs-property">FusedLocationApi</span>.<span class="hljs-title function_">requestLocationUpdates</span>( mGoogleApiClient, <span class="hljs-keyword">new</span> <span class="hljs-title class_">LocationRequest</span>(), <span class="hljs-variable language_">this</span>); }</pre></div><div id="90db"><pre> @Override <span class="hljs-built_in">public</span> <span class="hljs-type">void</span> onLocationChanged(<span class="hljs-keyword">Location</span> <span class="hljs-keyword">location</span>) { mLastLocation = <span class="hljs-keyword">location</span>; // Deliver the <span class="hljs-keyword">location</span> changes deliverResult(<span class="hljs-keyword">location</span>); }</pre></div><div id="050a"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">onConnectionSuspended</span><span class="hljs-params">(<span class="hljs-keyword">int</span> cause)</span> </span>{ <span class="hljs-comment">// Cry softly, hope it comes back on its own</span> }</pre></div><div id="4c23"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">public</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onConnectionFailed</span>(<span class="hljs-params"> <span class="hljs-meta">@NonNull</span> ConnectionResult connectionResult</span>) { mConnectionResult = connectionResult; <span class="hljs-comment">// Signal that something has gone wrong.</span> <span class="hljs-title function_">deliverResult</span>(<span class="hljs-literal">null</span>); }</pre></div><div id="cc68"><pre> <span class="hljs-comment">/**

  • Retrieve the ConnectionResult associated with a null
  • Location to aid in recovering from connection failures.
  • Call startResolutionForResult() and then restart the
  • loader when the result is returned.
  • @return The last ConnectionResult */</span> <span class="hljs-function"><span class="hljs-keyword">public</span> ConnectionResult <span class="hljs-title">getConnectionResult</span>()</span> { <span class="hljs-keyword">return</span> mConnectionResult; }</pre></div><div id="8569"><pre> <span class="hljs-meta">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-built_in">void</span> <span class="hljs-title function_">onReset</span>(<span class="hljs-params"></span>) { <span class="hljs-title class_">LocationServices</span>.<span class="hljs-property">FusedLocationApi</span> .<span class="hljs-title function_">removeLocationUpdates</span>(mGoogleApiClient, <span class="hljs-variable language_">this</span>); mGoogleApiClient.<span class="hljs-title function_">disconnect</span>(); } }</pre></div><p id="f676">So here we can see the three main components:</p><ul><li><i>onStartLoading()</i> kicks off the subscription process (in this case, by connecting to Google Play services)</li><li><i>onStopLoading()</i> is when we’re put into the background (either temporarily on rotation or when the Home button is pressed and the loader is put into the background) so we reduce battery/processor usage</li><li>As we get results we call <i>deliverResult()</i></li><li>Finally, we disconnect and clean up in <i>onReset()</i></li></ul><p id="4831">Here the Loader framework knew nothing about Google Play services, but <b>we can still encapsulate that logic in one place and rely on a single <i>onLoadFinished()</i></b> with an updated location. This type of encapsulation also helps with switching out location providers — the rest of your code does not care how or where the Location objects come from.</p><blockquote id="fd6e"><p><b>Note</b>: in this case, failures are reported by sending a null <i>Location</i>. This signals the listening <i>Activity</i>/<i>Fragment</i> to call <i>getConnectionResult()</i> and handle the failure. Remember that the <i>onLoadFinished()</i> includes a reference to the <i>Loader</i> so any status you have can be retrieved at that point.</p></blockquote><h1 id="2f5c">Loaders: for data only</h1><p id="ecd7">So a Loader has one goal in life: give you up-to-date information. It does that by surviving device configuration changes and containing its own data observers. This means that the rest of your <i>Activity</i>/<i>Fragment</i> doesn’t need to know those details. (Nor should your <i>Loader</i> know anything about how the data is being used!)</p><p id="9906">If you’ve been using retained Fragments (those that call <a href="http://developer.android.com/reference/android/support/v4/app/Fragment.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog#setRetainInstance(boolean)"><i>setRetainInstance(true)</i></a>) to store data across configuration changes, <b>strongly consider switching from a retained Fragment to a Loader</b>. Retained fragments, while aware of the overall Activity lifecycle, should be viewed as completely independent entities, while Loaders are tied directly into an Activity or Fragment lifecycle (even child fragments!) and therefore much more appropriate for retrieving exactly the data needed for display. Take for example a case where you are dynamically adding or removing a fragment — Loaders allow you to tie the loading process to <i>that</i> lifecycle and still avoid configuration changes destroying the loaded data.</p><p id="59e1">That single focus also means that <b>you can test the loading separately from the UI.</b> The examples here just passed in a Context, but you can certainly pass in any required classes (or mocks thereof!) to ease testing. Being entirely event driven, it is also possible to determine exactly what state the Loader is in at any time as well as expose additional state solely for testing.</p><blockquote id="052e"><p><b>Note</b>: while there’s a <a href="http://developer.android.com/reference/android/test/LoaderTestCase.html?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">LoaderTestCase</a> designed for framework classes, you’ll need to make a Support Library equivalent from the <a href="https://android.googlesource.com/platform/frameworks/base/+/master/test-runner/src/android/test/LoaderTestCase.java?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">LoaderTestCase source code</a> if you want to do something similar with the Support v4 Loader (something <a href="undefined">Nicholas Pike</a> <a href="http://www.npike.net/2016/unit-testing-loaders?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">already has done</a>!). This also gives you a good idea of how to interact with a <i>Loader</i> without a <i>LoaderManager</i>.</p></blockquote><p id="e342">Now, it is important to mention that <b>Loaders are reactive, recipients of data.</b> They’re not responsible for changing the underlying data. But for what they do, they do fill a needed gap of lifecycle aware components that survive configuration changes and get your data to your UI.</p><p id="6d53">#BuildBetterApps</p><p id="105f">Join the discussion on the <a href="https://plus.google.com/u/0/+AndroidDevelopers/posts/96FWwtN2Vhr">Google+ post</a> and follow the <a href="https://plus.google.com/collection/sLR0p?utm_campaign=adp_series_loaders_020216&amp;utm_source=medium&amp;utm_medium=blog">Android Development Patterns Collection</a> for more!</p><figure id="c5e9"><img src="https://cdn-images-1.readmedium.com/v2/resize:fit:800/1*S6K7IYkWhCzkS6YAgxLfXw.png"><figcaption></figcaption></figure></article></body>

Making loading data lifecycle aware

Note: if you’re looking for a modern, flexible solution to this problem that doesn’t rely on Loaders (the chosen solution here), check out the Lifecycle Aware Data Loading with Architecture Components blog post.

Building a dynamic Android app requires dynamic data. But I hope we’ve all moved beyond loading data on the UI thread (#perfmatters or something like that). That discussion can go on for seasons and seasons, but let’s focus in on one case: loading data specifically for display in your Activity or Fragment with Loaders.

Much of the talk on Loaders is around CursorLoader, but Loaders are much more versatile than only working with Cursors.

While Loaders exist as part of the framework on API 11 and higher, they’re also part of the Support v4 Library and bring the latest features (and bugfixes!) to every API 4 and higher devices.

What’s so special about Loaders?

By default, device configuration changes such as rotating your screen involve restarting your whole Activity (one of the many reasons it is so critical not to keep a reference to your Activity or any Views). The best part about Loaders is that Loaders survive configuration changes. That expensive data you just retrieved? Still there for immediate retrieval when the activity comes back up. Data is queued up for delivery so you aren’t going to lose data during device configurations either.

But even better: Loaders don’t stay around forever. They’ll be automatically cleaned up when the requesting Activity or Fragment is permanently destroyed. That means no lingering, unnecessary loads.

These two facts together mean that they perfectly match the lifecycle you actually care about: when you have data to show.

I don’t believe you

Perhaps an example will be enlightening. Let’s say you are converting a regular AsyncTask to the loader equivalent, aptly named AsyncTaskLoader:

public static class JsonAsyncTaskLoader extends
    AsyncTaskLoader<List<String>> {
  // You probably have something more complicated
  // than just a String. Roll with me
  private List<String> mData;
  public JsonAsyncTaskLoader(Context context) {
    super(context);
  }
  @Override
  protected void onStartLoading() {
    if (mData != null) {
      // Use cached data
      deliverResult(mData);
    } else {
      // We have no data, so kick off loading it
      forceLoad();
    }
  }
  @Override
  public List<String> loadInBackground() {
    // This is on a background thread
    // Good to know: the Context returned by getContext()
    // is the application context
    File jsonFile = new File(
      getContext().getFilesDir(), "downloaded.json");
    List<String> data = new ArrayList<>();
    // Parse the JSON using the library of your choice
    // Check isLoadInBackgroundCanceled() to cancel out early
  return data;
}
  @Override
  public void deliverResult(List<String> data) {
    // We’ll save the data for later retrieval
    mData = data;
    // We can do any pre-processing we want here
    // Just remember this is on the UI thread so nothing lengthy!
    super.deliverResult(data);
  }
}

Looks pretty similar to an AsyncTask, but we can now hold onto results in a member variable and immediately return them back after configuration changes by immediately calling deliverResult() in our onStartLoading() method. Note how we don’t call forceLoad() if we have cached data — this is how we save ourselves from constantly reloading the data!

You might have noticed the static keyword when declaring the JsonAsyncTaskLoader. It is incredibly important that your Loader does not contain any reference to any containing Activity or Fragment and that includes the implicit reference created by non-static inner classes. Obviously, if you’re not declaring your Loader as an inner class, you won’t need the static keyword.

Not good enough — what if my data changes?

What the simple example fails to get at is that you aren’t limited to just loading a single time — your Loader is also the perfect place to put in broadcast receivers, a ContentObserver (something CursorLoader does for you), a FileObserver, or a OnSharedPreferenceChangeListener. All of a sudden your Loader can react to changes elsewhere and reload its data. Let’s augment our previous Loader with a FileObserver:

public static class JsonAsyncTaskLoader extends
    AsyncTaskLoader<List<String>> {
  // You probably have something more complicated
  // than just a String. Roll with me
  private List<String> mData;
  private FileObserver mFileObserver;
  public JsonAsyncTaskLoader(Context context) {
    super(context);
  }
  @Override
  protected void onStartLoading() {
    if (mData != null) {
      // Use cached data
      deliverResult(mData);
    }
    if (mFileObserver == null) {
      String path = new File(
          getContext().getFilesDir(), "downloaded.json").getPath();
      mFileObserver = new FileObserver(path) {
          @Override
          public void onEvent(int event, String path) {
            // Notify the loader to reload the data
            onContentChanged();
            // If the loader is started, this will kick off
            // loadInBackground() immediately. Otherwise,
            // the fact that something changed will be cached
            // and can be later retrieved via takeContentChanged()
          }
      };
      mFileObserver.startWatching();
    }
    if (takeContentChanged() || mData == null) {
      // Something has changed or we have no data,
      // so kick off loading it
      forceLoad();
    }
  }
  @Override
  public List<String> loadInBackground() {
    // This is on a background thread
    File jsonFile = new File(
        getContext().getFilesDir(), "downloaded.json");
    List<String> data = new ArrayList<>();
    // Parse the JSON using the library of your choice
    return data;
  }
  @Override
  public void deliverResult(List<String> data) {
    // We’ll save the data for later retrieval
    mData = data;
    // We can do any pre-processing we want here
    // Just remember this is on the UI thread so nothing lengthy!
    super.deliverResult(data);
  }
  protected void onReset() {
    // Stop watching for file changes
    if (mFileObserver != null) {
      mFileObserver.stopWatching();
      mFileObserver = null;
    }
  }
}

So by hooking into the onStartLoading() callback to start our processing and the final onReset(), we can stay perfectly in sync with the underlying data. We could have used onStopLoading() as the ending callback, but onReset() ensures that we have continuous coverage (even mid-configuration change).

You’ll note the usage of takeContentChanged() in onStartLoading() — this is how your Loader knows that something has changed (i.e.,someone called onContentChanged()) while the Loader was stopped so even if there were cached results, a data load still needs to be done.

Note: we still deliver the old, cached data before loading the new — make sure that’s the right behavior for your app and change onStartLoading() as necessary. For example, you might check for takeContentChanged() and immediately throw away cached results, rather than redeliver them.

Working with the rest of your app: the LoaderManager

Of course, even the best Loader would be nothing if it wasn’t connected to something. That connection point for activities and fragments comes in the form of LoaderManager. You’ll call FragmentActivity’s getSupportLoaderManager() or a Fragment’s getLoaderManager() to get your instance.

In almost every case, there’s only one method you’ll need to call: initLoader(). This is generally called in onCreate() or onActivityCreated() — basically as soon as you know you’ll need to load some data. You’ll pass in a unique id (only within that Activity/Fragment though — not globally unique), pass an optional Bundle, and an instance of LoaderCallbacks.

Note: make sure to upgrade to version 24.0.0 or higher of the Android Support Library if you want to call initLoader() within a Fragment’s onCreate() — there were issues in previous versions of the Support Library (and all framework fragments this Lint request and this related Google+ post.

You might notice there’s a restartLoader() method in LoaderManager which gives you the ability to force a reload. In most cases, this shouldn’t be necessary if the Loader is managing its own listeners, but it is useful in cases where you want to pass in a different Bundle — you’ll find your existing Loader is destroyed and a new call to onCreateLoader() is done.

We mentioned using onReset() instead of onStopLoading() for our FileObserver example above — here we can see where this interacts with the normal lifecycle. Just by calling initLoader(), we’ve hooked into the Activity/Fragment lifecycle and onStopLoading() will be called when the corresponding onStop() is called. However, onReset() is only called when you specifically call destroyLoader() or automatically when the Activity/Fragment is completely destroyed.

LoaderCallbacks

LoaderCallbacks is where everything actually happens. And by ‘everything’, we mean three callbacks:

So our previous example might look like:

// A RecyclerView.Adapter which will display the data
private MyAdapter mAdapter;
// Our Callbacks. Could also have the Activity/Fragment implement
// LoaderManager.LoaderCallbacks<List<String>>
private LoaderManager.LoaderCallbacks<List<String>>
    mLoaderCallbacks =
    new LoaderManager.LoaderCallbacks<List<String>>() {
      @Override
      public Loader<List<String>> onCreateLoader(
          int id, Bundle args) {
        return new JsonAsyncTaskLoader(MainActivity.this);
      }
      @Override
      public void onLoadFinished(
          Loader<List<String>> loader, List<String> data) {
        // Display our data, for instance updating our adapter
        mAdapter.setData(data);
      }
      @Override
      public void onLoaderReset(Loader<List<String>> loader) {
        // Loader reset, throw away our data,
        // unregister any listeners, etc.
        mAdapter.setData(null);
        // Of course, unless you use destroyLoader(),
        // this is called when everything is already dying
        // so a completely empty onLoaderReset() is
        // totally acceptable
      }
    };
@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  // The usual onCreate() — setContentView(), etc.
  getSupportLoaderManager().initLoader(0, null, mLoaderCallbacks);
}

Of course, there’s no hard requirement to use LoaderManager, although you’ll find life much easier if you do. Feel free to look at the FragmentActivity source and the LoaderManager source for a detailed look into everything it is giving you.

Cool, but I don’t need a background thread

AsyncTaskLoader tries to make it easy to get off the background thread, but if you’ve already done your own background threading or rely on event bus / subscription models, AsyncTaskLoader is overkill. Let’s take an example of loading location changes without throwing all that code into your Activity/Fragment:

public static class LocationLoader extends Loader<Location>
    implements GoogleApiClient.ConnectionCallbacks,
    GoogleApiClient.OnConnectionFailedListener,
    LocationListener {
  private GoogleApiClient mGoogleApiClient;
  private Location mLastLocation;
  private ConnectionResult mConnectionResult;
  public LocationLoader(Context context) {
    super(context);
  }
  @Override
  protected void onStartLoading() {
    if (mLastLocation != null) {
      deliverResult(mLastLocation);
    }
    if (mGoogleApiClient == null) {
      mGoogleApiClient = 
            new GoogleApiClient.Builder(getContext(), this, this)
            .addApi(LocationServices.API)
            .build();
      mGoogleApiClient.connect();
    } else if (mGoogleApiClient.isConnected()) {
      // Request updates
      LocationServices.FusedLocationApi.requestLocationUpdates(
          mGoogleApiClient, new LocationRequest(), this);
    }
  }
  @Override
  protected void onStopLoading() {
    // Reduce battery usage when the activity is stopped
    // This helps us handle if the home button is pressed
    // And the loader is stopped but not yet destroyed 
    if (mGoogleApiClient.isConnected()) {
      LocationServices.FusedLocationApi.requestLocationUpdates(
        mGoogleApiClient,
        new LocationRequest()
            .setPriority(LocationRequest.PRIORITY_NO_POWER),
        this);
    }
  }
  @Override
  protected void onForceLoad() {
    // Resend the last known location if we have one
    if (mLastLocation != null) {
      deliverResult(mLastLocation);
    }
    // Try to reconnect if we aren’t connected
    if (!mGoogleApiClient.isConnected()) {
      mGoogleApiClient.connect();
    }
  }
  @Override
  public void onConnected(Bundle connectionHint) {
    mConnectionResult = null;
    // Try to immediately return a result
    mLastLocation = LocationServices.FusedLocationApi
        .getLastLocation(mGoogleApiClient);
    if (mLastLocation != null) {
      deliverResult(mLastLocation);
    }
    // Request updates
    LocationServices.FusedLocationApi.requestLocationUpdates(
        mGoogleApiClient, new LocationRequest(), this);
  }
  @Override
  public void onLocationChanged(Location location) {
    mLastLocation = location;
    // Deliver the location changes
    deliverResult(location);
  }
  @Override
  public void onConnectionSuspended(int cause) {
    // Cry softly, hope it comes back on its own
  }
  @Override
  public void onConnectionFailed(
      @NonNull ConnectionResult connectionResult) {
    mConnectionResult = connectionResult;
    // Signal that something has gone wrong.
    deliverResult(null);
  }
  /**
   * Retrieve the ConnectionResult associated with a null 
   * Location to aid in recovering from connection failures.
   * Call startResolutionForResult() and then restart the
   * loader when the result is returned.
   * @return The last ConnectionResult
   */
  public ConnectionResult getConnectionResult() {
    return mConnectionResult;
  }
  @Override
  protected void onReset() {
    LocationServices.FusedLocationApi
        .removeLocationUpdates(mGoogleApiClient, this);
    mGoogleApiClient.disconnect();
  }
}

So here we can see the three main components:

  • onStartLoading() kicks off the subscription process (in this case, by connecting to Google Play services)
  • onStopLoading() is when we’re put into the background (either temporarily on rotation or when the Home button is pressed and the loader is put into the background) so we reduce battery/processor usage
  • As we get results we call deliverResult()
  • Finally, we disconnect and clean up in onReset()

Here the Loader framework knew nothing about Google Play services, but we can still encapsulate that logic in one place and rely on a single onLoadFinished() with an updated location. This type of encapsulation also helps with switching out location providers — the rest of your code does not care how or where the Location objects come from.

Note: in this case, failures are reported by sending a null Location. This signals the listening Activity/Fragment to call getConnectionResult() and handle the failure. Remember that the onLoadFinished() includes a reference to the Loader so any status you have can be retrieved at that point.

Loaders: for data only

So a Loader has one goal in life: give you up-to-date information. It does that by surviving device configuration changes and containing its own data observers. This means that the rest of your Activity/Fragment doesn’t need to know those details. (Nor should your Loader know anything about how the data is being used!)

If you’ve been using retained Fragments (those that call setRetainInstance(true)) to store data across configuration changes, strongly consider switching from a retained Fragment to a Loader. Retained fragments, while aware of the overall Activity lifecycle, should be viewed as completely independent entities, while Loaders are tied directly into an Activity or Fragment lifecycle (even child fragments!) and therefore much more appropriate for retrieving exactly the data needed for display. Take for example a case where you are dynamically adding or removing a fragment — Loaders allow you to tie the loading process to that lifecycle and still avoid configuration changes destroying the loaded data.

That single focus also means that you can test the loading separately from the UI. The examples here just passed in a Context, but you can certainly pass in any required classes (or mocks thereof!) to ease testing. Being entirely event driven, it is also possible to determine exactly what state the Loader is in at any time as well as expose additional state solely for testing.

Note: while there’s a LoaderTestCase designed for framework classes, you’ll need to make a Support Library equivalent from the LoaderTestCase source code if you want to do something similar with the Support v4 Loader (something Nicholas Pike already has done!). This also gives you a good idea of how to interact with a Loader without a LoaderManager.

Now, it is important to mention that Loaders are reactive, recipients of data. They’re not responsible for changing the underlying data. But for what they do, they do fill a needed gap of lifecycle aware components that survive configuration changes and get your data to your UI.

#BuildBetterApps

Join the discussion on the Google+ post and follow the Android Development Patterns Collection for more!

Android App Development
AndroidDev
Buildbetterapps
Recommended from ReadMedium