Activity Lifecycle

The threads you create are not automatically aware of the changes in your activity's lifecycle. For example, a thread you spawned would not automatically be notified that your activity's onStop() method has been called, and the activity is not visible anymore, or that your activity's onDestroy() method has been called. This means you may need to do additional work to synchronize your threads with your application's lifecycle. Listing 5–20 shows a simply example of an AsyncTask still running even after the activity has been destroyed.

Listing 5–20. Computing a Fibonacci Number In the Background Thread and Updating the User Interface Accordingly

public class MyActivity extends Activity {
    private TextView mResultTextView;
    private Button mRunButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // layout contains TextView and Button        
        mResultTextView = (TextView) findViewById(R.id.resultTextView); // where result
will be displayed
        mRunButton = (Button) findViewById(R.id.runButton); // button to start
computation
    }


    public void onClick (View v) {    
        new AsyncTask<Integer, Void, BigInteger>() {
            @Override
            protected void onPreExecute() {
                // button is disabled so the user can only start one computation at a time
                mRunButton.setEnabled(false);
            }

            @Override
            protected void onCancelled() {
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
            }

            @Override
            protected BigInteger doInBackground(Integer... params) {
                return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
            }

            @Override
            protected void onPostExecute(BigInteger result) {
                mResultTextView.setText(result.toString());
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
            }
        }.execute(100000); // for simplicity here, we hard-code the parameter
    }
}

This example does two simple things when the user presses the button:

  • It computes a Fibonacci number in a separate thread.
  • The button is disabled while the computation is ongoing and enabled once the computation is completed so that the user can start only one computation at a time.

On the surface, it looks correct. However, if the user turns the device while the computation is being executed, the activity will be destroyed and created again. (We assume here that the manifest file does not specify that this activity will handle the orientation change by itself.) The current instance of MyActivity goes through the usual sequence of onPause(), onStop(), and onDestroy() calls, while the new instance goes through the usual sequence of onCreate(), onStart(), and onResume() calls. While all of this is happening, the AsyncTask's thread still runs as if nothing had happened, unaware of the orientation change, and the computation eventually completes. Again, it looks correct so far, and this would seem to be the behavior one would expect.

However, one thing happened that you may not have been expecting: the button became enabled again after the change of orientation was completed. This is easily explained since the Button you see after the change of orientation is actually a new button, which was created in onCreate() and is enabled by default. As a consequence, the user could start a second computation while the first one is still going. While relatively harmless, this breaks the user-interface paradigm you had established when deciding to disable the button while a computation is ongoing.

Passing Information

If you want to fix this bug, you may want the new instance of the activity to know whether a computation is already in progress so that is can disable the button after it is created in onCreate(). Listing 2–21 shows the modifications you could make to communicate information to the new instance of MyActivity.

Listing 5–21. Passing Information From One Activity Instance to Another

public class MyActivity extends Activity {
    private static final String TAG = “MyActivity”;

    private TextView mResultTextView;
    private Button mRunButton;
    private AsyncTask<Integer, Void, BigInteger> mTask; // we'll pass that object to the
other instance

    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // we add a log message here to know what instance of MyActivity is now created
        Log.i(TAG, “MyActivity instance is ” + MyActivity.this.toString());
        Log.i(TAG, “onCreate() called in thread ” + Thread.currentThread().getId());

        // layout contains TextView and Button
        mResultTextView = (TextView) findViewById(R.id.resultTextView); // where result
will be displayed
        mRunButton = (Button) findViewById(R.id.runButton); // button to start
computation

        // we get the object returned in onRetainNonConfigurationInstance() below
        mTask = (AsyncTask<Integer, Void, BigInteger>) getLastNonConfigurationInstance();
        if (mTask != null) {
            mRunButton.setEnabled(false); // computation still in progress so we disable
the button
        }
    }

    @Override
    public Object onRetainNonConfigurationInstance() {

        return mTask; // will be non-null if computation is in progress
    }

    public void onClick (View v) {
        // we keep a reference to the AsyncTask object
        mTask = new AsyncTask<Integer, Void, BigInteger>() {
            @Override
            protected void onPreExecute() {
                // button is disabled so the user can start only one computation at a
time
                mRunButton.setEnabled(false);
            }

            @Override
            protected void onCancelled() {
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
                mTask = null;
            }

            @Override
            protected BigInteger doInBackground(Integer... params) {
                return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
            }

            @Override
            protected void onPostExecute(BigInteger result) {
                mResultTextView.setText(result.toString());
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
                mTask = null;

                // we add a log message to know when the computation is done
                Log.i(TAG, “Computation completed in ” + MyActivity.this.toString());
                Log.i(TAG, “onPostExecute () called in thread ” +
Thread.currentThread().getId());
            }
        }.execute(100000); // for simplicity here, we hard-code the parameter
    }
}

NOTE: onRetainNonConfigurationInstance() is now deprecated in favor of the Fragment APIs available in API level 11 or on older platforms through the Android compatibility package. This deprecated method is used here for simplicity; you will find more sample code using this method. However, you should write new applications using the Fragment APIs.

If you execute that code, you'll then find that the button remains disabled when you rotate your device and a computation is in progress. This would seem to fix the problem we encountered in Listing 5–20. However, you may notice a new problem: if you rotate the device while a computation is in progress and wait until the computation is done, the button is not enabled again even though onPostExecute() was called. This is a much more significant problem since the button can never be enabled again! Moreover, the result of the computation is not propagated on the user interface. (This problem is also in Listing 5–20, so probably you would have noticed that issue before the fact that the button was enabled again after the orientation change.)

Once again this can easily be explained (but may not be obvious if you are relatively new to Java): while onPostExecute was called in the same thread as onCreate (the first activity was destroyed but the main thread is still the same), the mResultTextView and mRunButton objects used in onPostExecute actually belong to the first instance of MyActivity, not to the new instance. The anonymous inner class declared when the new AsyncTask object was created is associated with the instance of its enclosing class (this is why the AsyncTask object we created can reference the fields declared in MyActivity such as mResultTextView and mTask), and therefore it won't have access to the fields of the new instance of MyActivity. Basically, the code in Listing 5–21 has two major flaws when the user rotates the device while a computation is in progress:

  • The button is never enabled again, and the result is never showed.
  • The previous activity is leaked since mTask keeps a reference to an instance of its enclosing class (so two instances of MyActivity exist when the device is rotated).

Remembering State

One way to solve this problem is to simply let the new instance of MyActivity know that a computation was in progress and to start this computation again. The previous computation can be canceled in onStop() or onDestroy() using the AsyncTask.cancel() API. Listing 5–22 shows a possible implementation.

Listing 5–22. Remembering a Computation In Progress

public class MyActivity extends Activity {
    private static final String TAG = “MyActivity”;

    private static final String STATE_COMPUTE = “myactivity.compute”;

    private TextView mResultTextView;
    private Button mRunButton;
    private AsyncTask<Integer, Void, BigInteger> mTask;

    @Override
    protected void onStop() {
        super.onStop();
        if (mTask != null) {
            mTask.cancel(true); // although it is canceled now, the thread may still be
running for a while
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        // if called, it is guaranteed to be called before onStop()
        super.onSaveInstanceState(outState);
        if (mTask != null) {
            outState.putInt(STATE_COMPUTE, 100000); // for simplicity, hard-coded value
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        // we add a log message here to know what instance of MyActivity is now created
        Log.i(TAG, “MyActivity instance is ” + MyActivity.this.toString());
        Log.i(TAG, “onCreate() called in thread ” + Thread.currentThread().getId());

        // layout contains TextView and Button
        mResultTextView = (TextView) findViewById(R.id.resultTextView); // where result
will be displayed
        mRunButton = (Button) findViewById(R.id.runButton); // button to start
computation

        // make sure you check whether savedInstanceState is null
        if (savedInstanceState != null && savedInstanceState.containsKey(STATE_COMPUTE))
{
            int value = savedInstanceState.getInt(STATE_COMPUTE);
            mTask = createMyTask().execute(value); // button will be disabled in onPreExecute()
        }
    }

    // creation of AsyncTask moved to private method as it can now be created from 2
places
    private AsyncTask<Integer, Void, BigInteger> createMyTask() {
        return new AsyncTask<Integer, Void, BigInteger>() {
            @Override
            protected void onPreExecute() {
                // button is disabled so the user can start only one computation at a
time
                mRunButton.setEnabled(false);
            }

            @Override
            protected void onCancelled() {
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
                mTask = null;
            }

            @Override
            protected BigInteger doInBackground(Integer... params) {
                return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
            }

            @Override
            protected void onPostExecute(BigInteger result) {
                mResultTextView.setText(result.toString());
                // button is enabled again to let the user start another computation
                mRunButton.setEnabled(true);
                mTask = null;

                // we add a log message to know when the computation is done
                Log.i(TAG, “Computation completed in ” + MyActivity.this.toString());
                Log.i(TAG, “onPostExecute () called in thread ” +
Thread.currentThread().getId());
            }
        };
    }


    public void onClick (View v) {
        // we keep a reference to the AsyncTask object
        mTask = createMyTask.execute(100000); // for simplicity here, we hard-code the
parameter
    }
}

With this implementation, we basically tell the new instance that the previous instance was computing a certain value when it was destroyed. The new instance will then start the computation again, and the user interface will be updated accordingly.

A device does not have to be rotated to generate a change of configuration as other events are also considered a configuration change. For example, these include a change of locale or an external keyboard being connected. While a Google TV device may not be rotated (at least for now), you should still take the configuration change scenario into account when you target Google TV devices specifically as other events are still likely to occur. Besides, new events may be added in the future, which could also result in a configuration change.

NOTE: onSaveInstanceState() is not always called. It will basically be called only when Android has a good reason to call it. Refer to the Android documentation for more information.

Canceling an AsyncTask object does not necessarily mean the thread will stop immediately though. The actual behavior depends on several things:

  • Whether the task has been started already
  • Which parameter (true or false) was passed to cancel()

Calling AsyncTask.cancel() triggers a call to onCancelled() after doInBackground() returns, instead of a call to onPostExecute(). Because doInBackground() may still have to complete before onCancelled() is called, you may want to call AsyncTask.isCancelled() periodically in doInBackground() to return as early as possible. While this was not relevant in our example, this may make your code a little bit harder to maintain since you would have to interleave AsyncTask-related calls (isCancelled()) and code doing the actual work (which should ideally be AsyncTask-agnostic).

NOTE: Threads don't always have to be interrupted when the activity is destroyed. You can use the Activity.isChangingConfiguration() and Activity.isFinishing() APIs to learn more about what is happening and plan accordingly. For example, in Listing 5–22 we could decide to cancel the task in onStop() only when isFinishing() returns true.

In general, you should at least try to pause the background threads when your activity is paused or stopped. This prevents your application from using resources (CPU, memory, internal storage) other activities could be in dire need of.

TIP: Have a look at the source code of Shelves on http://code.google.com/p/shelves and PhotoStream on http://code.google.com/p/apps-for-android for more examples on saving a state between instantiations of activities.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.78.56