Getting background tasks right is important. We want our UIs to remain responsive while the background task does its thing. Here I'm going to focus on Android's 'bound services' and a neat pattern for getting background work done on some shared resource, such as an internet connection and updating UI and/or model components.
Motivation
Let's suppose we want our app to have an activity that can fetch some data from the internet when the user clicks a button. The internet request will likely take some time so we have some background task to perform.
The obvious way to get background work done on Android is to use an AsyncTask. So we go ahead and subclass AsyncTask, create the code to make a network query and then in onPostExecute()
we use the result to update the UI. Most likely we made the AsyncTask some nested class of our activity and it looks something like this:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onClick(View v) {
// Start task
new MyAsyncTask().execute("");
}
public class MyAsyncTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
// Do work and make requests over the network
return result;
}
@Override
protected void onPostExecute(String result) {
//Update the UI, here we
TextView textView = (TextView)findViewById(R.id.text);
textView.setText("Got result: " + result);
}
}
}
By doing this we create an implicit reference to our activity when we make the task. Which leads to...
First problem! What about config changes?
The problem with this solution is when we rotate the phone, the current activity instance is destroyed and a new one created, leaving the AsyncTask holding onto a dead object. This causes results to not be delivered to our UI. In fact, this will happen for any config changes that may happen to our activity (and there are a few). This problem is outlined in the developer docs, but often overlooked:
If you want to read more about this problem, here's another blog post.
Second problem
Now, let's say we also have another background task that is currently syncing the users data at the same time as we want to make our quick network fetch for the user. This may be ongoing for some time and hog the network connection and so seriously slow down the experience for the user. If your interaction is more timing-critical this becomes a real problem. There is no way to prioritise the button push over the ongoing sync.
So what we really need is for some way to do background work on a shared resource (the internet connection) and have the results delivered to the UI reliably.
Bad solutions
- Using singletons to hold the asynctask. A singleton is probably overkill for this problem. You would also difficulty to tracking what currently needs to use the network in order to resolve conflicts.
- Disabling config changes. This is a common solution to the rotation problem as it prevents the activity being destroyed but is actually bad practice and can cause trouble later. (see Romain Guy's comment on this answer)
- Making a new AsyncTask after each rotation. This solution can actually be preferable. See Romain's actual answer to the question linked above.
- Permanent service. We could use a regular service and leave it running whilever our app is active (or longer?). But this is a very poor memory management - we should use services sparingly - and trying to improve this by managing the service lifecycle ourself seems painful.
Rules and aims
So for our solution we can set out the following targets:
- Don't disable config changes.
- Allow multiple application components to access the resource at once. Possibly adding priorities to this to favour some operations over others.
- Be a good citizen. Don't just use a permanent service to host background work. We probably need some kind of service to allow different application components to access the same resource but we shouldn't leave that running forever more.
Now let's introduce a couple of techniques to solve this:
Retained Fragments
These are our solution to the first problem. As of Android API level 13, retained fragments are the new recommended alternative to the now deprecated onRetainNonConfigurationInstance()
. A retained fragment is not detroyed during a configuration change and can be reconnected to an activity after the change in onCreate()
. Declaring and using one is simple:
In the fragment:
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
In the activity:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()))
...
}
So, by storing objects related to ongoing work in the fragment we can keep stuff connected to the Activity across config changes and deliver our results to the UI, but we are still no closer to being able to prioritise internet connections in a central place without using a singleton or firing up a permanent service.
Enter: Bound services
The solution to problem #2, the docs sum these up nicely:
A bound service allows components (such as activities) to bind to the service, send requests, receive responses, and even perform interprocess communication (IPC). A bound service typically lives only while it serves another application component and does not run in the background indefinitely.
...
A client can bind to the service by calling bindService(). When it does, it must provide an implementation of ServiceConnection, which monitors the connection with the service.
...
When the last client unbinds from the service, the system destroys the service
The really nice thing about this is each application component can strictly worry about whether it needs the service. Here, the an activity unbinds from the service after its request is finished without any consideration for whether another object may be using the service:
@Override
public void onRequestFinished()
{
//Our request is finished and we don't need the service any more.
getApplicationContext().unbindService(dataFragment.mConnection);
dataFragment.mConnection = null;
//Update the UI
mQuickButton.setEnabled(true);
mQuickButton.setText("Quick Operation");
}
This is the key to ensuring we keep a service alive only exactly as long as required.
Bound services sound perfect, but we need to use them just right...
Specific questions about their use arise (see my original StackOverflow question about this here):
"Should I bind from the app context or the activity context?"
If we bind from the activity context, we would be left with the same problem we had in problem #1 - we would be leaking the activity after it's supposed to be destroyed. This may crash or even if we could still get the results delivered to the UI using retained fragments we'd likely be getting ourselves in trouble later. (It's worth having a read about context memory leaks here)
"But if I bind from the application context, can I bind from it multiple times in different places or do I need to reference count and work out when to unbind myself?"
This is actually poorly documented and inspection of the Android source reveals that this it is safe (again, see my StackOverflow answer here) to bind several times from the application context without worrying who else may have bound. It's key to keeping bound services easy to use.
Solution and explanation
Bringing this all together, I have created a test application for using bound services.
This uses two methods of making requests on our bound service. - one short, high priority request that updates the UI, "Quick Operation", and one long running background task that the model performs, "Slow Operation".
Our BoundService lies at the heart of the work. Via their connection (and binder), components can get access to the service and run their requests:
public void onServiceConnected(ComponentName className, IBinder binder)
{
if (mRequest != null)
{
// We've bound to LocalService, cast the IBinder and get BoundService instance
BoundService.LocalBinder localBinderinder = (BoundService.LocalBinder) binder;
BoundService service = localBinderinder.getService();
service.makeRequest(mRequest);
}
}
A retained fragment is set to be the listener to the results of requests:
@Override
public void onRequestFinished()
{
if (getActivity() != null)
{
((MainActivity)getActivity()).onRequestFinished();
}
else
{
cachedResult = true;
}
}
If the activity is currently not attached to fragment (as could happen during a rotation), we cache the result in the fragment until it reattaches.
@Override
public void onAttach(Activity activity)
{
if (cachedResult)
{
((MainActivity) activity).onRequestFinished();
cachedResult = false;
}
super.onAttach(activity);
}
The service connection and request mechanism is exactly the same for model objects, but they can listen for results themselves. The service maintains a simple priority-ordered queue of requests and stops ongoing requests if those with higher priority arrive:
private boolean cancelOnGoingIfNecessary()
{
if (mOnGoingRequest != null)
{
if (mQueue.peek().mPriority.ordinal() > mOnGoingRequest.mPriority.ordinal())
{
mOnGoingTask.cancel(false);
return true;
}
}
return false;
}
When is this appropriate
When you are working with a shared, scarce resource (internet, bluetooth connection, lengthy processing pipelines) and want to make sure your requests are guaranteed to make it to the UI. Even if you aren't using a shared resource and don't require a bound service, you can still revert to using an AsyncTask with a retained fragment. Ultimately, there are a couple of tools here you can put to good use in the right circumstances.
Other solutions
Loaders and loader manager. These function similarly to a retained fragment. They can be created and reconnected to across activity config changes. These are more appropriate for lists with cursors. Specifically, a Loader
should 'monitor the source of their data and deliver new results when the contents change'. For more information, check out the Loaders dev docs.