Chapter 13
In This Chapter
Discovering data storage
Creating an SQLite database
Querying your database
Using loaders and adapters
In certain types of applications, Android requires application developers to use data persistence, where information about a user’s preferences, such as favorite background colors or radio stations, is saved on the device for reuse later, after the device is turned off and then on again. For example, the Tasks application wouldn’t be useful if it didn’t save tasks, would it? Thankfully, the Android platform provides a robust set of tools that you can use to store user data.
This chapter delves deeply into creating and updating an SQLite database and producing a ContentProvider to access it. You need to be familiar with a certain level of database theory to tackle the data storage tasks in this chapter.
This chapter is code intensive — if you start feeling lost, you can download the completed application source code from this book’s website.
Depending on the requirements of your application, you may need to store data in a variety of places. For example, if an application interacts with music files and a user wants to play them in more than one music program, you have to store them in a location where all applications can access them. An application that needs to store sensitive data, such as encrypted usernames and password details, shouldn’t share data — placing it in a secure, local storage environment is the best strategy. Regardless of your situation, Android provides various options for storing data.
The Android ecosystem provides various locations where data can be persisted:
Local cache: The internal data directory for caching data rather than storing it persistently. Cached files may be deleted at any time. You use the getCacheDir() method, available on the Activity or Context objects in Android.
If you store data in an internal data directory and the internal storage space begins to run low, Android may delete files to reclaim space. Don’t rely on Android to delete your files for you though! You should delete your cache files yourself to stay within a reasonable limit (for example, around 1MB) of space consumed in the cache directory.
External storage: Every Android device supports shared external storage for files — either removable storage, such as a Secure Digital card (SD card) or non‐removable storage. Files saved to external storage are public (any person or application can alter them), and no level of security is enforced. Users can modify files by either using a file manager application or connecting the device to a computer via a USB cable and mounting the device as external storage. Before you work with external storage, check the current state of the external storage with the Environment object, using a call to getExternalStorageState() to check whether the media is available.
The main method is a call on the Context object — getExternalFilesDir(). This call takes a string parameter as a key to help define the type of media you’re saving, such as ringtones, music, or photos. For more information, view the external data storage examples and documents at
.http://d.android.com/guide/topics/data/data‐storage.html#filesExternal
The various data storage locations offer quite the palette of options. However, you have to figure out which one to use, and you may even want to use multiple storage mechanisms.
Suppose that your application communicates with a third‐party remote API such as Twitter, and network communication is slow and less than 100 percent reliable. You may want to retain a local copy of all data since the last update from the server, to allow the application to remain usable (in some fashion) until the next update. When you store the data in a local copy of an SQLite database and the user initiates an update, the new updates refresh the SQLite database with the new data.
The two fragments in the Tasks application need to perform various duties to operate. TaskEditFragment needs to complete these steps:
The TaskListFragment needs to perform these duties:
To work with an SQLite database, you communicate with the database via a ContentProvider. Programmers commonly remove as much of the database communication as possible from the Activity and Fragment objects. The database mechanisms are placed into a ContentProvider to help separate the application into layers of functionality. Therefore, if you need to alter code that affects the database, you know that you need to change the code in only one location to do so.
The first step to creating a new SQLite database ContentProvider is to create the SQLite database that it will use.
The table in SQL is what holds the data you manage. Visualizing a table in SQLite is similar to looking at a spreadsheet: Each row consists of data, and each column represents the data inside the row. Listing 13-1 defines column names for the database. These column names equate to the header values in a spreadsheet, as shown in Figure 13-1. Each row contains a value for each column, which is how data is stored in SQLite.
The SQL script to create a table like the one in the previous figure is shown in Listing 13-1 :
Listing 13‐1: Creating an SQL Database Table
create table tasks ( →1
_id integer primary key autoincrement, →2
title text not null, →3
notes text not null, →4
task_date_time integer not null ); →5
Getting into the details about SQL is beyond the scope of the book, but here’s a brief synopsis about what this SQL script does:
→ 1 Creates a table named tasks.
→ 2 Adds a primary key to that table named _id. Android assumes that the id field for every table begins with an underscore.
→ 3 Adds a non‐null field named title to the table. This field can be any length.
→ 4 Adds a non‐null notes field.
→ 5 Adds a date/time field to the table. In this table, the date/time is stored as an integer.
Android apps create SQLite databases using an SQLiteOpenHelper. Because this database is going to be used exclusively from a ContentProvider, you’re going to create an SQLiteOpenHelper class nested inside a ContentProvider.
Create a new class named TaskProvider in the directory com/dummies/tasks/provider. Add the following code to it:
public class TaskProvider extends ContentProvider { →1
// Database Columns →3
public static final String COLUMN_TASKID = "_id";
public static final String COLUMN_DATE_TIME = "task_date_time";
public static final String COLUMN_NOTES = "notes";
public static final String COLUMN_TITLE = "title";
// Database Related Constants →9
private static final int DATABASE_VERSION = 1; →10
private static final String DATABASE_NAME = "data"; →11
private static final String DATABASE_TABLE = "tasks"; →12
// The database itself
SQLiteDatabase db; →15
@Override →18
public boolean onCreate() {
// Grab a connection to our database
db = new DatabaseHelper(getContext()).getWritableDatabase(); →21
return true;
}
/**
* A helper class which knows how to create and update our database.
*/
protected static class DatabaseHelper extends SQLiteOpenHelper {
static final String DATABASE_CREATE = →30
"create table " + DATABASE_TABLE + " (" +
COLUMN_TASKID + " integer primary key autoincrement, " +
COLUMN_TITLE + " text not null, " +
COLUMN_NOTES + " text not null, " +
COLUMN_DATE_TIME + " integer not null);";
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION); →39
}
@Override →42
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override →48
public void onUpgrade(SQLiteDatabase db, int oldVersion,
int newVersion) {
throw new UnsupportedOperationException(); →51
}
}
}
The numbered lines are described in this list:
→ 1 A ContentProvider that knows how to read and write tasks from your tasks database. For now, it’s practically empty. The only thing it does is create a database using a SQLiteOpenHelper.
→ 3 The names of the various columns in the Task table. These correspond to the columns in Listing 13-1 and Figure 13-1. These column names are going to be needed outside of this class, so make them public.
→ 9 Various database‐related constants. These constants will not be needed outside this class, so they are private.
→ 10 The version number for this database. Because it’s the first version, give it a version of 1. Whenever you change the database, increment this number by one so that Android knows that the database has changed. This allows you to know whether you need to upgrade the database schema in onUpgrade on line 48 (an advanced topic outside of the scope of this book).
→ 11 The database name. This is the name of the file on the file system.
→ 12 The name of the database table. This table is named "tasks".
→ 15 The database object that will be created in onCreate using your SQLiteOpenHelper below. This is the object that your ContentProvider will use to read and write from your database.
→ 18 This method is called when the ContentProvider is created. This is usually done once on app startup.
→ 21 Creates the database object by using a DatabaseHelper. First you create a new DatabaseHelper, passing in the current context. Then, you call getWritableDatabase(). Some apps might want to use getReadableDatabase() instead, but because this app is reading and writing, it needs a writable database.
→ 30 The database creation script from Listing 13-1.
→ 39 The constructor for the database helper. It must call the super’s constructor, and pass in the current context, the database name, an optional CursorFactory for advanced usages, and the version of the database.
→ 42 This method is called when the app is first installed and no database has yet been created. This is where the magic happens and your database creation SQL script is executed.
→ 48 This method will be called in the future when version 2.0 of the Tasks app is released. At that point, you’ll need to upgrade the database from version 1.0 to version 2.0. For now, there’s nothing you need to do here.
→ 51 Because this method will never be called (because there was no version 0 of the database before version 1), just throw an UnsupportedOperationException here. You will need to change this code before you release version 2 of the database.
Now that you’ve created the basic SQLite table, you need to start providing all the methods you’ll need to read and write from that table using your ContentProvider. But first, you need to understand how ContentProviders use URIs.
An Android ContentProvider uses URIs to identify data. Typically, you can use a URI to identify a specific piece of data, such as a single task, or all the tasks in your database. If you store other types of data there, you can use URIs for them, too.
In your application, you use two kinds of URIs — content://com.dummies.tasks.provider.TaskProvider/task to retrieve a list of all tasks in your database, or content://com.dummies.tasks.provider.TaskProvider/task/9 to retrieve a specific task from the database (in this case the task with the ID of 9).
These ContentProvider URIs are undoubtedly similar to the URIs you’re already familiar with. Their main differences are described in this list:
Now you have to add the code to support these URIs in your ContentProvider. Open TaskProvider and add the following lines to the class:
// Content Provider Uri and Authority
public static final String AUTHORITY
= "com.dummies.tasks.provider.TaskProvider"; →3
public static final Uri CONTENT_URI
= Uri.parse("content://" + AUTHORITY + "/task"); →5
// MIME types used for listing tasks or looking up a single
// task
private static final String TASKS_MIME_TYPE
= ContentResolver.CURSOR_DIR_BASE_TYPE
+ "/vnd.com.dummies.tasks.tasks"; →11
private static final String TASK_MIME_TYPE
= ContentResolver.CURSOR_ITEM_BASE_TYPE
+ "/vnd.com.dummies.tasks.task";
// UriMatcher stuff
private static final int LIST_TASK = 0; →17
private static final int ITEM_TASK = 1;
private static final UriMatcher URI_MATCHER = buildUriMatcher(); →19
/**
* Builds up a UriMatcher for search suggestion and shortcut refresh
* queries.
*/
private static UriMatcher buildUriMatcher() {
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); →26
matcher.addURI(AUTHORITY, "task", LIST_TASK); →27
matcher.addURI(AUTHORITY, "task/#", ITEM_TASK); →28
return matcher;
}
/**
* This method is required in order to query the supported types.
*/
@Override
public String getType(Uri uri) {
switch (URI_MATCHER.match(uri)) { →37
case LIST_TASK:
return TASKS_MIME_TYPE;
case ITEM_TASK:
return TASK_MIME_TYPE;
default:
throw new IllegalArgumentException("Unknown Uri: " + uri);
}
}
This chunk of code may seem intimidating, but it consists mostly of constants with one useful method (getType()). Here’s how the numbered lines work:
→ 3 The authority for the ContentProvider — by convention, the same as the fully qualified class name. This value must match the value you will add to your AndroidManifest.xml file for the provider authorities.
→ 5 The base URI for the ContentProvider. Every time your application asks for data for this URI, Android routes the request to this ContentProvider.
The first type of URI is the CONTENT_URI, and the second one is the CONTENT_URI with the task ID appended to the end.
→ 11 Because the ContentProvider supports two types of data, it defines two types (or MIME types) for this data. MIME types are simply strings commonly used on the web to identify data types. For example, web HTML content typically has a MIME type of text/html, and audio MP3 files have audio/mpeg3. Because the tasks are of no known standard type, you can make up MIME type strings as long as you follow Android and MIME conventions.
The list MIME type begins with ContentResolver.CURSOR_DIR_BASE_TYPE, and the individual task MIME type begins with ContentResolver.CURSOR_ITEM_BASE_TYPE. DIR represents the list, and ITEM represents the item — simple enough.
The subtype (which follows the /) must begin with vnd. The subtype is followed by the fully qualified class name and the type of data — in this case, com.dummies.tasks and task. Visit
for more information about the Android conventions for MIME types.http://developer.android.com/reference/android/content/ContentResolver.html
→ 17 Uses another constant to identify list types versus item types, which are ints.
→ 19 The UriMatcher is used to determine the URI type: list or item. You build a UriMatcher using the method named buildUriMatcher() on line 25.
→ 26 Creates the UriMatcher, which can indicate whether a given URI is the list type or item type. The UriMatcher.NO_MATCH parameter tells the application which default value to return for a match.
→ 27 Defines the list type. Any URI that uses the com.dummies.tasks.provider.TaskProvider authority and has a path named "task" returns the value LIST_TASK.
→ 28 Defines the item type. Any URI that uses the com.dummies.tasks.TaskProvider authority and has a path that looks like task/# (where # is a number) returns the value ITEM_TASK.
→ 37 Uses the UriMatcher on line 19 to determine which MIME type to return. If the URI is a list URI, it returns TASKS_MIME_TYPE. If it’s an item URI, it returns TASK_MIME_TYPE.
Before you can use the TaskProvider, make sure that it’s listed in the AndroidManifest.xml file, by adding this code before the </application> tag:
<provider
android:name=".provider.TaskProvider"
android:authorities="com.dummies.tasks.provider.TaskProvider"
android:exported="false" />
It tells Android that a ContentProvider named TaskProvider will handle URIs that use the specific authority of com.dummies.tasks.TaskProvider. It also indicates that the data in the provider is not exported to other apps on the user’s phone. In general, you should set exported="false" unless you want to make your provider available to other apps.
Your ContentProvider needs to be able to deal with CRUD. Specifically, it needs to handle the following operations:
To do this, you must add the necessary methods to support these four operations to the TaskProvider. I’ll tackle these slightly out of order.
Adding a new item to the database is easy. Add the following method to your TaskProvider:
/**
* This method is called when someone wants to insert something
* into our content provider.
*/
@Override
public Uri insert(Uri uri, ContentValues values) { →6
// you can't choose your own task id
if( values.containsKey(COLUMN_TASKID))
throw new UnsupportedOperationException(); →9
long id = db.insertOrThrow(DATABASE_TABLE, null,
values); →12
getContext().getContentResolver().notifyChange(uri, null); →13
return ContentUris.withAppendedId(uri, id); →14
}
Here’s what the insert method is doing:
→ 6 The insert method takes two parameters. The first is the URI that identifies which table to insert into, which will always be CONTENT_URI for this ContentProvider. The second parameter is a hashmap with keys and values that represent the data being inserted into the database. Typically, this would include the task’s title and notes.
→ 9 When you insert something into the database, the database creates a new row and returns the ID to you. Because of this, it doesn’t make sense to allow you to specify a row id when you insert into the db. Doing so is an error, so throw an exception.
→ 12 Calls insertOrThrow on the database object to insert the value. As the name implies, this method throws an exception if there’s any problem inserting into the database. Typically, this would only happen if the user is running out of space on his or her phone. Because this is fairly rare, you do not need to add any explicit exception handling to catch this case. The insertOrThrow method returns the ID of the task that was added to the db.
→ 13 As mentioned before, one of the main responsibilities of a ContentProvider is to notify listeners of changes to their data. If a list page in your app is watching the tasks table, and the edit page adds a new item to the table, the list page needs to be notified of the change so that it can be refreshed. This is done on this line by calling notifyChange() on the context’s ContentResolver. The notifyChange() method takes the uri of the content that has changed. The second parameter of notifyChange() can be ignored.
→ 14 Returns the URI for the newly added task. To do this, take the URI and append the new ID using ContentUris.withAppendedId().
Editing (also known as updating) a task in the database is very similar to creating a new one. Add the following method to your TaskProvider:
/**
* This method is called when someone wants to update something
* in our content provider.
*/
@Override
public int update(Uri uri, ContentValues values, String ignored1,
String[] ignored2) { →7
// you can't change a task id
if( values.containsKey(COLUMN_TASKID))
throw new UnsupportedOperationException(); →10
int count = db.update( →12
DATABASE_TABLE,
values,
COLUMN_TASKID + "=?", →15
new String[]{Long.toString(ContentUris.parseId(uri))}); →16
if (count > 0)
getContext().getContentResolver().notifyChange(uri, null); →19
return count; →21
}
Here’s a description of what this listing is doing:
→ 7 The update method takes four parameters. The first is the URI, which is the same URI as the insert method, except this URI will also have the ID of the task to be edited appended to the end. For example, the URI might be content://com.dummies.tasks.provider.TaskProvider/task/8 to edit the eighth task in the db. The second parameter is the values to be set for that task. Typically this would include the title and/or the notes. The third and fourth parameters are SQL selection arguments for advanced usages and can be ignored.
→ 10 Just like in the insert method, it is illegal to try to change the ID of a given task, so throw an exception if anyone tries.
→ 12 Calls the update() method on the db object. Much like in the call to insertOrThrow() in the previous section, the first two parameters to the update call are the table to be edited and the values to be set. The next parameters, however, are different.
→ 15 Specifies the WHERE clause to the SQL query. In this case, the WHERE clause will be "_id=?", indicating that you want to update the row that has an _id of "?". The "?" will be replaced by the value on line 16.
→ 16 Computes the id of the task to be edited. This is done by parsing it from the URI using ContentUris.parseId(), converting the resulting long into a String, and then putting that String into an array of Strings to be passed as the whereArgs for the update call. Each "?" in the where clause will be replaced by the respective entry from the String array, so there should always be exactly as many question marks in the where clause as there are items in the String array.
→ 19 If anything in the table was changed, notify any listeners.
→ 21 Returns the count of items update. It should only ever be zero or one.
The delete method is even easier to implement than the update method.
/**
* This method is called when someone wants to delete something
* from our content provider.
*/
@Override
public int delete(Uri uri, String ignored1, String[] ignored2) { →6
int count = db.delete( →7
DATABASE_TABLE,
COLUMN_TASKID + "=?",
new String[]{Long.toString(ContentUris.parseId(uri))});
if (count > 0)
getContext().getContentResolver().notifyChange(uri, null); →13
return count;
}
By now, most of this should be familiar to you. However, there are some differences:
→ 6 As was the case with the update method, the last two arguments (the selection and the selectionArgs) can be ignored for delete.
→ 7 Calls the delete method, and passes in the table name, the where clause for the _id, and the _id.
→ 13 If anything was deleted, notify any listeners. Then return the count of rows that were deleted (should be zero or one).
Were insert, update, and delete too easy for you? Are you ready for a challenge? Well, let’s give you something a little trickier. Here’s how you implement the query (also known as Read) method:
/**
* This method is called when someone wants to read something from
* our content provider. We'll turn around and ask our database
* for the information, and then return it in a Cursor.
*/
@Override
public Cursor query(Uri uri, String[] ignored1, String selection, →7
String[] selectionArgs, String sortOrder) {
String[] projection = new String[]{ →10
COLUMN_TASKID,
COLUMN_TITLE,
COLUMN_NOTES,
COLUMN_DATE_TIME};
Cursor c;
switch (URI_MATCHER.match(uri)) { →17
case LIST_TASK: →19
c = db.query(DATABASE_TABLE, →20
projection, selection,
selectionArgs, null, null, sortOrder);
break;
case ITEM_TASK: →25
c = db.query(DATABASE_TABLE, projection, →26
COLUMN_TASKID + "=?",
new String[]{Long.toString(ContentUris.parseId
(uri))},
null, null, null, null);
if (c.getCount() > 0) {
c.moveToFirst(); →32
}
break;
default:
throw new IllegalArgumentException("Unknown Uri: " + uri); →36
}
c.setNotificationUri(getContext().getContentResolver(), uri); →39
return c;
}
Okay, that wasn’t so bad, but it still warrants some explanation:
→ 7 The query method takes a URI that represents the content to be queried. The selection parameter specifies an optional where clause (such as title=?), and the selectionArgs parameter is an array of strings that fill in any question marks in that selection parameter. The sortOrder parameter indicates how the results should be sorted.
→ 10 Creates a list of column names to represent the data and the order of the data that will be returned. This is called a projection to people who hold their pinkies up when they drink tea.
→ 17 Uses the UriMatcher to see what kind of query you have and formats the database query accordingly.
→ 19 You are asked to return a list of tasks.
→ 20 Queries the database table named "tasks" with the projection specified on line 10. The selection parameter indicates which tasks will be selected. If no selection is specified, this returns ALL of the rows in this table. The result is an SQL cursor that contains each of the columns specified in the projection.
→ 25 You are asked to return a specific task.
→ 26 Unlike line 20, line 26 is about querying a single specific task. To do that, you construct a where clause with an _id specified in the where args, exactly like you did for the update and delete methods. The other parameters of the db.query method can be ignored.
→ 32 If the query returned any results (for example, getCount() is larger than zero), then move the cursor to the first item in the list.
→ 36 If the URI wasn’t a list URI and it wasn’t an item URI, then something went wrong, so throw an error.
→ 39 Sets the notification URI for this cursor. This URI must agree with the URIs you used in insert, update, and delete. The loader (explained later in this chapter) uses this URI to watch for any changes to the data; and if the data changes, the loader automatically refreshes the UI.
Your ContentProvider is now complete! The next step is to use it in your app.
There are two fundamental things your ContentProvider is used for. The first is reading from your database, and the second is writing to your database. Let’s look at the simpler of the two first, which is writing to your database.
Open TaskEditFragment.java and add the following method:
private void save() {
// Put all the values the user entered into a
// ContentValues object
String title = titleText.getText().toString(); →4
ContentValues values = new ContentValues();
values.put(TaskProvider.COLUMN_TITLE, title);
values.put(TaskProvider.COLUMN_NOTES,
notesText.getText().toString());
values.put(TaskProvider.COLUMN_DATE_TIME,
taskDateAndTime.getTimeInMillis());
// taskId==0 when we create a new task,
// otherwise it's the id of the task being edited.
if (taskId == 0) {
// Create the new task and set taskId to the id of
// the new task.
Uri itemUri = getActivity().getContentResolver()
.insert(TaskProvider.CONTENT_URI, values); →19
taskId = ContentUris.parseId(itemUri); →20
} else {
// Update the existing task
Uri uri = ContentUris.withAppendedId(TaskProvider.CONTENT_URI,
taskId); →26
int count = getActivity().getContentResolver().update(
uri, values, null, null); →28
// If somehow we didn't edit exactly one task,
// throw an error
if (count != 1) →32
throw new IllegalStateException(
"Unable to update " + taskId);
}
Toast.makeText( →38
getActivity(),
getString(R.string.task_saved_message),
Toast.LENGTH_SHORT).show();
}
At a high level, the save method is doing three things:
Here is the code in more detail:
→ 4 Creates a new ContentValues map, then takes all the values that the user entered into the fragment (such as title, notes, date, and time), and puts them into the ContentValues instance. Note that you do not put the task ID into the ContentValues because it’s illegal to try to change it.
→ 19 This line gets a ContentResolver from the activity. It then calls insert() on that ContentResolver and specifies the URI of the task table and all the values that you want to insert. The ContentResolver will inspect that URI, figure out which ContentProvider is responsible for that URI, and ultimately call into your TaskProvider.insert method to insert the data for you.
→ 20 The call to insert() returns the URI of the data that was inserted, so parse out the ID of the newly inserted task and update the taskId field with the new value. That way, if the fragment does anything else later, the taskId will be set correctly and everything will work as it should. (In this case, it’s not strictly necessary because the fragment finishes itself as soon as the save is complete, but it’s usually better to leave yourself in a clean state than to open yourself up to future bugs.)
→ 26 In this section, you are updating an existing task rather than inserting a new one, so figure out what the URI is for that task by appending it to the CONTENT_URI using ContentUris.withAppendedId.
→ 28 Edit the task by giving the task’s URI and new values to the ContentResolver, like you did on line 19.
→ 32 If everything went well, then exactly one task should have been edited. If somehow more or less than one task was edited, throw an error.
→ 38 Notifies the user of the change using a Toast.
You added a new string, so add it to strings.xml:
<string name="task_saved_message">Task has been saved</string>
Now that you have a save() method, you need to call it. Uncomment the line you added in Chapter 11 in TaskEditFragment.onOptionsItemSelected that called save:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case MENU_SAVE:
save();
((OnEditFinished) getActivity()).finishEditingTask();
return true;
}
// If we can't handle this menu item, see if our parent can
return super.onOptionsItemSelected(item);
}
Now run your app! Click the Add button in the action bar and create a new task with whatever title you want, then click Save. A Toast message will pop up and indicate the task was saved. But how do you know for sure it was saved? The app has no way to show you the saved task yet.
You may not be able to view the data in the app, but if you are using an emulator or a rooted phone, you should be able to examine the SQLite database directly from the command line.
To view the database directly:
If your device has the sqlite3 command installed (most do), you can run sqlite3 data to examine and manipulate your database directly.
"data" is the DATABASE_NAME of the database you created in the SQLiteOpenHelper.
Try running select * from tasks; to get a list of your tasks.
You should now see the task you just created.
If your device does not have sqlite3 installed, you can search the App Store to find an sqlite3 binary that you can install on rooted phones.
You might think that reading from a database should be simpler than writing to a database. After all, you don’t have to change anything when you do a read. However, reading from a database is actually more complicated than writing for this example.
The reason is that when you’re doing any kind of I/O operation, such as reading from a network or from disk (reading a database, for example), you must do this work from a background thread. If you work from the main thread of the user interface, you run the risk of locking it up for an unknown period, which can cause it to feel jerky and unresponsive. Under particularly bad circumstances, it can even lead to displaying the dreaded Application Not Responsive dialog box, which can leave many users believing that your application has crashed.
Because the read operation is reading a bunch of items in a list, it may take a little time. It might take a few hundred milliseconds or so, for example. That may not seem like a long time, but it’s long enough to make your app stutter, and in rare circumstances it’s possible you might see an ANR.
Android provides a system based on loaders and adapters to read a list of data from a datastore (such as a database or file system) on a background thread.
In the next sections, you are going to create a loader to load data from your database, and an adapter to create views for that data.
The loader provides a mechanism by which you can launch background operations (such as reading from your database) and then get a callback when those operations finish so that you can update the user interface.
A typical example of a loader is a CursorLoader. You use a CursorLoader to load data from an SQLite database using a cursor. To add a CursorLoader to one of your list fragments, you implement the LoaderCallback interface in your callback and implement the three LoaderCallback methods:
To kick off a loader, you first obtain a LoaderManager from your activity by calling getLoaderManager() and then initLoader(). initLoader() starts loading data in the background by calling onCreateLoader(), and when it finishes it executes onLoaderFinished() in your LoaderCallback object.
Visit
for more information about loaders.http://developer.android.com/guide/components/loaders.html
Open TaskListFragment.java and add the following code in bold:
public class TaskListFragment extends Fragment
implements LoaderManager.LoaderCallbacks<Cursor> →2
{
@Override
public void onCreate(Bundle savedInstanceState) {
. . .
getLoaderManager().initLoader(0, null, this); →8
}
@Override
public Loader<Cursor> onCreateLoader(int ignored, Bundle args) {
return new CursorLoader(getActivity(), →13
TaskProvider.CONTENT_URI, null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.swapCursor(cursor); →19
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null); →24
}
}
You will get a couple of errors when you add this code, but skip those for now. What this code is doing:
→ 2 Adds the LoaderManager.LoaderCallbacks interface to this fragment, which is needed when we call initLoader on line 8.
→ 8 This is where you tell Android to start up a loader for you. Get a LoaderManager by calling getLoaderManager(), then initialize a loader by calling initLoader. initLoader takes three parameters:
→ 13 initLoader on line 8 will call onCreateLoader to create a new loader. Because you are going to be reading data from an SQL database, you will use Android’s built‐in CursorLoader to do the heavy lifting. Create a new CursorLoader and initialize it with the URI of the ContentProvider you want to use. The other parameters are for advanced usage; see
for more information.http://d.android.com/reference/android/content/CursorLoader.html
→ 19 When the loader is finished loading data into memory, it needs to do something with that data. You can do whatever you want with the data, but the traditional thing is to give the data to an adapter so that the adapter can display it to the user. The adapter has a method called swapCursor, which replaces whatever cursor it was using with a new cursor, so call swapCursor with the new cursor you just received.
→ 24 onLoaderReset is called when the last cursor provided to onLoadFinished() above is about to be closed. You need to make sure the adapter is no longer using it, so set it to null.
That is essentially all you need to do to use a loader. The next step is to implement the adapter that can take the data and create a view for it.
Adapters are objects that know how to create views for each item in a list. You created a simple adapter in Chapter 9, and now you are going to update it to read data from an SQL Cursor.
Open TaskListAdapter and add the bold lines to it:
public class TaskListAdapter
extends RecyclerView.Adapter<TaskListAdapter.ViewHolder>
{
static String[] fakeData = new String[] { →4
"One",
"Two",
"Three",
"Four",
"Five",
"Ah... ah... ah!"
};
Cursor cursor; →13
int titleColumnIndex; →14
int notesColumnIndex;
int idColumnIndex;
public void swapCursor(Cursor c) { →18
cursor = c; →19
if(cursor!=null) {
cursor.moveToFirst(); →21
titleColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_TITLE); →22
notesColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_NOTES);
idColumnIndex = cursor.getColumnIndex(TaskProvider.COLUMN_TASKID);
}
notifyDataSetChanged(); →26
}
@Override
public void onBindViewHolder(final ViewHolder viewHolder,
final int i) {
final Context context = viewHolder.titleView.getContext();
final long id = getItemId(i); →33
// set the text
cursor.moveToPosition(i); →36
viewHolder.titleView.setText(cursor.getString(titleColumnIndex)); →37
viewHolder.notesView.setText(cursor.getString(notesColumnIndex)); →38
// set the thumbnail image
Picasso.with(context)
.load(getImageUrlForTask(id)) →42
.into(viewHolder.imageView);
// Set the click action
viewHolder.cardView.setOnClickListener(
. . .
((OnEditTask) context).editTask(id); →48
});
viewHolder.cardView.setOnLongClickListener(
new View.OnLongClickListener()
{
. . .
deleteTask(context, id);→55
});
}
@Override
public long getItemId(int position) { →61
cursor.moveToPosition(position);
return cursor.getLong(idColumnIndex); →63
}
@Override
public int getItemCount() {
return cursor!=null ? cursor.getCount() : 0; →68
}
static class ViewHolder extends RecyclerView.ViewHolder {
CardView cardView;
TextView titleView;
TextView notesView; →74
ImageView imageView;
public ViewHolder(CardView card) {
super(card);
cardView = card;
titleView = (TextView)card.findViewById(R.id.text1);
notesView = (TextView) itemView.findViewById(R.id.text2); →81
imageView = (ImageView)card.findViewById(R.id.image);
}
}
}
These changes to TaskListAdapter make it possible to read the list of tasks from a cursor rather than from a hardcoded fakeData array. In more detail:
→ 4 Remove the fakeData array; it is no longer necessary. You also need to remove the call to titleView.setText(fakeData[position]) in onBindViewHolder.
→ 13 The TaskListAdapter is going to read data from a cursor, so add a field for the cursor here.
→ 14 When reading through the cursor, each column of data is referred to by an index. For example, the index of the title column might be 1, the index of notes might be 2, and so on. You don’t need the index for the date/time column because the list view does not display the date/time of each task. Store the indices of each column here for quick reference; you will determine their values on line 22.
→ 18 Creates a method named swapCursor. This method is called whenever the data in your database has changed. This might occur because someone added or deleted an item from the database, or because the app just started up and is reading all the previously created tasks for the first time. swapCursor is responsible for
→ 19 Replaces the previous cursor with the new cursor.
→ 21 Whenever you use a cursor, you must first move the cursor to its first location before you may attempt to read data from it. This line moves to the first position so that we can read the various column indices in the next few lines.
→ 22 Determines the column index for the title column in the cursor. This is done by asking the cursor for the index of the column named "title". Technically, you can skip this step entirely and just ask for columns by their name rather than by their index, but it’s more efficient to ask by index. On the next two lines, do the same thing for the notes and id columns.
→ 26 When the cursor has been swapped, that means that the data likely has changed. Notify any listeners (in particular, the RecyclerView from Chapter 9) that the data has changed so that they can refresh their displays.
→ 33 Each task in the database has an ID associated with it. You will need the id later, so find the ID for this task by calling getItemId and passing in the position of the item in the list.
→ 36 You are about to update the view with the data from the cursor, so make sure you move your cursor to the proper position before you begin to read.
→ 37 Reads the title string from the cursor using the getString method, and then uses that string to set the titleView TextView.
→ 38 Does the same for the notesView.
→ 42–55 In the old TaskListAdapter, items in the fakeData array didn’t have an ID, so we just used the position in the index as a sort of fake ID. In the new version of TaskListAdapter, every task has an ID that is stored in the database, so make sure to use that ID when calling getImageUrlForTask, editTask, and deleteTask.
→ 61 The implementation for getItemId which was called from line 33.
→ 63 After moving the cursor to the appropriate row in the database, this line asks the cursor what the ID is for that row.
→ 68 Updates getItemCount to return the count of items in the cursor, assuming that the cursor is not null. If the cursor is null, this line just returns 0.
→ 74 Adds the notesView TextView to your ViewHolder. Return to Chapter 9 for a reminder of what a ViewHolder does.
→ 81 Sets the notesView field by looking for the TextView named text2 in the card_task.xml layout.
If you run your app now, you should be able to add tasks! Give it a try.
There is one more thing to do. You need to add the ability to delete tasks from your database.
This is pretty straightforward. Update TaskListAdapter to implement the deleteTask method as shown:
private void deleteTask(Context context, long id) {
context.getContentResolver()
.delete(
ContentUris.withAppendedId(
TaskProvider.CONTENT_URI,
id),
null, null);
}
This code gets the ContentResolver from the context, calls delete on it, and passes in the URI of the task to be deleted.
Run the app and long‐press on an item in the list to try deleting it. You should see it automatically disappear from the list after the delete is confirmed.
The Edit page can now save data into the database, but it cannot yet read data from the database. This makes it impossible for users to edit existing tasks, so let’s wrap up this final bit of functionality now.
Now that you know how loaders work, let’s use a loader to read the task data from the database into the edit page. As you recall, loaders are the best way to perform I/O on a background thread without blocking the main UI thread.
Open TaskEditFragment and make the following changes:
public class TaskEditFragment extends Fragment
implements DatePickerDialog.OnDateSetListener,
TimePickerDialog.OnTimeSetListener,
LoaderManager.LoaderCallbacks<Cursor> →4
{
. . .
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout and set the container. The layout is the
// view that we will return.
View v = inflater.inflate(R.layout.fragment_task_edit,
container, false);
// From the layout, get a few views that we're going to work with
rootView = v.getRootView();
titleText = (EditText) v.findViewById(R.id.title);
notesText = (EditText) v.findViewById(R.id.notes);
imageView = (ImageView) v.findViewById(R.id.image);
dateButton = (TextView) v.findViewById(R.id.task_date);
timeButton = (TextView) v.findViewById(R.id.task_time);
// Set the thumbnail image →25
Picasso.with(getActivity())
.load(TaskListAdapter.getImageUrlForTask(taskId))
.into(. . .);
updateDateAndTimeButtons(); →30
// Tell the date and time buttons what to do when we click on
// them.
dateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showDatePicker();
}
});
timeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showTimePicker();
}
});
if (taskId == 0) { →47
updateDateAndTimeButtons();
} else {
// Fire off a background loader to retrieve the data from the
// database
getLoaderManager().initLoader(0, null, this); →55
}
return v;
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { →63
Uri taskUri = ContentUris.withAppendedId( →64
TaskProvider.CONTENT_URI, taskId);
return new CursorLoader( →67
getActivity(),
taskUri, null, null, null, null);
}
/**
* This method is called when the loader has finished loading its
* data
*/
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor task) {
if (task.getCount() == 0) { →78
getActivity().runOnUiThread( →79
new Runnable() {
@Override
public void run() {
((OnEditFinished) getActivity())
.finishEditingTask(); →84
}
});
return;
}
titleText.setText( →90
task.getString(
task.getColumnIndexOrThrow(TaskProvider.COLUMN_TITLE)));
notesText.setText(
task.getString(
task.getColumnIndexOrThrow(TaskProvider.COLUMN_NOTES)));
Long dateInMillis = task.getLong( →97
task.getColumnIndexOrThrow(TaskProvider.COLUMN_DATE_TIME));
Date date = new Date(dateInMillis);
taskDateAndTime.setTime(date);
Picasso.with(getActivity()) →103
.load(TaskListAdapter.getImageUrlForTask(taskId))
.into(. . .);
updateDateAndTimeButtons(); →107
}
@Override
public void onLoaderReset(Loader<Cursor> arg0) { →111
// nothing to reset for this fragment.
}
}
This code reads the task information from the database rather than from the fakeData list. It does it using a loader to avoid blocking the main UI thread. Here’s what the code does in more detail:
→ 4 Similar to what you did when you put a loader into the list view, you must implement LoaderManager.LoaderCallbacks in your fragment to use a loader here.
→ 25–30 It doesn’t make sense to try to download the image or update the date and time buttons yet if you don’t know what data has been loaded from the task, so move these lines from here to lines 103–107 when the loader has finished.
→ 47 If the task ID is 0, then you know you’re inserting a new item into the database. This means that there’s no data to load, so skip the loader and just update the time and date buttons instead.
→ 55 If the task ID was non‐zero, then the loader needs to read data out of the database. Start it up by calling initLoader, and pass in yourself as the LoaderManager.LoaderCallbacks object.
→ 63 onCreateLoader is called by initLoader when it is time to create the loader.
→ 54 Computes the URI for the task you want to load.
→ 67 Creates a cursor loader to load the specified task.
→ 78 Sanity check. If you weren’t able to load anything, just close this activity.
→ 79 onLoadFinished is called from a background thread. Many operations that affect the UI aren’t allowed from background threads. So make sure that you call finishEditingTask from the UI thread instead of from a background thread.
→ 84 Calls finishEditingTask from the main UI thread. You implemented finishEditingTask in Chapter 11.
→ 90 Sets the title and notes from the DB.
→ 97 Sets the task date/time from the DB.
→ 103–107 The code that you moved from lines 25–30.
→ 111 onLoaderReset is called when a previously created loader is being reset, thus making its data unavailable. In the list view, you needed to tell the adapter to stop using the old cursor. But in this fragment, there is nothing using the old cursor, so there is nothing to be done in this method.
Now you should have a fully working Tasks app that can create, read, update, and delete tasks from its database. Congratulations! Try running the app now and test it out.
44.200.94.150