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
In this section, we'll add a second Activity to the application.
This section also 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().
We will also allow the user to delete existing notes through a context menu. The new Activity assumes responsibility for creating new notes by collecting user input and packing it into a return Bundle provided by the intent. This exercise demonstrates:
- 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
When our application started:
At the click of "Add Note", then type in "BBB":
Here is our new class, MyNotepadB.java:
package com.bogotobogo.mynotepadb; import android.os.Bundle; import android.app.ListActivity; import android.content.Intent; import android.database.Cursor; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.AdapterView.AdapterContextMenuInfo; public class MyNotepadB extends ListActivity { private static final int ACTIVITY_CREATE=0; private static final int ACTIVITY_EDIT=1; private static final int INSERT_ID = Menu.FIRST; private static final int DELETE_ID = Menu.FIRST + 1; private NotesDbAdapter mDbHelper; private Cursor mNotesCursor; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notes_list); mDbHelper = new NotesDbAdapter(this); mDbHelper.open(); fillData(); registerForContextMenu(getListView()); } private void fillData() { // Get all of the rows from the database and create the item list mNotesCursor = mDbHelper.fetchAllNotes(); startManagingCursor(mNotesCursor); // Create an array to specify the fields we want to display in the list (only TITLE) String[] from = new String[]{NotesDbAdapter.KEY_TITLE}; // and an array of the fields we want to bind those fields to (in this case just text1) int[] to = new int[]{R.id.text1}; // Now create a simple cursor adapter and set it to display SimpleCursorAdapter notes = new SimpleCursorAdapter(this, R.layout.notes_row, mNotesCursor, from, to); setListAdapter(notes); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, INSERT_ID,0, R.string.menu_insert); return true; } @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch(item.getItemId()) { case INSERT_ID: createNote(); return true; } return super.onMenuItemSelected(featureId, item); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, DELETE_ID, 0, R.string.menu_delete); } @Override public boolean onContextItemSelected(MenuItem item) { switch(item.getItemId()) { case DELETE_ID: AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); mDbHelper.deleteNote(info.id); fillData(); return true; } return super.onContextItemSelected(item); } private void createNote() { Intent i = new Intent(this, NoteEdit.class); startActivityForResult(i, ACTIVITY_CREATE); } @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); Cursor c = mNotesCursor; c.moveToPosition(position); Intent i = new Intent(this, NoteEdit.class); i.putExtra(NotesDbAdapter.KEY_ROWID, id); i.putExtra(NotesDbAdapter.KEY_TITLE, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE))); i.putExtra(NotesDbAdapter.KEY_BODY, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY))); startActivityForResult(i, ACTIVITY_EDIT); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); Bundle extras = intent.getExtras(); switch(requestCode) { case ACTIVITY_CREATE: String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.createNote(title, body); fillData(); break; case ACTIVITY_EDIT: Long rowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (rowId != null) { String editTitle = extras.getString(NotesDbAdapter.KEY_TITLE); String editBody = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.updateNote(rowId, editTitle, editBody); } fillData(); break; } } }
Here is a summary for our new class:
- At the top of the MyNotepadB class, we will notice several new constants have been defined along with a new mNotesCursor field used to hold the cursor we are using.
- Note also that the fillData() method has a few more comments and now uses the new field to store the notes Cursor. The onCreate() method is unchanged from the first exercise. Also notice that the member field used to store the notes Cursor is now called mNotesCursor. The m denotes a member field and is part of the Android coding style standards.
- There are also a couple of new overridden methods (onCreateContextMenu(), onContextItemSelected(), onListItemClick() and onActivityResult())
- putExtra() is the method to add items into the extras Bundle to pass in to intent invocations. Here, we are using the Bundle to pass in the title, body and mRowId of the note we want to edit.
- The details of the note are pulled out from our query Cursor, which we move to the proper position for the element that was selected in the list, with the moveToPosition() method.
- With the extras added to the Intent, we invoke the Intent on the NoteEdit class by passing startActivityForResult() the Intent and the request code. (The request code will be returned to onActivityResult as the requestCode parameter.)
- requestCode - the original request code specified in the Intent invocation (either ACTIVITY_CREATE or ACTIVITY_EDIT for us).
- resultCode - the result (or error code) of the call, this should be zero if everything was OK, but may have a non-zero code indicating that something failed. There are standard result codes available, and we can also create our own constants to indicate specific problems.
- intent - this is an Intent created by the Activity returning results. It can be used to return data in the Intent "extras."
- We are handling both the ACTIVITY_CREATE and ACTIVITY_EDIT activity results in this method.
- In the case of a create, we pull the title and body from the extras (retrieved from the returned Intent) and use them to create a new note.
- In the case of an edit, we pull the mRowId as well, and use that to update the note in the database.
- fillData() at the end ensures everything is up to date .
- Inside onCreate(), set up the layout:
setContentView(R.layout.note_edit);
- Find the edit and button components we need:
These are found by the IDs associated to them in the R class, and need to be cast to the right type of View (EditText for the two text views, and Button for the confirm button):
mTitleText = (EditText) findViewById(R.id.title); mBodyText = (EditText) findViewById(R.id.body); Button confirmButton = (Button) findViewById(R.id.confirm);
Note that mTitleText and mBodyText are member fields (we need to declare them at the top of the class definition). - At the top of the class, we declare a Long mRowId private field to store the current mRowId being edited (if any).
- Continuing inside onCreate(), we added code to initialize the title, body and mRowId from the extras Bundle in the Intent (if it is present):
mRowId = null; Bundle extras = getIntent().getExtras(); if (extras != null) { String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (title != null) { mTitleText.setText(title); } if (body != null) { mBodyText.setText(body); } }
- We are pulling the title and body out of the extras Bundle that was set from the Intent invocation.
- We also null-protect the text field setting (i.e., we don't want to set the text fields to null accidentally).
- We created an onClickListener() for the button:
Listeners can be one of the more confusing aspects of UI implementation, but what we are trying to achieve in this case is simple. We want an onClick() method to be called when the user presses the confirm button, and use that to do some work and return the values of the edited note to the Intent caller. We do this using something called an anonymous inner class. This is a bit confusing to look at unless we have seen them before, but all we really need to take away from this is that we can refer to this code in the future to see how to create a listener and attach it to a button. (Listeners are a common idiom in Java development, particularly for user interfaces.) Here's the listener:confirmButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { Bundle bundle = new Bundle(); bundle.putString(NotesDbAdapter.KEY_TITLE, mTitleText.getText().toString()); bundle.putString(NotesDbAdapter.KEY_BODY, mBodyText.getText().toString()); if (mRowId != null) { bundle.putLong(NotesDbAdapter.KEY_ROWID, mRowId); } Intent mIntent = new Intent(); mIntent.putExtras(bundle); setResult(RESULT_OK, mIntent); finish(); } });
- Create a Bundle and put the title and body text into it using the constants defined in MyNotepadB as keys:
Bundle bundle = new Bundle(); bundle.putString(NotesDbAdapter.KEY_TITLE, mTitleText.getText().toString()); bundle.putString(NotesDbAdapter.KEY_BODY, mBodyText.getText().toString()); if (mRowId != null) { bundle.putLong(NotesDbAdapter.KEY_ROWID, mRowId); }
- Set the result information (the Bundle) in a new Intent and finish the Activity:
Intent mIntent = new Intent(); mIntent.putExtras(bundle); setResult(RESULT_OK, mIntent); finish();
- The Intent is simply our data carrier that carries our Bundle (with the title, body and mRowId).
- The setResult() method is used to set the result code and return Intent to be passed back to the Intent caller. In this case everything worked, so we return RESULT_OK for the result code.
- The
call is used to signal that the Activity is done (like a return call). Anything set in the Result will then be returned to the caller, along with execution control.
Let's take a closer look at the class.
Context menus should always be used when performing actions upon specific elements in the UI. When we register a View to a context menu, the context menu is revealed by performing a "long-click" on the UI component (press and hold the touchscreen or highlight and hold down the selection key for about two seconds).
In order for each list item in the ListView to register for the context menu, we call registerForContextMenu() and pass it our ListView. So, at the very end of the onCreate() method add this line:
registerForContextMenu(getListView());
Because our Activity extends the ListActivity class, getListView() will return us the local ListView object for the Activity. Now, each list item in this ListView will activate the context menu.
onCreateContextMenu() callback is similar to the other menu callback used for the options menu. Here, we add just one line, which will add a menu item to delete a note. Call menu.add() like so:
public boolean onCreateContextMenu(Menu menu, View v ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, DELETE_ID, 0, R.string.menu_delete); }
The onCreateContextMenu() callback passes some other information in addition to the Menu object, such as the View that has been triggered for the menu and an extra object that may contain additional information about the object selected. However, we don't care about these here, because we only have one kind of object in the Activity that uses context menus. In the next step, we'll handle the menu item selection.
Now that we've registered our ListView for a context menu and defined our context menu item, we need to handle the callback when it is selected. For this, we need to identify the list ID of the selected item, then delete it. So fill in the onContextItemSelected() method like this:
public boolean onContextItemSelected(MenuItem item) { switch(item.getItemId()) { case DELETE_ID: AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); mDbHelper.deleteNote(info.id); fillData(); return true; } return super.onContextItemSelected(item); }
Here, we retrieve the AdapterContextMenuInfo with getMenuInfo(). The id field of this object tells us the position of the item in the ListView. We then pass this to the deleteNote() method of our NotesDbAdapter and the note is deleted. That's it for the context menu - notes can now be deleted.
We creates a new Intent to create a note (ACTIVITY_CREATE) using the NoteEdit class. Then fire the Intent using the startActivityForResult() method call:
Intent i = new Intent(this, NoteEdit.class); startActivityForResult(i, ACTIVITY_CREATE);
This form of the Intent call targets a specific class in our Activity, in this case NoteEdit. Since the Intent class will need to communicate with the Android operating system to route requests, we also have to provide a Context (this).
The startActivityForResult() method fires the Intent in a way that causes a method in our Activity to be called when the new Activity is completed. The method in our Activity that receives the callback is called onActivityResult() and we will implement it in a later step. The other way to call an Activity is using startActivity() but this is a "fire-and-forget" way of calling it - in this manner, our Activity is not informed when the Activity is completed, and there is no way to return result information from the called Activity with startActivity().
onListItemClick() is a callback method that we'll override. It is called when the user selects an item from the list. It is passed four parameters: the ListView object it was invoked from, the View inside the ListView that was clicked on, the position in the list that was clicked, and the mRowId of the item that was clicked. In this instance we can ignore the first two parameters (we only have one ListView it could be), and we ignore the mRowId as well. All we are interested in is the position that the user selected. We use this to get the data from the correct row, and bundle it up to send to the NoteEdit Activity.
In our implementation of the callback, the method creates an Intent to edit the note using the NoteEdit class. It then adds data into the extras Bundle of the Intent, which will be passed to the called Activity. We use it to pass in the title and body text, and the mRowId for the note we are editing. Finally, it will fire the Intent using the startActivityForResult() method call. Here's the code that belongs in onListItemClick():
super.onListItemClick(l, v, position, id); Cursor c = mNotesCursor; c.moveToPosition(position); Intent i = new Intent(this, NoteEdit.class); i.putExtra(NotesDbAdapter.KEY_ROWID, id); i.putExtra(NotesDbAdapter.KEY_TITLE, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE))); i.putExtra(NotesDbAdapter.KEY_BODY, c.getString( c.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY))); startActivityForResult(i, ACTIVITY_EDIT);
We assign the mNotesCursor field to a local variable at the start of the method. This is done as an optimization of the Android code. Accessing a local variable is much more efficient than accessing a field in the Dalvik VM, so by doing this we make only one access to the field, and five accesses to the local variable, making the routine much more efficient. It is recommended that we use this optimization when possible.
The above createNote() and onListItemClick() methods use an asynchronous Intent invocation. We need a handler for the callback, so here we fill in the body of the onActivityResult(). onActivityResult() is the overridden method which will be called when an Activity returns with a result. (Remember, an Activity will only return a result if launched with startActivityForResult.) The parameters provided to the callback are:
The combination of startActivityForResult() and onActivityResult() can be thought of as an asynchronous RPC (remote procedure call) and forms the recommended way for an Activity to invoke another and share services.
Here's the code that belongs in our onActivityResult():
super.onActivityResult(requestCode, resultCode, intent); Bundle extras = intent.getExtras(); switch(requestCode) { case ACTIVITY_CREATE: String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.createNote(title, body); fillData(); break; case ACTIVITY_EDIT: Long mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (mRowId != null) { String editTitle = extras.getString(NotesDbAdapter.KEY_TITLE); String editBody = extras.getString(NotesDbAdapter.KEY_BODY); mDbHelper.updateNote(mRowId, editTitle, editBody); } fillData(); break; }
Here is the note_edit.xml file:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title" /> <EditText android:id="@+id/title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1"/> </LinearLayout> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/body" /> <EditText android:id="@+id/body" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" android:scrollbars="vertical" /> <Button android:id="@+id/confirm" android:text="@string/confirm" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
Let's take a look at the file. This is the UI code for the Note Editor.
used here that we haven't seen before: android:layout_weight (in this case set to use the value 1 in each case).
layout_weight is used in LinearLayouts to assign "importance" to Views within the layout. All Views have a default layout_weight of zero, meaning they take up only as much room on the screen as they need to be displayed. Assigning a value higher than zero will split up the rest of the available space in the parent View, according to the value of each View's layout_weight and its ratio to the overall layout_weight specified in the current layout for this and other View elements.
To give an example: let's say we have a text label and two text edit elements in a horizontal row. The label has no layout_weight specified, so it takes up the minimum space required to render. If the layout_weight of each of the two text edit elements is set to 1, the remaining width in the parent layout will be split equally between them (because we claim they are equally important). If the first one has a layout_weight of 1 and the second has a layout_weight of 2, then one third of the remaining space will be given to the first, and two thirds to the second (because we claim the second one is more important).
This layout also demonstrates how to nest multiple layouts inside each other to achieve a more complex and pleasant layout. In this example, a horizontal linear layout is nested inside the vertical one to allow the title label and text field to be alongside each other, horizontally.
Here is the source code for NoteEdit class, NoteEdit.java.
package com.bogotobogo.mynotepadb; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; public class NoteEdit extends Activity { private EditText mTitleText; private EditText mBodyText; private Long mRowId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.note_edit); mTitleText = (EditText) findViewById(R.id.title); mBodyText = (EditText) findViewById(R.id.body); Button confirmButton = (Button) findViewById(R.id.confirm); mRowId = null; Bundle extras = getIntent().getExtras(); if (extras != null) { String title = extras.getString(NotesDbAdapter.KEY_TITLE); String body = extras.getString(NotesDbAdapter.KEY_BODY); mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID); if (title != null) { mTitleText.setText(title); } if (body != null) { mBodyText.setText(body); } } confirmButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { Bundle bundle = new Bundle(); bundle.putString(NotesDbAdapter.KEY_TITLE, mTitleText.getText().toString()); bundle.putString(NotesDbAdapter.KEY_BODY, mBodyText.getText().toString()); if (mRowId != null) { bundle.putLong(NotesDbAdapter.KEY_ROWID, mRowId); } Intent mIntent = new Intent(); mIntent.putExtras(bundle); setResult(RESULT_OK, mIntent); finish(); } }); } }
Note that NoteEdit class extends android.app.Activity.
Look at the onCreate(). This will set the title of our new Activity to say "Edit Note" (one of the strings defined in strings.xml). It will also set the content view to use our note_edit.xml layout file. We can then grab handles to the title and body text edit views, and the confirm button, so that our class can use them to set and get the note title and body, and attach an event to the confirm button for when it is pressed by the user.
We can then unbundle the values that were passed in to the Activity with the extras Bundle attached to the calling Intent. We'll use them to pre-populate the title and body text edit views so that the user can edit them. Then we will grab and store the mRowId so we can keep track of what note the user is editing.
The onClick() method of the
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.bogotobogo.mynotepadb" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="14" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".MyNotepadB" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".NoteEdit" /> </application> </manifest>
Finally, the new Activity is defined in the manifest file:
Before the new Activity can be seen by Android, it needs its own Activity entry in the AndroidManifest.xml file. This is to let the system know that it is there and can be called. We could also specify which IntentFilters the activity implements here, but we are going to skip this for now and just let Android know that the Activity is defined.
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">My Notepad B</string> <string name="no_notes">No Notes Yet</string> <string name="menu_insert">Add Note</string> <string name="menu_delete">Delete Note</string> <string name="title">Title</string> <string name="body">Body</string> <string name="confirm">Confirm</string> <string name="edit_note">Edit Note</string> </resources>
Now time to run it!
We should now be able to add real notes from the menu, as well as delete an existing one. Notice that in order to delete, we must first use the directional controls on the device to highlight the note. Furthermore, selecting a note title from the list should bring up the note editor to let ys edit it. Press confirm when finished to save the changes back to the database.
Files used in MyNotePadB project:
MyNotePadB.zipPrevious sections:
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization