Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

18. Coroutines & Jetpack
Written by Luka Kordić

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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
  }
}

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)
    )
}

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()
override fun testMainSafety() {
  // Simulate a blocking call
  Thread.sleep(2000)
  println("World")
}
fun testMainSafety() {
  viewModelScope.launch { 
    disneyRepo.testMainSafety()
  }
  println("Hello")
}
private val viewModel: DisneyViewModel by viewModels {
  DisneyViewModelFactory(DependencyHolder.disneyRepository)
}
viewModel.testMainSafety()
I/System.out: World
I/System.out: Hello
override suspend fun testMainSafety() = withContext(Dispatchers.IO) {
  // Simulate a blocking call
  Thread.sleep(2000)
  println("World")
}
18:04:45.605 I/System.out: Hello
18:04:47.607 I/System.out: World

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.

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() }
}
@JvmOverloads
public fun <T> Flow<T>.asLiveData(
  context: CoroutineContext = EmptyCoroutineContext,
  timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
  collect { (it) }
}
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)
}
private fun getFreshData() = viewModel.getFreshData()

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()
)

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)
    }
  }
}

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.

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
)
@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)
  }
}
MainDisneyScreen(viewModel = viewModel) { showToast() }
private suspend fun showToast() {
  delay(200)
  Toast.makeText(this, "Data loaded", Toast.LENGTH_SHORT).show()
}

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()
}
LaunchedEffect(Unit) {
  onScreenLoaded()
}
val mainScreenScope = rememberCoroutineScope()
Toolbar {
  viewModel.getFreshData()
  mainScreenScope.launch {
    onScreenLoaded()
  }
}

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)
  }
}
@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)
  }
}

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'
@Test
fun `test getFreshData calls repository to get data`() = runTest {
  viewModel.getFreshData()
  yield()

  verify(disneyRepoMock).getFreshData()
}
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

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.

private val testDispatcher = StandardTestDispatcher()
@ExperimentalCoroutinesApi
@Before
fun setUp() {
  Dispatchers.setMain(testDispatcher)
  viewModel = DisneyViewModel(disneyRepoMock)
}
Dispatchers.resetMain()
@ExperimentalCoroutinesApi
@After
fun tearDown() {
  Dispatchers.resetMain()
}

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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now