Home | History | Annotate | Download | only in notepad
      1 page.title=Notepad Exercise 3
      2 parent.title=Notepad Tutorial
      3 parent.link=index.html
      4 @jd:body
      5 
      6 
      7 <p><em>In this exercise, you will use life-cycle event callbacks to store and
      8 retrieve application state data. This exercise demonstrates:</em></p>
      9 <ul>
     10 <li><em>Life-cycle events and how your application can use them</em></li>
     11 <li><em>Techniques for maintaining application state</em></li>
     12 </ul>
     13 
     14 <div style="float:right;white-space:nowrap">
     15 	[<a href="notepad-ex1.html">Exercise 1</a>]
     16 	[<a href="notepad-ex2.html">Exercise 2</a>]
     17 	<span style="color:#BBB;">
     18 		[<a href="notepad-ex3.html" style="color:#BBB;">Exercise 3</a>]
     19 	</span>
     20 	[<a href="notepad-extra-credit.html">Extra Credit</a>]
     21 </div>
     22 
     23 <h2>Step 1</h2>
     24 
     25 <p>Import <code>Notepadv3</code> into Eclipse. If you see an error about
     26 <code>AndroidManifest.xml,</code> or some problems related to an Android zip
     27 file, right click on the project and select <strong>Android Tools</strong> &gt;
     28 <strong>Fix Project Properties</strong> from the popup menu. The starting point for this exercise is
     29 exactly where we left off at the end of the Notepadv2. </p>
     30 <p>The current application has some problems &mdash; hitting the back button when editing
     31 causes a crash, and anything else that happens during editing will cause the
     32 edits to be lost.</p>
     33 <p>To fix this, we will move most of the functionality for creating and editing
     34 the note into the NoteEdit class, and introduce a full life cycle for editing
     35 notes.</p>
     36 
     37   <ol>
     38     <li>Remove the code in <code>NoteEdit</code> that parses the title and body
     39     from the extras Bundle.
     40     <p>Instead, we are going to use the <code>DBHelper</code> class
     41     to access the notes from the database directly. All we need passed into the
     42     NoteEdit Activity is a <code>mRowId</code> (but only if we are editing, if creating we pass
     43     nothing). Remove these lines:</p>
     44       <pre>
     45 String title = extras.getString(NotesDbAdapter.KEY_TITLE);
     46 String body = extras.getString(NotesDbAdapter.KEY_BODY);</pre>
     47     </li>
     48     <li>We will also get rid of the properties that were being passed in
     49     the <code>extras</code> Bundle, which we were using to set the title
     50     and body text edit values in the UI. So delete:
     51     <pre>
     52 if (title != null) {
     53     mTitleText.setText(title);
     54 }
     55 if (body != null) {
     56     mBodyText.setText(body);
     57 }</pre>
     58     </li>
     59   </ol>
     60 
     61 <h2>Step 2</h2>
     62 
     63 <p>Create a class field for a <code>NotesDbAdapter</code> at the top of the NoteEdit class:</p>
     64     <pre>&nbsp;&nbsp;&nbsp; private NotesDbAdapter mDbHelper;</pre>
     65 <p>Also add an instance of <code>NotesDbAdapter</code> in the
     66     <code>onCreate()</code> method (right below the <code>super.onCreate()</code> call):</p>
     67     <pre>
     68 &nbsp;&nbsp;&nbsp; mDbHelper = new NotesDbAdapter(this);<br>
     69 &nbsp;&nbsp;&nbsp; mDbHelper.open();</pre>
     70 
     71 <h2>Step 3</h2>
     72 
     73 <p>In <code>NoteEdit</code>, we need to check the <var>savedInstanceState</var> for the
     74 <code>mRowId</code>, in case the note
     75     editing contains a saved state in the Bundle, which we should recover (this would happen
     76   if our Activity lost focus and then restarted).</p>
     77   <ol>
     78     <li>
     79       Replace the code that currently initializes the <code>mRowId</code>:<br>
     80       <pre>
     81         mRowId = null;
     82 
     83         Bundle extras = getIntent().getExtras();
     84         if (extras != null) {
     85             mRowId = extras.getLong(NotesDbAdapter.KEY_ROWID);
     86         }
     87         </pre>
     88         with this:
     89         <pre>
     90         mRowId = (savedInstanceState == null) ? null :
     91             (Long) savedInstanceState.getSerializable(NotesDbAdapter.KEY_ROWID);
     92         if (mRowId == null) {
     93             Bundle extras = getIntent().getExtras();
     94             mRowId = extras != null ? extras.getLong(NotesDbAdapter.KEY_ROWID)
     95                                     : null;
     96         }
     97         </pre>
     98     </li>
     99     <li>
    100       Note the null check for <code>savedInstanceState</code>, and we still need to load up
    101       <code>mRowId</code> from the <code>extras</code> Bundle if it is not
    102       provided by the <code>savedInstanceState</code>. This is a ternary operator shorthand
    103       to safely either use the value or null if it is not present.
    104     </li>
    105     <li>
    106       Note the use of <code>Bundle.getSerializable()</code> instead of
    107       <code>Bundle.getLong()</code>.  The latter encoding returns a <code>long</code> primitive and
    108       so can not be used to represent the case when <code>mRowId</code> is <code>null</code>.
    109     </li>
    110   </ol>
    111 
    112 <h2>Step 4</h2>
    113 
    114 <p>Next, we need to populate the fields based on the <code>mRowId</code> if we
    115     have it:</p>
    116     <pre>populateFields();</pre>
    117     <p>This goes before the <code>confirmButton.setOnClickListener()</code> line.
    118     We'll define this method in a moment.</p>
    119 
    120 <h2>Step 5</h2>
    121 
    122 <p>Get rid of the Bundle creation and Bundle value settings from the
    123     <code>onClick()</code> handler method. The Activity no longer needs to
    124     return any extra information to the caller. And because we no longer have
    125     an Intent to return, we'll use the shorter version
    126     of <code>setResult()</code>:</p>
    127     <pre>
    128 public void onClick(View view) {
    129     setResult(RESULT_OK);
    130     finish();
    131 }</pre>
    132     <p>We will take care of storing the updates or new notes in the database
    133     ourselves, using the life-cycle methods.</p>
    134 
    135     <p>The whole <code>onCreate()</code> method should now look like this:</p>
    136     <pre>
    137 super.onCreate(savedInstanceState);
    138 
    139 mDbHelper = new NotesDbAdapter(this);
    140 mDbHelper.open();
    141 
    142 setContentView(R.layout.note_edit);
    143 
    144 mTitleText = (EditText) findViewById(R.id.title);
    145 mBodyText = (EditText) findViewById(R.id.body);
    146 
    147 Button confirmButton = (Button) findViewById(R.id.confirm);
    148 
    149 mRowId = (savedInstanceState == null) ? null :
    150     (Long) savedInstanceState.getSerializable(NotesDbAdapter.KEY_ROWID);
    151 if (mRowId == null) {
    152     Bundle extras = getIntent().getExtras();
    153     mRowId = extras != null ? extras.getLong(NotesDbAdapter.KEY_ROWID)
    154                             : null;
    155 }
    156 
    157 populateFields();
    158 
    159 confirmButton.setOnClickListener(new View.OnClickListener() {
    160 
    161     public void onClick(View view) {
    162         setResult(RESULT_OK);
    163         finish();
    164     }
    165 
    166 });</pre>
    167 
    168 <h2>Step 6</h2>
    169 
    170 <p>Define the <code>populateFields()</code> method.</p>
    171     <pre>
    172 private void populateFields() {
    173     if (mRowId != null) {
    174         Cursor note = mDbHelper.fetchNote(mRowId);
    175         startManagingCursor(note);
    176         mTitleText.setText(note.getString(
    177 	            note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)));
    178         mBodyText.setText(note.getString(
    179                 note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY)));
    180     }
    181 }</pre>
    182 <p>This method uses the <code>NotesDbAdapter.fetchNote()</code> method to find the right note to
    183 edit, then it calls <code>startManagingCursor()</code> from the <code>Activity</code> class, which
    184 is an Android convenience method provided to take care of the Cursor life-cycle. This will release
    185 and re-create resources as dictated by the Activity life-cycle, so we don't need to worry about
    186 doing that ourselves. After that, we just look up the title and body values from the Cursor
    187 and populate the View elements with them.</p>
    188 
    189 
    190 <h2>Step 7</h2>
    191 
    192   <div class="sidebox-wrapper">
    193   <div class="sidebox">
    194     <h2>Why handling life-cycle events is important</h2>
    195     <p>If you are used to always having control in your applications, you
    196     might not understand why all this life-cycle work is necessary. The reason
    197     is that in Android, you are not in control of your Activity, the
    198     operating system is!</p>
    199     <p>As we have already seen, the Android model is based around activities
    200     calling each other. When one Activity calls another, the current Activity
    201     is paused at the very least, and may be killed altogether if the
    202     system starts to run low on resources. If this happens, your Activity will
    203     have to store enough state to come back up later, preferably in the same
    204     state it was in when it was killed.</p>
    205     <p>
    206     Activities have a <a
    207 href="{@docRoot}guide/topics/fundamentals/activities.html#Lifecycle">well-defined life
    208 cycle</a>.
    209     Lifecycle events can happen even if you are not handing off control to
    210     another Activity explicitly. For example, perhaps a call comes in to the
    211     handset. If this happens, and your Activity is running, it will be swapped
    212     out while the call Activity takes over.</p>
    213   </div>
    214   </div>
    215 
    216 <p>Still in the <code>NoteEdit</code> class, we now override the methods
    217    <code>onSaveInstanceState()</code>, <code>onPause()</code> and
    218    <code>onResume()</code>. These are our life-cycle methods
    219    (along with <code>onCreate()</code> which we already have).</p>
    220 
    221 <p><code>onSaveInstanceState()</code> is called by Android if the
    222     Activity is being stopped and <strong>may be killed before it is
    223     resumed!</strong> This means it should store any state necessary to
    224     re-initialize to the same condition when the Activity is restarted. It is
    225     the counterpart to the <code>onCreate()</code> method, and in fact the
    226     <code>savedInstanceState</code> Bundle passed in to <code>onCreate()</code> is the same
    227     Bundle that you construct as <code>outState</code> in the
    228     <code>onSaveInstanceState()</code> method.</p>
    229 
    230 <p><code>onPause()</code> and <code>onResume()</code> are also
    231     complimentary methods. <code>onPause()</code> is always called when the
    232     Activity ends, even if we instigated that (with a <code>finish()</code> call for example).
    233     We will use this to save the current note back to the database. Good
    234     practice is to release any resources that can be released during an
    235     <code>onPause()</code> as well, to take up less resources when in the
    236     passive state. <code>onResume()</code> will call our <code>populateFields()</code> method
    237     to read the note out of the database again and populate the fields.</p>
    238 
    239 <p>So, add some space after the <code>populateFields()</code> method
    240   and add the following life-cycle methods:</p>
    241   <ol type="a">
    242     <li><code>
    243       onSaveInstanceState()</code>:
    244       <pre>
    245     &#64;Override
    246     protected void onSaveInstanceState(Bundle outState) {
    247         super.onSaveInstanceState(outState);
    248         saveState();
    249         outState.putSerializable(NotesDbAdapter.KEY_ROWID, mRowId);
    250     }</pre>
    251     <p>We'll define <code>saveState()</code> next.</p>
    252     </li>
    253     <li><code>
    254       onPause()</code>:
    255       <pre>
    256     &#64;Override
    257     protected void onPause() {
    258         super.onPause();
    259         saveState();
    260     }</pre>
    261     </li>
    262     <li><code>
    263       onResume()</code>:
    264       <pre>
    265     &#64;Override
    266     protected void onResume() {
    267         super.onResume();
    268         populateFields();
    269     }</pre>
    270     </li>
    271   </ol>
    272 <p>Note that <code>saveState()</code> must be called in both <code>onSaveInstanceState()</code>
    273 and <code>onPause()</code> to ensure that the data is saved.  This is because there is no
    274 guarantee that <code>onSaveInstanceState()</code> will be called and because when it <em>is</em>
    275 called, it is called before <code>onPause()</code>.</p>
    276 
    277 
    278 <h2 style="clear:right;">Step 8</h2>
    279 
    280 <p>Define the <code>saveState()</code> method to put the data out to the
    281 database.</p>
    282     <pre>
    283      private void saveState() {
    284         String title = mTitleText.getText().toString();
    285         String body = mBodyText.getText().toString();
    286 
    287         if (mRowId == null) {
    288             long id = mDbHelper.createNote(title, body);
    289             if (id > 0) {
    290                 mRowId = id;
    291             }
    292         } else {
    293             mDbHelper.updateNote(mRowId, title, body);
    294         }
    295     }</pre>
    296   <p>Note that we capture the return value from <code>createNote()</code> and if a valid row ID is
    297   returned, we store it in the <code>mRowId</code> field so that we can update the note in future
    298   rather than create a new one (which otherwise might happen if the life-cycle events are
    299   triggered).</p>
    300 
    301 
    302 <h2 style="clear:right;">Step 9</h2>
    303 
    304 <p>Now pull out the previous handling code from the
    305     <code>onActivityResult()</code> method in the <code>Notepadv3</code>
    306     class.</p>
    307 <p>All of the note retrieval and updating now happens within the
    308     <code>NoteEdit</code> life cycle, so all the <code>onActivityResult()</code>
    309     method needs to do is update its view of the data, no other work is
    310     necessary. The resulting method should look like this:</p>
    311 <pre>
    312 &#64;Override
    313 protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    314     super.onActivityResult(requestCode, resultCode, intent);
    315     fillData();
    316 }</pre>
    317 
    318 <p>Because the other class now does the work, all this has to do is refresh
    319       the data.</p>
    320 
    321 <h2>Step 10</h2>
    322 
    323 <p>Also remove the lines which set the title and body from the
    324     <code>onListItemClick()</code> method (again they are no longer needed,
    325     only the <code>mRowId</code> is):</p>
    326 <pre>
    327     Cursor c = mNotesCursor;
    328     c.moveToPosition(position);</pre>
    329 <br>
    330 and also remove:
    331 <br>
    332 <pre>
    333     i.putExtra(NotesDbAdapter.KEY_TITLE, c.getString(
    334                     c.getColumnIndex(NotesDbAdapter.KEY_TITLE)));
    335     i.putExtra(NotesDbAdapter.KEY_BODY, c.getString(
    336                     c.getColumnIndex(NotesDbAdapter.KEY_BODY)));</pre>
    337 <br>
    338 so that all that should be left in that method is:
    339 <br>
    340 <pre>
    341     super.onListItemClick(l, v, position, id);
    342     Intent i = new Intent(this, NoteEdit.class);
    343     i.putExtra(NotesDbAdapter.KEY_ROWID, id);
    344     startActivityForResult(i, ACTIVITY_EDIT);</pre>
    345 
    346   <p>You can also now remove the mNotesCursor field from the class, and set it back to using
    347   a local variable in the <code>fillData()</code> method:
    348 <br><pre>
    349     Cursor notesCursor = mDbHelper.fetchAllNotes();</pre></p>
    350   <p>Note that the <code>m</code> in <code>mNotesCursor</code> denotes a member field, so when we
    351   make <code>notesCursor</code> a local variable, we drop the <code>m</code>. Remember to rename the
    352   other occurrences of <code>mNotesCursor</code> in your <code>fillData()</code> method.
    353 </ol>
    354 <p>
    355 Run it! (use <em>Run As -&gt; Android Application</em> on the project right
    356 click menu again)</p>
    357 
    358 <h2>Solution and Next Steps</h2>
    359 
    360 <p>You can see the solution to this exercise in <code>Notepadv3Solution</code>
    361 from
    362 the zip file to compare with your own.</p>
    363 <p>
    364 When you are ready, move on to the <a href="notepad-extra-credit.html">Tutorial
    365 Extra Credit</a> exercise, where you can use the Eclipse debugger to
    366 examine the life-cycle events as they happen.</p>
    367