Android 4
14. On Notepad Example - Part A
This chapter
- 14.0 Notepad Example
- 14.1 ListActivity, SQLite database, Binding data
- 14.2 Constructing a new Activity and Asynchronous Activity
At this stage of learning, I think it's the right time to look back what we've learned. In this chapter, we'll reconstruct the Notepad example from Android tutorial. Some of the topics are we've already dealt with and some of them are new.
Construct a simple notes list that lets the user add new notes but not edit them. Demonstrates the basics of ListActivity and creating and handling menu options. Uses a SQLite database to store the notes.
Next, we will add a second Activity to the application. Demonstrates constructing a new Activity, adding it to the Android manifest, passing data between the activities, and using more advanced screen layout. Also shows how to invoke another Activity to return a result, using startActivityForResult().
Finally, we'll add handling of life-cycle events to the application, to let it maintain application state across the life cycle.
By going through this exercise, we'll learn:
-
Section 14.1 ListActivity, SQLite database, Binding data
- The basics of ListActivities and creating and handling menu options.
- How to use a SQLite database to store the notes.
- How to bind data from a database cursor into a ListView using a SimpleCursorAdapter.
- The basics of screen layouts, including how to lay out a list view, how we can add items to the activity menu, and how the activity handles those menu selections.
- Constructing a new Activity and adding it to the Android manifest Invoking another.
- Activity asynchronously using startActivityForResult().
- Passing data between Activity in Bundle objects.
- How to use a more advanced screen layout.
- How to create a context menu.
Section 14.2 Constructing a new Activity and Asynchronous Activity
In this section, we'll construct a simple notes list that lets the user add new notes but not edit them. It will demonstrate the basics of ListActivity and creating and handling menu options. Uses a SQLite database to store the notes.
We'll start by looking at the file already written, NotesDbAdapter.java.:
package com.bogotobogo.mynotepada; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; /** * Simple notes database access helper class. Defines the basic CRUD operations * for the notepad example, and gives the ability to list all notes as well as * retrieve or modify a specific note. * * This has been improved from the first version of this tutorial through the * addition of better error handling and also using returning a Cursor instead * of using a collection of inner classes (which is less scalable and not * recommended). */ public class NotesDbAdapter { public static final String KEY_TITLE = "title"; public static final String KEY_BODY = "body"; public static final String KEY_ROWID = "_id"; private static final String TAG = "NotesDbAdapter"; private DatabaseHelper mDbHelper; private SQLiteDatabase mDb; /** * Database creation sql statement */ private static final String DATABASE_CREATE = "create table notes (_id integer primary key autoincrement, " + "title text not null, body text not null);"; private static final String DATABASE_NAME = "data"; private static final String DATABASE_TABLE = "notes"; private static final int DATABASE_VERSION = 2; private final Context mCtx; private static class DatabaseHelper extends SQLiteOpenHelper { DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS notes"); onCreate(db); } } /** * Constructor - takes the context to allow the database to be * opened/created * * @param ctx the Context within which to work */ public NotesDbAdapter(Context ctx) { this.mCtx = ctx; } /** * Open the notes database. If it cannot be opened, try to create a new * instance of the database. If it cannot be created, throw an exception to * signal the failure * * @return this (self reference, allowing this to be chained in an * initialization call) * @throws SQLException if the database could be neither opened or created */ public NotesDbAdapter open() throws SQLException { mDbHelper = new DatabaseHelper(mCtx); mDb = mDbHelper.getWritableDatabase(); return this; } public void close() { mDbHelper.close(); } /** * Create a new note using the title and body provided. If the note is * successfully created return the new rowId for that note, otherwise return * a -1 to indicate failure. * * @param title the title of the note * @param body the body of the note * @return rowId or -1 if failed */ public long createNote(String title, String body) { ContentValues initialValues = new ContentValues(); initialValues.put(KEY_TITLE, title); initialValues.put(KEY_BODY, body); return mDb.insert(DATABASE_TABLE, null, initialValues); } /** * Delete the note with the given rowId * * @param rowId id of note to delete * @return true if deleted, false otherwise */ public boolean deleteNote(long rowId) { return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0; } /** * Return a Cursor over the list of all notes in the database * * @return Cursor over all notes */ public Cursor fetchAllNotes() { return mDb.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_TITLE, KEY_BODY}, null, null, null, null, null); } /** * Return a Cursor positioned at the note that matches the given rowId * * @param rowId id of note to retrieve * @return Cursor positioned to matching note, if found * @throws SQLException if note could not be found/retrieved */ public Cursor fetchNote(long rowId) throws SQLException { Cursor mCursor = mDb.query(true, DATABASE_TABLE, new String[] {KEY_ROWID, KEY_TITLE, KEY_BODY}, KEY_ROWID + "=" + rowId, null, null, null, null, null); if (mCursor != null) { mCursor.moveToFirst(); } return mCursor; } /** * Update the note using the details provided. The note to be updated is * specified using the rowId, and it is altered to use the title and body * values passed in * * @param rowId id of note to update * @param title value to set note title to * @param body value to set note body to * @return true if the note was successfully updated, false otherwise */ public boolean updateNote(long rowId, String title, String body) { ContentValues args = new ContentValues(); args.put(KEY_TITLE, title); args.put(KEY_BODY, body); return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0; } }
Take a look at the NotesDbAdapter class - this class is provided to encapsulate data access to a SQLite database that will hold our notes data and allow us to update it.
At the top of the class are some constant definitions that will be used in the application to look up data from the proper field names in the database. There is also a database creation string defined, which is used to create a new database schema if one doesn't exist already.
public static final String KEY_TITLE = "title"; public static final String KEY_BODY = "body"; public static final String KEY_ROWID = "_id"; private static final String TAG = "NotesDbAdapter"; private DatabaseHelper mDbHelper; private SQLiteDatabase mDb;
Our database will have the name data, and have a single table called notes, which in turn has three fields: _id, title and body.
/** * Database creation sql statement */ private static final String DATABASE_CREATE = "create table notes (_id integer primary key autoincrement, " + "title text not null, body text not null);"; private static final String DATABASE_NAME = "data"; private static final String DATABASE_TABLE = "notes"; private static final int DATABASE_VERSION = 2;The _id is named with an underscore convention used in a number of places inside the Android SDK and helps keep a track of state. The _id usually has to be specified when querying or updating the database (in the column projections and so on). The other two fields are simple text fields that will store data.
The constructor for NotesDbAdapter takes a Context, which allows it to communicate with aspects of the Android operating system. This is quite common for classes that need to touch the Android system in some way. The Activity class implements the Context class, so usually we will just pass this from our Activity, when needing a Context.
public NotesDbAdapter(Context ctx) { this.mCtx = ctx; }
The open() method calls up an instance of DatabaseHelper, which is our local implementation of the SQLiteOpenHelper class. It calls getWritableDatabase(), which handles creating/opening a database for us. Basically, this opens notes database. If it cannot be opened, it will try to create a new instance of the database. If it cannot be created, throw an exception to signal the failure. It returns this, self reference, allowing this to be chained in an initialization call.
public NotesDbAdapter open() throws SQLException { mDbHelper = new DatabaseHelper(mCtx); mDb = mDbHelper.getWritableDatabase(); return this; }close() just closes the database, releasing resources related to the connection.
public void close() { mDbHelper.close(); }
createNote() takes strings for the title and body of a new note, then creates that note in the database. Assuming the new note is created successfully, the method also returns the row _id value for the newly created note.
public long createNote(String title, String body) { ContentValues initialValues = new ContentValues(); initialValues.put(KEY_TITLE, title); initialValues.put(KEY_BODY, body); return mDb.insert(DATABASE_TABLE, null, initialValues); }deleteNote() takes a rowId for a particular note, and deletes that note from the database. It returns true if deleted, false otherwise.
public boolean deleteNote(long rowId) { return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0; }
fetchAllNotes() issues a query to return a Cursor over all notes in the database. The query() call is worth examination and understanding. The first field is the name of the database table to query (in this case DATABASE_TABLE is "notes"). The next is the list of columns we want returned, in this case we want the _id, title and body columns so these are specified in the String array. The remaining fields are, in order: selection, selectionArgs, groupBy, having and orderBy. Having these all null means we want all data, need no grouping, and will take the default order. See SQLiteDatabase for more details.
public Cursor fetchAllNotes() { return mDb.query(DATABASE_TABLE, new String[] {KEY_ROWID, KEY_TITLE, KEY_BODY}, null, null, null, null, null); }
Note that a Cursor is returned rather than a collection of rows. This allows Android to use resources efficiently -- instead of putting lots of data straight into memory the cursor will retrieve and release data as it is needed, which is much more efficient for tables with lots of rows.
fetchNote() is similar to fetchAllNotes() but just gets one note with the rowId we specify. In other words, it gets a Cursor positioned at the note that matches the given rowId. It uses a slightly different version of the SQLiteDatabase query() method. The first parameter (set true) indicates that we are interested in one distinct result. The selection parameter (the fourth parameter) has been specified to search only for the row "where _id =" the rowId we passed in. So we are returned a Cursor on the one row.
public Cursor fetchNote(long rowId) throws SQLException { Cursor mCursor = mDb.query(true, DATABASE_TABLE, new String[] {KEY_ROWID,KEY_TITLE, KEY_BODY}, KEY_ROWID + "=" + rowId, null,null, null, null, null); if (mCursor != null) { mCursor.moveToFirst(); } return mCursor; }
And finally, updateNote() takes a rowId, title and body, and uses a ContentValues instance to update the note of the given rowId.
public boolean updateNote(long rowId, String title, String body) { ContentValues args = new ContentValues(); args.put(KEY_TITLE, title); args.put(KEY_BODY, body); return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0; }
Let's look at the file, notepad_list.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> </LinearLayout>
This is a mostly-empty layout definition file. Here are some things we should know about a layout file:
<?xml version="1.0" encoding="utf-8"?>.
xmlns:android="http://schemas.android.com/apk/res/android"
We need to create the layout to hold our list. Add code inside of the LinearLayout element so the whole file looks like this:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ListView android:id="@android:id/list" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <TextView android:id="@android:id/empty" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/no_notes"/> </LinearLayout>
- The @ symbol in the id strings of the ListView and TextView tags means that the XML parser should parse and expand the rest of the id string and use an ID resource.
- The ListView and TextView can be thought as two alternative views, only one of which will be displayed at once. ListView will be used when there are notes to be shown, while the TextView (which has a default value of "No Notes Yet!" defined as a string resource in res/values/strings.xml) will be displayed if there aren't any notes to display.
- The list and empty IDs are provided for us by the Android platform, so, we must prefix the id with android: (e.g., @android:id/list).
- The View with the empty id is used automatically when the ListAdapter has no data for the ListView. The ListAdapter knows to look for this name by default. Alternatively, we could change the default empty view by using setEmptyView(View) on the ListView.
More broadly, the android.R class is a set of predefined resources provided for us by the platform, while our project's R class is the set of resources our project has defined. Resources found in the android.R resource class can be used in the XML files by using the android: name space prefix (as we see here).
To make the list of notes in the ListView, we also need to define a View for each row:
- Create a new file under res/layout called notes_row.xml.
- Add the following contents (note: again the XML header is used, and the first node defines the Android XML namespace)
<?xml version="1.0" encoding="utf-8"?> <TextView android:id="@+id/text1" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
This is the View that will be used for each notes title row - it has only one text field in it. In this case we create a new id called text1. The + after the @ in the id string indicates that the id should be automatically created as a resource if it does not already exist, so we are defining text1 on the fly and then using it. - Save the file.
Open the R.java class in the project and look at it, we should see new definitions for notes_row and text1, (our new definitions) meaning we can now gain access to these from our code.
Next, open the "MyNotepadA" class in the source. In the following steps, we are going to alter this class to become a list adapter and display our notes, and also allow us to add new notes.
Take a look through the existing code in
package com.bogotobogo.mynotepada; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; public class MyNotepadA extends Activity { private int mNoteNumber = 1; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public boolean onCreateOptionsMenu(Menu menu) { // TODO Auto-generated method stub return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { // TODO Auto-generated method stub return super.onOptionsItemSelected(item); } }
- onCreate() is called when the activity is started - it is a little like the "main" method for an Activity. We use this to set up resources and state for the activity when it is running.
- onCreateOptionsMenu() is used to populate the menu for the Activity. This is shown when the user hits the menu button, and has a list of options they can select (like "Create Note").
- onOptionsItemSelected() is the other half of the menu equation, it is used to handle events generated from the menu (e.g., when the user selects the "Create Note" item).
Change the inheritance of MyNotepadA from Activity to ListActivity:
public class MyNotepadA extends ListActivity
We will have to import ListActivity into the MyNotepadA class.
Fill out the body of the onCreate() method. Here we will set the title for the Activity (shown at the top of the screen), use the notepad_list layout we created in XML, set up the NotesDbAdapter instance that will access notes data, and populate the list with the available note titles:
public class MyNotepadA extends ListActivity { private int mNoteNumber = 1; private NotesDbAdapter mDbHelper; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notepad_list); mDbHelper = new NotesDbAdapter(this); mDbHelper.open(); fillData(); }
- In the onCreate method, call super.onCreate() with the savedInstanceState parameter that's passed in.
- Call setContentView() and pass R.layout.notepad_list.
- At the top of the class, create a new private class field called mDbHelper of class NotesDbAdapter.
- Back in the onCreate method, construct a new NotesDbAdapter instance and assign it to the mDbHelper field (pass this into the constructor for DBHelper)
- Call the open() method on mDbHelper to open (or create) the database.
- Finally, call a new method fillData(), which will get the data and populate the ListView using the helper - we haven't defined this method yet.
Here is the onCreateOptionsMenu() method:
@Override public boolean onCreateOptionsMenu(Menu menu) { boolean result = super.onCreateOptionsMenu(menu); menu.add(0, INSERT_ID, 0, R.string.menu_insert); return result; }
We will now create the "Add Item" button that can be accessed by pressing the menu button on the device. We'll specify that it occupy the first position in the menu.
- In strings.xml resource (under res/values), add a new string named "menu_insert" with its value set to Add Item:
<string name="menu_insert">Add Item</string>
Then save the file and return to MyNotepadA. - Create a menu position constant at the top of the class: public static final int INSERT_ID = Menu.FIRST;
The arguments passed to add() indicate: a group identifier for this menu (none, in this case), a unique ID (defined above), the order of the item (zero indicates no preference), and the resource of the string to use for the item.
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case INSERT_ID: createNote(); return true; } return super.onOptionsItemSelected(item); }
This is going to handle our new "Add Note" menu item. When this is selected, the onOptionsItemSelected() method will be called with the item.getId() set to INSERT_ID (the constant we used to identify the menu item). We can detect this, and take the appropriate actions:
- The super.onOptionsItemSelected(item) method call goes at the end of this method - we want to catch our events first!
- Write a switch statement on item.getItemId(). In the case of INSERT_ID, call a new method, createNote(), and return true, because we have handled this event and do not want to propagate it through the system.
- Return the result of the superclass' onOptionsItemSelected() method at the end.
private void createNote() { String noteName = "Note " + mNoteNumber++; mDbHelper.createNote(noteName, ""); fillData(); }
In this first version of our application, createNote() is not going to be very useful. We will simply create a new note with a title assigned to it based on a counter ("Note 1", "Note 2"...) and with an empty body. At present we have no way of editing the contents of a note, so for now we will have to be content making one with some default values:
- Construct the name using "Note" and the counter we defined in the class:
String noteName = "Note " + mNoteNumber++
- Call mDbHelper.createNote() using noteName as the title and "" for the body
- Call fillData() to populate the list of notes (inefficient but simple) - we'll create this method next.
private void fillData() { // Get all of the notes from the database and create the item list Cursor c = mDbHelper.fetchAllNotes(); startManagingCursor(c); String[] from = new String[] { NotesDbAdapter.KEY_TITLE }; int[] to = new int[] { R.id.text1 }; // Now create an array adapter and set it to display using our row SimpleCursorAdapter notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to); setListAdapter(notes); }
This method uses SimpleCursorAdapter, which takes a database Cursor and binds it to fields provided in the layout. These fields define the row elements of our list (in this case we use the text1 field in our notes_row.xml layout), so this allows us to easily populate the list with entries from our database.
To do this we have to provide a mapping from the title field in the returned Cursor, to our text1 TextView, which is done by defining two arrays: the first a string array with the list of columns to map from (just "title" in this case, from the constant NotesDbAdapter.KEY_TITLE) and, the second, an int array containing references to the views that we'll bind the data to (the R.id.text1 TextView).
Here's what we've done:
- After obtaining the Cursor from mDbHelper.fetchAllNotes(), we use an Activity method called startManagingCursor() that allows Android to take care of the Cursor lifecycle instead of us needing to worry about it. (We will cover the implications of the lifecycle in exercise 3, but for now just know that this allows Android to do some of our resource management work for us.)
- Then we create a string array in which we declare the column(s) we want (just the title, in this case), and an int array that defines the View(s) to which we'd like to bind the columns (these should be in order, respective to the string array, but here we only have one for each).
- Next is the SimpleCursorAdapter instantiation. Like many classes in Android, the SimpleCursorAdapter needs a Context in order to do its work, so we pass in this for the context (since subclasses of Activity implement Context). We pass the notes_row View we created as the receptacle for the data, the Cursor we just created, and then our arrays.
In the future, remember that the mapping between the from columns and to resources is done using the respective ordering of the two arrays. If we had more columns we wanted to bind, and more Views to bind them in to, we would specify them in order, for example we might use
{ NotesDbAdapter.KEY_TITLE, NotesDbAdapter.KEY_BODY }
and
{ R.id.text1, R.id.text2 }
to bind two fields into the row (and we would also need to define text2 in the notes_row.xml, for the body text). This is how we can bind multiple fields into a single row (and get a custom row layout as well).
strings.xml: looks like this:
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">MyNotepadA</string> <string name="no_notes">No Notes Yet</string> <string name="menu_insert">Add Item</string> </resources>
Final version of MyNotepadA.java should look like this:
package com.bogotobogo.mynotepada; import android.app.ListActivity; import android.database.Cursor; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.SimpleCursorAdapter; public class MyNotepadA extends ListActivity { public static final int INSERT_ID = Menu.FIRST; private int mNoteNumber = 1; private NotesDbAdapter mDbHelper; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notepad_list); mDbHelper = new NotesDbAdapter(this); mDbHelper.open(); fillData(); } @Override public boolean onCreateOptionsMenu(Menu menu) { boolean result = super.onCreateOptionsMenu(menu); menu.add(0, INSERT_ID, 0, R.string.menu_insert); return result; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case INSERT_ID: createNote(); return true; } return super.onOptionsItemSelected(item); } private void createNote() { String noteName = "Note " + mNoteNumber++; mDbHelper.createNote(noteName, ""); fillData(); } private void fillData() { // Get all of the notes from the database and create the item list Cursor c = mDbHelper.fetchAllNotes(); startManagingCursor(c); String[] from = new String[] { NotesDbAdapter.KEY_TITLE }; int[] to = new int[] { R.id.text1 }; // Now create an array adapter and set it to display using our row SimpleCursorAdapter notes = new SimpleCursorAdapter(this, R.layout.notes_row, c, from, to); setListAdapter(notes); } }
Time to run it!
- Right click on the MyNotepadA project.
- From the popup menu, select Run As > Android Application.
- If we see a dialog come up, select Android Launcher as the way of running the application (we can also use the link near the top of the dialog to set this as our default for the workspace; this is recommended as it will stop the plugin from asking us this every time).
- Add new notes by hitting the menu button and selecting Add Item from the menu.
Files used in MyNotepadA project:
MyNotePadA.zipNext sections:
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization