Most Java developers who come to Android from writing server code have a difficult time wrapping their heads around the Android threading model. There are few unique ways to do Android threading that are not intuitive or are departures from regular Java conventions.
Let’s start at the most important distinction.
The UI/Main Thread
Not all threads are created equal. In the Java server world a server would be multi-threaded and most likely a thread would be handling each request. The thread would do all it’s work ( DB read, business logic, HTML generation ) in serial and not respond until it’s done. While this is happening the user would see nothing in the browser until it’s finished.
In Android, everything runs on a single thread unless you tell it not too. “Everything” includes user interaction, file reads, DB calls, long computations. These types of things will lock up the app as user interaction (taps) won’t register until the operations are complete.
The thread that all this happens on is called the “main” thread, or the “UI” thread. It handles user interaction and should never be hindered. Any potentially blocking or long-running operation should be off-loaded onto another thread. In fact, Android enforces this practice by considering any app that doesn’t respond to user interaction within 5 seconds as non-responsive and will prompt the user with a “App isn’t responding” and ask the user if they want to close the app, not a great user experience.
Essentially, the upper limit on what you can do on the main thread is limited to 5 seconds. But your app should never come close to that, anything close to blocking or CPU intensive should be handled off the main thread. The Android SDK comes with tools to make this easy. We will talk about some of them.
Some operations have to happen on the Main thread
Although blocking operations should be off-loaded from the main thread, there are some operations that only the main thread can do. One of them is manipulating the View hierarchy. For example, only the UI can set the text in a TextView, remove a view from the hierarchy, or run an Animation. This allows for less confusion on the state of View hierarchy. This means communication between the main thread and a worker thread is essential. Let’s examine a way to do this with a quick example:
Let’s read some numbers from a file, sum them and display them in a TextView
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.calculate).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
calculate();
}
});
}
public void calculate() {
long sum = 0;
long startTime = System.currentTimeMillis();
try {
BufferedReader in = new BufferedReader(new InputStreamReader(getAssets().open("inputs.txt")));
String line;
while ((line = in.readLine()) != null) {
try {
sum += Long.parseLong(line);
} catch (NumberFormatException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
((TextView) findViewById(R.id.text)).setText("SUM=" + sum + " took " + (System.currentTimeMillis() - startTime)/1000 + "s");
}
For a sufficiently large input, say over 2 million numbers, this could take several seconds. Trying to interact with the app while this is happening will most likely result in an “App isn’t responding” popup. Not good. Luckily, Android provides a simple tool to help you offload this work onto a worker thread.
AsyncTask, the easiest way to offload work
AsyncTask is a class that handles the thread pooling, scheduling, and callback structure for you. Let’s rewrite our class to incorporate AsyncTask.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.calculate).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
calculate();
}
});
}
public void calculate() {
CalculateTask calculateTask = new CalculateTask(){
@Override
protected void onPostExecute(Pair<Long, Long> results) {
((TextView) findViewById(R.id.text)).setText("SUM=" + results.first + " took " + results.second + "s");
}
};
calculateTask.execute("inputs.txt");
}
public class CalculateTask extends AsyncTask<String,Void,Pair<Long,Long>> {
@Override
protected Pair<Long, Long> doInBackground(String... params) {
long sum = 0;
long startTime = System.currentTimeMillis();
try {
BufferedReader in = new BufferedReader(new InputStreamReader(getAssets().open(params[0])));
String line;
while ((line = in.readLine()) != null) {
try {
sum += Long.parseLong(line);
} catch (NumberFormatException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
return new Pair<>(sum, (System.currentTimeMillis() - startTime)/1000);
}
}
AsyncTask handles the threading for you. It will create a ThreadPool to handle the scheduling so you can worry about the business logic. It comes equipped with callbacks:
doInBackground
runs in the background on a worker thread. This is where the blocking code should go.onPreExecute
runs on the UI thread before doInBackground.onProgressUpdate
this is called in the UI thread when you callpublishProgress
. It’s a good place to update progress dialogs and show the user that things are still working.onPostExecute
runs on the UI thread and will be delivered the result ofdoInBackground
In the above code we only overrode doInBackground
. Our app runs it but the user has no idea what is happening or whether that app has stuck or in an infinite loop. Let’s add a pretty dialog that tells the user we are still working.
public void calculate() {
CalculateTask calculateTask = new CalculateTask(this);
calculateTask.execute("inputs.txt");
}
public class CalculateTask extends AsyncTask<String,Void,Pair<Long,Long>> {
private Context context;
private ProgressDialog progressDialog;
public CalculateTask(Context context) {
this.context = context;
}
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Calculating...");
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
protected Pair<Long, Long> doInBackground(String... params) {
long sum = 0;
long startTime = System.currentTimeMillis();
try {
BufferedReader in = new BufferedReader(new InputStreamReader(getAssets().open(params[0])));
String line;
while ((line = in.readLine()) != null) {
try {
sum += Long.parseLong(line);
} catch (NumberFormatException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
return new Pair<>(sum, (System.currentTimeMillis() - startTime)/1000);
}
@Override
protected void onPostExecute(Pair<Long, Long> results) {
((TextView) findViewById(R.id.text)).setText("SUM=" + results.first + " took " + results.second + "s");
progressDialog.dismiss();
}
}
Here we are using a ProgressDialog that will run while we are calculating the numbers:
This is pretty good but we can do better. It’s nice that the user knows we are working but they want to know how long it’s going to take. Let’s use the the progress dialog and publishProgres
to show this:
public void calculate() {
CalculateTask calculateTask = new CalculateTask(this);
calculateTask.execute("inputs.txt");
}
public class CalculateTask extends AsyncTask<String,Integer,Pair<Long,Long>> {
private Context context;
private ProgressDialog progressDialog;
public CalculateTask(Context context) {
this.context = context;
}
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Calculating...");
progressDialog.setIndeterminate(false);
progressDialog.setProgress(0);
progressDialog.setMax(100);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setCancelable(false);
progressDialog.show();
}
@Override
protected Pair<Long, Long> doInBackground(String... params) {
long sum = 0;
long startTime = System.currentTimeMillis();
try {
long fileBytes = getAssets().open(params[0]).available();
long readBytes = 0;
long bytesSinceLastPublish = 0;
BufferedReader in = new BufferedReader(new InputStreamReader(getAssets().open(params[0])));
String line;
while ((line = in.readLine()) != null) {
try {
// plus 1 because this line.length() wont include the newline
readBytes += line.length() + 1;
bytesSinceLastPublish += line.length();
sum += Long.parseLong(line);
// lets only publish progress every 1% of the file read. This way we dont spend
// too much time updating the UI
if(bytesSinceLastPublish > fileBytes / 100) {
publishProgress((int) ((double) readBytes / (double) fileBytes * 100));
bytesSinceLastPublish = 0;
}
} catch (NumberFormatException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} catch (IOException e) {
Log.e(TAG, e.getMessage(), e);
}
return new Pair<>(sum, (System.currentTimeMillis() - startTime)/1000);
}
@Override
protected void onPostExecute(Pair<Long, Long> results) {
((TextView) findViewById(R.id.text)).setText("SUM=" + results.first + " took " + results.second + "s");
progressDialog.dismiss();
}
@Override
protected void onProgressUpdate(Integer... values) {
progressDialog.setProgress(values[0]);
}
}
This is a little prettier:
Now the user knows approximately how long before the operation will finish and is much happier. AsyncTask is not the only tool you have to handle threading in your Android app. In part 2 (coming soon) we will talk about some tools for handling multiple background threads.