Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView - Truiton
Skip to content

Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView

Android SimpleCursorTreeAdapter

Recently I learned a new concept, Android SimpleCursorTreeAdapters. As its name describes, its from Android SimpleCursorAdaper family. The only difference here is that Android SimpleCursorTreeAdapter is an adapter for ExpandableListView widget. In Android, SimpleCursorTreeAdapters are used in situations where one has to populate an expandable list view through a ContentProvider where the data to be displayed is available. Another interesting concept that is often used with ListViews is Android CursorLoaders. In this Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView Tutorial, I would also demonstrate how to use Android CursorLoader class with ExpandableListView.

While working with ExpandableListView in Android many of you must have faced the problem of showing fresh content on screen. Consider a situation where you populate an ExpandableListView with the help of BaseExpandableListAdapter. In this situation one would have to write some sort of refresh logic, as here old data would be displayed when you navigate to some other screen and return back to the original screen where the list was displayed. Here steps in the Android CursorLoader class. CursorLoaders is one of the essential elements which should be used with ExpandableListView if one wants to display large set of data efficiently and automatically without any refresh logic.

In this Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView tutorial I would be covering two android fundamentals :

  1. Android SimpleCursorTreeAdapter
  2. Android CursorLoader

In this tutorial I would use both SimpleCursorTreeAdapter and CursorLoader with Android Contacts Provider to show android phone book contacts in an ExpandableListView. To start off lets define a layout :

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".TruitonExpandableListActivity" >

    <ExpandableListView
        android:id="@+id/expandableListView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true" >
    </ExpandableListView>

</RelativeLayout>

As you can see the layout for this Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView tutorial is very simple as only an ExpandableListView has to be populated with contacts. Lets move on to main part.

1. Android SimpleCursorTreeAdapter

Android SimpleCursorTreeAdapter needs no introduction since it belongs to the CursorAdapter family of android. The added advantage of this type of adapter is that it can be used with ExpandableListViews and thus even the child elements can be queried by cursor. This gives us the freedom from maintaining states of database cursor. Lets define the SimpleCursorTreeAdapter class:

package com.truiton.simplecursortreeadapter;

import java.util.HashMap;

import android.content.Context;
import android.database.Cursor;
import android.provider.ContactsContract;
import android.content.Loader;
import android.util.Log;
import android.widget.SimpleCursorTreeAdapter;

public class TruitonSimpleCursorTreeAdapter extends SimpleCursorTreeAdapter {

 private final String LOG_TAG = getClass().getSimpleName().toString();
 private TruitonExpandableListActivity mActivity;
 protected final HashMap<Integer, Integer> mGroupMap;

 // Please Note: Here cursor is not provided to avoid querying on main
 // thread.
 public TruitonSimpleCursorTreeAdapter(Context context, int groupLayout,
 int childLayout, String[] groupFrom, int[] groupTo,
 String[] childrenFrom, int[] childrenTo) {

 super(context, null, groupLayout, groupFrom, groupTo, childLayout,
 childrenFrom, childrenTo);
 mActivity = (TruitonExpandableListActivity) context;
 mGroupMap = new HashMap<Integer, Integer>();
 }

 @Override
 protected Cursor getChildrenCursor(Cursor groupCursor) {
 // Logic to get the child cursor on the basis of selected group.
 int groupPos = groupCursor.getPosition();
 int groupId = groupCursor.getInt(groupCursor
 .getColumnIndex(ContactsContract.Contacts._ID));

 Log.d(LOG_TAG, "getChildrenCursor() for groupPos " + groupPos);
 Log.d(LOG_TAG, "getChildrenCursor() for groupId " + groupId);

 mGroupMap.put(groupId, groupPos);
 Loader<Cursor> loader = mActivity.getLoaderManager().getLoader(groupId);
 if (loader != null && !loader.isReset()) {
 mActivity.getLoaderManager()
 .restartLoader(groupId, null, mActivity);
 } else {
 mActivity.getLoaderManager().initLoader(groupId, null, mActivity);
 }

 return null;
 }

 public HashMap<Integer, Integer> getGroupMap() {
 return mGroupMap;
 }

}

Two important points to be noted in the constructor of this adapter are: first, that a cursor is not passed as an argument, as it would start loading on the UI thread, which can make the app slow. Since we are using Android CursorLoader, it will be initialized from there. Second point, that a group map is also initialized here, to keep the group position and ContactsContract.Contacts._ID mapping.

Now the main method which is used in this implementation is the getChildrenCursor callback method. This method is called when user clicks on any group. Therefore here the logic to get child cursor can be applied.

2. Android CursorLoader

Android is full of complex features one of those complex features is Android CursorLoader. CursorLoader is a class that queries the ContentProvider asynchronously in a background thread and delivers the result to the fragment or activity. When implementing the Android CursorLoader with SimpleCursorTreeAdapter do not forget to implement the interface LoaderManager.LoaderCallbacks<Cursor> on the calling activity or fragment. The documentation states that this needs to be implemented on the UI thread.

The minimum requirement to start a loader is to define an id through which a particular instance of Android CursorLoader is identified. Lets define the activity now:

package com.truiton.simplecursortreeadapter;

import java.util.HashMap;

import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.content.CursorLoader;
import android.app.Activity;
import android.content.Loader;
import android.database.Cursor;
import android.util.Log;
import android.view.Menu;
import android.widget.ExpandableListView;
import android.app.LoaderManager;

public class TruitonExpandableListActivity extends Activity implements
 LoaderManager.LoaderCallbacks<Cursor> {

 private final String LOG_TAG = getClass().getSimpleName().toString();

 private static final String[] PHONE_PROJECTION = new String[] {
 ContactsContract.CommonDataKinds.Phone._ID,
 ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
 ContactsContract.CommonDataKinds.Phone.NUMBER,
 ContactsContract.CommonDataKinds.Phone.TYPE };

 private static final String[] CONTACT_PROJECTION = new String[] {
 ContactsContract.Contacts._ID,
 ContactsContract.Contacts.DISPLAY_NAME };

 TruitonSimpleCursorTreeAdapter mAdapter;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);

 ExpandableListView expandableContactListView = (ExpandableListView) findViewById(R.id.expandableListView1);

 mAdapter = new TruitonSimpleCursorTreeAdapter(this,
 android.R.layout.simple_expandable_list_item_1,
 android.R.layout.simple_expandable_list_item_1,
 new String[] { ContactsContract.Contacts.DISPLAY_NAME },
 new int[] { android.R.id.text1 },
 new String[] { ContactsContract.CommonDataKinds.Phone.NUMBER },
 new int[] { android.R.id.text1 });

 expandableContactListView.setAdapter(mAdapter);

 Loader<Cursor> loader = getLoaderManager().getLoader(-1);
 if (loader != null && !loader.isReset()) {
 getLoaderManager().restartLoader(-1, null, this);
 } else {
 getLoaderManager().initLoader(-1, null, this);
 }
 }

 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
 Log.d(LOG_TAG, "onCreateLoader for loader_id " + id);
 CursorLoader cl;
 if (id != -1) {
 // child cursor
 Uri contactsUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
 String selection = "("
 + ContactsContract.CommonDataKinds.Phone.CONTACT_ID
 + " = ? )";
 String sortOrder = ContactsContract.CommonDataKinds.Phone.TYPE
 + " COLLATE LOCALIZED ASC";
 String[] selectionArgs = new String[] { String.valueOf(id) };

 cl = new CursorLoader(this, contactsUri, PHONE_PROJECTION,
 selection, selectionArgs, sortOrder);
 } else {
 // group cursor
 Uri contactsUri = ContactsContract.Contacts.CONTENT_URI;
 String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME
 + " NOTNULL) AND ("
 + ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
 + ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
 String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
 + " COLLATE LOCALIZED ASC";
 cl = new CursorLoader(this, contactsUri, CONTACT_PROJECTION,
 selection, null, sortOrder);
 }

 return cl;
 }

 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
 // Setting the new cursor onLoadFinished. (Old cursor would be closed
 // automatically)
 int id = loader.getId();
 Log.d(LOG_TAG, "onLoadFinished() for loader_id " + id);
 if (id != -1) {
 // child cursor
 if (!data.isClosed()) {
 Log.d(LOG_TAG, "data.getCount() " + data.getCount());

 HashMap<Integer, Integer> groupMap = mAdapter.getGroupMap();
 try {
 int groupPos = groupMap.get(id);
 Log.d(LOG_TAG, "onLoadFinished() for groupPos " + groupPos);
 mAdapter.setChildrenCursor(groupPos, data);
 } catch (NullPointerException e) {
 Log.w(LOG_TAG,
 "Adapter expired, try again on the next query: "
 + e.getMessage());
 }
 }
 } else {
 mAdapter.setGroupCursor(data);
 }

 }

 public void onLoaderReset(Loader<Cursor> loader) {
 // Called just before the cursor is about to be closed.
 int id = loader.getId();
 Log.d(LOG_TAG, "onLoaderReset() for loader_id " + id);
 if (id != -1) {
 // child cursor
 try {
 mAdapter.setChildrenCursor(id, null);
 } catch (NullPointerException e) {
 Log.w(LOG_TAG, "Adapter expired, try again on the next query: "
 + e.getMessage());
 }
 } else {
 mAdapter.setGroupCursor(null);
 }
 }
}

In the above activity I have defined two projections for Android CursorLoader, first PHONE_PROJECTION and second one the CONTACT_PROJECTION.

Please Note: Do not forget to define the _ID in projections as this would result in an “java.lang.IllegalStateException: Couldn’t read row 0, col -1 from CursorWindow.  Make sure the Cursor is initialized correctly before accessing data from it.” exception.

In onCreate Android SimpleCursorTreeAdapter along with CursorLoader is initialized. Once the Android CursorLoader is initialized its callback methods like onCreateLoader(), onLoadFinished(), and onLoaderReset() are called accordingly. The flow is like, in onCreateLoader() method a query to the ContentProvider is made. Then when load is finished onLoadFinished() method is called and the cursor is set in Android SimpleCursorTreeAdapter. When the cursor is about to be closed onLoaderReset() method is called and adapter cursors are nullified as they need to be re initialized. Usually this method is called by android framework when the data has changed. After the implementation it would look something like this:

Android SimpleCursorTreeAdapter
Android SimpleCursorTreeAdapter

After implementing this you would see a screen displaying all the contacts of your device, by clicking on any of the contacts you would be able to view the phone numbers associated with that contact. Now if you press the home button and edit a contact and then open this screen again, you would see the changed data here. The point to be noted here is that this is done through the Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView and not by re-querying the content provider manually in onResume(). Hope this helped, if it did please share this with your friends on Facebook and Google+ also please like our page at Facebook for updates.

7 thoughts on “Android SimpleCursorTreeAdapter with CursorLoader for ExpandableListView”

  1. Hi Mohit,
    This tutorial has been a big help for me.
    Just one question, on line 26 of the adapter, it says
    mActivity = (TruitonExpandableListActivity) context;
    What do you think I should do if I have the adapter initialized from a fragment?

  2. Hi Mohit,

    Thank you very much for this example it’s been really useful for me to understand how ExpandableListView and SimpleCursorTreeAdapter works.

    I have used your example in my pet app but instead of using the contacts local provider I’m using one I have developed that provides an interface to a SQLite database. It woks as follows.

    The UI thread would follow your example to show a ExpandableListView in a Fragment using two given uris (one for groups, another for childs) in the LoaderCallbacks funcions.

    The same UI thread would launch at some point an AsyncTask that in the doInBackground function would query the server and call the insert function of the ContentResolver to modify the underlaying data that is being shown by ExpandableListView. Once the insert has been successful the ContentProvider calls notifyChange with the same uri that was used in the function onCreateLoader. The problem is that the SimpleCursorTreeAdapter seems to be missing the notification and it is not refreshing the data.

    Reading your article I got the impression that I could expect the data shown in the UI to be refreshed automatically so I wonder if that is not the case when the notifyChange is being called from the same application (although different thread). do you know if that is the case? is there anything else I might be missing?

    thanks in advanced
    Javier

  3. @Javier: I had the same problem like you, and I solved it replacing the type of uri my ContentProvider was setting to notify changes through the ContentResolver. To be more specific in the update method of my ContentProvider I replaced the following statement:

    if(rowsUpdated > 0) { getContext().getContentResolver().notifyChange(uri, null);
    }

    with this:

    if(rowsUpdated > 0) { getContext().getContentResolver().notifyChange(BgContract.Measurements.CONTENT_URI, null);
    }

    I hope that will help you!

  4. I am not sure I got it correctly. Should the reset method look like this:

    int groupPos = groupMap.get(id);
    mTreeAdapter.setChildrenCursor(groupPos, null);

  5. Thanks a lot for this.

    I was looking for an example of expandable list views with loaders and yours was simple and easy to implement.

    Saved my day 🙂

  6. Great tutorial for how to use the SimpleCursorTreeAdapter with CursorLoader, it is exactly what I needed!

    I have one thing that I’ve been struggling to find out still though. How would I go about adding a row under each expanded group view that acts as a header and is the first child view in the list. I have 3 textviews that are populated on each child row so I’m looking to add a title such as Cost($), Date, Name to each child.

Leave a Reply

Your email address will not be published. Required fields are marked *