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
satya - 9/20/2014 5:57:00 PM
The normal managedquery method on activity is deprecated
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: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: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?
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()
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
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