Wednesday, April 10, 2013

The Android Loaders Story

Loader story begins with a LoaderManager. A LoaderManager creates and manages the Loaders for our activity and Fragment. An activity or Fragment gets a LoaderManager using getLoaderManager().

A LoaderManager creates a new Loader using getLoaderManager().initLoader( int id, Bundle args, LoaderCallbacks<D> callback).

initLoader() calls the Loader<Cursor> onCreateLoader(int id, Bundle args).  onCreateLoader creates the new Loader if it does not exist already. We provide the information in onCreateLoader() required to load data from any source of data. onCreateLoader() returns the Loader created. As soon as onCreateLoader() create and return a new loader, it starts loading data. 

void onLoadFinished (Loader<D> loader, D data) gets called when previously created loader has finished loading data. onLoadFinished() receives the Loader that is created recently by onCreateLoader() and the Cursor that points to data that is loaded by Loader. Inside onLoadFinished() we provide or attaches the cursor to some adapter and this adapter shows the data in some View.


First of all we should understand that what are Loaders?

Loader

Loader is used to load data asynchronously in an activity and fragment. Important points about Loaders are :
  • They are available to every Activity and Fragment.
  • They provide asynchronous loading of data.
  • They monitor the source of their data and deliver new results when the content changes.
  • They automatically reconnect to the last loader's cursor when being recreated after a configuration change. Thus, they don't need to re-query their data
To use Loaders in our android application Activity or Fragment we should know about classes and interface that are required to create Loaders. These classes and interfaces are:

LoaderManager: An abstract class associated with an Activity or Fragment for managing one or more Loader instances. This helps an application manage longer-running operations in conjunction with the Activity or Fragment lifecycle, the most common use of this is with a CursorLoader, however applications are free to write their own loaders for loading other types of data.
There is only one LoaderManager per activity or fragment. But a LoaderManager can have multiple loaders.

LoaderManager.LoaderCallbacks: A callback interface for a client to interact with the LoaderManager. For example, you use the onCreateLoader()callback method to create a new loader.

Loader: An abstract class that performs asynchronous loading of data. This is the base class for a loader. You would typically use CursorLoader (a Loader that loads/fetch data from a Cursor), but we can implement our own subclass. While loaders are active they should monitor the source of their data and deliver new results when the contents change.

AsyncTaskLoader: Abstract loader that provides an AsyncTask to do the work.

CursorLoader: A subclass of AsyncTaskLoader that queries the ContentResolver and returns a Cursor. This class implements the Loader abstract class or protocol in a standard way for querying cursors, building on AsyncTaskLoader to perform the cursor query on a background thread so that it does not block the application's UI. Using this loader is the best way to asynchronously load data from a ContentProvider, instead of performing a managed query through the fragment or activity's APIs.

I recommend to keep all the above 4 class and 1 interface open in new tabs for quick reference.


Now here we will implement a simple Loader example:

Lets start our example with creating a new Android Project and name it whatever you want and choose a minimum API 14.

Now open the MainActivity.java in your project. We will add a fragment to the Main Activity and will create a Loader in the CursorLoaderListFragment, so MainActivity.java should look like this.

package org.icewheel.loadersexample;

import android.app.Activity;
import android.app.FragmentManager;
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.widget.SimpleCursorAdapter;

public class MainActivity extends Activity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);

     FragmentManager fm = getFragmentManager();

     // Create the list fragment and add it as our sole content.
     if (fm.findFragmentById(android.R.id.content) == null) {
         CursorLoaderListFragment list = new CursorLoaderListFragment();
         fm.beginTransaction().add(android.R.id.content, list).commit();
     }
}


public static class CursorLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks {


}

The above code created a default Activity and defines a Fragment in the local class. In the MainActivity we have a FragmentTransaction which adds the Fragment to activity at run-time.  All the work now will be done in the CursorLoaderListFragment class.

Getting a LoaderManager:

In the above code we defined a fragment which will show the data loaded by a Loader. But before we create a loader. An Activity or a Fragment can have any number of Loaders. So Android provide a way to easily manage all these Loaders. This is done using LoaderManager class.

An activity or fragment which needs the loader must get an instance of LoaderManager instance. The LoaderManager manages one or more Loader instances within an Activity or Fragment. There is only one LoaderManager per activity or fragment.

But the LoaderManager itself is an abstract class and it provides an interface. So any Activity or Fragment that need a LoaderManager instance must implement LoaderManager.LoaderCallbacks<D> interface in which the Loader has to be created. Below are the three methods that have to be implemented.

public Loader onCreateLoader(int id, Bundle args)
public void onLoadFinished(Loader<Cursor> loader, Cursor data)
public void onLoaderReset(Loader<Cursor> loader)

After implementing the LoaderCallback interface. The Fragment class CursorLoaderListFragment class should look like this:

public static class CursorLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks {

  @Override
  public Loader onCreateLoader(int id, Bundle args) {
   // TODO Auto-generated method stub
   return null;
  }

  @Override
  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   // TODO Auto-generated method stub
   
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
   // TODO Auto-generated method stub
   
  }


}

Now we can initialize a Loader within the activity's onCreate() method, or within the fragment's onActivityCreated() method. We do it as follows:

public static class CursorLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks {

    // This is the Adapter being used to display the list's data.
    SimpleCursorAdapter mAdapter;

    // If non-null, this is the current filter the user has provided.
    String mCurFilter;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        // Prepare the loader.  Either re-connect with an existing one,or start a new one.
        //Ensures a loader is initialized and active.
        //initLoader(int id, Bundle args, LoaderCallbacks callback)

        getLoaderManager().initLoader(0, null, this);

        //A unique ID that identifies the loader. In this example, the ID is 0.
        //Optional arguments to supply to the loader at construction (null in this example).
        //A LoaderManager.LoaderCallbacks implementation, which the LoaderManager calls to report loader events. In this example,
        //the local class implements the LoaderManager.LoaderCallbacks interface, so it passes a reference to itself, this.
        //If the loader specified by the ID already exists, the last created loader is reused otherwise it triggers call to onCreateLoader()

    }

  @Override
  public Loader onCreateLoader(int id, Bundle args) {
   // TODO Auto-generated method stub
   return null;
  }

  @Override
  public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   // TODO Auto-generated method stub
   
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
   // TODO Auto-generated method stub
   
  }


}

Creating Loader:

Lets come to the Loader. An Activity or a Fragment can have any number of Loaders. Loader is a abstract class so we can not create new objects directly. See the inheritance hierarchy below.

java.lang.Object

Loader class is inherited by AsyncTaskLoader and AsyncTaskLoader is inherited by CursorLoader. Both Loader class and AsyncTaskLoader class are abstract classes and the CursorLoader provides the default implementation for Loader  We can create our own Loader by inheriting Loader or AsyncTaskLoader classes.

In the above code getLoaderManager.initLoader() triggers call to onCreateLoader(). Inside onCreateLoader() method a new Loader is created which is typically a full implementation of Loader protocol or class. Here in this example an instance of CursorLoader is created and returned by onCreateLoader(). We provide all the information that will be required by the Loader to load data from some source. The full implementation of onCreateLoader() looks like it:

     // These are the Contacts rows that we will retrieve.
     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
         Contacts._ID,
         Contacts.DISPLAY_NAME,
         Contacts.CONTACT_STATUS,
         Contacts.CONTACT_PRESENCE,
         Contacts.PHOTO_ID,
         Contacts.LOOKUP_KEY,
     };

     public Loader onCreateLoader(int id, Bundle args) {
         // This is called when a new Loader needs to be created.  This
         // sample only has one Loader, so we don't care about the ID.
         // First, pick the base URI to use depending on whether we are
         // currently filtering.
         Uri baseUri;
         if (mCurFilter != null) {
             baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                     Uri.encode(mCurFilter));
         } else {
             baseUri = Contacts.CONTENT_URI;
         }

         // Now create and return a CursorLoader that will take care of
         // creating a Cursor for the data being displayed.
         String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))";
         
         //CursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
         return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
     }

onCreateLoader() return a Loader which is CursorLoader here. CursorLoader starts loading data as soon as it is created and onLoadFinished is called when the CursorLoader just created finishes its load. You should see what happen in the background by viewing the CursorLoader class source.

onLoadFinished() gets called when previously created loader has finished loading data. onLoadFinished() receives the Loader that is created recently by onCreateLoader() and the Cursor that points to data that is loaded by Loader. Inside onLoadFinished() we provide or attaches the cursor to some adapter and this adapter shows the data in some View.

     public void onLoadFinished(Loader loader, Cursor data) {
                // void onLoadFinished (Loader<D> loader, D data) gets called when
                //previously created loader has finished loading data.
                //onLoadFinished() receives the Loader that is created recently by
                //  onCreateLoader() and the Cursor that points to data that is
                // loaded by Loader. Inside onLoadFinished() we provide or attaches
                // the cursor to some adapter and this adapter shows the data in
                //some View.

         //Swap the new cursor in.  (The framework will take care of closing
                //the old cursor once we return.)
         mAdapter.swapCursor(data);
     }

We also implement the onLoaderReset() which is called when a previously created loader is being reset and thus making its data unavailable. The application should at this point remove any references it has to the Loader's data.
-->
Finally the whole complete code should look like this:

package org.icewheel.loadersexample;

import android.app.Activity;
import android.app.FragmentManager;
import android.app.ListFragment;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.widget.SimpleCursorAdapter;

public class MainActivity extends Activity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
     super.onCreate(savedInstanceState);

     FragmentManager fm = getFragmentManager();

     // Create the list fragment and add it as our sole content.
     if (fm.findFragmentById(android.R.id.content) == null) {
         CursorLoaderListFragment list = new CursorLoaderListFragment();
         fm.beginTransaction().add(android.R.id.content, list).commit();
     }
}


public static class CursorLoaderListFragment extends ListFragment implements LoaderManager.LoaderCallbacks {

     // This is the Adapter being used to display the list's data.
     SimpleCursorAdapter mAdapter;

     // If non-null, this is the current filter the user has provided.
     String mCurFilter;

     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);

         // Give some text to display if there is no data.  In a real
         // application this would come from a resource.
         setEmptyText("No phone numbers");

         // We have a menu item to show in action bar.
         //setHasOptionsMenu(true);
         String[] from = new String[] { Contacts.DISPLAY_NAME, Contacts.CONTACT_STATUS };
         int[] to = new int[] { android.R.id.text1, android.R.id.text2 };
         
         // Create an empty adapter we will use to display the loaded data.
         //SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to, int flags)
         mAdapter = new SimpleCursorAdapter(getActivity(), android.R.layout.simple_list_item_2, null, from , to , 0);
         setListAdapter(mAdapter);

         // Start out with a progress indicator.
         setListShown(false);

         // Prepare the loader.  Either re-connect with an existing one,
         // or start a new one.
         getLoaderManager().initLoader(0, null, this);
     }

     
     // These are the Contacts rows that we will retrieve.
     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
         Contacts._ID,
         Contacts.DISPLAY_NAME,
         Contacts.CONTACT_STATUS,
         Contacts.CONTACT_PRESENCE,
         Contacts.PHOTO_ID,
         Contacts.LOOKUP_KEY,
     };

     public Loader onCreateLoader(int id, Bundle args) {
         // This is called when a new Loader needs to be created.  This
         // sample only has one Loader, so we don't care about the ID.
         // First, pick the base URI to use depending on whether we are
         // currently filtering.
         Uri baseUri;
         if (mCurFilter != null) {
             baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
                     Uri.encode(mCurFilter));
         } else {
             baseUri = Contacts.CONTENT_URI;
         }

         // Now create and return a CursorLoader that will take care of
         // creating a Cursor for the data being displayed.
         String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND (" + Contacts.HAS_PHONE_NUMBER + "=1) AND (" + Contacts.DISPLAY_NAME + " != '' ))";
         
         //CursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
         return new CursorLoader(getActivity(), baseUri, CONTACTS_SUMMARY_PROJECTION, select, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
     }

     public void onLoadFinished(Loader loader, Cursor data) {
         // Swap the new cursor in.  (The framework will take care of closing the
         // old cursor once we return.)
         mAdapter.swapCursor(data);
     }

     public void onLoaderReset(Loader loader) {
         // This is called when the last Cursor provided to onLoadFinished()
         // above is about to be closed.  We need to make sure we are no
         // longer using it.
         mAdapter.swapCursor(null);
     }
 }

}

You may need an actual Android device to test it.