To understand Android development more fully it is useful to have understanding of the Android activity lifecycle.
Mobile apps are not quite like standard desktop applications. In particular, the application may be interrupted, most commonly by the user answering a phone call. Also, users commonly switch from one app to another. Unless the user quits the app using the "back" button, when this happens the original app is still running in the background.
To manage these possiblities, Android activities have a defined
lifecycle. The idea is to code activities to respond to the different
points in the lifecycle. The lifecycle consists of a series of methods which run
one after the other, as follows. When coding your app, you override whichever
methods you want specific behaviour to occur at. With practically every
activity, this includes onCreate()
, but your activity might need
to override the others too.
It is described in full on the Android website, here.
The original article describes the lifecycle in full but to summarise, the following methods run when certain events occur:
onCreate()
: runs when the application is first launched.
Note also that certain actions, such as changing the device orientation,
can force the activity to be destroyed and recreated, which will cause onCreate()
to re-run.onStart()
and onResume()
: both run when the activity becomes visible. This can either be when it first becomes visible, or becomes visible again after being hidden. For example, it will be called
after you return to the current Activity after using a completely different application, or after closing a second activity within the same application, e.g. a
preferences screen, causing the original activity to become visible again.
See the discussion of onPause()
and onStop()
, below, for the difference between the two.onPause()
: when the activity pauses after being fully visible, and focused. This happens on shutdown of the activity, or when another activity comes in front of the current activity, including activities not occupying the whole of the screen.onStop()
: is called immediately after onPause()
but only on shutdown, or if the activity coming in front of the current one occupies the whole of the screen. onResume()
is the opposite of onPause()
and onStart()
is the opposite of onStop()
. Thus, onResume()
is called when the current activity becomes visible and focused again after removing an activity in front of it (or when launching the activity), including cases where the activity on top of the current activity, which has just been removed, does not occupy the whole of the screen. By contrast, onStart()
is only called either on startup or when the activity on top of the current activity, now being removed, occupies the whole of the screen.onDestroy()
: runs when the user dismisses the application. This will happen
when the user presses the Back button from the main activity. It can also be called under other
circumstances, e.g. Android kills the activity due to low memory, or when the user rotates the
device.The lifecycle can have important consequences for development. For example, in a mapping app you might want to stop GPS communication when the activity becomes invisible and start it again when it becomes visible again (to save battery), in which case you would stop the GPS in onPause()
and start it in onResume()
. If you did this in onDestroy()
and onCreate()
instead, the GPS would still be running if the activity was running but invisible, which for a mapping app would be unnecessary.
The differences between onStart()
/onStop()
and onResume()
/onPause()
are quite subtle and need only be considered if you are writing activities which do not occupy the whole of the screen, which is not so common. Generally, onPause()
and onResume()
are more commonly used.
Frequently in an app we need to perform a task when the activity has been completely shut down. For example, in a music player, we probably want the music to continue to play when the user has closed the player's main activity - and we want the user to be able to pause or rewind the same music when they relaunch the activity. Another example might be a mapping application in which the user would like to record their walking route using GPS. We want the recording to continue even if the user closes the activity - and allow the user to stop the recording if they re-launch the activity.
Service
s
(see the Android developer documentation for full details - here).<service android:name=".MusicService"></service>
androidx.lifecycle.LifecycleService
class rather than an ordinary Service
; this requires the dependency androidx.lifecycle:lifecycle-service:X.Y.Z
where X.Y.Z
is the version; the current version is 2.7.0.To understand services, you need to understand Intent
s, which are used to communicate between activities and services. What is an Intent
? Essentially, it's a message which can be sent in between Android application components, such as between an activity and a service (in both directions) or to launch a second activity from the main activity. They can also be used to launch entirely separate applications; for example you can launch the standard camera app from your own app in order to take a picture. Intents contain two important components:
android.app.Service
and then launch it from the main activity.onBind()
method in your service, but it can return null
.import android.app.Service import android.content.Intent import android.os.IBinder class MyService: Service() { // start handler override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { return START_STICKY // we will look at this return value below } // bind handler - not needed in many cases but defined as an abstract // method in Service, therefore must be overridden override fun onBind(intent: Intent?): IBinder? { return null // can just return null if binding is not needed } }
Rather like activities, services have lifecycle methods including:
onCreate()
- when the service is created;onStartCommand(intent: Intent?, startFlags: Int, id: Int): Int
- runs when a service is started (see below); onBind(intent: Intent?): IBinder?
- runs when a service is bound. As seen above, you have to implement this even if you do not use binding, but it can just return null
onDestroy()
- when a service is destroyed.val startIntent = Intent(this, MusicService::class.java) startService(startIntent)
onStartCommand()
is called when a service is started with startService()
START_STICKY
(see here)START_NOT_STICKY
insteadSTART_STICKY
:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // do something, e.g. start GPS, or music playing, etc. return START_STICKY // Important if you want service to be restarted! }
intent
parameter will be null (again, see here; otherwise, this is the intent used to start the service)stopService()
with the same Intent used to start the service, for example:
stopService(serviceIntent)
onDestroy()
method of the service will be called. In the Service's onDestroy()
, you must stop any threads, background tasks, etc. that the Service is running, as otherwise, the thread will continue to run after its parent Service has been destroyed, resulting in possible memory leaks and unintended behaviourWhile starting a service is best for long running services, it has the disadvantage that you will not have a reference to the Service in the Activity. If you want to be able to easily control your Service from your Activity, there are two main approaches:
We will first look at binding the service. You must bind it instead of, or in addition to, starting the service (see the Android documentation for more details).
Binding a service is a little trickier than starting it as you need to provide a Binder
object. This is an interface to the service which gives the outside world (e.g. the activity) access to it. The Binder
object is an object which inherits from android.os.Binder
.
The Activity is able to access the Binder (see below) and, as long as the Binder has a method which returns the Service, can obtain the Service. Thus, the Binder should provide a method to return the service.
As seen above, you must also provide an onBind()
method within the Service to create a new Binder object and return it when the Activity binds to the service. Remember that you need this method even if you do not intend to use binding, but it can return null
in that case.
A common pattern is to have the Binder as an inner class of the Service (an inner class is a class within another class)
To bind a service, we need two more components, a Binder
(as we have seen) and a ServiceConnection
, which is used to provide a connection between the activity and the service, and is discussed further below.
The architecture of binding a service, showing the Binder
and ServiceConnection
, is shown below:
Here is an example.
(Note that IBinder
is an interface which Binder
implements).
class MusicService: Service() { inner class MusicServiceBinder(val musicService: MusicService): android.os.Binder() override fun onBind(intent:Intent) : IBinder { return MusicServiceBinder(this) } }Note the following:
onBind()
method. This method runs when the activity binds to it.musicService
. This is needed as the activity will receive only the binder, not the service. Thus, if the activity wants the service, it must obtain it from the binder.As we have seen, in our activity, we must create a ServiceConnection
object, to obtain a connection to the service. Here is an example - this might go in your onCreate()
:
val serviceConn = object: ServiceConnection { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { service = (binder as MusicService.MusicServiceBinder).musicService } override fun onServiceDisconnected(name: ComponentName?) { } }It is an instance of an anonymous class (a class with no name, which inherits from the abstract class
ServiceConnection
and overrides the required methods on-the-fly. These methods are:
onServiceConnected()
method is a callback method which runs as soon as the Activity has been bound to the Service. The Binder object (the inner class within our Service, see above) is provided as a parameter to onServiceConnected()
, so we cast it to the correct object (MusicService.MusicServiceBinder
) and obtain our Service using its service
attribute.onServiceDisconnected()
runs when the service is disconnected from the activity. Here, we're not doing anything in the method but we still need to override it.bindService()
to initiate the bindingFinally, In your main activity, you bind the service using bindService()
which, like startService()
takes an Intent for the Service.
val bindIntent = Intent(this, MusicService::class.java); bindService(bindIntent, serviceConn, Context.BIND_AUTO_CREATE)This will bind the activity to the service and trigger the onBind() method in the MusicService. The Context.BIND_AUTO_CREATE flag will "automatically create the service as long as the binding exists" (see here), without this flag, you will also need to call
startService()
to start the service.
onDestroy()
is called), you should call unbindService()
to let Android know that the activity does not wish to be connected anymore.unbindService()
takes the ServiceConnection as an argumentoverride fun onDestroy() { super.onDestroy() unbindService(gpsServiceConn) }
startService()
and bindService()
.startService()
- and allowing the activity to control the service - this is done with bindService()
.unbindService()
when the activity is destroyed.Intent
and as such can pass data to another
application component as a Bundle
BroadcastReceiver
objectaction
IntentFilter
val broadcast = Intent().apply { action = "sendTime" putExtra("time", System.currentTimeMillis()) } sendBroadcast(broadcast)
intent
with an action
of sendTime
time
, containing the current time in
milliseconds since Jan 1st 1970sendBroadcast()
to send the Intent as a broadcastService
(so that the Service can broadcast updates to one or more activities) but could equally well be placed in any application component, such as an activityreceiver = object:BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { "sendTime" -> toast("${intent?.getLongExtra("time", 0)}") } } }
IntentFilter
in our activity to explicitly state
that this activity is capable of receiving intents with an action of sendTime
:
val filter = IntentFilter().apply { addAction("sendTime") }
sendTime
. Even though this is specified by the
intent filter, it is recommended to do this as "it is possible for senders
to force delivery to specific recipients, bypassing filter resolution"
(see the documentation)ContextCompat.registerReceiver(this, receiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED)
ContextCompat.RECEIVER_NOT_EXPORTED
specifies that we cannot receive broadcasts from other apps, which is useful for security. In theory, an app could send a malicious broadcast to another app which could have harmful side effects, so if we don't need inter-app communication when receiving broadcasts, we should turn it off.ContextCompat
because we are using the compatibility version of the call; RECEIVER_NOT_EXPORTED
was only introduced in API 33. The compatibility (appcompat
) library allows us to use newer API features on older versions of Android.onDestroy()
override fun onDestroy() { super.onDestroy() unregisterReceiver(receiver) }
It's important to note that it might take time to start a service. The process of starting a service takes place asynchronously and might take time to complete. This has important implications if you do something in the main activity (such as testing whether a permission has been granted) while waiting for the service to start. You might, for example, send a broadcast to tell the service to start listening for GPS updates as soon as the permissions have been checked. But at this stage (if the user does not need to explicitly grant permission via the dialog) the service may not have been started yet, so there will be no service to broadcast to! Consequently, your broadcast intent will go un-noticed.
A way round this is to send a broadcast from the service to the activity as soon as onStartCommand()
has completed. This then tells the activity that the service is started, and ready to receive broadcasts. So as a result, the activity can then check permissions when this broadcast has been received from the service.
You are going to write a stopwatch application using services and broadcasts. The idea is to develop a front end allowing the user to start, stop and reset a stopwatch via buttons on the UI. The stopwatch should be controlled by a service. Each button (start, stop and reset) on the UI should send a broadcast to the service, and the service should act accordingly.
build.gradle.kts
:
implementation("androidx.lifecycle:lifecycle-service:2.7.0")This allows you to launch coroutines from the service. If you are using an older version of Android Studio it's possible that you'll have to use an earlier version than 2.7.0; if you encounter problems, try reducing it by one version e.g. 2.6.0, 2.5.0 etc.
androidx.lifecycle.LifecycleService
count
, an integer which increases by one every second (see below).onStartCommand()
in the service should launch a coroutine which should include a loop which loops until the service is destroyed. This can be done by using a boolean which is set to false in onDestroy()
.Dispatchers.IO
and in here, call delay()
with a parameter of 1000 (milliseconds). This will pause the coroutine but in the background context so the UI does not become unresponsive. After the delay()
, increase the counter by one. The code would look something like this:
lifecycleScope.launch { // ... other code ... withContext(Dispatchers.IO) { delay(1000) // increase counter... } }
ViewModel
containing the counter as LiveData
to implement this.