18.
Coroutines & Jetpack
Written by Luka Kordić
Congratulations, you’ve reached the final chapter of the book. You’ve amassed much knowledge about Kotlin coroutines and Kotlin Flow APIs. But you still have a couple of important things to learn about using coroutines in Android, which you’ll learn in this chapter. Here’s a short overview of the things you’ll focus on:
- Adding a viewmodel to hold the state of your UI.
- Learning about viewModelScope and main safety.
- Comparing LiveData and StateFlow for holding the UI state in viewmodels.
- Testing coroutines in Android.
- Using coroutines in Jetpack Compose code.
Before working with the code, make sure you’re using JDK 11 in your Android Studio. Open Android Studio preferences by navigating to Android Studio > Preferences > Build, Execution, Deployment > Build Tools > Gradle to check the version. You’ll see a window like the image below:
Your Android Studio should come with JDK 11 bundled. Select Embedded JDK and click OK. You’re ready to proceed.
Coroutines in ViewModels
One of the most commonly used architectural patterns in Android is MVVM. Its usage increased dramatically when Google released Android Architecture Components(AAC). A ViewModel is a component whose primary role is to provide data to the UI and to survive configuration changes. It also acts as a communication center between a repository and the UI. Another great thing about the ViewModel is that it’s lifecycle aware and you usually associate one ViewModel with one activity or fragment.
Adding a ViewModel to the Project
Until now, you’ve invoked repository methods directly from the activity. You already know a ViewModel should be responsible for the communication between the repository and the activity, and it’s ready for you to use in the project. Open DisneyViewModel.kt and inspect the code inside.
class DisneyViewModel(private val disneyRepo: DisneyRepository) : ViewModel() {
}
class DisneyViewModelFactory(private val repo: DisneyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DisneyViewModel(repo) as T
}
}
DisneyViewModel is empty for now, but you’ll start adding methods to it in a minute. DisneyViewModelFactory is a custom factory class that you need to implement to pass in dependencies through the constructor.
ViewModelScope
All the things mentioned in the introduction make a viewmodel an ideal place to launch and manage coroutines. Being lifecycle-aware, you can be sure it won’t leak any work or waste resources if you do everything correctly. The Google team also recognized the potential in viewmodels, so they built in the viewModelScope that gets canceled when onCleared is called. The definition looks like this:
public val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(
JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
)
}
It’s defined as an extension property on the ViewModel class and uses a custom getter that will try to return an existing instance of CoroutineScope. Otherwise, it creates a new CloseableCoroutineScope with SupervisorJob and Dispatchers.Main.immediate. These two parameters are quite important.
Because the scope is created with a SupervisorJob, you can launch multiple coroutines and if one of them fails, others won’t be canceled. This is quite important because you’ll often have more than one coroutine launched in your viewmodels.
Understanding Dispatchers.Main.immediate is even more important for using viewModelScope properly. You might think it’s weird to have a coroutine scope that launches coroutines on the main thread by default, but it makes a lot of sense. There are two main reasons:
- ViewModel is a concept related to UI and is often involved in updating it. Using a different dispatcher would introduce at least two extra thread switches that might not be necessary.
immediateexecutes the code immediately when the coroutine is already in the proper context. Also, a viewmodel shouldn’t be responsible for switching execution context because its primary purpose is to hold UI data. - You should write all your functions to be main-safe. That means the function should never block UI updates on the main thread. The most common way to make your functions main-safe is to mark them as suspending and then wrap the blocking code with
withContext(Dispatchers.IO)orwithContext(Dispatchers.Default), depending on the type of work it’s doing. If you ensure all your functions are main-safe, launching a coroutine in a viewmodel on the main dispatcher isn’t a problem.
Practicing Main Safety
Go through an example to see how implementing main-safe functions look in practice. Open DisneyRepository.kt and add the following to the bottom of the interface:
fun testMainSafety()
Go to DisneyRepositoryImpl.kt and add the implementation for the method.
override fun testMainSafety() {
// Simulate a blocking call
Thread.sleep(2000)
println("World")
}
testMainSafety is a simple function that will put the current thread to sleep for two seconds to simulate a blocking call, like executing a network request. With this code in place, open DisneyViewModel.kt and put in the following method:
fun testMainSafety() {
viewModelScope.launch {
disneyRepo.testMainSafety()
}
println("Hello")
}
Here, you create a new coroutine in viewModelScope and call the test function you just wrote. Remember that all coroutines created in viewModelScope execute on the main thread by default. Outside the coroutine, you simply print Hello to the console.
Open DisneyActivity.kt from the last chapter. Remove the disneyRepository variable and add:
private val viewModel: DisneyViewModel by viewModels {
DisneyViewModelFactory(DependencyHolder.disneyRepository)
}
Add the missing imports if needed. In onCreate, below initUi() add the following call:
viewModel.testMainSafety()
Build and run. Open the Logcat window and enter System.out in the search field to filter out unnecessary outputs. Tap Networking, Persistence, Jetpack and check the logs. You’ll notice the UI freezes for two seconds. After that, the output appears like this:
I/System.out: World
I/System.out: Hello
Although you launched a new coroutine to execute testMainSafety, the UI thread is still blocked. That means the function you wrote isn’t main-safe. You’ll fix this. First, go back to DisneyRepository.kt and add suspend modifier to testMainSafety. Then, replace the old implementation in DisneyRepositoryImpl.kt with this one:
override suspend fun testMainSafety() = withContext(Dispatchers.IO) {
// Simulate a blocking call
Thread.sleep(2000)
println("World")
}
There are two significant changes: adding the suspend modifier and using withContext(Dispatchers.IO). Build and run the app and repeat the steps from the previous example. The output will look like this:
18:04:45.605 I/System.out: Hello
18:04:47.607 I/System.out: World
Some things were removed from the output to make it easier to read. Notice that you now see Hello printed as soon as you tap on Networking, Persistence, Jetpack and World comes out two seconds later. Because you’ve changed the thread of execution with Dispatchers.IO and added the ability to suspend, this function is now main-safe and fine to invoke from viewModelScope. Always follow this pattern when using coroutines in Android apps.
Note: You can now remove
testMainSafetyfrom the repository and viewmodel. You won’t need that anymore. Remove its usage from the activity as well.
Comparing LiveData to Kotlin Flow
LiveData was created in 2017 as an easy-to-use observable data class. Since then, it has been a go-to solution for many developers for holding the UI state data. It’s simple to start with and provides a reactive way to update the UI without much complication. LiveData is an observable data holder designed to be used in ViewModels and observed by activities or fragments. It’s lifecycle-aware, which means the views will only receive updates if they’re active. This means you don’t need to cancel subscriptions manually.
Since Kotlin Flow came out, developers have wondered whether they should switch to StateFlow for holding UI state instead of using LiveData. And the truth is, there’s no correct answer to this. As with most questions, the answer is: It depends. In this chapter, you’ll see and compare examples of using both LiveData and StateFlow. The goal is to make you aware of the pros and cons of each tool, so you can decide which one to use.
Storing UI State With LiveData
To start, open DisneyViewModel.kt and add the following code to the DisneyViewModel class:
// 1
val charactersLiveData = disneyRepo.getDisneyCharacters().asLiveData()
// 2
fun getFreshData() {
viewModelScope.launch { disneyRepo.getFreshData() }
}
Here’s a short breakdown of the snippet above:
-
disneyRepocallsgetDisneyCharacters(), which returns aFlow<List<DisneyCharacter>>and converts it to LiveData usingasLiveData(). Whenever the underlying Flow changes, each activecharactersLiveDataobserver will receive the updated data. - You launch a new coroutine in
viewModelScopeand call the repository to fetch new data.
asLiveData is an extension function on Flow, which creates a LiveData instance that has values collected from the original Flow. Its implementation looks like this:
@JvmOverloads
public fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect { (it) }
}
The context parameter lets you specify the context to collect the upstream flow in. It uses EmptyCoroutineContext with Dispatchers.Main.immediate by default. timeoutInMs represents the timeout in milliseconds before canceling the block when there are no active observers. DEFAULT_TIMEOUT is five seconds.
It uses the liveData coroutine builder, which will start executing when LiveData becomes active and have values yielded from the given block. You can also use the liveData builder directly when you don’t have a Flow to convert from and you need only to fetch one object from the repository. For example:
val user: LiveData<User> = liveData {
emit(repo.getUser())
}
Observing LiveData
Now that you have your ViewModel up and running, return to DisneyActivity.kt to fetch the data. Navigate to fetchDisneyCharacters and fill in the empty body with this:
viewModel.charactersLiveData.observe(this) {
showResults(it)
}
Observing LiveData is rather simple. All you need to do is call observe and pass in an instance of a LifecycleOwner and a lambda that will run when the data changes. There’s no need to worry about canceling the observer because LiveData will only emit updates to active observers.
To complete the example, replace the empty getFreshData with this:
private fun getFreshData() = viewModel.getFreshData()
The work of obtaining new data is delegated to the ViewModel. Build and run the project to make sure everything works well. Tap Networking, Persistence, Jetpack and you should see Mickey and Simba in your list. Tap the refresh icon in the top-right corner to fetch a full list of characters.
The list should now contain new characters below the initial two, like in the image below:
Storing UI State With StateFlow
A StateFlow represents an observable read-only state with a single updateable property value. Changes to value emit updates to its collectors. It’s important to remember StateFlow is hot, meaning it’s active even when no collectors are present. Here’s an example of its usage. Start by opening DisneyViewModel.kt and replacing the line val charactersLiveData = disneyRepo.getDisneyCharacters().asLiveData() with the following snippet:
val charactersFlow = disneyRepo.getDisneyCharacters().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
disneyRepo.getDisneyCharacters() returns Flow<List<DisneyCharacter>>. To convert from Flow to StateFlow, you use the stateIn extension function, which accepts three parameters:
- scope: Represents a coroutine scope in which the upstream flow will start.
- started: The strategy controls when the flow is started and stopped.
- initialValue: This is self-explanatory, simply accepting a value to start the flow with.
started parameter is a bit more complex than the other two and requires more explanation. started implements the SharingStarted interface. There are three possible values for this parameter:
- Lazily: Starts when the first subscriber appears and never stops.
- Eagerly: Starts immediately and never stops.
-
WhileSubscribed: By default, this means it starts when the first subscriber appears, stops as soon as the last subscriber disappears and keeps the last item forever. But it has two parameters you can tweak to change its behavior:
-
stopTimeoutMillis: sets up a delay between when the last subscriber disappears and stopping the upstream flow. This comes in handy when users rotate their screens, for example. You don’t want to cancel the flow because the subscriber went away for a short period. That’s why you passed in
5000as an argument. It will stop the flow only if there’s no subscriber for longer than five seconds. -
replayExpirationMillis: You can use this parameter when you return to the screen after a longer period and don’t want to show stale data. For example, if you pass in
2000as an argument, StateFlow forgets the last emitted value and reverts it to the initialValue two seconds after stopping the upstream flow.
-
stopTimeoutMillis: sets up a delay between when the last subscriber disappears and stopping the upstream flow. This comes in handy when users rotate their screens, for example. You don’t want to cancel the flow because the subscriber went away for a short period. That’s why you passed in
Observing StateFlow
Now that you have set up your ViewModel to hold UI state with StateFlow, it’s time to collect it in the Activity. Open DisneyActivity.kt and navigate to fetchDisneyCharacters. Insert this code in place of the old method:
private fun fetchDisneyCharacters() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.charactersFlow.collect(::showResults)
}
}
}
This code should look familiar from Chapter 15, “Coroutines in the UI Layer”, but here’s a quick recap. To collect a flow, you need to be inside a coroutine. The launch builder takes care of that. Because flows are not lifecycle-aware like LiveData is, you need to take care of canceling the work when the UI is no longer active. You do that with repeatOnLifecycle, which will collect the flow as long as the activity is in the STARTED state. The coroutine will be canceled when the activity reaches the STOPPED state and will execute again on the next ON_START event.
Build and run to confirm that it works properly. The app should behave the same as with LiveData in the previous example.
Should You Replace LiveData With Flow?
If you’re using LiveData in layers other than the presentation layer (like in repositories), the answer is definitely, Yes, switch to Flow. The reason is that LiveData isn’t built to handle asynchronous streams and data transformations. There is a Transformations class available, and it provides a few transformation methods, but all those work exclusively on the main thread. On the other hand, Flow has many operators and is much more flexible. If you need to deal with data streams in lower app layers, go with the flow.
When talking about the presentation layer and holding UI state, it’s a harder decision to make. Here are a few things to keep in mind:
-
LiveDatais lifecycle-aware, so you don’t have to cancel observers manually. -
LiveDataexposes a simple API that’s easy to learn. You can observe changes to the value or query the current value on demand. -
StateFlow’s value can also be accessed on demand by reading its value property. You lose that ability if you need more customization and change it toSharedFlow. - If you need to transform some data or need custom behavior, it’s better to use
StateFloworSharedFlow. They have more operators and can be executed on another thread. - Kotlin Flow isn’t as simple to start with, having a bit of a steeper learning curve. If your team consists of less experienced developers, maybe stick with LiveData.
- You can also combine both tools, as you’ve seen in the example here. Converting
FlowtoLiveDatais as easy as callingFlow.asLiveData.
Coroutines & Flow in Jetpack Compose
Jetpack Compose is a new toolkit for building Android UIs. One of the goals for it is to simplify and accelerate UI development on Android with less code than the current View system. This part of the chapter won’t focus on building UI with Jetpack Compose or any theory behind it. It’s targeted to readers already familiar with Jetpack Compose but who want to learn how to add coroutines and flows to their Compose code. That being said, this part of the chapter is optional. Feel free to skip it if you’re not interested in Jetpack Compose.
Running Suspend Functions in Composables
In Chapter 15, “Coroutines in the UI Layer”, you learned that you could use lifecycleScope to launch coroutines in the scope of a given lifecycle owner. That means the coroutines will get canceled when that lifecycle owner gets destroyed. You’re going to apply the same concept to composables. But instead of lifecycleScope, you’re going to use LaunchedEffect. It’s a composable function that will launch a new coroutine in the composition’s CoroutineContext upon entering the composition. Here’s the definition:
@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
)
It accepts a vararg of keys which, when changed, will signal that the coroutine needs to be canceled and re-launched. The coroutine will also be canceled when LaunchEffect leaves the composition. The second parameter is a suspending lambda that will be executed inside the coroutine.
Look at an example of how to use this. Open DisneyComposeActivity.kt and go to the MainDisneyScreen composable. Change it to:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
LaunchedEffect(Unit) {
onScreenLoaded()
}
Column {
Toolbar {viewModel.getFreshData()}
val charactersList by viewModel.charactersFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
This code creates a new coroutine and executes onScreenLoaded when it enters the composition. You passed Unit as a key argument because you don’t want any value change to re-launch the coroutine. Your MainDisneyScreen is ready. You just need to update its invocation in onCreate, from MainDisneyScreen(viewModel = viewModel) to:
MainDisneyScreen(viewModel = viewModel) { showToast() }
showToast is defined as:
private suspend fun showToast() {
delay(200)
Toast.makeText(this, "Data loaded", Toast.LENGTH_SHORT).show()
}
Build and run to see the result.
You’ll notice that a toast message appeared as soon as the LaunchedEffect composable function entered the composition. Try to increase the delay in showToast to 2000. Run the app again, tap Jetpack Compose and quickly tap the back button. The toast message didn’t appear even after two seconds had passed. That’s because the coroutine was canceled as soon as LaunchedEffect exited the composition.
Getting a Composition-Aware Coroutine Scope
LaunchedEffect comes in handy in certain situations. But because it’s a composable function, it can only be called in the context of other composable functions. This limits where you can use it. To solve this problem, use the rememberCoroutineScope composable function. This function returns CoroutineScope, which is bound to the point of the composition where it’s called. The scope will be canceled when the call leaves the Composition. Because you have access to CoroutineScope, you can launch multiple coroutines with this approach and can manually cancel them if needed. To test this behavior, modify the previous example to use rememberCoroutineScope. Start by changing showToast to the following:
private suspend fun showToast() {
delay(200)
Toast.makeText(this, "Refreshing Data", Toast.LENGTH_SHORT).show()
}
You changed the toast text message and reverted the delay value to 200. Go to MainDisneyScreen and replace
LaunchedEffect(Unit) {
onScreenLoaded()
}
with this:
val mainScreenScope = rememberCoroutineScope()
Now that you have an instance of CoroutineScope, you can use it to start coroutines manually. Replace Toolbar { viewModel.getFreshData() } with the following:
Toolbar {
viewModel.getFreshData()
mainScreenScope.launch {
onScreenLoaded()
}
}
Because you have access to a properly scoped CoroutineScope, you can safely use launch to create a new coroutine. Build and run. Tap the refresh option to see the toast message.
You can do the same test as in the previous example, increase the delay to 2000 and leave the screen after tapping the refresh option. Notice that the toast won’t show up because the coroutine got canceled.
Collecting Flows in Compose
You’ve already learned about collecting flows from the view-based UI, and the same concepts apply to compose code as well. Some things are compose-specific, though. Here’s an example showing them. Currently, the MainDisneyScreen composable looks like this:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
val mainScreenScope = rememberCoroutineScope()
Column {
Toolbar {
viewModel.getFreshData()
mainScreenScope.launch {
onScreenLoaded()
}
}
val charactersList by viewModel.charactersFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
The main focus in this code snippet is on the collectAsState invocation. Whereas in the view system you simply collect and use the values, in compose you need to put the collected values in State<T>. This causes the state to update and the composable to get recomposed with the new state whenever the underlying flow changes.
The code above successfully collects values from a flow, but an issue remains. If you send the app to the background, the upstream flow stays active. You’ll fix this next. Replace the implementation of MainDisneyScreen with:
@Composable
fun MainDisneyScreen(
viewModel: DisneyViewModel,
onScreenLoaded: suspend () -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current // 1
val mainScreenScope = rememberCoroutineScope()
Column {
Toolbar {
viewModel.getFreshData()
mainScreenScope.launch {
onScreenLoaded()
}
}
val uiStateFlow = remember(viewModel.charactersFlow, lifecycleOwner) { // 2
viewModel.charactersFlow.flowWithLifecycle( // 3
lifecycleOwner.lifecycle,
Lifecycle.State.STARTED
)
}
// 4
val charactersList by uiStateFlow.collectAsState(emptyList())
CharacterList(characterList = charactersList)
}
}
Here’s the breakdown of the function:
- You get the current lifecycle of the nearest CompositionLocalProvider.
- This line basically says that while
viewModel.charactersFlowandlifecycleOwnerhaven’t changed, you’ll always get the same value from the computation in the succeeding block. - Use
flowWithLifecycleto make the flow automatically start and cancel collecting from an upstream flow as the lifecycle moves in and out of the target state. - Call
collectAsStateon the newly createduiStateFlow.
These changes give you a safe way of observing flows from your compose code. Build and run the app to ensure everything works well. The output should be the same as before, showing the list of characters.
Testing Coroutines on Android
In Chapter 13, “Testing Coroutines”, you learned a lot about testing coroutine code in pure Kotlin projects. That all applies to testing on Android as well. But there’s one big difference when it comes to testing coroutines on Android: The main thread gets involved in the process now. The problem arises when you want to unit test entities that depend on the existence of the main thread, such as ViewModel. Unit tests usually run in isolation on your local machine. That means you don’t have access to Android’s main thread while running your unit tests. It’s time to see how this problem presents itself in practice.
Testing ViewModel
Double-press Shift to open the search field and search for DisneyViewModelTest.kt. Open the file and inspect the code a bit. You’ll find the basic testing setup there. Because you’re going to write a test for a method in DisneyViewModel, you need to create an instance of it. To do that, you need to pass a repository instance as a constructor parameter. You create a mocked repository instance, provide that to the constructor and build an instance of DisneyViewModel. Before proceeding, add a gradle dependency for testing coroutines. Open your app-level build.gradle and add the following:
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0'
With that out of the way, you can focus on the test that has been written for you.
@Test
fun `test getFreshData calls repository to get data`() = runTest {
viewModel.getFreshData()
yield()
verify(disneyRepoMock).getFreshData()
}
You invoke viewModel.getFreshData() and then yield() to allow the code to run. Then, you verify that the app invoked the getFreshData repository method. Run the test to see what happens. This is the result:
Exception in thread "Test worker @coroutine#3" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used
The test failed, which was expected because there’s no main thread to work with. This is easy to fix, so get to it.
Replacing Main Dispatcher
In the unit test, you invoke viewModel.getFreshData(), which uses viewModelScope to launch a new coroutine. Remember that viewModelScope is bound to Dispatcher.Main. That’s exactly why the test fails, and there’s a way to replace the main dispatcher with a special test dispatcher.
Return to DisneyViewModelTest.kt and add this line below the class declaration:
private val testDispatcher = StandardTestDispatcher()
This creates a new dispatcher designed for testing purposes. In setUp, add the test dispatcher as a replacement for the main dispatcher, like this:
@ExperimentalCoroutinesApi
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
viewModel = DisneyViewModel(disneyRepoMock)
}
By calling Dispatchers.setMain(testDispatcher), you resolved the problem of not having the main thread available. Because this is implemented in a method annotated with @Before, the main dispatcher will be replaced before running each test. It’s also important to restore it once you finish testing. A good place to do this is a method annotated with @After. Add:
Dispatchers.resetMain()
It should look like this:
@ExperimentalCoroutinesApi
@After
fun tearDown() {
Dispatchers.resetMain()
}
resetMain resets the state of Dispatchers.Main to the original main dispatcher. Try running the test again; it should pass.
It’s a special feeling when you see that green check mark, isn’t it? :]
Key Points
- Use a ViewModel as a mediator between your UI and a repository.
- ViewModel is an ideal place to launch coroutines by using viewModelScope.
- Make your functions main-safe.
- Use flows in lower layers of your architecture if you need to deal with streams or want to be reactive.
- In the presentation layer, use StateFlow, SharedFlow or LiveData depending on your use case.
- When observing flows from the UI, make sure to use repeatOnLifecycle or flowWithLifecycle.
- Use LaunchedEffect or rememberCoroutineScope to launch coroutines in composables.
- When writing a unit test for ViewModels, swap the main dispatcher for a test dispatcher.
Where to Go From Here?
Congratulations, you just completed the last chapter of this book!
On this journey, you’ve learned many new concepts about Kotlin Coroutines and Flows. You’re now ready to start implementing them into your everyday work.
Don’t be afraid to dig more deeply into the subject. There’s a lot to discover about Kotlin Coroutines and Flows. Check out the links below for more great examples and explanations. Wishing you all the best in your future adventures with coroutines! :]
- https://medium.com/androiddevelopers/migrating-from-livedata-to-kotlins-flow-379292f419fb
- https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
- https://developer.android.com/jetpack/compose/side-effects#sideeffect-publish
- https://elizarov.medium.com/shared-flows-broadcast-channels-899b675e805c