Android Master Class Listener Aware AsyncTasks

The AsyncTask class provided within the Android framework is a convenient and powerful mechanism for integrating background tasks into applications with very little effort or thought from the developer. So long as the semantics are followed, it’s almost trivial to perform complex background processing, asynchronous progress updates and UI manipulations on completion. However, some caveats can lay hidden landmines for the unwary. We’ll explore a Listener-based paradigm to provide a bit more safety to this fundamental tool.

A quick review of AsyncTask

So we’re all on the same page, let’s provide a quick review of AsyncTask usage. AsyncTask has a flexible implementation that makes it possible to process any type of input, provide any type of progress, and return any type of final data. Let’s create a very simple – read: useless – example that counts some number of seconds down, providing some progress every second and creates a Toast when complete.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.twotoasters.toastmo;

import android.app.Activity;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;

public class DontUseAsyncTaskLikeThisActivity extends Activity {

  private ProgressBar _progressBar = null;

  
  /* (non-Javadoc)
  * @see android.app.Activity#onDestroy()
  */
  @Override
  protected void onDestroy() {
      super.onDestroy();
      _progressBar = null;
  }


  /* (non-Javadoc)
  * @see android.app.Activity#onCreate(android.os.Bundle)
  */
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      _progressBar = (ProgressBar)findViewById(R.id.progressBar1);
      
      Button button = (Button)findViewById(R.id.switch_button);
      button.setText(R.string.switch_to_good);
      button.setOnClickListener(new OnClickListener() {
          
          @Override
          public void onClick(View v) {
              Intent i = new Intent(DontUseAsyncTaskLikeThisActivity.this, ListenerAwareAsyncTaskDemoActivity.class);
              startActivity(i);
              finish();
          }
      });
      
      AsyncTask<Integer, Integer, Void> asyncTask = new AsyncTask<Integer, Integer, Void>() {

          @Override
          protected Void doInBackground(Integer... params) {
              int totalTime = params[0];
              int count = 0;
              
              while(count <= totalTime && !isCancelled()) {
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      // don't care!
                  }
                  
                  this.publishProgress((int)(((float)count++ / (float)totalTime) * 100f));
              }
              return null;
          }

          /* (non-Javadoc)
          * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
          */
          @Override
          protected void onPostExecute(Void result) {
              Toast.makeText(DontUseAsyncTaskLikeThisActivity.this, "Done!", Toast.LENGTH_LONG).show();
          }

          /* (non-Javadoc)
          * @see android.os.AsyncTask#onProgressUpdate(Progress[])
          */
          @Override
          protected void onProgressUpdate(Integer... progress) {
              // oh dear
              _progressBar.setProgress(progress[0]);
          }
      };
      
      asyncTask.execute(120);
      
  }
}

All of this is reasonably okay and a common usage of the AsyncTask paradigm. Unfortunately, it also crashes horribly in certain circumstances. Why? Because the AsyncTask lifecycle is completely decoupled from the Activity lifecycle (technically it’s almost completely decoupled… but for this conversation, it may as well be). Unless cancelled, the AsyncTask lives on regardless what happens to the Activity that launched it.

So what’s wrong with this?

The easiest way to see the problem is to investigate what happens when we rotate the screen. There are other scenarios that might expose the issue, but the screen rotation configuration change is the most direct and easiest to exercise. By default, when Android processes a configuration change, the Activity is torn down and recreated from scratch.

In our case, we’re setting our ProgressBar to null in the onDestroy() method of our Activity. So on a configuration change, our Activity is torn down, our _progressBar field is set to null and our landmine is set. The next time our AsyncTask calls onProgressUpdate() a NullPointException will be thrown. This is rarely good.

Is this a contrived example? Of course! However, similar, less-contrived logic is often used in AsyncTask objects. “Wait a minute, why don’t you just not set the field to null!” Sure, that might avoid an NPE in this precise case, but it doesn’t address the underlying issue. Our problem, at its root, is that the reference to what we’re updating within our AsyncTask is stale.

How do we fix this?

An approach that we often use is extending the AsyncTask to use a listener that we can register with as needed and unregister with if desired. This allows a tighter coupling with the Activity so that AsyncTask can always operate with a fresh reference. Here’s our AsyncTask subclass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package com.twotoasters.toastmo;

import android.app.Activity;
import android.os.AsyncTask;

public abstract class ListenerAwareAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {

  /**
  * Our completion and progress listener. This is the thing that we'll call when things get
  * completed.
  * 
  * @author bjdupuis
  *
  * @param <Progress>
  * @param <Result>
  */
  public interface OnCompleteListener<Progress, Result> {
      public void onComplete(Result result);
      
      public void onProgress(Progress... progress);
  }
  
  /**
  * Constructor that registers a listener.
  * 
  * @param the base <code>Activity</code> that the listener runs on
  * @param listener the listener to register.
  */
  public ListenerAwareAsyncTask(Activity activity, OnCompleteListener<Progress, Result> listener) {
      this();
      
      register(activity, listener);
  }
  
  /* (non-Javadoc)
  * @see android.os.AsyncTask#onPostExecute(java.lang.Object)
  */
  @Override
  final protected void onPostExecute(Result result) {
      if(_listener != null) {
          _listener.onComplete(result);
      } else {
          // save the result so we can defer calling the completion
          // listener when (and if) it re-registers
          _result = result;
      }
  }

  /* (non-Javadoc)
  * @see android.os.AsyncTask#onProgressUpdate(Progress[])
  */
  @Override
  final protected void onProgressUpdate(Progress... values) {
      if(_listener != null) {
          _listener.onProgress(values);
      }
  }

  /**
  * Register a listener to be notified when the task completes and updates progress.
  * 
  * @param listener the listener to call
  */
  public void register(Activity activity, OnCompleteListener<Progress, Result> listener) {
      _listener = listener;
      
      // see if we had a deferred result available
      if(_result != null) {
          activity.runOnUiThread(new Runnable() {
              
              @Override
              public void run() {
                  _listener.onComplete(_result);
                  _result = null;
              }
          });
      }
  }
  
  /**
  * Unregister the registered listener. If it's desirable for more than one listener 
  * to be notified, it's trivial to have a list of listeners.
  */
  public void unregister() {
      _listener = null;
  }
  
  // the completion listener we'll call.
  private OnCompleteListener<Progress, Result> _listener = null;
  
  // a temporary storage for the result.
  private Result _result = null;

  private ListenerAwareAsyncTask() {
  }
  
}

We can use this new class as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package com.twotoasters.toastmo;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.Toast;

import com.twotoasters.toastmo.ListenerAwareAsyncTask.OnCompleteListener;

public class ListenerAwareAsyncTaskDemoActivity extends Activity {

  static ListenerAwareAsyncTask<Integer, Integer, Void> _myTask = null;
  private ProgressBar _progressBar = null;
  
  private OnCompleteListener<Integer, Void> _myTaskListener = new OnCompleteListener<Integer, Void>() {

      @Override
      public void onComplete(Void result) {
          Toast.makeText(ListenerAwareAsyncTaskDemoActivity.this, "Done!", Toast.LENGTH_LONG).show();
      }

      @Override
      public void onProgress(Integer... progress) {
          _progressBar.setProgress(progress[0]);
      }
  };

  /* (non-Javadoc)
  * @see android.app.Activity#onDestroy()
  */
  @Override
  protected void onDestroy() {
      super.onDestroy();
      _progressBar = null;
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);
      _progressBar = (ProgressBar)findViewById(R.id.progressBar1);

      Button button = (Button)findViewById(R.id.switch_button);
      button.setText(R.string.switch_to_bad);
      button.setOnClickListener(new OnClickListener() {
          
          @Override
          public void onClick(View v) {
              Intent i = new Intent(ListenerAwareAsyncTaskDemoActivity.this, DontUseAsyncTaskLikeThisActivity.class);
              startActivity(i);
              finish();
          }
      });
      

  }

  /*
  * (non-Javadoc)
  * 
  * @see android.app.Activity#onPause()
  */
  @Override
  protected void onPause() {
      super.onPause();
      
      if(_myTask != null) {
          _myTask.unregister();
      }
  }

  /*
  * (non-Javadoc)
  * 
  * @see android.app.Activity#onResume()
  */
  @Override
  protected void onResume() {
      super.onResume();

      if(_myTask == null) {
          _myTask = new ListenerAwareAsyncTask<Integer, Integer, Void>(this, _myTaskListener) {

              @Override
              protected Void doInBackground(Integer... params) {
                  int totalTime = params[0];
                  int count = 0;
                  
                  while(count <= totalTime && !isCancelled()) {
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          // don't care!
                      }
                      
                      this.publishProgress((int)(((float)count++ / (float)totalTime) * 100f));
                  }
                  return null;
              }
          };
          _myTask.execute(120);
      } else {
          _myTask.register(this, _myTaskListener);
      }
  }

}

You’ll notice our doInBackground(), onProgress(), and onComplete() do precisely the same things at the Activity level as their less-safe alternatives. The biggest difference is that our references will always be fresh. Another benefit we derive is that if the background task completes while we’re away, we’ll be notified as soon as we register of the completed result. This is particularly nice for AsyncTask objects that perform networking operations, since we won’t have “wasted” an out-and-back request/response.

Example project

I’ve created a sample project showing the “bad” way of doing things and the “better” way of doing things. When run on the emulator or device, try rotating the screen a few times in each case. When on the “bad” case, you’ll NPE with great regularity.


Comments