366 lines
14 KiB
Plaintext
366 lines
14 KiB
Plaintext
|
page.title=Notepad Exercise 3
|
||
|
parent.title=Notepad Tutorial
|
||
|
parent.link=index.html
|
||
|
@jd:body
|
||
|
|
||
|
|
||
|
<p><em>In this exercise, you will use life-cycle event callbacks to store and
|
||
|
retrieve application state data. This exercise demonstrates:</em></p>
|
||
|
<ul>
|
||
|
<li><em>Life-cycle events and how your application can use them</em></li>
|
||
|
<li><em>Techniques for maintaining application state</em></li>
|
||
|
</ul>
|
||
|
|
||
|
<div style="float:right;white-space:nowrap">
|
||
|
[<a href="notepad-ex1.html">Exercise 1</a>]
|
||
|
[<a href="notepad-ex2.html">Exercise 2</a>]
|
||
|
<span style="color:#BBB;">
|
||
|
[<a href="notepad-ex3.html" style="color:#BBB;">Exercise 3</a>]
|
||
|
</span>
|
||
|
[<a href="notepad-extra-credit.html">Extra Credit</a>]
|
||
|
</div>
|
||
|
|
||
|
<h2>Step 1</h2>
|
||
|
|
||
|
<p>Import <code>Notepadv3</code> into Eclipse. If you see an error about
|
||
|
<code>AndroidManifest.xml,</code> or some problems related to an Android zip
|
||
|
file, right click on the project and select <strong>Android Tools</strong> >
|
||
|
<strong>Fix Project Properties</strong> from the popup menu. The starting point for this exercise is
|
||
|
exactly where we left off at the end of the Notepadv2. </p>
|
||
|
<p>The current application has some problems — hitting the back button when editing
|
||
|
causes a crash, and anything else that happens during editing will cause the
|
||
|
edits to be lost.</p>
|
||
|
<p>To fix this, we will move most of the functionality for creating and editing
|
||
|
the note into the NoteEdit class, and introduce a full life cycle for editing
|
||
|
notes.</p>
|
||
|
|
||
|
<ol>
|
||
|
<li>Remove the code in <code>NoteEdit</code> that parses the title and body
|
||
|
from the extras Bundle.
|
||
|
<p>Instead, we are going to use the <code>DBHelper</code> class
|
||
|
to access the notes from the database directly. All we need passed into the
|
||
|
NoteEdit Activity is a <code>mRowId</code> (but only if we are editing, if creating we pass
|
||
|
nothing). Remove these lines:</p>
|
||
|
<pre>
|
||
|
String title = extras.getString(NotesDbAdapter.KEY_TITLE);
|
||
|
String body = extras.getString(NotesDbAdapter.KEY_BODY);</pre>
|
||
|
</li>
|
||
|
<li>We will also get rid of the properties that were being passed in
|
||
|
the <code>extras</code> Bundle, which we were using to set the title
|
||
|
and body text edit values in the UI. So delete:
|
||
|
<pre>
|
||
|
if (title != null) {
|
||
|
mTitleText.setText(title);
|
||
|
}
|
||
|
if (body != null) {
|
||
|
mBodyText.setText(body);
|
||
|
}</pre>
|
||
|
</li>
|
||
|
</ol>
|
||
|
|
||
|
<h2>Step 2</h2>
|
||
|
|
||
|
<p>Create a class field for a <code>NotesDbAdapter</code> at the top of the NoteEdit class:</p>
|
||
|
<pre> private NotesDbAdapter mDbHelper;</pre>
|
||
|
<p>Also add an instance of <code>NotesDbAdapter</code> in the
|
||
|
<code>onCreate()</code> method (right below the <code>super.onCreate()</code> call):</p>
|
||
|
<pre>
|
||
|
mDbHelper = new NotesDbAdapter(this);<br>
|
||
|
mDbHelper.open();</pre>
|
||
|
|
||
|
<h2>Step 3</h2>
|
||
|
|
||
|
<p>In <code>NoteEdit</code>, we need to check the <var>savedInstanceState</var> for the
|
||
|
<code>mRowId</code>, in case the note
|
||
|
editing contains a saved state in the Bundle, which we should recover (this would happen
|
||
|
if our Activity lost focus and then restarted).</p>
|
||
|
<ol>
|
||
|
<li>
|
||
|
Replace the code that currently initializes the <code>mRowId</code>:<br>
|
||
|
<pre>
|
||
|
mRowId = null;
|
||
|
|
||
|
Bundle extras = getIntent().getExtras();
|
||
|
if (extras != null) {
|
||
|
mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID);
|
||
|
}
|
||
|
</pre>
|
||
|
with this:
|
||
|
<pre>
|
||
|
mRowId = (savedInstanceState == null) ? null :
|
||
|
(Long) savedInstanceState.getSerializable(NotesDbAdapter.KEY_ROWID);
|
||
|
if (mRowId == null) {
|
||
|
Bundle extras = getIntent().getExtras();
|
||
|
mRowId = extras != null ? extras.getLong(NotesDbAdapter.KEY_ROWID)
|
||
|
: null;
|
||
|
}
|
||
|
</pre>
|
||
|
</li>
|
||
|
<li>
|
||
|
Note the null check for <code>savedInstanceState</code>, and we still need to load up
|
||
|
<code>mRowId</code> from the <code>extras</code> Bundle if it is not
|
||
|
provided by the <code>savedInstanceState</code>. This is a ternary operator shorthand
|
||
|
to safely either use the value or null if it is not present.
|
||
|
</li>
|
||
|
<li>
|
||
|
Note the use of <code>Bundle.getSerializable()</code> instead of
|
||
|
<code>Bundle.getLong()</code>. The latter encoding returns a <code>long</code> primitive and
|
||
|
so can not be used to represent the case when <code>mRowId</code> is <code>null</code>.
|
||
|
</li>
|
||
|
</ol>
|
||
|
|
||
|
<h2>Step 4</h2>
|
||
|
|
||
|
<p>Next, we need to populate the fields based on the <code>mRowId</code> if we
|
||
|
have it:</p>
|
||
|
<pre>populateFields();</pre>
|
||
|
<p>This goes before the <code>confirmButton.setOnClickListener()</code> line.
|
||
|
We'll define this method in a moment.</p>
|
||
|
|
||
|
<h2>Step 5</h2>
|
||
|
|
||
|
<p>Get rid of the Bundle creation and Bundle value settings from the
|
||
|
<code>onClick()</code> handler method. The Activity no longer needs to
|
||
|
return any extra information to the caller. And because we no longer have
|
||
|
an Intent to return, we'll use the shorter version
|
||
|
of <code>setResult()</code>:</p>
|
||
|
<pre>
|
||
|
public void onClick(View view) {
|
||
|
setResult(RESULT_OK);
|
||
|
finish();
|
||
|
}</pre>
|
||
|
<p>We will take care of storing the updates or new notes in the database
|
||
|
ourselves, using the life-cycle methods.</p>
|
||
|
|
||
|
<p>The whole <code>onCreate()</code> method should now look like this:</p>
|
||
|
<pre>
|
||
|
super.onCreate(savedInstanceState);
|
||
|
|
||
|
mDbHelper = new NotesDbAdapter(this);
|
||
|
mDbHelper.open();
|
||
|
|
||
|
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 = (savedInstanceState == null) ? null :
|
||
|
(Long) savedInstanceState.getSerializable(NotesDbAdapter.KEY_ROWID);
|
||
|
if (mRowId == null) {
|
||
|
Bundle extras = getIntent().getExtras();
|
||
|
mRowId = extras != null ? extras.getLong(NotesDbAdapter.KEY_ROWID)
|
||
|
: null;
|
||
|
}
|
||
|
|
||
|
populateFields();
|
||
|
|
||
|
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||
|
|
||
|
public void onClick(View view) {
|
||
|
setResult(RESULT_OK);
|
||
|
finish();
|
||
|
}
|
||
|
|
||
|
});</pre>
|
||
|
|
||
|
<h2>Step 6</h2>
|
||
|
|
||
|
<p>Define the <code>populateFields()</code> method.</p>
|
||
|
<pre>
|
||
|
private void populateFields() {
|
||
|
if (mRowId != null) {
|
||
|
Cursor note = mDbHelper.fetchNote(mRowId);
|
||
|
startManagingCursor(note);
|
||
|
mTitleText.setText(note.getString(
|
||
|
note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)));
|
||
|
mBodyText.setText(note.getString(
|
||
|
note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY)));
|
||
|
}
|
||
|
}</pre>
|
||
|
<p>This method uses the <code>NotesDbAdapter.fetchNote()</code> method to find the right note to
|
||
|
edit, then it calls <code>startManagingCursor()</code> from the <code>Activity</code> class, which
|
||
|
is an Android convenience method provided to take care of the Cursor life-cycle. This will release
|
||
|
and re-create resources as dictated by the Activity life-cycle, so we don't need to worry about
|
||
|
doing that ourselves. After that, we just look up the title and body values from the Cursor
|
||
|
and populate the View elements with them.</p>
|
||
|
|
||
|
|
||
|
<h2>Step 7</h2>
|
||
|
|
||
|
<div class="sidebox-wrapper">
|
||
|
<div class="sidebox">
|
||
|
<h2>Why handling life-cycle events is important</h2>
|
||
|
<p>If you are used to always having control in your applications, you
|
||
|
might not understand why all this life-cycle work is necessary. The reason
|
||
|
is that in Android, you are not in control of your Activity, the
|
||
|
operating system is!</p>
|
||
|
<p>As we have already seen, the Android model is based around activities
|
||
|
calling each other. When one Activity calls another, the current Activity
|
||
|
is paused at the very least, and may be killed altogether if the
|
||
|
system starts to run low on resources. If this happens, your Activity will
|
||
|
have to store enough state to come back up later, preferably in the same
|
||
|
state it was in when it was killed.</p>
|
||
|
<p>
|
||
|
Android has a <a href="{@docRoot}guide/topics/fundamentals.html#lcycles">well-defined life
|
||
|
cycle</a>.
|
||
|
Lifecycle events can happen even if you are not handing off control to
|
||
|
another Activity explicitly. For example, perhaps a call comes in to the
|
||
|
handset. If this happens, and your Activity is running, it will be swapped
|
||
|
out while the call Activity takes over.</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<p>Still in the <code>NoteEdit</code> class, we now override the methods
|
||
|
<code>onSaveInstanceState()</code>, <code>onPause()</code> and
|
||
|
<code>onResume()</code>. These are our life-cycle methods
|
||
|
(along with <code>onCreate()</code> which we already have).</p>
|
||
|
|
||
|
<p><code>onSaveInstanceState()</code> is called by Android if the
|
||
|
Activity is being stopped and <strong>may be killed before it is
|
||
|
resumed!</strong> This means it should store any state necessary to
|
||
|
re-initialize to the same condition when the Activity is restarted. It is
|
||
|
the counterpart to the <code>onCreate()</code> method, and in fact the
|
||
|
<code>savedInstanceState</code> Bundle passed in to <code>onCreate()</code> is the same
|
||
|
Bundle that you construct as <code>outState</code> in the
|
||
|
<code>onSaveInstanceState()</code> method.</p>
|
||
|
|
||
|
<p><code>onPause()</code> and <code>onResume()</code> are also
|
||
|
complimentary methods. <code>onPause()</code> is always called when the
|
||
|
Activity ends, even if we instigated that (with a <code>finish()</code> call for example).
|
||
|
We will use this to save the current note back to the database. Good
|
||
|
practice is to release any resources that can be released during an
|
||
|
<code>onPause()</code> as well, to take up less resources when in the
|
||
|
passive state. <code>onResume()</code> will call our <code>populateFields()</code> method
|
||
|
to read the note out of the database again and populate the fields.</p>
|
||
|
|
||
|
<p>So, add some space after the <code>populateFields()</code> method
|
||
|
and add the following life-cycle methods:</p>
|
||
|
<ol type="a">
|
||
|
<li><code>
|
||
|
onSaveInstanceState()</code>:
|
||
|
<pre>
|
||
|
@Override
|
||
|
protected void onSaveInstanceState(Bundle outState) {
|
||
|
super.onSaveInstanceState(outState);
|
||
|
saveState();
|
||
|
outState.putSerializable(NotesDbAdapter.KEY_ROWID, mRowId);
|
||
|
}</pre>
|
||
|
<p>We'll define <code>saveState()</code> next.</p>
|
||
|
</li>
|
||
|
<li><code>
|
||
|
onPause()</code>:
|
||
|
<pre>
|
||
|
@Override
|
||
|
protected void onPause() {
|
||
|
super.onPause();
|
||
|
saveState();
|
||
|
}</pre>
|
||
|
</li>
|
||
|
<li><code>
|
||
|
onResume()</code>:
|
||
|
<pre>
|
||
|
@Override
|
||
|
protected void onResume() {
|
||
|
super.onResume();
|
||
|
populateFields();
|
||
|
}</pre>
|
||
|
</li>
|
||
|
</ol>
|
||
|
<p>Note that <code>saveState()</code> must be called in both <code>onSaveInstanceState()</code>
|
||
|
and <code>onPause()</code> to ensure that the data is saved. This is because there is no
|
||
|
guarantee that <code>onSaveInstanceState()</code> will be called and because when it <em>is</em>
|
||
|
called, it is called before <code>onPause()</code>.</p>
|
||
|
|
||
|
|
||
|
<h2 style="clear:right;">Step 8</h2>
|
||
|
|
||
|
<p>Define the <code>saveState()</code> method to put the data out to the
|
||
|
database.</p>
|
||
|
<pre>
|
||
|
private void saveState() {
|
||
|
String title = mTitleText.getText().toString();
|
||
|
String body = mBodyText.getText().toString();
|
||
|
|
||
|
if (mRowId == null) {
|
||
|
long id = mDbHelper.createNote(title, body);
|
||
|
if (id > 0) {
|
||
|
mRowId = id;
|
||
|
}
|
||
|
} else {
|
||
|
mDbHelper.updateNote(mRowId, title, body);
|
||
|
}
|
||
|
}</pre>
|
||
|
<p>Note that we capture the return value from <code>createNote()</code> and if a valid row ID is
|
||
|
returned, we store it in the <code>mRowId</code> field so that we can update the note in future
|
||
|
rather than create a new one (which otherwise might happen if the life-cycle events are
|
||
|
triggered).</p>
|
||
|
|
||
|
|
||
|
<h2 style="clear:right;">Step 9</h2>
|
||
|
|
||
|
<p>Now pull out the previous handling code from the
|
||
|
<code>onActivityResult()</code> method in the <code>Notepadv3</code>
|
||
|
class.</p>
|
||
|
<p>All of the note retrieval and updating now happens within the
|
||
|
<code>NoteEdit</code> life cycle, so all the <code>onActivityResult()</code>
|
||
|
method needs to do is update its view of the data, no other work is
|
||
|
necessary. The resulting method should look like this:</p>
|
||
|
<pre>
|
||
|
@Override
|
||
|
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
|
||
|
super.onActivityResult(requestCode, resultCode, intent);
|
||
|
fillData();
|
||
|
}</pre>
|
||
|
|
||
|
<p>Because the other class now does the work, all this has to do is refresh
|
||
|
the data.</p>
|
||
|
|
||
|
<h2>Step 10</h2>
|
||
|
|
||
|
<p>Also remove the lines which set the title and body from the
|
||
|
<code>onListItemClick()</code> method (again they are no longer needed,
|
||
|
only the <code>mRowId</code> is):</p>
|
||
|
<pre>
|
||
|
Cursor c = mNotesCursor;
|
||
|
c.moveToPosition(position);</pre>
|
||
|
<br>
|
||
|
and also remove:
|
||
|
<br>
|
||
|
<pre>
|
||
|
i.putExtra(NotesDbAdapter.KEY_TITLE, c.getString(
|
||
|
c.getColumnIndex(NotesDbAdapter.KEY_TITLE)));
|
||
|
i.putExtra(NotesDbAdapter.KEY_BODY, c.getString(
|
||
|
c.getColumnIndex(NotesDbAdapter.KEY_BODY)));</pre>
|
||
|
<br>
|
||
|
so that all that should be left in that method is:
|
||
|
<br>
|
||
|
<pre>
|
||
|
super.onListItemClick(l, v, position, id);
|
||
|
Intent i = new Intent(this, NoteEdit.class);
|
||
|
i.putExtra(NotesDbAdapter.KEY_ROWID, id);
|
||
|
startActivityForResult(i, ACTIVITY_EDIT);</pre>
|
||
|
|
||
|
<p>You can also now remove the mNotesCursor field from the class, and set it back to using
|
||
|
a local variable in the <code>fillData()</code> method:
|
||
|
<br><pre>
|
||
|
Cursor notesCursor = mDbHelper.fetchAllNotes();</pre></p>
|
||
|
<p>Note that the <code>m</code> in <code>mNotesCursor</code> denotes a member field, so when we
|
||
|
make <code>notesCursor</code> a local variable, we drop the <code>m</code>. Remember to rename the
|
||
|
other occurrences of <code>mNotesCursor</code> in your <code>fillData()</code> method.
|
||
|
</ol>
|
||
|
<p>
|
||
|
Run it! (use <em>Run As -> Android Application</em> on the project right
|
||
|
click menu again)</p>
|
||
|
|
||
|
<h2>Solution and Next Steps</h2>
|
||
|
|
||
|
<p>You can see the solution to this exercise in <code>Notepadv3Solution</code>
|
||
|
from
|
||
|
the zip file to compare with your own.</p>
|
||
|
<p>
|
||
|
When you are ready, move on to the <a href="notepad-extra-credit.html">Tutorial
|
||
|
Extra Credit</a> exercise, where you can use the Eclipse debugger to
|
||
|
examine the life-cycle events as they happen.</p>
|