The TutList application that we've been working with has a pretty big flaw right now: the article data is not “live”, but static content. In this tutorial, you take several more steps towards a flexible and expandable solution by modifying the application to act as a data-backed content provider.
The Android framework uses a concept called content providers to enable applications to share and use data across the platform. Typically, a provider is backed by a SQLite database where the underlying data is stored. The current state of the TutList application is such that it gets the data for the ListView from static string arrays in the resources. In this tutorial, we'll remove those fixed data resources and build a flexible database-driven content provider in their place. You'll see that the user interface code won't change much. The back-end, however, will get more complex. The advantage here is that when we finally switch the application over to retrieving live data from the Internet, we'll have a place to store and manage it easily.
The pacing of this tutorial will be faster than some of our beginner tutorials; you may have to review some of the other Android tutorials on this site or even in the Android SDK reference if you are unfamiliar with any of the basic Android concepts and classes discussed in this tutorial. You can read the SQLite Crash Course for Android Developers tutorial to refresh your SQLite knowledge. The final sample code that accompanies this tutorial is available for download as open-source from the Google code hosting.
Step 0: Getting Started
This tutorial assumes you will start where our last tutorial, Android Compatibility: Working with Fragments, left off. You can download that code and build from there or you can download the code for this tutorial and follow along. Either way, get ready by downloading one or the other project and importing it into Eclipse.
Step 1: Creating the Database Class
First, you must create the application’s underlying SQLite database. Begin by creating a new class named TutListDatabase that extends from SQLiteOpenHelper. We placed it in the com.mamlambo.tutorial.tutlist.data package to separate it out from the user interface portion of the app. While you're at it, define the database configuration information in the class, such as the name of the database and its version number. We've done this with constants. Finally, use update the TutListDatabase class constructor to reference these values, as follows:
public class TutListDatabase extends SQLiteOpenHelper { private static final String DEBUG_TAG = "TutListDatabase"; private static final int DB_VERSION = 1; private static final String DB_NAME = "tutorial_data"; public TutListDatabase(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { }
We'll get to the implementation of the onCreate() and onUpgrade() methods shortly.
Step 2: Defining the Database Schema
Our initial article data had just two data fields: a title and a link. There’s no reason not to keep this sort of structure in our SQLite database. We'll need a mandatory _id field, too, which acts as a unique identifier for each record. We'll call this table tutorials.
This simple schema will suite us for now. To simplify usage and assist with other aspects of the system, we'll define the columns, tables, and even the create statement as static strings in the TutListDatabase class, like so:
public static final String TABLE_TUTORIALS = "tutorials"; public static final String ID = "_id"; public static final String COL_TITLE = "title"; public static final String COL_URL = "url"; private static final String CREATE_TABLE_TUTORIALS = "create table " + TABLE_TUTORIALS + " (" + ID + " integer primary key autoincrement, " + COL_TITLE + " text not null, " + COL_URL + " text not null);"; private static final String DB_SCHEMA = CREATE_TABLE_TUTORIALS;
Step 3: Creating the Database
Database creation should now be fairlystraightforward. Within the onCreate() method, simply execute the DB_SCHEMA string as SQL to create the table you defined in the previous step:
@Override public void onCreate(SQLiteDatabase db) { db.execSQL(DB_SCHEMA); }
This might be a good opportunity to insert some sample data (articles) for use. You could use a typical "INSERT INTO" SQLite command. See the code download for an example of how to do this.
Step 4: Upgrading the Database
Since the database is brand new, we have no upgrade policy we need to follow. Therefore, we'll just drop the table and recreate it if an upgrade request is made.
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(DEBUG_TAG, "Upgrading database. Existing contents will be lost. [" + oldVersion + "]->[" + newVersion + "]"); db.execSQL("DROP TABLE IF EXISTS " + TABLE_TUTORIALS); onCreate(db); }
If the database version is incremented and the app installed over an existing installation, this code will be triggered. The warning, only shown to LogCat, simply states that existing contents will be lost. If you implemented sample content creation as part of the previous step, the initial content will be restored during the creation. In a published application that stores important user data, you would likely want to do everything possible to keep that data by migrating it to from the old schema to the new schema. However, for this simple example, there is little need for such provisions.
As the database schema hasn't changed and remains compatible, there's no reason to update the database version. The database contents will remain intact.
Step 5: Creating the Content Provider Class
Now that your application has a functional database, you can turn your attention to implementing a content provider to access, expose, and manage the article data stored there. Begin by createing a new class named TutlistProvider which extends the ContentProvider class. Give it a private member variable to hold an instance of a TutListDatabase. Instantiate the database in the onCreate() method:
public class TutListProvider extends ContentProvider { private TutListDatabase mDB; @Override public boolean onCreate() { mDB = new TutListDatabase(getContext()); return true; }
Step 6: Preparing Helper Constants and the Matcher
Content providers work with data at the URI level. For instance, this URI identifies all of the tutorials:
content:// com.mamlambo.tutorial.tutlist.data.TutListProvider/tutorials
However, this identification doesn't actually happen by magic. Instead, content provider classes generally provide some public constants that can be used by apps to identify the data they want to query. Inside the content provider, some help is available for determining what kind of URI is being passed in. This will become clearer with the coming examples and code.
For now, here is a set of constants found in the TutListProvider class to use in the various methods and for use by external classes using this content provider :
private static final String AUTHORITY = "com.mamlambo.tutorial.tutlist.data.TutListProvider"; public static final int TUTORIALS = 100; public static final int TUTORIAL_ID = 110; private static final String TUTORIALS_BASE_PATH = "tutorials"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TUTORIALS_BASE_PATH); public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/mt-tutorial"; public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/mt-tutorial";
Note which definitions are private and which are public—this is purposeful. The public definitions will be used by the other portions of the app (or other apps who want to access the content provider data). The private definitions are for internal use by the class only and not exposed to others. To determine what kinds of URI addresses are passed to the content provider, we can leverage a helper class called UriMatcher to define specific URI patterns the content provider will support. Here's the UriMatcher for our content provider, defined statically at the class level inside TutListProvider:
private static final UriMatcher sURIMatcher = new UriMatcher( UriMatcher.NO_MATCH); static { sURIMatcher.addURI(AUTHORITY, TUTORIALS_BASE_PATH, TUTORIALS); sURIMatcher.addURI(AUTHORITY, TUTORIALS_BASE_PATH + "/#", TUTORIAL_ID); }
This UriMatcher defines two types of URIs. One looks like the sample one from above. The other is simply appended with a forward slash (/) following by a number. That type of URI is used to supply the unique identifier for a specific article, so as to return a single entry.
Step 6: Handling Content Provider Queries
A content provider has several methods we must override. The only one we're currently interested in is the query() method. The others are delete(), getType(), insert(), and update() -- we'll get to those near the end of this tutorial.
The query() method initially looks complex, with five parameters, including two arrays. As it turns out, the Android SDK has another helper class that greatly simplifies the implementation of this method. The SQLiteQueryBuilder class is just what we're looking for. Here's the complete, and simple, implementation of the query() method:
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(TutListDatabase.TABLE_TUTORIALS); int uriType = sURIMatcher.match(uri); switch (uriType) { case TUTORIAL_ID: queryBuilder.appendWhere(TutListDatabase.ID + "=" + uri.getLastPathSegment()); break; case TUTORIALS: // no filter break; default: throw new IllegalArgumentException("Unknown URI"); } Cursor cursor = queryBuilder.query(mDB.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; }
To start, we get a new instance of the SQLiteQueryBuilder class. Then we use the setTables() method to specify the tables we're working with—in this case, just the tutorials table. Next we use the UriMatcher class to do the heavy lifting of determining if the query is for a single entry or all entries. If it's a single entry, we add a where clause to filter by just the unique id.
Next, we call the query() method of the SQLiteQueryBuilder class. Turns out that it takes many of the same parameters as were passed to our query() method -- so we just pass them along. Then we return the newly created cursor.
The setNotificationUri() method simply places a watch on the caller's content resolver such that if the data changes and the caller has a registered change watcher, they'll be notified. Here we just use the same URI.
Step 7: Registering the Content Provider
Like an Activity class, a Content Provider must be properly registered in the Android Manifest file. This means adding a section within the section of the file, like follows:
<provider android:authorities="com.mamlambo.tutorial.tutlist.data.TutListProvider" android:multiprocess="true" android:name="com.mamlambo.tutorial.tutlist.data.TutListProvider"></provider>
The authorities attribute should match the AUTHORITY constant defined in the TutListProvider class as that's the authority used with the URIs. The name attribute must be the fully qualified class name of the content provider.
Step 8: Updating the ListView
With the content provider implementation complete, let's update the application to use it! Within TutListFragment class, update the onCreate() method to use the new content provider as follows:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); String[] projection = { TutListDatabase.ID, TutListDatabase.COL_TITLE }; String[] uiBindFrom = { TutListDatabase.COL_TITLE }; int[] uiBindTo = { R.id.title }; Cursor tutorials = getActivity().managedQuery( TutListProvider.CONTENT_URI, projection, null, null, null); CursorAdapter adapter = new SimpleCursorAdapter(getActivity() .getApplicationContext(), R.layout.list_item, tutorials, uiBindFrom, uiBindTo); setListAdapter(adapter); }
A projection is simply a list of columns to use with the adapter. The ListView uses the titles and can provide an id when an item is clicked. For use with an adapter, the id column must be named "_id" -- which we've done.
Next, you'll need to update the onListItemClick() method of the ListView. Before, we simply used the position to look up the link in an array. Now we'll use the unique id – which matches the database id, conveniently enough -- and look up the link in the database via a simple query to the content provider:
@Override public void onListItemClick(ListView l, View v, int position, long id) { String projection[] = { TutListDatabase.COL_URL }; Cursor tutorialCursor = getActivity().getContentResolver().query( Uri.withAppendedPath(TutListProvider.CONTENT_URI, String.valueOf(id)), projection, null, null, null); if (tutorialCursor.moveToFirst()) { String tutorialUrl = tutorialCursor.getString(0); tutSelectedListener.onTutSelected(tutorialUrl); } tutorialCursor.close(); }
Here, we request the URL column and use the content URI with the id appended to it. Pretty straightforward, right?
And guess what? You're done! You can run the application now and it should look -- and behave -- exactly as it did before. However, instead of the data being sourced from the fixed resources, the app is now storing its data in a new, improved home—a database, and accessed through a straightforward mechanism—a content provider.
All of that, and we're right back where we started. Feels a bit anticlimactic, huh? Actually, you're not quite finished. You should finish off the rest of the content provider methods.
Step 9: Finishing the Content Provider
Although the application does not yet use the insert(), update(), getType(), or delete() methods, it will in the future when you start grabbing “live” tutorial data from a remote source. The implementation of each of these methods follows a pattern similar to that of the query() method. For instance, here's the implementation of the delete() method:
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { int uriType = sURIMatcher.match(uri); SQLiteDatabase sqlDB = mDB.getWritableDatabase(); int rowsAffected = 0; switch (uriType) { case TUTORIALS: rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS, selection, selectionArgs); break; case TUTORIAL_ID: String id = uri.getLastPathSegment(); if (TextUtils.isEmpty(selection)) { rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS, TutListDatabase.ID + "=" + id, null); } else { rowsAffected = sqlDB.delete(TutListDatabase.TABLE_TUTORIALS, selection + " and " + TutListDatabase.ID + "=" + id, selectionArgs); } break; default: throw new IllegalArgumentException("Unknown or Invalid URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return rowsAffected; }
The first couple of lines determine the type of the incoming URI and open a writable database. Then, if the URI points to the list, possibly with a filter, we just delete that. Note that the URI with no filter (selection and selectionArgs) will delete all entries. Otherwise, we delete based on a specific ID -- with or without a filter.
The rest of the methods are found in the downloadable (and online viewable) open source code. They are similar and you should be able to read through them to see how they work. The gist is that they each do the "right" thing depending on the type of URI. Since this content provider is database-backed, the right thing usually involves calling an equivalent SQLite method, greatly simplifying the interface implementation.
Conclusion
This tutorial has taught you not only how to create a SQLite database and wrap it inside of a content provider, but also how straightforward it is to use a content provider to populate a ListView control. In future tutorials, we'll extend this application further to populate the application’s database with fresh, live tutorial content and more.
We hope you’ve enjoyed this tutorial. We look forward to your feedback on the pacing and complexity of the material covered.
About the Authors
Mobile developers Lauren Darcey and Shane Conder have coauthored several books on Android development: an in-depth programming book entitled Android Wireless Application Development and Sams Teach Yourself Android Application Development in 24 Hours. When not writing, they spend their time developing mobile software at their company and providing consulting services. They can be reached at via email to [email protected], via their blog at androidbook.blogspot.com, and on Twitter @androidwireless.
Comments