Adapter / CursorAdapter / CursorLoader / Loader

Adapters – Part 5

Previously in this series we’ve looked at how to use Adapters in various ways, and how to keep the scrolling of our ListViews smooth. In this article, we’ll turn our attention to how to bind a ListView to data which is stored in a SQLite database.

As this series focuses on Adapters, I’m not going to provide a tutorial on SQLite databases and ContentProviders. For some useful information on these topics, please refer to Lars Vogel’s excellent tutorial. Grokking Android’s Wolfram Rittmeyer has also written some useful tutorials on this topic.

For our example, I have set up a simple database and ContentProvider. The database contains two columns: The obligatory ID column, and a column containing arbitrary text. We’ll simply store a set of numbers in text form (i.e. “One”, “Two”, “Three”, etc). To keep things simple, the database has been limited to the numbers 1-20. I won’t include this code in the article, but it’s available in the source if you want to look at it.

Before we dive in to the code, it’s worth looking at a fundamental shift in the recommended usage of CursorAdapters which occurred in API 11 Honeycomb 3.0. Prior to that the accepted usage was to use the FLAG_AUTO_REQUERY flag which would cause the Cursor to be re-retrieved when it detected a change to the database. The only problem with this is that the database queries are performed on the UI thread which can make your UI laggy or, in extreme cases, present the user with the dreaded “Application Not Responding” dialog. Looking at the Javadocs for CursorAdapter the suggestion is to use CursorLoader instead, but doesn’t give much more information. So we’ll implement things using a CursorLoader to see how to do this.

In actual fact, using a CursorLoader is simplicity itself, and it does a surprising amount for us. We used a Loader in the previous article, but this time we’ll implement our LoaderCallback in our ListFragment instead of in our ViewHolder as we don’t need a ViewHolder in this case:

public class CursorAdapterFragment 
	extends ListFragment 
	implements LoaderCallbacks
{
	private static final String[] columns =
	{ 
		CounterHelper.COLUMN_ID, 
		CounterHelper.COLUMN_TEXT 
	};
	
	private static final int[] controlIds =
	{ 
		android.R.id.text1, 
		android.R.id.text2 
	};

	@Override
	public View onCreateView(LayoutInflater inflater, 
		ViewGroup container,
		Bundle savedInstanceState)
	{
		setHasOptionsMenu(true);
		return inflater.inflate(R.layout.listview, 
			container, false);
	}

	@Override
	public void onActivityCreated(Bundle savedInstanceState)
	{
		super.onActivityCreated(savedInstanceState);
		getLoaderManager().restartLoader(0, null, this);
	}

	@Override
	public Loader onCreateLoader(int id, Bundle args)
	{
		Loader loader = null;
		if (id == 0)
		{
			loader = new CursorLoader(getActivity(),
				CounterContentProvider.CONTENT_URI,
				CounterContentProvider.PROJECTION, 
				null, null,
				CounterHelper.COLUMN_ID + " ASC");
		}
		return loader;
	}

	@Override
	public void onLoadFinished(Loader loader, 
		Cursor cursor)
	{
		ListAdapter adapter = getListAdapter();
		if (adapter == null || 
			!(adapter instanceof CursorAdapter))
		{
			adapter = new SimpleCursorAdapter(getActivity(),
				android.R.layout.simple_list_item_2, 
				cursor, columns, controlIds, 0);
			getActivity().invalidateOptionsMenu();
			setListAdapter(adapter);
		}
		else
		{
			((CursorAdapter) adapter).swapCursor(cursor);
		}
	}

	@Override
	public void onLoaderReset(Loader loader)
	{
	}
	@Override
	public void onCreateOptionsMenu(Menu menu, 
		MenuInflater inflater)
	{
		inflater.inflate(R.menu.cursor, menu);
	}

	@Override
	public void onPrepareOptionsMenu(Menu menu)
	{
		MenuItem item = menu.findItem(R.id.add);
		if (item != null)
		{
			item.setEnabled(getListAdapter() != null ? 
				getListAdapter().getCount() < 
				CounterHelper.MAX_ROWS : false);
		}
		super.onPrepareOptionsMenu(menu);
	}

	@Override
	public boolean onOptionsItemSelected(final MenuItem item)
	{
		if (item.getItemId() == R.id.add)
		{
			int count = getListAdapter().getCount();
			if(count < CounterHelper.MAX_ROWS)
			{
				item.setEnabled(false);
				AsyncQueryHandler queryHandler = 
					new AsyncQueryHandler(
						getActivity().getContentResolver())
				{
					@Override
					protected void onInsertComplete(int token, 
							Object cookie,
							Uri uri)
					{
						getActivity().invalidateOptionsMenu();
						super.onInsertComplete(token, 
								cookie, uri);
					}
				};
				ContentValues values = new ContentValues();
				values.put(CounterHelper.COLUMN_TEXT,
						CounterHelper.NUMBERS[count]);
				queryHandler.startInsert(0, null, 
						CounterContentProvider.CONTENT_URI, 
						values);
			}
			else
			{
				getActivity().invalidateOptionsMenu();
			}
		}
		return super.onOptionsItemSelected(item);
	}
}

In our onActivityCreated() method we kick off the Loader and it is in the onCreateLoader() method where we actually create our CursorLoader using some constants defined in the ContentProvider. We're actually using the stock CursorLoader and we don't have to subclass it because it actually does an awful lot for us, as we'll explore in a while.

In our onLoadFinished() method we check whether a CursorAdapter already exists for the ListView and create one, if necessary. The Adapter that we're using is a SimpleCursorAdapter which work in a similar way to the SimpleAdapter that we looked at in part 2 of this series. It allows us to map columns in our database to controls in the layout rather than the Map that we used in SimpleAdapter.

The remainder of the code is just adding a simple menu which adds a new item to the database which allows us to see quite what the relatively simple CursorLoader & Adapter are actually doing for us.

If we run this we'll see an empty list, but if we click the add button a few times, we'll see items automatically appear in the list:

CursorAdapter

None of the code that we've added detects the data changing, and the reason that it's happening is because the CursorLoader does it for us. When we first ran the CursorLoader it registered a ContentObserver on the data so it is automatically run again (on a background thread, of course) whenever the data changes. You can see this happening if you set a breakpoint in the onLoadFinished() method and hit the "Add" button.

We also don't need to worry about cleaning things up because the CursorLoader is tied to the Activity lifecycle so will be properly cleaned up when our Activity goes out of scope.

All in all we get a lot of functionality for just a few lines of code.

Apologies that this article has focused a little more on CursorLoader than CursorAdpater (and this is, after all a series on Adapters), but the Adapter itself is pretty straightforward if you already understand how SimpleAdapter works, and the implementation is made even easier when we use a CursorLoader instead of acquiring a Cursor directly from the ContentResolver. Of course the CursorLoader pattern is applicable wherever you use Cursors and is not limited solely to CursorAdapters.

In the next article we'll have a look at how we use Adapters to populate ViewPagers.

The source code for this article is available here.

© 2013 - 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

4 Comments

  1. Don’t set CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER there is already an observer on CursorLaoder.

    With CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER you create a memory leak, you will see it with MAT after an orientation change …

  2. Fragments have their own LoaderManager (even for the Support one).
    Why are you doing :
    Activity activity = getActivity();
    if (activity instanceof FragmentActivity)
    {
    LoaderManager lm =
    ((FragmentActivity) activity)
    .getSupportLoaderManager();
    lm.restartLoader(0, null, this);
    }

    while you could do :
    LoaderManager im = getLoaderManager();
    im.restartLoader(0, null, this);

    Fabien

    1. You’re absolutely right. I’ve updated the code accordingly. Thanks for letting me know.

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.