Let’s cut to the chase: State management on Android is tricky. From a developer’s perspective I don’t believe that the issue is so much a technical one as it is a matter of familiarity. The Activity life-cycle on Android is unlike anything I’ve seen on any other platform. It’s not intuitive, and if you spend much time in the Android code forums, you’ll find that the management (and mismanagement) of application state comes up time and time again.
On two previous occasions, I’ve mentioned that there are three keys to managing state in your Android application. Those are:
- user interface state
- internal variable state
- long-running I/O
This post on long-running I/O marks the last (and most difficult) tutorial in the series.
What is long-running I/O?
Well, it can be any sort of task that if ran on the current activity’s UI thread would cause the application to appear non-responsive to the user. A common example is making a call to an external webservice. If it takes 10 seconds for you to request, retrieve, and parse results from a URL, you certainly can’t have your user interface locked up for that amount of time.
On most platforms the way long-running I/O is typically handled is to spawn the I/O off on a background thread, and let that thread call back or message the UI when the task is complete. The issue with Android is, if your user takes an action that restarts the activity life-cycle (such as rotating the phone), the reference the background thread holds becomes invalid. Any attempt to reference that pointer from the I/O thread results in a crash.
Over time, a number of techniques have evolved for dealing with this scenario. I’ve tried several, including rolling my own thread logic, using background services, and Android’s asynchronous task manager. Each approach has its pros and cons, but in general I’ve found that the AsyncTask class is a bit easier to get my head around, and it tends to be well suited for a large range of applications. The following tutorial demonstrates using an inner static extension of AsyncTask to handle long-running I/O. At times, the code may seem a bit convoluted, but hopefully by the time we are finished you’ll start to see how AsyncTask can make your life easier. If you’d like to simply download and import the project you may do so here.
Tutorial on how to use AsyncTask to handle long-running I/O
1. Begin a new project in Eclipse targeting Android SDK 1.6 or greater.
2. We need a simple XML layout that consists of a couple text labels and a button.
main.xml
<?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" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dip"
android:text="Long Running I/O Demo"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Begin"
android:layout_gravity="center"
android:layout_margin="12dip"
android:id="@+id/begin_button"/>
<TextView
android:id="@+id/status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="8dip"/>
</LinearLayout>
3. We will move to our /src folder and modify the Main.java file. Let’s start by implementing a barebones on create override.
Main.java
package com.authorwjf;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
public class Main extends Activity implements OnClickListener{
private LongRunningIO mLongRunningIO = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button b = (Button)findViewById(R.id.begin_button);
b.setOnClickListener(this);
}
}
4. Eclipse doesn’t like that private variable because it can’t resolve the class LongRunningIO — it doesn’t exist. We are going to create it statically within our Main.class. If you aren’t familiar with AsyncTask, our extension has several functions you might wonder about. It’s worthwhile to read the official documentation for the AsyncTask class. Here are brief highlights about some of the methods:
- doInBackground() is the worker method that gets called on the background thread when execute() is invoked on an instance of AysncTask. For the purpose of the example, this function does nothing but count to ten.
- OnProgressUpdate() gets executed on the UI thread whenever publishProgress() is called on the background thread.
- isDone() returns a flag indicating whether an AsyncTask has completed. This is so important because the OS only lets us execute an AsyncTask one time. If we need to run it again, the first one must be destroyed and a new one created.
- attach() and detach() are used to register and unregister the reference to an activity’s UI.
Main.java
static class LongRunningIO extends AsyncTask <Void, Integer, Void> {
private Activity mActivity = null;
private boolean mDone = false;
boolean isDone() { return mDone; }
@Override
protected Void doInBackground(Void... params) {
for (int i = 1; i<11; i++) {
SystemClock.sleep(1000);
if (mActivity!= null) {
publishProgress(i);
}
}
mDone = true;
return null;
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (mActivity != null) {
TextView tv = (TextView)mActivity.findViewById(R.id.status_text);
tv.setText(Integer.toString(progress[0])+"/10");
}
}
void attach(Activity a) {
mActivity = a;
}
void detach() {
mActivity = null;
}
}
5. With the static inner class in place, we can go back and modify the on create, as well as add the on click handler. The theory is we want to look at the last non configuration instance and if there is already a long-running I/O task executing we just attach to it. We take a similar approach in the on click handler, except in that case we also need to handle the case when the user clicks the button after the AsyncTask has already completed.
Main.java
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button b = (Button)findViewById(R.id.begin_button);
b.setOnClickListener(this);
mLongRunningIO = (LongRunningIO)getLastNonConfigurationInstance();
if (mLongRunningIO != null) {
TextView tv = (TextView)findViewById(R.id.status_text);
if (mLongRunningIO.isDone()) {
tv.setText("10/10");
} else {
tv.setText("Re-initializing");
}
mLongRunningIO.attach(this);
}
}
@Override
public void onClick(View v) {
boolean needToCreate = false;
if (mLongRunningIO == null) {
needToCreate=true;
} else {
if (mLongRunningIO.isDone()) {
mLongRunningIO.detach();
mLongRunningIO = null;
needToCreate=true;
}
}
if (needToCreate) {
mLongRunningIO = new LongRunningIO();
mLongRunningIO.attach(this);
mLongRunningIO.execute();
}
}
6. You’ll want to override the on retain non instance configuration, as this is how we pass the asynchronous task from activity to activity. It’s important to call detach, or you will “leak” your activity, effectively preventing the garbage collector from ever being able to reclaim the memory.
Main.java
@Override
public Object onRetainNonConfigurationInstance() {
if (mLongRunningIO != null) {
mLongRunningIO.detach();
}
return(mLongRunningIO);
}
That’s it! Unlike a lot of the sample programs we’ve done in the App Builder blog, you really need to run this one a few times to get a feel for what is happening. Load the APK to a phone or emulator, click the Begin button, and then observe the counter (Figure A). Try rotating the phone while the count is ticking and see what happens.
Figure A
As I said when we began this tutorial, managing state on the Android platform can be a tricky business. Hopefully you’ll find the code in this post helps to demystify long-running I/O and adds some handy techniques to your digital toolbox.