Saving Your ToDo List

In Chapter 6 you enhanced the To-Do List example to persist the Activity's UI state across sessions. That was only half the job; in the following example you'll create a database to save the to-do items.

1. Start by creating a new ToDoDBAdapter class. It will be used to manage your database interactions. Create private variables to store the SQLiteDatabase object and the Context of the calling application. Add a constructor that takes an application Context, and create static class variables for the name and version of the database, as well as a name for the to-do item table.

package com.paad.todolist;

import android.content.ContentValues;

import android.content.Context;

import android.database.Cursor;

import android.database.SQLException;

import android.database.sqlite.SQLiteException;

import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log;

public class ToDoDBAdapter {

private static final String DATABASE_NAME = "todoList.db"; private static final String DATABASE_TABLE = "todoItems"; private static final int DATABASE_VERSION = 1;

private SQLiteDatabase db; private final Context context;

public ToDoDBAdapter(Context _context) { this.context = _context;

2. Create public convenience variables that define the column names: this will make it easier to find the correct columns when extracting values from query result Cursors.

public static final String KEY_ID = "_id"; public static final String KEY_TASK = "task";

public static final String KEY_CREATION_DATE = "creation_date";

3. Create a new taskDBOpenHelper class within the ToDoDBAdapter that extends SQLiteOpen-Helper. It will be used to simplify version management of your database. Within it, overwrite the onCreate and onUpgrade methods to handle the database creation and upgrade logic.

private static class toDoDBOpenHelper extends SQLiteOpenHelper {

public toDoDBOpenHelper(Context context, String name,

CursorFactory factory, int version) { super(context, name, factory, version);

// SQL Statement to create a new database.

private static final String DATABASE_CREATE = "create table " +

DATABASE_TABLE + " (" + KEY_ID + " integer primary key autoincrement, " + KEY_TASK + " text not null, " + KEY_CREATION_DATE + " long);";

©Override public void onCreate(SQLiteDatabase _db) { _db.execSQL(DATABASE_CREATE);

©Override public void onUpgrade(SQLiteDatabase _db, int _oldVersion, int _newVersion) { Log.w("TaskDBAdapter", "Upgrading from version " +

_newVersion + ", which will destroy all old data");

_db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE);

onCreate(_db);

4. Within the ToDoDBAdapter class, add a private variable to store an instance of the toDoDBOpenHelper class you just created, and assign it within the constructor.

private toDoDBOpenHelper dbHelper;

public ToDoDBAdapter(Context _context) { this.context = _context;

dbHelper = new toDoDBOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION);

5. Still in the adapter class, create open and close methods that encapsulate the open and close logic for your database. Start with a close method that simply calls close on the database object.

6. The open method should use the toDoDBOpenHelper class. Call getWritableDatabase to let the helper handle database creation and version checking. Wrap the call to try to provide a readable database if a writable instance can't be opened.

public void open() throws SQLiteException { try {

db = dbHelper.getWritableDatabase(); } catch (SQLiteException ex) {

db = dbHelper.getReadableDatabase();

7. Add strongly typed methods for adding, removing, and updating items.

// Insert a new task public long insertTask(ToDoItem _task) { // Create a new row of values to insert. ContentValues newTaskValues = new ContentValues(); // Assign values for each row. newTaskValues.put(KEY_TASK, _task.getTask());

newTaskValues.put(KEY_CREATION_DATE, _task.getCreated().getTime()); // Insert the row.

return db.insert(DATABASE_TABLE, null, newTaskValues);

// Remove a task based on its index public boolean removeTask(long _rowIndex) {

return db.delete(DATABASE_TABLE, KEY_ID + "=" + _rowIndex, null) > 0;

// Update a task public boolean updateTask(long _rowIndex, String _task) { ContentValues newValue = new ContentValues(); newValue.put(KEY_TASK, _task);

return db.update(DATABASE_TABLE, newValue, KEY_ID + "=" + _rowIndex, null) > 0;

8. Now add helper methods to handle queries. Write three methods — one to return all the items, another to return a particular row as a Cursor, and finally one that returns a strongly typed ToDoItem.

public Cursor getAllToDoItemsCursor() { return db.query(DATABASE_TABLE, new String[] { KEY_ID, KEY_TASK, KEY_CREATION_DATE}, null, null, null, null, null);

public Cursor setCursorToToDoItem(long _rowIndex) throws SQLException { Cursor result = db.query(true, DATABASE_TABLE, new String[] {KEY_ID, KEY_TASK},

KEY_ID + "=" + _rowIndex, null, null, null, null, null);

if ((result.getCount() ==0) || !result.moveToFirst()) {

throw new SQLException("No to do items found for row: " + _rowIndex);

return result;

public ToDoItem getToDoItem(long _rowIndex) throws SQLException { Cursor cursor = db.query(true, DATABASE_TABLE, new String[] {KEY_ID, KEY_TASK},

KEY_ID + "=" + _rowIndex, null, null, null, null, null);

if ((cursor.getCount() ==0) || !cursor.moveToFirst()) {

throw new SQLException("No to do item found for row: " + _rowIndex);

String task = cursor.getString(TASK_COLUMN);

long created = cursor.getLong(CREATION_DATE_COLUMN);

ToDoItem result = new ToDoItem(task, new Date(created)); return result;

9. That completes the database helper class. Return the ToDoList Activity and update it to persist the to-do list array. Start by updating the Activity's onCreate method to create an instance of the toDoDBAdapter and open a connection to the database. Also include a call to the populateTodoList method stub.

ToDoDBAdapter toDoDBAdapter;

public void onCreate(Bundle icicle) { [ ... existing onCreate logic ... ]

toDoDBAdapter = new ToDoDBAdapter(this);

// Open or create the database toDoDBAdapter.open();

populateTodoList();

private void populateTodoList() { }

10. Create a new instance variable to store a Cursor over all the to-do items in the database. Update the populateTodoList method to use the toDoDBAdapter instance to query the database, and call startManagingCursor to let the Activity manage the Cursor. It should also make a call to updateArray, a method that will be used to repopulate the to-do list array using the Cursor.

Cursor toDoListCursor;

private void populateTodoList() {

// Get all the todo list items from the database. toDoListCursor = toDoDBAdapter. getAllToDoItemsCursor(); startManagingCursor(toDoListCursor);

// Update the array. updateArray();

private void updateArray() { }

11. Now implement the updateArray method to update the current to-do list array. Call requery on the result Cursor to ensure it's fully up to date, then clear the array and iterate over the result set. When the update is complete call notifyDataSetChanged on the Array Adapter.

private void updateArray() { toDoListCursor.requery();

todoItems.clear();

if (toDoListCursor.moveToFirst()) do {

String task = toDoListCursor.getString(ToDoDBAdapter.TASK_COLUMN);

long created = toDoListCursor.getLong(ToDoDBAdapter.CREATION_DATE_COLUMN);

ToDoItem newItem = new ToDoItem(task, new Date(created)); todoItems.add(0, newItem); } while(toDoListCursor.moveToNext());

aa.notifyDataSetChanged();

12. To join the pieces together, modify the OnKeyListener assigned to the text entry box in the onCreate method, and update the removeItem method. Both should now use the toDoDBAdapter to add and remove items from the database rather than modifying the to-do list array directly.

12.1. Start with the OnKeyListener, insert the new item into the database, and refresh the array.

public void onCreate(Bundle icicle) { super.onCreate(icicle); setContentView(R.layout.main);

myListView = (ListView)findViewById(R.id.myListView); myEditText = (EditText)findViewById(R.id.myEditText);

todoItems = new ArrayList<ToDoItem>();

int resID = R.layout.todolist_item;

aa = new ToDoItemAdapter(this, resID, todoItems);

myListView.setAdapter(aa);

myEditText.setOnKeyListener(new OnKeyListener() {

public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {

ToDoItem newItem = new ToDoItem(myEditText.getText().toString());

toDoDBAdapter.insertTask(newItem);

updateArray();

myEditText.setText("");

aa.notifyDataSetChanged();

cancelAdd();

return true;

return false;

registerForContextMenu(myListView); restoreUIState();

toDoDBAdapter = new ToDoDBAdapter(this);

// Open or create the database toDoDBAdapter.open();

populateTodoList();

12.2. Then modify the removeItem method to remove the item from the database and refresh the array list.

private void removeItem(int _index) {

// Items are added to the listview in reverse order, so invert the index.

toDoDBAdapter.removeTask(todoItems.size()-_index);

updateArray();

13. As a final step, override the onDestroy method of your activity to close your database connection.

©Override public void onDestroy() { super.onDestroy();

// Close the database toDoDBAdapter.close();

All code snippets in this example are part of the Chapter 7 Todo List project, available for download at Wrox.com.

Your to-do items will now be saved between sessions. As a further enhancement you could change the Array Adapter to a Simple Cursor Adapter and have the List View update dynamically with changes to the database.

Because you're using a private database your tasks are not available to other applications. To provide access to your tasks in other applications, expose them using a Content Provider. You'll do exactly that next.

CREATING A NEW CONTENT PROVIDER

To create a new Content Provider, extend the abstract ContentProvider class. Override the onCreate method to create (and initialize) the underlying data source you're planning to publish with this provider. Sample skeleton code for a new Content Provider is shown in Listing 7-9.

LISTING 7-9: Creating a new Content Provider Available for download on import android.content.*; Wrax.com import android.database.Cursor; import android.net.Uri; import android.database.SQLException;

public class MyProvider extends ContentProvider {

©Override public boolean onCreate() {

// TODO Construct the underlying database. return true;

You should expose a public static content_uri property that returns the full URI of this provider. A Content Provider URI must be unique to the provider, so it's good practice to base the URI path on your package name. The general form for defining a Content Provider's URI is:

content://com.<CompanyName>.provider.<ApplicationName>/<DataPath>

For example:

content://com.paad.provider.myapp/elements

Content URIs can represent either of two forms. The previous URI represents a request for all values of that type (in this case all elements).

A trailing /<rownumber>, as shown in the following code, represents a request for a single record (in this case the fifth element).

content://com.paad.provider.myapp/elements/5

It's good practice to support access to your provider for both of these forms.

The simplest way to do this is to use a UriMatcher. Create and configure a Uri Matcher to parse URIs and determine their forms. This is particularly useful when you're processing Content Resolver requests. Listing 7-10 shows the skeleton code for this pattern.

LISTING 7-10: Using the UriMatcher to handle single or multiple query requests

Available for download on Wrox.com public class MyProvider extends ContentProvider {

private static final String myURI = "content://com.paad.provider.myapp/items"; public static final Uri CONTENT_URI = Uri.parse(myURI);

©Override public boolean onCreate() {

// TODO: Construct the underlying database. return true;

// Create the constants used to differentiate between the different URI // requests.

private static final int ALLROWS = 1; private static final int SINGLE_ROW = 2;

private static final UriMatcher uriMatcher;

// Populate the UriMatcher object, where a URI ending in 'items' will // correspond to a request for all items, and 'items/[rowID]' // represents a single row. static {

uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("com.paad.provider.myApp", "items", ALLROWS); uriMatcher.addURI("com.paad.provider.myApp", "items/#", SINGLE_ROW);

You can use the same technique to expose alternative URIs for different subsets of data, or different tables within your database, using the same Content Provider.

It's also good practice to expose the name of each of the columns available in your provider, to simplify extracting data from a query-result Cursor.

Mobile Apps Made Easy

Mobile Apps Made Easy

Quick start guide to skyrocket your offline and online business success with mobile apps. If you know anything about mobile devices, you’ve probably heard that famous phrase coined by one of the mobile device’s most prolific creators proclaiming that there’s an app for pretty much everything.

Get My Free Training Guide


Post a comment