Chapter    26

Understanding Loaders

This chapter looks at loading data from data sources through the recommended mechanism of Loaders. The API of Loaders is designed to deal with two issues with loading data by activities and fragments.

The first is the non-deterministic nature of activities where an activity can be hidden partially or fully, restarted due to device rotation, or removed from memory when in background due to low-memory conditions. These events are called activity life cycle events. Any code that retrieves data must work in harmony with the activity life cycle events. Prior to the introduction of Loaders in 3.0 (API 11), this was handled through Managed Cursors. This mechanism is now discontinued in favor of Loaders.

The second issue with loading data in activities and fragments is that data access could take longer on the main thread resulting in application-not-responding (ANR) messages. Loaders solve this by doing the work on a worker thread and providing callbacks to the activities and fragments to respond to the asynchronous nature of data fetch.

Understanding the Architecture of Loaders

Loaders make it easy to asynchronously load data in an activity or a fragment. Multiple loaders, each with its own set of data, can be associated with an activity or a fragment. Loaders also monitor the source of their data and deliver new results when the data content changes. Loaders automatically reconnect to the previously retrieved data structure, like a cursor, when being re-created after a configuration change. As the previous cursor is not destroyed, data is not requeried.

When we talk about loaders in this chapter all aspects of loaders apply to both activities and fragments unless we indicate otherwise from now on.

Every activity uses a single LoaderManager object to manage the loaders associated with that activity. Once a loader is registered with a loader manager, the LoaderManager will facilitate the necessary callbacks to a) create and initialize the Loader, b) read the data when the Loader finishes loading the data, and c) close the resource when the loader is about to be destroyed as the activity is no longer needed. The LoaderManager is hidden from you and you work with it through callbacks and LoaderManager public APIs. The creation of the LoaderManager is controlled by the activity. LoaderManager is almost like an integral part of the activity itself.

It is the responsibility of the registered Loader to work with its data source and also with the LoaderManager to read the data and send the results back to the LoaderManager. The LoaderManager will then invoke the callbacks on the activity that data is ready. The Loader is also responsible for pausing the data access or monitoring data changes or working with the LoaderManager to understand and react to the activity life cycle events.

While you can write a loader from scratch for your specific data needs by extending the loader API, you typically use the Loaders that are already implemented in the SDK. Most loaders extend the AsyncTaskLoader which provides the basic ability to do its work on a worker thread freeing the main thread. When the worker thread returns data, the LoaderManager will invoke the main callbacks to the activity that the data is ready on the main thread.

The most used of these prebuilt loaders is the CursorLoader. With the availability of CursorLoader, using Loaders becomes really, really trivial with a few lines of code. This is because all the details are hidden behind the LoaderManager, Loader, AsyncTaskLoader, and the CursorLoader.

Listing Basic Loader API Classes

Listing 26-1 lists the key classes involved in the Loader API.

Listing 26-1. Android Loader API Key Participating Classes

LoaderManager
LoaderManager.LoaderCallbacks
Loader
AsyncTaskLoader
CursorLoader

The APIs that are most often used are the LoaderManager.LoaderCallbacks and the CursorLoader. However, let us briefly introduce each of these classes.

There is one LoaderManager object per activity. This is the object that defines the protocol of how Loaders should work. So LoaderManager is the orchestrator for the loaders associated with an activity. LoaderManager's interaction with the activity is through the LoaderManager.LoaderCallbacks. These loader callbacks are where you are given the data by the Loader via the LoaderManager and expected to interact with the activity.

The Loader class defines the protocol that must be adhered to if one wants to design their own loader. AsyncTaskLoader is one example where it implements the loader protocol in an asynchronous manner on a worker thread. It is typically the AsyncTaskLoader that is the base class to implement most of your loaders. CursorLoader is an implementation of this AsyncTaskLoader that knows how to load cursors from content providers. If one is implementing their own loader it is important to understand that all interaction with the loader from a LoaderManager happens on the main thread. Even the LoaderManager callbacks that are implemented by the activity take place on the main thread.

Demonstrating the Loaders

We will now show you how to use Loaders by implementing a simple one-page application (Figure 26-1) that loads contacts from the contact provider database on an Android device. This application is typical of how one would develop Android activities. You could even use this sample project as a starter application template.

9781430246800_Fig26-01.jpg

Figure 26-1. Filtered list of contacts loaded through loaders

We want the activity in Figure 26-1 to exhibit the following characteristics: 1) It should display all the contacts on the device; b) It should retrieve data asynchronously; c) While data is being retrieved, the activity should show a progress bar view in place of the list view; d) On retrieving data successfully, the code should replace the progress view with the filled-in list view; e) The activity should provide a search mechanism to filter the necessary contacts; f) When the device is rotated, it should show the contacts again without making a requery to the contacts content provider; g) The code should allow us to see the order of callbacks along with the activity life cycle callbacks.

We will first present the source code for the activity and then explain each section. By the end of the chapter you will have a clear understanding of how Loaders work and how to use them in your code. With that, Listing 26-2 shows the code for the activity of Figure 26-1. Please note that the code in Listing 26-2 relies on a number of resources that are presented here. Some of these string resources you can see in Figure 26-1, but for others and the code that is not included here, please see the downloadable project. As always, the code presented here is sufficient for the topic at hand.

Listing 26-2. An Activity Loading Data with Loaders

public class TestLoadersActivity
extends      MonitoredListActivity //very simple class to log activity callbacks
implements   LoaderManager.LoaderCallbacks<Cursor> //Loader Manager callbacks
             ,OnQueryTextListener //Search text callback to filter contacts
{
   private static final String tag = "TestLoadersActivity";

   //Adapter for displaying the list's data
   //Initialized to null cursor in onCreate and set on the list
   //Use it in later callbacks to swap cursor
   //This is reinitialized to null cursor when rotation occurs
    SimpleCursorAdapter mAdapter;

    //Search filter working with OnQueryTextListener
    String mCurFilter;

    //Contacts columns that we will retrieve
    static final String[] PROJECTION = new String[] {ContactsContract.Data._ID,
            ContactsContract.Data.DISPLAY_NAME};

    //select criteria for the contacts URI
    static final String SELECTION = "((" +
            ContactsContract.Data.DISPLAY_NAME + " NOTNULL) AND (" +
            ContactsContract.Data.DISPLAY_NAME + " != '' ))";

    public TestLoadersActivity()  {
       super(tag);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState)  {
       super.onCreate(savedInstanceState);
       this.setContentView(R.layout.test_loaders_activity_layout);

       //Initialize the adapter
       this.mAdapter = createEmptyAdapter();
       this.setListAdapter(mAdapter);

       //Hide the listview and show the progress bar
       this.showProgressbar();

       //Initialize a loader for an id of 0
       getLoaderManager().initLoader(0, null, this);
    }
    //Create a simple list adapter with a null cursor
    //The good cursor will come later in the loader callback
    private SimpleCursorAdapter createEmptyAdapter() {
       // For the cursor adapter, specify which columns go into which views
        String[] fromColumns = {ContactsContract.Data.DISPLAY_NAME};
        int[] toViews = {android.R.id.text1}; // The TextView in simple_list_item_1
        //Return the cursor
       return new SimpleCursorAdapter(this,
             android.R.layout.simple_list_item_1,
             null, //cursor
             fromColumns,
             toViews);
    }
   //This is a LoaderManager callback. Return a properly constructed CursorLoader
   //This gets called only if the loader does not previously exist.
   //This means this method will not be called on rotation because
   //a previous loader with this ID is already available and initialized.
   //This also gets called when the loader is "restarted" by calling
   //LoaderManager.restartLoader()
   @Override
   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
      Log.d(tag,"onCreateLoader for loader id:" + id);
      Uri baseUri;
      if (mCurFilter != null) {
          baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
                 Uri.encode(mCurFilter));
      } else {
          baseUri = Contacts.CONTENT_URI;
      }
      String[] selectionArgs = null;
      String sortOrder = null;
      return new CursorLoader(this, baseUri,
            PROJECTION, SELECTION, selectionArgs, sortOrder);
   }
   //This is a LoaderManager callback. Use the data here.
   //This gets called when he loader finishes. Called on the main thread.
   //Can be called multiple times as the data changes underneath.
   //Also gets called after rotation with out requerying the cursor.
   @Override
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
       Log.d(tag,"onLoadFinished for loader id:" + loader.getId());
       Log.d(tag,"Number of contacts found:" + cursor.getCount());
        this.hideProgressbar();
        this.mAdapter.swapCursor(cursor);
   }
   //This is a LoaderManager callback. Remove any references to this data.
   //This gets called when the loader is destroyed like when activity is done.
   //FYI - this does NOT get called because of loader "restart"
   //This can be seen as a "destructor" for the loader.
   @Override
   public void onLoaderReset(Loader<Cursor> loader) {
      Log.d(tag,"onLoaderReset for loader id:" + loader.getId());
      this.showProgressbar();
      this.mAdapter.swapCursor(null);
   }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Place an action bar item for searching.
        MenuItem item = menu.add("Search");
        item.setIcon(android.R.drawable.ic_menu_search);
        item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
        SearchView sv = new SearchView(this);
        sv.setOnQueryTextListener(this);
        item.setActionView(sv);
        return true;
    }
    //This is a Searchview callback. Restart the loader.
    //This gets called when user enters new search text.
    //Call LoaderManager.restartLoader to trigger the onCreateLoader
    @Override
    public boolean onQueryTextChange(String newText) {
        // Called when the action bar search text has changed.  Update
        // the search filter, and restart the loader to do a new query
        // with this filter.
        mCurFilter = !TextUtils.isEmpty(newText) ? newText : null;
        Log.d(tag,"Restarting the loader");
        getLoaderManager().restartLoader(0, null, this);
        return true;
    }
    @Override
    public boolean onQueryTextSubmit(String query) {
        return true;
    }
    private void showProgressbar() {
       //show progress bar
       View pbar = this.getProgressbar();
       pbar.setVisibility(View.VISIBLE);
       //hide listview
       this.getListView().setVisibility(View.GONE);
       findViewById(android.R.id.empty).setVisibility(View.GONE);
    }
    private void hideProgressbar()  {
       //show progress bar
       View pbar = this.getProgressbar();
       pbar.setVisibility(View.GONE);
       //hide listview
       this.getListView().setVisibility(View.VISIBLE);
    }
    private View getProgressbar()  {
       return findViewById(R.id.tla_pbar);
    }
}//eof-class

We will explain each section from Listing 26-2 after we show you the supporting layout for the activity code in Listing 26-3. This layout in Listing 26-3 should clarify the view in Figure 26-1 (please note that a number of resources are not included here but are available in the downloadable file at apress.com/9781430246800).

Listing 26-3. A Typical ListActivity Layout for Loaders

<?xml version="1.0" encoding="utf-8"?>
<!--
*********************************************
* /res/layout/test_loaders_activity_layout.xml
* corresponding activity: TestLoadersActicity.java
* prefix: tla_ (Used for prefixing unique identifiers)
*
* Use:
*    Demonstrate loading a cursor using loaders
* Structure:
*    Header message: text view (tla_header)
*    ListView Heading, ListView (fixed)
*    ProgressBar (To show when data is being fetched)
*    Empty View (To show when the list is empty): ProgressBar
*    Footer: text view (tla_footer)
************************************************
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"  android:layout_height="match_parent"
    android:paddingLeft="2dp"  android:paddingRight="2dp">
    <!--  Header and Main documentation text -->
    <TextView android:id="@+id/tla_header"
        android:layout_width="match_parent"  android:layout_height="wrap_content"
        android:background="@drawable/box2"
        android:layout_marginTop="4dp" android:padding="8dp"
        android:text="@string/tla_header"/>
    <!--  Heading for the list view -->
    <TextView android:id="@+id/tla_listview_heading"
        android:layout_width="match_parent"    android:layout_height="wrap_content"
        android:background="@color/gray"
        android:layout_marginTop="4dp"  android:padding="8dp"
        android:textColor="@color/black" style="@android:style/TextAppearance.Medium"
        android:text="List of Contacts"/>
    <!--  ListView used by the ListActivity. Uses a standard id needed by a list view -->
    <!--  Fix the height of the listview in a production setting -->
    <ListView android:id="@android:id/list"
        android:layout_width="match_parent"  android:layout_height="wrap_content"
        android:background="@drawable/box2"
        android:layout_marginTop="4dp" android:layout_marginBottom="4dp"
        android:drawSelectorOnTop="false"/>
    <!--  ProgressBar: To show and hide the progress bar as loaders load data -->
    <ProgressBar android:id="@+id/tla_pbar"
        android:layout_width="match_parent" android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:indeterminate="true"/>
     <!--  Empty List: Uses a standard id needed by a list view -->
     <TextView android:id="@android:id/empty"
        android:layout_width="match_parent" android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_marginTop="4dp"  android:layout_marginBottom="4dp"
        android:padding="8dp"
        android:text="No Contacts to Match the Criteria"/>
     <!--  Footer: Additional documentation text and the footer-->
     <TextView android:id="@+id/tla_footer"
        android:layout_width="match_parent"  android:layout_height="wrap_content"
        android:background="@drawable/box2"  android:padding="8dp"
        android:text="@string/tla_footer"/>
</LinearLayout>

Let’s now turn to understanding the code in Listing 26-2. We will explain this code through a series of steps you would follow to code with loaders. Let’s start with step 1, where the activity needs to be extended to support the LoaderManager callbacks.

Step 1: Preparing the Activity to Load Data

Code necessary to load data using loaders is remarkably small, because most of the work is done by the CursorLoader. The first thing you need to do is to have your activity extend the LoaderManager.LoaderCallbacks<Cursor> and implement the three needed methods: onCreateLoader(), onLoadFinished(), and onLoaderReset(). You can see how in Listing 26-2. By implementing this interface, you have enabled the activity to become a receiver for the LoaderManager events through these three callbacks.

Step 2: Initializing the Loader

Next, you have to tell the activity that you want a Loader object to load the data. This is done by registering and initializing a Loader during the onCreate() method of the activity as shown in Listing 26-4. You can also see this in the onCreate() of Listing 26-3 as well, in context of the overall code.

Listing 26-4. Initializing a Loader

int loaderid = 0; Bundle argBundle = null;
LoaderCallbacks<Cursor> loaderCallbacks = this; //this activity itself
getLoaderManager().initLoader(loaderid, argBundle, loaderCallbacks);

The loaderid argument is a developer-assigned unique number in the context of this activity to uniquely identify this Loader from other Loaders registered with this activity. Note that in the example here, we are using only one Loader.

The second argsBundle argument is used to pass additional arguments to the onCreateLoader() callback if needed. This “bundle of arguments” approach follows the usual pattern of differed factory object construction in many of the managed components in Android. Activities, fragments, and loaders are all examples of this pattern.

The third argument, loaderCallbacks, is a reference to an implementation of the callbacks required by the LoaderManager. In Listings 26-2 and 26-4, the activity itself is playing this role, so we pass this variable referring to the activity as the argument value.

Once the Loader is registered and initialized, the LoaderManager will schedule a call to the onCreateLoader() callback if necessary. If a call was previously made to the onCreateLoader() and a loader object is available corresponding to this loader ID, then the method onCreateLoader() will not be triggered. As stated earlier, the exception is if the developer overrides this behavior by calling LoaderManager.restartLoader(). You will see this call explained later when we talked about providing search-based filtering capabilities to locate a sub-selection of contacts.

Delving into the Structure of ListActivity

The ListActivity in Figure 26-1 is extending a list activity with a content view that is a custom layout through setContentView(). This gives us a lot more flexibility to place other controls in addition to the list view on the activity. For example, we have provided a header view, a footer view, and also a progress bar to show that we are in the process of fetching data. The only constraint placed by a ListActivity is to name a control with the reserved @android:id/listview to identify the list view that the list activity would be using. In addition to the listview ID, we can also provide a view that the list activity uses if the list is empty. This view is identified by the predefined ID @android:id/empty.

Working with Asynchronous Loading of Data

Loaders load data asynchronously. Because of this we have an added responsibility in the Activity.onCreate() to hide the listview and show the progress indicator until the list data is ready. To do this, we have a ProgressBar component in the layout in Listing 26-3. In the Activity.onCreate() method, we set the initial state of the layout so that the list view is hidden and the progress bar is shown. This functionality is coded in the method showProgressbar() in Listing 26-2. In the same Listing 26-2, when the data is ready we call hideProgressbar() to hide the progress bar and show the populated list view or an empty list view if there is no data.

Step 3: Implementing onCreateLoader()

The onCreateLoader() is triggered by the initialization of the loader. You can see the signature and implementation of this method in Listing 26-2. This method constructs a Loader object for the corresponding loader ID that is passed in from the initialization stemming from the call to LoaderManager.initLoader(). This method also receives the argument bundle that is provided during the loader initialization for this loader ID.

This method returns a properly typed (through Java generics) Loader object to the LoaderManager. In our case this type is Loader<Cursor>. The LoaderManager caches the Loader object and will reuse it. This is useful because when the device rotates and the loader is reinitialized due to Activity.onCreate(), LoaderManager recognizes the loader ID and the presence of an existing loader. The LoaderManager then will not trigger a duplicate call to the onCreateLoader(). However, if the activity is to realize that the input data to the loader has changed, the activity code can call the LoaderManager.restartLoader(), which will trigger a call to the onLoaderCreate() again. In that case, the LoaderManager will first destroy the old loader and use the new one returned by the onLoaderCreate(). The LoaderManager does guarantee that the older loader will hang around until the new loader is created and available.

The onCreateLoader() method has full access to the local variables of the activity. So it can use them in any conceivable manner to construct the needed loaders. In case of a CursorLoader this construction is limited to the arguments available to the constructor of the CursorLoader, which is specifically built to allow cursors from an Android content provider.

In our example, we have used the content URIs provided by the contacts content provider. Refer to Chapter 25, on content provider, for how to use content URIs to retrieve cursors from content provider data sources. It is quite simple: just indicate the URI you want to get the data from, supply the filter string as an argument or a path segment on that URI as per the documentation available for the contacts content provider, specify the columns you want, specify the where clause as a string, and construct the CursorLoader.

Step 4: Implementing onLoadFinished()

Once the CursorLoader is returned to the LoaderManager, the CursorLoader will be instructed to start its work on a worker thread and the main thread will go on to the UI chores. At a later point this method onLoadFinished() is called when the data is ready.

This method could be called multiple times. When the data from a content provider changes, as the CursorLoader has registered itself with the data source, it will be alerted. CursorLoader then will trigger the onLoadFinished() again.

In the onLoadFinished() method, all you need to do is to swap the data cursor that is held by the list adapter. The list adapter was initialized originally with a null cursor. Swapping with a populated cursor will show the new data on the list view. As we have hidden the listview in Activity.onCreate(), we need to show the listview and hide the progress bar. Subsequently we can go on swapping the new cursors for old cursors as data changes. The changes will reflect automatically on the list view.

When the device rotates, a couple of things happen. The Activity.onCreate() will be called again. This will set the list cursor to null and also hide the list view. The code in Activity.onCreate() will also initialize the loader again. The LoaderManager is programmed so that this repeat initialization is harmless. The onCreateLoader() will not be called. The Cursor will not be requeried. However, the onLoadFinished() gets called again, which is what we needed to break out of this conundrum of initializing the data to null first and wondering how and when it will be populated if we were not to requery. As the onLoadFinished() gets called again on rotation, we are able to remove the ProgressBar, show the list view, and swap the valid cursor from the null cursor. All works. Yes, it is sneaky and round-about, but it works.

Step 5: Implementing onLoaderReset()

This callback is invoked when a previously registered loader is no longer necessary and hence destroyed. This can happen when an activity is destroyed due to a back button or explicitly instructed to be finished by code. In such cases, this callback allows an opportunity to close resources or references that are no longer needed. However, it is important not to close the cursors as they are managed by the corresponding loaders and will be closed for you by the framework. This might suggest that the LoaderManager.restartLoader() might result in a call to the onLoaderReset() as the arguments to the old loader are no longer valid. But tests show that this is not the case. The method LoaderManager.restartLoader() will not trigger a call to the method onLoaderReset(). The onLoaderReset() method is only called when the loader is actively destroyed by the activity no longer being needed. You can also explicitly instruct the LoaderManager to destroy the loader by calling LoaderManager.destroyLoader(loaderid).

Using Search with Loaders

We will use search in our sample application to demonstrate the dynamic nature of loaders. We have attached a search view to the menu. You can see this in the method onCreateOptionsMenu() in Listing 26-2. Here we have attached a SearchView to the menu and provided the activity as the callback to the SearchView when new text is provided in the SearchView. The SearchView callback is handled in the method onQueryTextchange() of Listing 26-2.

In the onQueryTextChange() method, we take the new search text and set the local variable mCurFilter. We then call LoaderManager.restartLoader() with the same arguments as the LoaderManager.initializeLoader(). This will trigger the onCreateLoader() again, which will then use the mCurFilter to alter the parameters to the CursorLoader resulting in a new cursor. This new cursor will replace the old one in the onLoadFinished() method.

Understanding the Order of LoaderManager Callbacks

Because Android programming is largely event-based, it is important to know the order of event callbacks. To help you understand the timing of the LoaderManager callbacks, we have rigged the sample program with log messages. Here are some results showing the order of callbacks.

Listing 26-5 shows the order of calls when the activity is first created.

Listing 26-5. Loader Callbacks on Activity Creation

Application.onCreate()
Activity.onCreate()
  LoaderManager.LoaderCallbacks.onCreateLoader()
  Activity.onStart()
  Activity.onResume()
  LoaderManager.LoaderCallbacks.onLoadFinished()

When the search view fires a new search criteria through its callback, the order of callbacks is as shown in Listing 26-6.

Listing 26-6. Loader Callbacks on a New Search Criteria triggered by RestartLoader

RestartLoader //log message from onQueryTextChange
LoaderManager.LoaderCallbacks.onCreateLoader()
LoaderManager.LoaderCallbacks.onLoadFinished()
//Notice, no call to onLoaderReset()

Listing 26-7 shows the order of calls on configuration change.

Listing 26-7. Loader Callbacks on a Configuration Change

Application:config changed
Activity: onCreate
  Activity.onStart
  [No call to the onCreateLoader]
  LoaderManager.LoaderCallbacks.onLoadFinished
  [optionally if searchview has text in it]
    SearchView.onQueryChangeText
    RestartLoader //just a log message
    LoaderManager.LoaderCallbacks.onCreateLoader
    LoaderManager.LoaderCallbacks.onLoadFinished

Listing 26-8 shows the order of callbacks on navigating back or navigating Home as those action results in the activity are being destroyed.

Listing 26-8. Loader Callbacks when the Activity is destroyed

ActivityonStop()
Activity.onDestroy()
LoaderManager.LoaderCallbacks.onLoaderReset() //Notice this method is called

Writing Custom Loaders

As you have seen with the CursorLoader, Loaders are specific to their data sources. So you may need to write your own loaders. Very likely you will need to derive from the AsyncTaskLoader and specialize it using the principles and contracts laid out by the Loader protocol. See the SDK documentation for the Loader class to get more details. You can also use the CursorLoader source code as a guide in writing your own loaders. The source code is available online from multiple sources (you can just google it) or as part of the Android source download.

Resources

Here are additional resources for the topics covered in this chapter:

Summary

Loaders are essential to load data from data sources both from a timing perspective and also in terms of the ability to deal with the managed life cycle of activities and fragments. You have seen in this chapter how easy it is to use loaders to load data from content providers. The resulting code is responsive, able to deal with configuration changes, and simple.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.67.16