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 fellowStackOverflow 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 uselessActivity
object. Take this, for example:
1
2
3
4
5
6
7
|
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, likerotating 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 theAndroid Compatibility Package, we can create an extra layer aroundAsyncTask
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 properFragment
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.
- Create a
Fragment
with two additional members: aboolean
to reflect whether the parentActivity
is ready for events, and aList
to enqueue callbacks triggered when it’s not.12protected
Boolean mReady =
true
;
protected
List<Runnable> mPendingCallbacks =
new
LinkedList<Runnable>();
- Ensure our
Fragment
is retained, overridingonCreate()
:12345@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setRetainInstance(
true
);
}
- Track the parent
Activity
life-cycle withFragment
callbacks:1234567891011@Override
public
void
onDetach() {
super
.onDetach();
mReady =
false
;
}
@Override
public
void
onActivityCreated(Bundle savedInstanceState) {
super
.onActivityCreated(savedInstanceState);
mReady =
true
;
}
- Provide a method that allows us to enqueue a
Runnable
to be executed whenever theFragment
deems appropiate (which could be right now):1234567public
void
runWhenReady(Runnable runnable) {
if
(mReady)
getActivity().runOnUiThread(runnable);
else
mPendingCallbacks.add(runnable);
}
- 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 previousonActivityCreated()
callback:12345678910@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 aFragment
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:
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
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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:
-
ManagedAsyncTask
takes aFragmentActivity
as construction parameter. - Inside the callbacks,
getActivity()
is used instead ofMyActivity.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
orManagedAsyncTask
, or find bugs/errors, please contact me.