Today we will continue to look at further Android architecture components by examining lifecycle observers: objects which can observe the lifecycle of an activity or fragment. We will also look a little more at the use of coroutines in Android.
You will remember from last year that Android activities have a lifecycle, with certain methods being called when the activity is in a certain state. For example, onCreate() is called when the activity is first created; onPause() is called when the activity is hidden; onResume() is called when the activity becomes visible again and onDestroy() is called when the activity is destroyed. The diagram below shows the lifecycle of an activity:
Furthermore, other components (such as fragments, and services, which we will come onto soon) also have a lifecycle with very similar methods.
What is the consequence of this? As many different Android components (e.g. activities and fragments) have a lifecycle, the developers of Android decided it would make sense to abstract out the lifecycle functionality into a Lifecycle class, and make all components with a lifecycle (Activity, Fragment, etc) contain a Lifecycle object. The Lifecycle object contains the current state of the app, which could be (ref: Android documentation):
Lifecycle.State.CREATED - following onCreate() and before onStart()Lifecycle.State.STARTED - following onStart() and before onResume()Lifecycle.State.RESUMED - following onResume()Lifecycle.State.PAUSED - following onPause() and before onStop()Lifecycle.State.STOPPED - following onStop() and before onDestroy()Lifecycle.State.DESTROYED - following onDestroy()Lifecycle object using its lifecycle property, and test for its state with lifecycle.currentState. This allows us to write code which will only run if the component is in a particular lifecycle state.
For example, inside an activity or fragment we could have:
if(lifecycle.currentState == Lifecycle.State.RESUMED) {
// code which should only run if we're in the "resumed" state
}
Furthermore, we can use isAtLeast() to test whether the component is in at least a certain state. For example:
if(lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
}
could be use to test that we are at least in the STARTED state. So the code inside the if statement would run if we are either in the STARTED state or in the RESUMED state. This can be useful if we want to execute code which depends on the activity or fragment being fully created (we could use isAtLeast(Lifecycle.State.CREATED)) or fully visible.
We can attach one or more lifecycle observers to any component with a lifecycle. Often, functionality might depend on the lifecycle of the component. For example, in a GPS application, we might only want to receive GPS updates when the activity or fragment showing the location is currently visible. Or, in a game, we would want the game to stop playing when the activity or fragment hosting the game is not currently visible (e.g. when a call is received). We could put this functionality in the relevant activity or fragment. However, as we saw last week, this can lead to large and bloated activities or fragments, and thus they do not have high cohesion.
So instead, we can create one lifecycle observer for each lifecycle-related behaviour we want to add. The intention is to put the code in the lifecycle observer, to keep the activities and fragments small and "clean". So we could, for example, create a lifecycle observer for turning the GPS listener on and off (so it turns on when the activity or fragment resumes, and off when the component pauses) or another lifecycle observer for stopping and starting a game when its parent component pauses and resumes.
Lifecycle observers are known as lifecycle-aware components because they react to the lifecycle of another component, such as an activity or fragment.
Lifecycle observers need to inherit from DefaultLifecycleObserver and override the appropriate methods, i.e. onCreate(), onPause(), onResume() etc. The methods however have different signatures to the usual ones: they all take a parameter of type LifecycleOwner. What is this? The LifecycleOwner is the parent component that owns the given lifecycle, i.e the Activity, Fragment, etc which launched the lifecycle observer.
Lifecycle observers are created in the normal way, by instantiating an object of our lifecycle observer class. This allows us pass in any data the lifecycle observer needs, such as a ViewModel
The code below shows an example of a lifecycle observer.
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class TestLifecycleObserver: DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
}
override fun onPause(owner: LifecycleOwner) {
}
override fun onResume(owner: LifecycleOwner) {
}
override fun onDestroy(owner: LifecycleOwner) {
}
}
You can see that you create methods corresponding to the equivalent activity or fragment lifecycle method, but supply the LifecycleOwner as an argument.
As explained above, we can create a lifecycle observer from a component, such as an activity or fragment. Here is an example. Note how we add the lifecycle observer to the lifecycle attribute of our activity, which represents the Lifecycle object (discussed above) associated with the activity.
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val lifecycleObserver = TestLifecycleObserver()
lifecycle.addObserver(lifecycleObserver)
}
}
We could pass in a ViewModel to the lifecycle observer, e.g.:
class TestLifecycleObserver2(val viewModel: MyViewModel) : DefaultLifecycleObserverand then we could access the
ViewModel in any method of TestLifecycleObserver. We'd pass the ViewModel across from the activity, e.g:
class MyActivity : AppCompatActivity() {
val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
val lifecycleObserver = TestLifecycleObserver2(viewModel)
lifecycle.addObserver(lifecycleObserver)
}
}
Last year we looked at coroutines in the SQLite and Room topic. A coroutine is a block of code which can run either in a foreground or a background context, and can switch contexts (switch between foreground and background). This is because a coroutine can be suspended. So when we switch to the background context, the code in the foreground context is suspended while the code running in the background, completes. We need to launch a coroutine from a scope, which represents the scope the coroutine "lives" in (often the Android lifecycle). Here is an example of an Android coroutine:
lifecycleScope.launch {
// This lambda is the coroutine
// Interact with UI in the foreground (read a search query)
val searchQuery = getDataFromUI()
var results = ""
// Switch to a background context to communicate with the web and with a
// database. The foreground code suspends while waiting for the background code to complete
withContext(Dispatchers.IO) {
results = fetchDataFromWeb(searchQuery) // must be done in the background
saveToRoomDatabase(results) // must be done in the background
}
// The foreground code resumes here
// Update the UI with the data from the web
updateUI(results)
}
We can actually put code which switches context in a suspend function and call that from our coroutine. For example:
suspend fun search() {
// Interact with UI in the foreground (read a search query)
val searchQuery = getDataFromUI()
// Switch to a background context to communicate with the web and with a
// database. The foreground code suspends while waiting for the background code to complete
withContext(Dispatchers.IO) {
results = fetchDataFromWeb(searchQuery) // must be done in the background
saveToRoomDatabase(results) // must be done in the background
}
// The foreground code resumes here
// Update the UI with the data from the web
updateUI(results)
}
A suspend function is a function in which the code can be suspended to allow other code to run in the background; it's always called from a coroutine. So we could call it like this:
lifecycleScope.launch {
search()
}
Typically coroutines are implemented using threads. A thread is a general concept in programming, used by many languages (Java, C and C++ as well, for example) and is a separate strand of execution. We have the main thread, which corresponds to the UI thread in Android or the main() method in console-mode Java. Then, we can run separate background threads to do background work (such as network communication) without blocking the main thread and causing it to become unresponsive. Kotlin coroutines are typically implemented using threads (with the Dispatchers.Main context the main Android UI thread) however, the architecture of coroutines is such that they could be implemented using something else if it was seen as desirable.
To more fully understand coroutines, it is helpful to see examples of their use outside of Android, in console-mode applications. Please see these notes; we may have time to look at them in the lecture, but if not, please read in your own time and let me know if you have any questions.
So far, all our coroutines have been launched from an activity. However they can be launched from any coroutine scope. The most usual scope is the lifecycleScope which is available from any component with a lifecycle (so fragments as well as activities). An interesting feature of coroutines launched from a lifecycleScope is that they are automatically terminated if the parent component is destroyed.
Interestingly, though, a ViewModel has a scope to launch coroutines, viewModelScope. This is useful, as the ViewModel is the recommended place to send network requests (Android documentation). Given the ViewModel's role is to fetch data and prepare it for presentation, it makes sense for network communication to be placed there. So a method of your ViewModel can launch a coroutine which fetches data from a server (in a background context) then updates some LiveData which can be observed by an activity or fragment.
So inside a ViewModel we can launch a coroutine as follows:
viewModelScope.launch {
}
It's worth reading this article in the Android documentation. These notes cover how you can control coroutines according to the current lifecycle state.
Here is a full example, which uses a LifecycleObserver as well as a coroutine to display a timer in seconds. The timer pauses when the activity pauses, but is not reset - when the activity resumes, the timer resumes where it left off.
Firstly, the main activity. Note how we create a TimerLifecycleObserver (our own custom lifecycle observer) and attach it to the activity's lifecycle. The activity also observes a piece of LiveData, liveTime, from the view model and updates a text view accordingly.
package com.example.lifecycle1
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import androidx.activity.viewModels
import androidx.lifecycle.Observer
class MainActivity :AppCompatActivity() {
val timerViewModel: TimerViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val observer = TimerLifecycleObserver(timerViewModel)
this.lifecycle.addObserver(observer)
timerViewModel.liveTime.observe(this, Observer {
findViewById(R.id.tv1).text = "$it seconds have passed."
})
}
}
Secondly, the view model:
package com.example.lifecycle1
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class TimerViewModel : ViewModel() {
var time: Long = 0
set(value) {
field = value
liveTime.value = value
}
var liveTime = MutableLiveData<Long>()
}
This is a simple ViewModel that holds a time value.
Thirdly, the LifecycleObserver:
package com.example.lifecycle1
import androidx.lifecycle.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class TimerLifecycleObserver(val viewModel: TimerViewModel) :
DefaultLifecycleObserver {
private var running = false
override fun onCreate(owner: LifecycleOwner) {
var currentTime: Long
var lastTime = 0L
var secondsPassed = 0L
// Launch the coroutine from the lifecycleScope of the lifecycle owner (i.e. the Activity in this case)
owner.lifecycleScope.launch {
// Implement the counter in the background so it doesn't block the
// main thread
withContext(Dispatchers.IO) {
// Display the timer for one hour (3600 seconds)
while (secondsPassed < 3600) {
// Only update the time if we are running
if (running) {
currentTime = System.currentTimeMillis()
// Update secondsPassed if 1000 milliseconds have passed
if (currentTime - lastTime > 1000) {
lastTime = currentTime
secondsPassed++
// Must switch back to main context as modifying
// ViewModel will result in a UI update
withContext(Dispatchers.Main) {
viewModel.time = secondsPassed
}
}
}
}
}
}
}
// Stop the time update when we pause
override fun onPause(owner: LifecycleOwner) {
running = false
}
// Start the time update when we resume
override fun onResume(owner: LifecycleOwner) {
running = true
}
}
This is a bit more complex but note how we override the onCreate(), onPause() and onResume() methods. First, in the onCreate() we:
owner.lifecycleScope), which will be the activity in this case.Dispatchers.IO context) it will not update the main thread and the UI will not freeze and become unresponsive.secondsPassed variable if 1000 milliseconds (1 second) have passed since the last update, but only if the variable running is true. This is related to whether the lifecycle owner (the activity) is in the resumed state, as discussed below.secondsPassed is updated, we update the time property of the view model, so it will be displayed to the user (due to the live data being observed from the activity).onPause() and onResume(), we set the variable running to false and true, respectively. This will mean that, due to the logic of the coroutine, the secondsPassed variable will stop updating if the lifecycle owner is currently paused.TextView. It should look like the screenshot below:
MutableList of strings, and provide LiveData returning that list - this is similar to what you did last week. It should have a addStatusMessage() method which adds a string to the MutableList.ViewModel as follows:
onCreate(), the message "Created" should be passed to the view model via the addStatusMessage() method;onResume(), the message "Resumed" should be passed to the view model via the addStatusMessage() method;onPause(), the message "Paused" should be passed to the view model via the addStatusMessage() method;onDestroy(), the message "Destroyed" should be passed to the view model via the addStatusMessage() method;TextView to display all of them - like the examples last week.TextView as a result.https://github.com/nwcourses/LifecycleMapsStarter.gitThis is a starter project for a GPS mapping application making use of a lifecycle observer and coroutines. It should have this functionality:
ViewModel, which should store the current lat/lon and provide that as LiveData;LifecycleObserver.LifecycleObserver to initialise the GPS listener in onCreate(), start GPS updates in onResume() and stop GPS updates in onPause(). In onLocationChanged(), pass the latitude and longitude to the ViewModel. The lifecycle observer contains comments to help you get started.ViewModel so that it has a method which uses a coroutine to look up the latitude and longitude of a given place by calling the web API:
https://opentrailview.org/nomproxy.php?q=the place name the user enteredThe API sends the data back as JSON. Parse the JSON (just take the first result if there are multiple results) and reset the
LatLon object within the ViewModel to that value. You might find it helpful to look at last year's notes for this. You will not need to use Fuel; you can just use the URL.readText() method and then create a JSONObject from the data returned.
LifecycleObserver in your activity and add it to the activity's lifecycle.ViewModel. In the map fragment you should observe the LiveData and update the map appropriately.