Monday, June 27, 2011

Clean Up After Your AsyncTasks

Do you ever have a problem where you think an Activity is launching twice in a row without reason? Or when an Activity starts up for no reason? Or maybe some other code is getting executed unexpectedly? It might be that you are forgetting to clean up your AsyncTask instances.

Let's say you have an activity that does a Twitter search and sends the results to another activity to be displayed. Your search Activity might look like the following:

public class TestTwitterSearchActivity extends Activity {
    
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button goButton = (Button) findViewById(R.id.button_go);
        goButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.setEnabled(false);
                AsyncTask<String, Void, String> task = new TwitterAndroidSearch();
                task.execute("android");
            }
        });
    }
    
    /**
     * Runs a query on Twitter. 
     */
    class TwitterAndroidSearch extends AsyncTask<String, Void, String> {
        @Override protected String doInBackground(String... params) {
            try {
                String queryUrl = "http://search.twitter.com/search.json?q=" + params[0];
                URLConnection conn = new URL(queryUrl).openConnection();
                InputStream in = conn.getInputStream();
                Reader reader = new InputStreamReader(in);
                String json = CharStreams.toString(reader); // See Google Guava-libraries
                return json;
            } catch (IOException e) {
                // Treat IOExceptions as having no result
                return "";
            }
        }
        
        @Override protected void onPostExecute(String json) {
            super.onPostExecute(json);
            if ( json != null && json.trim().length() != 0 ) {
                Intent intent = new Intent(TestTwitterSearchActivity.this, 
                        TestViewResultActivity.class);
                intent.putExtra("searchResult", json);
                startActivity(intent);
                Button goButton = (Button) TestTwitterSearchActivity.this.findViewById(R.id.button_go);
                goButton.setEnabled(true);
            }
        }
    }
}

The code looks pretty good. Your Twitter search is running within an AsyncTask so that it does not block the UI thread. But there is a usability problem here: When you start an AsyncTask, you should cancel it when your Activity moves into background. This is because AsyncTask will continue to run in a separate thread even when the Activity that started it is hidden (paused or even killed). Use cases where this might happen are: User going to the home screen; user hitting the "back" button; the user being directed to another Activity while the AsyncTask is still running.

For example, imagine in this Activity, the user clicks on the "Go" button to initiate a search, then hits the "back" button. Hitting the "back" button will hide the Twitter Search Activity, but it will not automatically stop any AsyncTasks; therefore when the task is completed, the user will suddenly be shown the search results, even though he is now in a totally different activity. Your AsyncTask code will continue to execute even when your Task has been moved to background!

One solution to this problem is to use the `onPause` hook to perform clean-up. The `onPause` method is always called when your Activity goes into background.

public class TestTwitterSearchActivity extends Activity {

    private AsyncTask mCurrentAsyncTask;
    private final Object mAsyncTaskLock = new Object();
    
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button goButton = (Button) findViewById(R.id.button_go);
        goButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.setEnabled(false);
                synchronize (mAsyncTaskLock) {
                    if ( mCurrentAsyncTask != null ) return;
                    AsyncTask<String, Void, String> task = new TwitterAndroidSearch();
                    mCurrentAsyncTask = task;
                    task.execute("android");
                }
            }
        });
    }
    
    @Override protected void onPause() {
        super.onPause();
        synchronized (mAsyncTaskLock) {
            if ( mCurrentAsyncTask != null ) {
                mCurrentAsyncTask.cancel(true);
                mCurrentAsyncTask = null;
            }
        }
    }

    /**
     * Runs a query on Twitter. 
     */
    class TwitterAndroidSearch extends AsyncTask<String, Void, String> {
        @Override protected String doInBackground(String... params) {
            try {
                String queryUrl = "http://search.twitter.com/search.json?q=" + params[0];
                URLConnection conn = new URL(queryUrl).openConnection();
                InputStream in = conn.getInputStream();
                Reader reader = new InputStreamReader(in);
                String json = CharStreams.toString(reader); // See Google Guava-libraries
                return json;
            } catch (IOException e) {
                // Treat IOExceptions as having no result
                return "";
            }
        }
        
        @Override protected void onPostExecute(String json) {
            super.onPostExecute(json);
            if ( json != null && json.trim().length() != 0 ) {
                Intent intent = new Intent(TestTwitterSearchActivity.this, 
                        TestViewResultActivity.class);
                intent.putExtra("searchResult", json);
                startActivity(intent);
                Button goButton = (Button) TestTwitterSearchActivity.this.findViewById(R.id.button_go);
                goButton.setEnabled(true);
            }
        }
    }
}

In the above code, we store an instance of the currently running AsyncTask on the Activity and check for it in onPause(). To program defensively, access to mCurrentAsyncTask is carefully synchronized. This approach allows you to provide a better user experience by cleaning up unexpected background tasks.

No comments:

Post a Comment