Android application user interfaces are decomposed into ‘screens’ called activities. The user navigates from screen to screen thanks to buttons, clickable elements, keyboard keys, gestures, etc. Each activity manages one ‘screen’. Beyond those screens, ‘activity instances’ are created, configured, destroyed, recreated following a somewhat sophisticated lifecycle. To develop an activity, developers need to create a class extending the
Activity base class and override a couple of methods to react to lifecycle transition, events etc. This blog post highlights the threading issues encountered when developing a responsive Activity.
Activity concept is one of the corner stones of Android. An activity is a single, focused ‘thing’ that the user can do. Almost all activities interact with the user, so the Activity takes care to create a ‘screen’ and to manage user requests.
To create an
Activity, you need to extend the
Activity class. There are two methods almost all subclasses of Activity need to implement namely,
onCreate(Bundle)which acts as a ‘constructor’. Inside this method you (re-) initialize the activity (for example, set the content View with the setContentView method, set members…).
onPausewhich is called when the user leaves the activity (the activity is no more visible). An interesting aspect of this method is its ‘killable’ aspect. After this method, the system can decide to kill the activity instance without calling any other methods. So, before exiting this method everything needs to be ‘saved’.
However theses two methods are only the tip of the iceberg. The activity lifecycle is fairly sophisticated.
The activity’s adventure playground
Activities are managed inside a stack composing a ‘task‘ . When a new activity is started, it is placed on the top of the stack and becomes the running and visible activity. The previous activities remain in the stack but are hidden until it becomes the top again (because all other activities started after it are closed).
Basically, activities have four states:
- Visible and focused: the activity is visible (i.e. on the top of the task), the user can interact with the activity.
- Visible but without the focus: the activity is still visible, but the user cannot interact with the activity (because of a view that has the focus on the top of the activity (like a dialog)). In that case, the activity is paused, but maintains the state (i.e. member values). It may be killed by the system in extreme low memory situation
- Hidden: the activity is completely hidden by another activity (full-screen). The activity is stopped. It still retains all state and members BUT is often killed by the system when memory is needed elsewhere.
- Finished: an activity that is paused or stopped can be killed by the system. The system can either call the
onDestroymethod or simply kill the process (without any notification). The state is lost in that case. If the activity is displayed again to the user, it must be completely restarted and restore its previous state itself (if at all).
The previous diagram illustrates the complete activity lifecycle. Method with red outlines are ‘killable‘, i.e. the system may kill the process with no notification after one of these methods. This makes the
onPause methods pretty important to store everything persistently. The
onCreate method restores the state.
To persist the state, you can either use the
Bundle object injected in the
onSaveInstanceState method or use files, preferences, and content providers… In the case of a
Bundle object, this
Bundle is re-injected in the
Lifecycle callbacks, Services and UI Thread
Despite this fairly complex lifecycle, the activity based development model is very flexible. To react to lifecycle transitions, activities override the adequate methods and change the UI and the state accordingly. Something to notice about activities, however, is the predominance of the UI thread (also called main thread). All callbacks (in fact all methods starting with ‘on‘) are called in the UI Thread and so, can modify the current view from those methods without any issues. However, doing long task inside the UI thread decreases the responsiveness of the application as well as the user experience.
A good example of this kind of problems is when an application tries to use a progress bar or a progress dialog. Updating the progress is made in the UI thread. So, to obtain a smooth animation, nothing else must be done in the UI thread.
It starts to be more tricky when interacting with an Android services (running in the background and managing long running task). If you call
onCreate method from the service runs also in the UI Thread, regardless of the thread you use to call the
startService method. The same behavior applies for
onBind method (called on the Service) runs in the UI Thread regardless of the thread you use to call the bindService method. Moreover, the
onServiceConnected method (invoked when the service is bound to retrieve the service object) is also called in the UI thread.
This forces you to reduce the time spent in the
onBind method of a service, to delegate the complexity to methods called by the service consumer in another thread. So, if you plan to do long operations with your service, you must create new threads and be careful to call such operations outside the UI thread… It can become a nightmare if the created threads are not finished when the activity instance is destroyed (either by the system, or after a reconfiguration like e.g., on orientation changes).
The morale of this story is simple. Remember that all the on* method of your activity run inside the UI Thread. So they can create dialogs, toast, etc. It is also the case if you bind to a service. But if you want to do anything long, you must create another thread. This can become really complex if you have:
- A lot of threads
- The activity dying before the threads is done
- or you need to react correctly to user actions (how to stop work to do something else)
The next posts will explain how to deal with background tasks.