Let me begin by saying that AsyncTask is an amazing piece of abstraction. With almost no additional effort, you can schedule a task to run in a background thread (or thread pool, just as easily) and have it post progress and results back to the calling thread.

It has, however, one tiny little problem. Or, in the words of a fellow StackOverflow contributor, it’s “massively flawed”.

Improper use

Whenever you fire an AsyncTask from an Activity, which is mostly the point, there’s a chance that by the time onPostExecute() is called the original context for the task no longer exists.

If, during the background execution, the Activity instance is destroyed and recreated in the event of a configuration change (which always happens by default), you’ll be left with a stale reference to a useless Activity object. Take this, for example:

private Button mMyButton = new Button();

// ...

protected void onPostExecute(Long result) {
    mMyButton.setText("Finished execution");
}

The call to mMyButton.setText() is bound to the Activity instance that fired the AsyncTask. Say the user does something weird and unexpected, like rotating his phone, and the configuration change forces a full recreation: setText() will be invoked upon a Button object that only exists in your callback, doing nothing.

Working around this issue is, of course, entirely possible — a couple of workarounds are mentioned in the question I linked above. Most approaches, however, break the abstraction, and the magic of AsyncTask is lost.

Fragments to the rescue

Using Android’s 3.0 Fragments API, available for versions 1.6+ in the Android Compatibility Package, we can create an extra layer around AsyncTask that fixes the problem while preserving the abstraction.

A Fragment can outlive its parent Activity through configuration changes by way of setRetainInstance(true). Even better, overriding the proper Fragment event callbacks, it’s possible to distinguish between a recreation and a real destruction, as well as knowing for sure when an Activity is ready to receive events.

We’ll see how to create a Fragment to manage our AsyncTask instances, and ensure a live Activity instance at the time onPostExecute() is called. The mechanism is completely independent from the specifics of the background work, so it’s simple to create a full abstraction that gives a safe execution environment for callbacks and preserves the ease of use of AsyncTask.

A TaskManagerFragment

The basic idea is as follows. I say basic because the following code eschews a lot of necessary precautions and stylish methods for the sake of brevity. Everything is (to my knowledge, at least) properly taken care of in the actual implementation.

  1. Create a Fragment with two additional members: a boolean to reflect whether the parent Activity is ready for events, and a List to enqueue callbacks triggered when it’s not.
    protected Boolean mReady = true;
    protected List<Runnable> mPendingCallbacks = new LinkedList<Runnable>();
    
  2. Ensure our Fragment is retained, overriding onCreate():
    @Override
    public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
    	setRetainInstance(true);
    }
    
  3. Track the parent Activity life-cycle with Fragment callbacks:
    @Override
    public void onDetach() {
        super.onDetach();
        mReady = false;
    }
    
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mReady = true;
    }
    
  4. Provide a method that allows us to enqueue a Runnable to be executed whenever the Fragment deems appropiate (which could be right now):
    public void runWhenReady(Runnable runnable) {
        if (mReady)
            getActivity().runOnUiThread(runnable);
    
        else
            mPendingCallbacks.add(runnable);
    }
    
  5. Ensure all the Runnable objects that were marked as pending when we weren’t ready are executed the moment we are. To achieve that, we’ll rewrite our previous onActivityCreated() callback:
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mReady = true;
    
        int pendingCallbacks = mPendingCallbacks.size();
    
        while (pendingCallbacks-- > 0)
            getActivity().runOnUiThread(mPendingCallbacks.remove(0));
    }
    

Simple, right? Packing our callbacks into Runnable objects and using a Fragment such as the above to schedule their execution, code that affects an Activity will run only when the Activity is ready for it. Let’s take a look at a properly managed callback:

public class MyActivity extends FragmentActivity {
    private TaskManagerFragment mManager;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // ...
        mManager = new TaskManagerFragment();
        
        getSupportFragmentManager().beginTransaction()
            .add(mManager, "TaskManagerFragment")
        .commit();
        // ...
    }

    public void fireTask() {
        new AsyncTask<Void, Void, Void>() {

            protected Void doInBackground(Void... params) {
                // ...
            }
            
            protected void onPostExecute(Void result) {
                mManager.runWhenReady(new Runnable() {
                    @Override
                    public void run() {
                        mMyButton.setText("Finished execution");
                    }
                });
            };
            
        }.execute();
    }

Works like a charm and looks… absolutely horrible. Who wants his front-end Activity code littered with background execution management logic?

Preserving the abstraction

Since an Activity‘s FragmentManager is publicly accessible, it’s completely possible to move all of that logic outside. We can put it into a class that behaves exactly like AsyncTask, but takes care of synchronizing its callbacks through our TaskManagerFragment.

Unfortunately, extending the AsyncTask class is not the way to go, since some of its methods are final. We have to use composition rather than inheritance, losing a nice parking spot in the class hierarchy but gaining full control.

This post is already quite long as it is, so I will not go into detail here. A complete implementation of such a class, named ManagedAsyncTask, is available in the repository. I tried to mimic the regular AsyncTask interface as much as possible, so before we take a look at how you’d actually use this Fragment in your code, let’s go back to an unsafe AsyncTask for comparison:

public void fireTask() {
    new AsyncTask<Void, Void, Void>() {

        protected Void doInBackground(Void... params) {
            // ...
        }
        
        protected void onPostExecute(Void result) {
            mMyButton.setText("Finished execution");                        
        };
        
    }.execute();
}

Nothing out of the ordinary. Now, let’s take a look at a perfectly safe task managed with TaskManagerFragment:

public void fireTask() {
    new ManagedAsyncTask<Void, Void, Void>(this) {

        protected Void doInBackground(Void... params) {
            // ...
        }
        
        protected void onPostExecute(Void result) {
            ((MyActivity) getActivity()).b.setText("Finished execution");
        };
        
    }.execute();
}

There we go! Thanks to the Fragment API, this turned out to be incredibly unobtrusive, allowing us to fully preserve the abstraction layer around the background worker. There’s little more than a hint in our Activity code of the underlying context book-keeping, seen in the two minor differences between the snippets:

  1. ManagedAsyncTask takes a FragmentActivity as construction parameter.
  2. Inside the callbacks, getActivity() is used instead of MyActivity.this (or direct member access).

It’s a pity that you have to cast the return of getActivity(), but it was either that or using another object/adding a fourth generic parameter.

If you come up with any improvements for TaskManagerFragment or ManagedAsyncTask, or find bugs/errors, please contact me.

Saludos!

About these ads