This document is a collection of research notes on Android Loaders. These are managed asynchronous objects that help to retrieve data by activities and fragments as these later objects go through their life cycle. You will find here links to key guides on loaders, links to key classes, a key set of questions, answers to some or all of those questions, clarification on the order and timing of callbacks, sample code, and more notes as this documents is maintained as I know more.

satya - 9/20/2014 5:54:43 PM

Here is the developer guide on loaders from android

Here is the developer guide on loaders from android

satya - 9/20/2014 5:57:00 PM

The normal managedquery method on activity is deprecated

The normal managedquery method on activity is deprecated

satya - 9/20/2014 6:22:14 PM

LoaderManager Callbacks

LoaderManager Callbacks

satya - 11/30/2014, 12:22:05 PM

So What are loaders?

loaders make it easy to asynchronously load data in an activity or fragment

satya - 11/30/2014, 12:22:28 PM

Basics of Loaders

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.

satya - 11/30/2014, 12:26:18 PM

Key classes and packages


Loader
  AsyncTaskLoader
     CursorLoader
LoaderManager

satya - 11/30/2014, 12:31:00 PM

Loader

An abstract template-hook-patterned base class whose implemented template methods provide the loader protocol and whose to-be-provided-by-derivation hook methods provide the mechanics of reading data.

There can be multiple loaders per activity or fragment

A class that performs asynchronous loading of data. While Loaders are active they should monitor the source of their data and deliver new results when the contents change. See LoaderManager for more detail.

satya - 11/30/2014, 12:31:35 PM

Thread Restrictions: Loader must be called and interacted with on the main thread

Clients of loaders should as a rule perform any calls on to a Loader from the main thread of their process (that is, the thread the Activity callbacks and other things occur on). Subclasses of Loader (such as AsyncTaskLoader) will often perform their work in a separate thread, but when delivering their results this too should be done on the main thread.

satya - 11/30/2014, 12:31:50 PM

Link: Loader API

Link: Loader API

satya - 11/30/2014, 12:32:22 PM

Hook Methods


onStartLoading(), 
onStopLoading(), 
onForceLoad(), 
onReset()

satya - 11/30/2014, 12:33:11 PM

The favorite descendent: AsyncTaskLoader

Most implementations should not derive directly from this class, but instead inherit from AsyncTaskLoader

satya - 11/30/2014, 12:57:29 PM

Minimum Touch with the loaders

The LoaderManager starts and stops loading when necessary, and maintains the state of the loader and its associated content. As this implies, you rarely interact with loaders directly

satya - 11/30/2014, 12:59:31 PM

Key callbacks in an activity: LoaderManager.LoaderCallbacks


onCreateLoader
onLoadFinished
onLoaderReset

satya - 11/30/2014, 1:06:30 PM

Because onCreateLoader returns a loader...

You can do whatever it takes to create a loader the way you want

No need to pass details of the loader itself to the loader manager

Loader is a self sustaining object

Loader has all it needs to work in union with the LoaderManager and activity state

satya - 11/30/2014, 1:10:01 PM

Example


return new CursorLoader(
  activity,
  contenturi,
  column-projection,
  where-clause,
  where-clause-args,
  sort-order);

satya - 11/30/2014, 1:14:11 PM

onLoadFinished

You get a new cursor

This is not necessarily an old cursor

So you must pass the new cursor to the UI

and abandon, not close, the old cursor

You use the swapCursor method on the data dependent adapters

satya - 11/30/2014, 1:20:29 PM

CursorLoader.java

CursorLoader.java

Search for: CursorLoader.java

satya - 11/30/2014, 1:21:53 PM

Here is the source code for CursorLoader.java to see how it is orchestrated

Here is the source code for CursorLoader.java to see how it is orchestrated

satya - 12/1/2014, 10:53:05 AM

More on onLoadFinished

It can get called multiple times

When data changes Loader calls this method

Loader will not close the old data until this method returns

Avoid closing the previous cursor yourself

Loader will do that

satya - 12/1/2014, 11:00:42 AM

LoaderManager.restartLoader()

You an reinitialize the loader

You do this when the loader parameters have changed

Need a new set of data

Search parameters may have changed

Inputs to the previous loader may have changed

Results in a call to onloadfinished right after (I think)

satya - 12/1/2014, 11:21:40 AM

on onLoaderReset(Loader l)

((I am a bit extrapolating my understanding, but this may be right))

Loader data is no longer valid

Will be closed soon

Indicate to the user data is beign regathered

Set your cursors to null

May be called prior to onLoadFinished to indicate new data set is coming. Not sure if it happens the very first time!

I am not sure of the order yet. I think it will precede the onLoadfinished. I think these will be called like a pair. (invalid, valid)

Probably triggered by Loader.reset()

Docs say LoaderManager calls reset() when destroying a loader. Does that mean as part of restartLoader()? Or is it the same as data changing? Not sure

The Loader should at this point free all of its resources, since it may never be called again;

however, its startLoading() may later be called at which point it must be able to start running again.

satya - 12/1/2014, 11:22:58 AM

is onLoaderReset() triggered by data change?

is onLoaderReset() triggered by data change?

Search for: is onLoaderReset() triggered by data change?

satya - 12/1/2014, 11:25:40 AM

Better way to browse through the CursorLoader.java related source code

Better way to browse through the CursorLoader.java related source code

satya - 12/1/2014, 12:02:51 PM

Timing of Android onLoaderReset()

Timing of Android onLoaderReset()

Search for: Timing of Android onLoaderReset()

satya - 12/1/2014, 1:42:58 PM

Loader object is separate from the Cursor object it carries

Loader object is separate from the Cursor object it carries

satya - 12/1/2014, 1:46:43 PM

Sequence of data change

  1. Content Changes
  2. forceLoad
  3. execute the task to get a new cursor
  4. onLoadFinished called
  5. No change to the loader object
  6. but a change of the cursor object
  7. Old cursor may be closed after returning from the onLoadFinished

satya - 12/1/2014, 1:47:19 PM

So, data change MAY not cause a callback to onLoaderReset()

So, data change MAY not cause a callback to onLoaderReset()

satya - 12/1/2014, 1:53:34 PM

More on onLoaderReset()

It seemed to be triggered by a "destroy" of the loader object reference

destroy can be triggered by restartLoader() call

Hopefully onLoaderReset() will be called on the new one first

Then onLoadFinished() will be called on the new

satya - 12/1/2014, 1:59:33 PM

Order of calls


restartLoader
destroy
   onLoaderReset callback
go get new data
onLoadFinished

satya - 12/1/2014, 2:11:27 PM

Calling restartLoader() will trigger


destroy of old loader
onLoaderReset()
onCreateLoader
onLoadFinished

in that order

satya - 12/1/2014, 2:13:07 PM

Data change WILL NOT trigger the onLoaderReset()

Because it the loader is not destroyed as a consequence, merely a new cursor is delivered as a result and result in calling the onLoadFinished()

satya - 12/1/2014, 2:16:42 PM

If you are a Loader its reset is equivalent to a class destructor

If you are a Loader its reset is equivalent to a class destructor that should release its resources that it may be holding on to.

satya - 12/3/2014, 10:33:19 AM

Loaders is also well documented while covering ListView usage in android docs

Loaders is also well documented while covering ListView usage in android docs

satya - 12/3/2014, 10:33:54 AM

Here are my notes on working with list views, activities, and adapters

Here are my notes on working with list views, activities, and adapters

satya - 12/3/2014, 10:36:17 AM

onCreateLoader will not be called if the loader ID already exists

If you intend to call it again, then you need to restart loader or destroy that loader and restart.

satya - 12/3/2014, 10:37:02 AM

It is important to consider the behavior of ListView on activity flip

It is important to consider the behavior of activity flip

satya - 12/3/2014, 10:39:27 AM

What we would have done with out loaders for a ListView


//First time onCreate
Load the view
Instantiate and get a cursor
Instantiate Get a suitable adapter
Attach the adapter to the listview

//On Flip
Load the view again
[again] Instantiate and get a cursor
[again] Instantiate Get a suitable adapter
[again] Attach the adapter to the listview

//Remembering position
Not sure if listview remembers it
If not you need to explicitly do this

satya - 12/3/2014, 10:40:30 AM

Why not the framework remember the previous adapter?

Because an adapter is a view. This view may be entirely different on a device flip. So it makes sense to expect that the listview be initialized with a new adapter.

satya - 12/3/2014, 10:45:53 AM

Likely pattern to do using Loaders


//First time onCreate
Load the view
Instantiate Get a suitable adapter with a NULL cursor
Attach the adapter to the listview
Initialize a loader with an ID
Create a loader in onCreateLoader callback
Get a cursor in the onLoadFinished callback
   Set the adapter with this cursor
   attach it to the list view
   Do this repeatedly for every callback
Release resources on onLOaderResert

//On Flip
[again] Load the view
[again] Instantiate Get a suitable adapter with a NULL cursor
[again] Attach the adapter to the listview
[again] Initialize a loader with an ID
[will not be called] Create a loader in onCreateLoader callback
[hopefully will be called] Get a cursor in the onLoadFinished callback
   set the adapter with this cursor
   attach it to the list view
   Do this repeatedly for every callback
Release resources on onLOaderResert

satya - 12/3/2014, 10:47:42 AM

Suspicion, Assumption (To be verified)

That on a flip, onLoadFinished() gets called while omitting the onCreateLoader() and that the cursor will retain its old data and not make a new call.

satya - 12/3/2014, 4:08:36 PM

Given these here is how a list activity can be coded with loaders


public class TestLoadersActivity 
extends MonitoredListActivity
implements LoaderManager.LoaderCallbacks<Cursor> 
{
   private static final String tag = "TestLoadersActivity";
   
    // This is the Adapter being used to display the list's data
   //Initialized in onCreate and set on the list
   //You can use it later to swap cursors on this adapter
   //You get a new one when rotation occurs
    SimpleCursorAdapter mAdapter;
   
    // These are the Contacts rows that we will retrieve
    static final String[] PROJECTION = new String[] {ContactsContract.Data._ID,
            ContactsContract.Data.DISPLAY_NAME};

    // This is the select criteria
    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);
       
       this.mAdapter = createEmptyAdapter();
       this.setListAdapter(mAdapter);
       
       //Initialzie a loader for an id of 0
        getLoaderManager().initLoader(0, null, this);
    }
    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 new SimpleCursorAdapter(this, 
             android.R.layout.simple_list_item_1,
             null, //curosr 
             fromColumns, 
             toViews);
    }
    private ListAdapter getCurrentAdapter() {
       return this.getListAdapter();
    }

   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
      Log.d(tag,"onCreateLoader for loader id:" + id);
        return new CursorLoader(this, ContactsContract.Data.CONTENT_URI,
                PROJECTION, SELECTION, null, null);
   }
   public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
       Log.d(tag,"onLoadFinished for loader id:" + loader.getId());
        this.mAdapter.swapCursor(cursor);
   }
   public void onLoaderReset(Loader<Cursor> loader) {
      Log.d(tag,"onLoadFinished for loader id:" + loader.getId());
      this.mAdapter.swapCursor(null);
   }   
}//eof-class

satya - 12/5/2014, 1:35:06 PM

Here is a more fully functional version of this activity


public class TestLoadersActivity 
extends MonitoredListActivity
implements LoaderManager.LoaderCallbacks<Cursor>, OnQueryTextListener 
{
   private static final String tag = "TestLoadersActivity";
   
    // This is the Adapter being used to display the list's data
   //Initialized in onCreate and set on the list
   //You can use it later to swap cursors on this adapter
   //You get a new one when rotation occurs
    SimpleCursorAdapter mAdapter;
    
    //Search filter
    String mCurFilter;    
   
    // These are the Contacts rows that we will retrieve
    static final String[] PROJECTION = new String[] {ContactsContract.Data._ID,
            ContactsContract.Data.DISPLAY_NAME};

    // This is the select criteria
    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);
       
       this.mAdapter = createEmptyAdapter();
       this.setListAdapter(mAdapter);

       this.showProgressbar();
       //Initialzie a loader for an id of 0
        getLoaderManager().initLoader(0, null, this);
    }
    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 new SimpleCursorAdapter(this, 
             android.R.layout.simple_list_item_1,
             null, //curosr 
             fromColumns, 
             toViews);
    }
    private ListAdapter getCurrentAdapter() {
       return this.getListAdapter();
    }
   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
      Log.d(tag,"onCreateLoader for loader id:" + id);
      Uri baseUri;
      if (mCurFilter != null) {
          baseUri = Uri.withAppendedPath(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);
   }
   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);
   }
   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;
    }
    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

satya - 12/5/2014, 1:35:28 PM

Here is the corresponding layout file


<?xml version="1.0" encoding="utf-8"?>
<!-- 
*********************************************
* 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 (fixed)
*    Footer: text view (tla_footer)
*    Empty View (To show when the list is empty): ProgressBar
************************************************
--> 

<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="8dp"
         android:paddingRight="8dp">

     <TextView android:id="@+id/tla_header"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:text="Sample Message"/>
     
     <!--  Uses a standard id needed by a list view -->
     <ListView android:id="@android:id/list"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:drawSelectorOnTop="false"/>

     <ProgressBar android:id="@+id/tla_pbar"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:layout_gravity="center"
               android:indeterminate="true"/>
     
     <!--  Uses a standard id needed by a list view -->
     <!--  This can be a progress bar view -->
     <TextView android:id="@android:id/empty"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:visibility="gone"
               android:text="No Contacts to Match the Criteria"/>
     
     <TextView android:id="@+id/tla_footer"
               android:layout_width="match_parent"
               android:layout_height="wrap_content"
               android:text="No data, Footer"/>
     </LinearLayout>

satya - 12/5/2014, 1:36:45 PM

The order of calls when the activity is first created


Application:onCreate
Activity: onCreate
  onCreateLoader
  onStart
  onResume
  onLoadFinished

satya - 12/5/2014, 1:37:49 PM

When the search view fires a new search criteria through its callback


RestartLoader
onCreateLoader
onLoadFinished

satya - 12/5/2014, 1:38:05 PM

Importantly no onLoaderReset!! keep that in mind

Importantly no onLoaderReset!! keep that in mind

satya - 12/5/2014, 1:40:20 PM

on Config Change


Application:config changed
Activity: onCreate
  onStart
  [No call to the onCreateLoader]
  onLoadFinished
  [optionally if searchview has text in it]
    onQueryChangeText
    RestartLoader
    onCreateLoader
    onLoadFinished

satya - 12/5/2014, 1:40:32 PM

Note onLoaderReset not called!!

Note onLoaderReset not called!!

satya - 12/5/2014, 1:41:01 PM

On Back or home as Activity is destroyed


onStop
onDestroy
onLoaderReset

satya - 12/5/2014, 1:41:21 PM

Notice that the onLoaderReset is truly a destructor of the corresponding high level loader object.

Notice that the onLoaderReset is truly a destructor of the corresponding high level loader object.

satya - 12/6/2014, 1:19:30 PM

The activity will look like this

satya - 12/6/2014, 1:24:53 PM

Here is a more complete layout that matches this view


<?xml version="1.0" encoding="utf-8"?>
<!-- 
*********************************************
* 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 (fixed)
*    Footer: text view (tla_footer)
*    Empty View (To show when the list is empty): ProgressBar
************************************************
--> 

<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"/>

    <!--  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"/>
     
     <!--  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"/>
     
     <!--  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>

satya - 12/14/2014, 9:12:49 AM

What about accessing SQLite directly through Loaders?

What about accessing SQLite directly through Loaders? The CursorLoader allows only content provider URIs to access data. How about accessing local SQLite directly? I have a feeling you have to write your derived Loader to do this!!. Need some research.

satya - 12/14/2014, 9:13:29 AM

In Android Using Loaders to load data from SQLite

In Android Using Loaders to load data from SQLite

Search for: In Android Using Loaders to load data from SQLite