Use Kotlin Coroutines in your Android App

1. Before you begin

In this codelab you'll learn how to use Kotlin Coroutines in an Android app—the recommended way of managing background threads that can simplify code by reducing the need for callbacks.

Coroutines are a Kotlin feature that converts async callbacks for long-running tasks, such as database or network access, into sequential code.

Here is a code snippet to give you an idea of what you'll be doing:

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

The callback-based code will be converted to sequential code using coroutines:

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

You will start with an existing app, built using Architecture Components, that uses a callback style for long-running tasks.

By the end of this codelab, you'll have enough experience to use coroutines in your app to load data from the network, and you will be able to integrate coroutines into an app. You'll also be familiar with best practices for coroutines, and how to write a test against code that uses coroutines.

Prerequisites

  • Familiarity with the Architecture Components ViewModel, LiveData, Repository, and Room.
  • Experience with Kotlin syntax, including extension functions and lambdas.
  • A basic understanding of using threads on Android, including the main thread, background threads, and callbacks.

What you'll do

  • Call code written with coroutines and obtain results.
  • Use suspend functions to make async code sequential.
  • Use launch and runBlocking to control how code executes.
  • Learn techniques to convert existing APIs to coroutines using suspendCoroutine.
  • Use coroutines with Architecture Components.
  • Learn best practices for testing coroutines.

What you'll need

  • Android Studio 4.1 (the codelab may work with other versions, but some things might be missing or look different).

If you run into any issues (code bugs, grammatical errors, unclear wording, etc.) as you work through this codelab, please report the issue via the Report a mistake link in the lower left corner of the codelab.

2. Getting set up

Download the code

Click the following link to download all the code for this codelab:

... or clone the GitHub repository from the command line by using the following command:

$ git clone https://github.com/android/codelab-kotlin-coroutines.git

Frequently asked questions

3. Run the starting sample app

First, let's see what the starting sample app looks like. Follow these instructions to open the sample app in Android Studio.

  1. If you downloaded the kotlin-coroutines zip file, unzip the file.
  2. Open the coroutines-codelab project in Android Studio.
  3. Select the start application module.
  4. Click the execute.pngRun button, and either choose an emulator or connect your Android device, which must be capable of running Android Lollipop (the minimum SDK supported is 21). The Kotlin Coroutines screen should appear:

1d65055551232a9.png

This starter app uses threads to increment the count a short delay after you press the screen. It will also fetch a new title from the network and display it on screen. Give it a try now, and you should see the count and message change after a short delay. In this codelab you'll convert this application to use coroutines.

This app uses Architecture Components to separate the UI code in MainActivity from the application logic in MainViewModel. Take a moment to familiarize yourself with the structure of the project.

cbc7d16909facb7c.png

  1. MainActivity displays the UI, registers click listeners, and can display a Snackbar. It passes events to MainViewModel and updates the screen based on LiveData in MainViewModel.
  2. MainViewModel handles events in onMainViewClicked and will communicate to MainActivity using LiveData.
  3. Executors defines BACKGROUND, which can run things on a background thread.
  4. TitleRepository fetches results from the network and saves them to the database.

Adding coroutines to a project

To use coroutines in Kotlin, you must include the coroutines-core library in the build.gradle (Module: app) file of your project. The codelab projects have already done this for you, so you don't need to do this to complete the codelab.

Coroutines on Android are available as a core library, and Android specific extensions:

  • kotlinx-coroutines-core — Main interface for using coroutines in Kotlin
  • kotlinx-coroutines-android — Support for the Android Main thread in coroutines

The starter app already includes the dependencies in build.gradle.When creating a new app project, you'll need to open build.gradle (Module: app) and add the coroutines dependencies to the project.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

You can find the latest version number of the Coroutines library to substitute for "x.x.x" on the Kotlin Coroutines releases page.

4. Coroutines in Kotlin

On Android, it's essential to avoid blocking the main thread. The main thread is a single thread that handles all updates to the UI. It's also the thread that calls all click handlers and other UI callbacks. As such, it has to run smoothly to guarantee a great user experience.

For your app to display to the user without any visible pauses, the main thread has to update the screen roughly every 16ms, which is about 60 frames per second. Many common tasks take longer than this, such as parsing large JSON datasets, writing data to a database, or fetching data from the network. Therefore, calling code like this from the main thread can cause the app to pause, stutter, or even freeze. And if you block the main thread for too long, the app may even crash and present an Application Not Responding dialog.

Watch the video below for an introduction to how coroutines solve this problem for us on Android by introducing main-safety.

The callback pattern

One pattern for performing long-running tasks without blocking the main thread is callbacks. By using callbacks, you can start long-running tasks on a background thread. When the task completes, the callback is called to inform you of the result on the main thread.

Take a look at an example of the callback pattern.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Because this code is annotated with @UiThread, it must run fast enough to execute on the main thread. That means, it needs to return very quickly, so that the next screen update is not delayed. However, since slowFetch will take seconds or even minutes to complete, the main thread can't wait for the result. The show(result) callback allows slowFetch to run on a background thread and return the result when it's ready.

Using coroutines to remove callbacks

Callbacks are a great pattern, however they have a few drawbacks. Code that heavily uses callbacks can become hard to read and harder to reason about. In addition, callbacks don't allow the use of some language features, such as exceptions.

Kotlin coroutines let you convert callback-based code to sequential code. Code written sequentially is typically easier to read, and can even use language features such as exceptions.

In the end, they do the exact same thing: wait until a result is available from a long-running task and continue execution. However, in code they look very different.

The keyword suspend is Kotlin's way of marking a function, or function type, available to coroutines. When a coroutine calls a function marked suspend, instead of blocking until that function returns like a normal function call, it suspends execution until the result is ready then it resumes where it left off with the result. While it's suspended waiting for a result, it unblocks the thread that it's running on so other functions or coroutines can run.

For example in the code below, makeNetworkRequest() and slowFetch() are both suspend functions.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Just like with the callback version, makeNetworkRequest must return from the main thread right away because it's marked @UiThread. This means that usually it could not call blocking methods like slowFetch. Here's where the suspend keyword works its magic.

Compared to callback-based code, coroutine code accomplishes the same result of unblocking the current thread with less code. Due to its sequential style, it's easy to chain several long running tasks without creating multiple callbacks. For example, code that fetches a result from two network endpoints and saves it to the database can be written as a function in coroutines with no callbacks. Like so:

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

You will introduce coroutines to the sample app in the next section.

5. Controlling the UI with coroutines

In this exercise you will write a coroutine to display a message after a delay. To get started, make sure you have the module start open in Android Studio.

Understanding CoroutineScope

In Kotlin, all coroutines run inside a CoroutineScope. A scope controls the lifetime of coroutines through its job. When you cancel the job of a scope, it cancels all coroutines started in that scope. On Android, you can use a scope to cancel all running coroutines when, for example, the user navigates away from an Activity or Fragment. Scopes also allow you to specify a default dispatcher. A dispatcher controls which thread runs a coroutine.

For coroutines started by the UI, it is typically correct to start them on Dispatchers.Main which is the main thread on Android. A coroutine started on Dispatchers.Main won't block the main thread while suspended. Since a ViewModel coroutine almost always updates the UI on the main thread, starting coroutines on the main thread saves you extra thread switches. A coroutine started on the Main thread can switch dispatchers any time after it's started. For example, it can use another dispatcher to parse a large JSON result off the main thread.

Using viewModelScope

The AndroidX lifecycle-viewmodel-ktx library adds a CoroutineScope to ViewModels that's configured to start UI-related coroutines. To use this library, you must include it in the build.gradle (Module: start) file of your project. That step is already done in the codelab projects.

dependencies {
  ...
  // replace x.x.x with latest version
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

The library adds a viewModelScope as an extension function of the ViewModel class. This scope is bound to Dispatchers.Main and will automatically be cancelled when the ViewModel is cleared.

Switch from threads to coroutines

In MainViewModel.kt find the next TODO along with this code:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

This code uses the BACKGROUND ExecutorService (defined in util/Executor.kt) to run in a background thread. Since sleep blocks the current thread it would freeze the UI if it were called on the main thread. One second after the user clicks the main view, it requests a snackbar.

You can see that happen by removing the BACKGROUND from the code and running it again. The loading spinner won't display and everything will "jump" to the final state one second later.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Replace updateTaps with this coroutine based code that does the same thing. You will have to import launch and delay.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

This code does the same thing, waiting one second before showing a snackbar. However, there are some important differences:

  1. viewModelScope. launch will start a coroutine in the viewModelScope. This means when the job that we passed to viewModelScope gets canceled, all coroutines in this job/scope will be cancelled. If the user left the Activity before delay returned, this coroutine will automatically be cancelled when onCleared is called upon destruction of the ViewModel.
  2. Since viewModelScope has a default dispatcher of Dispatchers.Main, this coroutine will be launched in the main thread. We'll see later how to use different threads.
  3. The function delay is a suspend function. This is shown in Android Studio by the 716807c07961aacd.png icon in the left gutter. Even though this coroutine runs on the main thread, delay won't block the thread for one second. Instead, the dispatcher will schedule the coroutine to resume in one second at the next statement.

Go ahead and run it. When you click on the main view you should see a snackbar one second later.

In the next section we'll consider how to test this function.

6. Testing coroutines through behavior

In this exercise you'll write a test for the code you just wrote. This exercise shows you how to test coroutines running on Dispatchers.Main using the kotlinx-coroutines-test library. Later in this codelab you'll implement a test that interacts with coroutines directly.

Review the existing code

Open MainViewModelTest.kt in the test folder.

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

A rule is a way to run code before and after the execution of a test in JUnit. Two rules are used to allow us to test MainViewModel in an off-device test:

  1. InstantTaskExecutorRule is a JUnit rule that configures LiveData to execute each task synchronously
  2. MainCoroutineScopeRule is a custom rule in this codebase that configures Dispatchers.Main to use a TestCoroutineDispatcher from kotlinx-coroutines-test. This allows tests to advance a virtual-clock for testing, and allows code to use Dispatchers.Main in unit tests.

In the setup method, a new instance of MainViewModel is created using testing fakes – these are fake implementations of the network and database provided in the starter code to help write tests without using the real network or database.

For this test, the fakes are only needed to satisfy the dependencies of MainViewModel. Later in this code lab you'll update the fakes to support coroutines.

Write a test that controls coroutines

Add a new test that ensures that taps are updated one second after the main view is clicked:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

By calling onMainViewClicked, the coroutine we just created will be launched. This test checks that the taps text stays "0 taps" right after onMainViewClicked is called, then 1 second later it gets updated to "1 taps".

This test uses virtual-time to control the execution of the coroutine launched by onMainViewClicked. The MainCoroutineScopeRule lets you pause, resume, or control the execution of coroutines that are launched on the Dispatchers.Main. Here we're calling advanceTimeBy(1_000) which will cause the main dispatcher to immediately execute coroutines that are scheduled to resume 1 second later.

This test is fully deterministic, which means it will always execute the same way. And, because it has full control over the execution of coroutines launched on the Dispatchers.Main it doesn't have to wait one second for the value to be set.

Run the existing test

  1. Right click on the class name MainViewModelTest in your editor to open a context menu.
  2. In the context menu choose execute.pngRun ‘MainViewModelTest'
  3. For future runs you can select this test configuration in the configurations next to the execute.png button in the toolbar. By default, the configuration will be called MainViewModelTest.

You should see the test pass! And it should take quite a bit less than one second to run.

In the next exercise you'll learn how to convert from an existing callback APIs to use coroutines.

7. Moving from callbacks to coroutines

In this step, you will start converting a repository to use coroutines. To do this, we will add coroutines to the ViewModel, Repository, Room and Retrofit.

It's a good idea to understand what each part of the architecture is responsible for before we switch them to using coroutines.

  1. MainDatabase implements a database using Room that saves and loads a Title.
  2. MainNetwork implements a network API that fetches a new title. It uses Retrofit to fetch titles. Retrofit is configured to randomly return errors or mock data, but otherwise behaves as if it's making real network requests.
  3. TitleRepository implements a single API for fetching or refreshing the title by combining data from the network and database.
  4. MainViewModel represents the screen's state and handles events. It will tell the repository to refresh the title when the user taps on the screen.

Since the network request is driven by UI-events and we want to start a coroutine based on them, the natural place to start using coroutines is in the ViewModel.

The callback version

Open MainViewModel.kt to see the declaration of refreshTitle.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

This function is called every time the user clicks on the screen – and it will cause the repository to refresh the title and write the new title to the database.

This implementation uses a callback to do a few things:

  • Before it starts a query, it displays a loading spinner with _spinner.value = true
  • When it gets a result, it clears the loading spinner with _spinner.value = false
  • If it gets an error, it tells a snackbar to display and clears the spinner

Note that the onCompleted callback is not passed the title. Since we write all titles to the Room database, the UI updates to the current title by observing a LiveData that's updated by Room.

In the update to coroutines, we'll keep the exact same behavior. It's a good pattern to use an observable data source like a Room database to automatically keep the UI up to date.

The coroutines version

Let's rewrite refreshTitle with coroutines!

Since we'll need it right away, let's make an empty suspend function in our repository (TitleRespository.kt). Define a new function that uses the suspend operator to tell Kotlin that it works with coroutines.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

When you're done with this codelab, you will update this to use Retrofit and Room to fetch a new title and write it to the database using coroutines. For now, it'll just spend 500 milliseconds pretending to do work and then continue.

In MainViewModel, replace the callback version of refreshTitle with one that launches a new coroutine:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Let's step through this function:

viewModelScope.launch {

Just like the coroutine to update the tap count, begin by launching a new coroutine in viewModelScope. This will use Dispatchers.Main which is OK. Even though refreshTitle will make a network request and database query it can use coroutines to expose a main-safe interface. This means it'll be safe to call it from the main thread.

Because we're using viewModelScope, when the user moves away from this screen the work started by this coroutine will automatically be cancelled. That means it won't make extra network requests or database queries.

The next few lines of code actually call refreshTitle in the repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Before this coroutine does anything it starts the loading spinner – then it calls refreshTitle just like a regular function. However, since refreshTitle is a suspending function, it executes differently than a normal function.

We don't have to pass a callback. The coroutine will suspend until it is resumed by refreshTitle. While it looks just like a regular blocking function call, it will automatically wait until the network and database query are complete before resuming without blocking the main thread.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Exceptions in suspend functions work just like errors in regular functions. If you throw an error in a suspend function, it will be thrown to the caller. So even though they execute quite differently, you can use regular try/catch blocks to handle them. This is useful because it lets you rely on the built-in language support for error handling instead of building custom error handling for every callback.

And, if you throw an exception out of a coroutine – that coroutine will cancel its parent by default. That means it's easy to cancel several related tasks together.

And then, in a finally block, we can make sure that the spinner is always turned off after the query runs.

Run the application again by selecting the start configuration then pressing execute.png, you should see a loading spinner when you tap anywhere. The title will stay the same because we haven't hooked up our network or database yet.

In the next exercise you'll update the repository to actually do work.

8. Making main-safe functions from blocking code

In this exercise you'll learn how to switch the thread a coroutine runs on in order to implement a working version of TitleRepository.

Review the existing callback code in refreshTitle

Open TitleRepository.kt and review the existing callback-based implementation.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

In TitleRepository.kt the method refreshTitleWithCallbacks is implemented with a callback to communicate the loading and error state to the caller.

This function does quite a few things in order to implement the refresh.

  1. Switch to another thread with BACKGROUND ExecutorService
  2. Run the fetchNextTitle network request using the blocking execute() method. This will run the network request in the current thread, in this case one of the threads in BACKGROUND.
  3. If the result is successful, save it to the database with insertTitle and call the onCompleted() method.
  4. If the result was not successful, or there is an exception, call the onError method to tell the caller about the failed refresh.

This callback based implementation is main-safe because it won't block the main thread. But, it has to use a callback to inform the caller when the work completes. It also calls the callbacks on the BACKGROUND thread that it switched too.

Calling blocking calls from coroutines

Without introducing coroutines to the network or database, we can make this code main-safe using coroutines. This will let us get rid of the callback and allow us to pass the result back to the thread that initially called it.

You can use this pattern anytime you need to do blocking or CPU intensive work from inside a coroutine such as sorting and filtering a large list or reading from disk.

To switch between any dispatcher, coroutines uses withContext. Calling withContext switches to the other dispatcher just for the lambda then comes back to the dispatcher that called it with the result of that lambda.

By default, Kotlin coroutines provides three Dispatchers: Main, IO, and Default. The IO dispatcher is optimized for IO work like reading from the network or disk, while the Default dispatcher is optimized for CPU intensive tasks.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

This implementation uses blocking calls for the network and database – but it's still a bit simpler than the callback version.

This code still uses blocking calls. Calling execute() and insertTitle(...) will both block the thread that this coroutine is running in. However, by switching to Dispatchers.IO using withContext, we're blocking one of the threads in the IO dispatcher. The coroutine that called this, possibly running on Dispatchers.Main, will be suspended until the withContext lambda is complete.

Compared to the callback version, there are two important differences:

  1. withContext returns its result back to the Dispatcher that called it, in this case Dispatchers.Main. The callback version called the callbacks on a thread in the BACKGROUND executor service.
  2. The caller doesn't have to pass a callback to this function. They can rely on suspend and resume to get the result or error.

Run the app again

If you run the app again, you'll see that the new coroutines-based implementation is loading results from the network!

In the next step you'll integrate coroutines into Room and Retrofit.

9. Coroutines in Room & Retrofit

To continue the coroutines integration, we're going to use the support for suspend functions in the stable version of Room and Retrofit, then simplify the code we just wrote substantially by using the suspend functions.

Coroutines in Room

First open MainDatabase.kt and make insertTitle a suspend function:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

When you do this, Room will make your query main-safe and execute it on a background thread automatically. However, it also means that you can only call this query from inside a coroutine.

And – that's all you have to do to use coroutines in Room. Pretty nifty.

Coroutines in Retrofit

Next let's see how to integrate coroutines with Retrofit. Open up MainNetwork.kt and change fetchNextTitle to a suspend function. Also change the return type from Call<String> to String.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

To use suspend functions with Retrofit you have to do two things:

  1. Add a suspend modifier to the function
  2. Remove the Call wrapper from the return type. Here we're returning String, but you could return complex json-backed type as well. If you still wanted to provide access to Retrofit's full Result, you can return Result<String> instead of String from the suspend function.

Retrofit will automatically make suspend functions main-safe so you can call them directly from Dispatchers.Main.

Using Room and Retrofit

Now that Room and Retrofit support suspend functions, we can use them from our repository. Open up TitleRepository.kt and see how using suspending functions greatly simplifies logic, even compared to the blocking version:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Wow, that's a lot shorter. What happened? It turns out relying on suspend and resume lets code be much shorter. Retrofit lets us use return types like String or a User object here, instead of a Call. That's safe to do, because inside the suspend function, Retrofit is able to run the network request on a background thread and resume the coroutine when the call completes.

Even better, we got rid of the withContext. Since both Room and Retrofit provide main-safe suspending functions, it's safe to orchestrate this async work from Dispatchers.Main.

Fixing compiler errors

Moving to coroutines does involve changing the signature of functions as you can't call a suspend function from a regular function. When you added the suspend modifier in this step, a few compiler errors were generated that show what would happen if you changed a function to suspend in a real project.

Go through the project and fix the compiler errors by changing the function to suspend created. Here are the quick resolutions for each:

TestingFakes.kt

Update the testing fakes to support the new suspend modifiers.

TitleDaoFake

  1. Hit alt-enter (option-enter on a Mac) add suspend modifiers to all functions in the hierarchy

MainNetworkFake

  1. Hit alt-enter add suspend modifiers to all functions in the hierarchy
  2. Replace fetchNextTitle with this function
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Hit alt-enter add suspend modifiers to all functions in the hierarchy
  2. Replace fetchNextTitle with this function
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Delete the refreshTitleWithCallbacks function as it is not used anymore.

Run the app

Run the app again, once it compiles, you will see that it's loading data using coroutines all the way from the ViewModel to Room and Retrofit!

Congratulations, you've completely swapped this app to using coroutines! To wrap up we'll talk a bit about how to test what we just did.

10. Testing coroutines directly

In this exercise, you'll write a test that calls a suspend function directly.

Since refreshTitle is exposed as a public API it will be tested directly, showing how to call coroutines functions from tests.

Here's the refreshTitle function you implemented in the last exercise:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Write a test that calls a suspend function

Open TitleRepositoryTest.kt in the test folder which has two TODOS.

Try to call refreshTitle from the first test whenRefreshTitleSuccess_insertsRows.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Since refreshTitle is a suspend function Kotlin doesn't know how to call it except from a coroutine or another suspend function, and you will get a compiler error like, "Suspend function refreshTitle should be called only from a coroutine or another suspend function."

The test runner doesn't know anything about coroutines so we can't make this test a suspend function. We could launch a coroutine using a CoroutineScope like in a ViewModel, however tests need to run coroutines to completion before they return. Once a test function returns, the test is over. Coroutines started with launch are asynchronous code, which may complete at some point in the future. Therefore to test that asynchronous code, you need some way to tell the test to wait until your coroutine completes. Since launch is a non-blocking call, that means it returns right away and can continue to run a coroutine after the function returns - it can't be used in tests. For example:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

This test will sometimes fail. The call to launch will return immediately and execute at the same time as the rest of the test case. The test has no way to know if refreshTitle has run yet or not – and any assertions like checking that the database was updated would be flakey. And, if refreshTitle threw an exception, it will not be thrown in the test call stack. It will instead be thrown into GlobalScope's uncaught exception handler.

The library kotlinx-coroutines-test has the runBlockingTest function that blocks while it calls suspend functions. When runBlockingTest calls a suspend function or launches a new coroutine, it executes it immediately by default. You can think of it as a way to convert suspend functions and coroutines into normal function calls.

In addition, runBlockingTest will rethrow uncaught exceptions for you. This makes it easier to test when a coroutine is throwing an exception.

Implement a test with one coroutine

Wrap the call to refreshTitle with runBlockingTest and remove the GlobalScope.launch wrapper from subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

This test uses the fakes provided to check that "OK" is inserted to the database by refreshTitle.

When the test calls runBlockingTest, it will block until the coroutine started by runBlockingTest completes. Then inside, when we call refreshTitle it uses the regular suspend and resume mechanism to wait for the database row to be added to our fake.

After the test coroutine completes, runBlockingTest returns.

Write a timeout test

We want to add a short timeout to the network request. Let's write the test first then implement the timeout. Create a new test:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

This test uses the provided fake MainNetworkCompletableFake, which is a network fake that's designed to suspend callers until the test continues them. When refreshTitle tries to make a network request, it'll hang forever because we want to test timeouts.

Then, it launches a separate coroutine to call refreshTitle. This is a key part of testing timeouts, the timeout should happen in a different coroutine than the one runBlockingTest creates. By doing so, we can call the next line, advanceTimeBy(5_000) which will advance time by 5 seconds and cause the other coroutine to timeout.

This is a complete timeout test, and it will pass once we implement timeout.

Run it now and see what happens:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

One of the features of runBlockingTest is that it won't let you leak coroutines after the test completes. If there are any unfinished coroutines, like our launch coroutine, at the end of the test, it will fail the test.

Add a timeout

Open up TitleRepository and add a five second timeout to the network fetch. You can do this by using the withTimeout function:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Run the test. When you run the tests you should see all tests pass!

17c2c9cab594f2f5.png

In the next exercise you'll learn how to write higher order functions using coroutines.

11. Using coroutines in higher order functions

In this exercise you'll refactor refreshTitle in MainViewModel to use a general data loading function. This will teach you how to build higher order functions that use coroutines.

The current implementation of refreshTitle works, but we can create a general data loading coroutine that always shows the spinner. This might be helpful in a codebase that loads data in response to several events, and wants to ensure the loading spinner is consistently displayed.

Reviewing the current implementation every line except repository.refreshTitle() is boilerplate to show the spinner and display errors.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Using coroutines in higher order functions

Add this code to MainViewModel.kt

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Now refactor refreshTitle() to use this higher order function.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

By abstracting the logic around showing a loading spinner and showing errors, we've simplified our actual code needed to load data. Showing a spinner or displaying an error is something that's easy to generalize to any data loading, while the actual data source and destination needs to be specified every time.

To build this abstraction, launchDataLoad takes an argument block that is a suspend lambda. A suspend lambda allows you to call suspend functions. That's how Kotlin implements the coroutine builders launch and runBlocking we've been using in this codelab.

// suspend lambda

block: suspend () -> Unit

To make a suspend lambda, start with the suspend keyword. The function arrow and return type Unit complete the declaration.

You don't often have to declare your own suspend lambdas, but they can be helpful to create abstractions like this that encapsulate repeated logic!

12. Using coroutines with WorkManager

In this exercise you'll learn how to use coroutine based code from WorkManager.

What is WorkManager

There are many options on Android for deferrable background work. This exercise shows you how to integrate WorkManager with coroutines. WorkManager is a compatible, flexible and simple library for deferrable background work. WorkManager is the recommended solution for these use cases on Android.

WorkManager is part of Android Jetpack, and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.

Because of this, WorkManager is a good choice for tasks that must complete eventually.

Some examples of tasks that are a good use of WorkManager:

  • Uploading logs
  • Applying filters to images and saving the image
  • Periodically syncing local data with the network

Using coroutines with WorkManager

WorkManager provides different implementations of its base ListenableWorker class for different use cases.

The simplest Worker class allows us to have some synchronous operation executed by WorkManager. However, having worked so far to convert our codebase to use coroutines and suspend functions, the best way to use WorkManager is through the CoroutineWorker class that allows to define our doWork()function as a suspend function.

To get started, open up RefreshMainDataWork. It already extends CoroutineWorker, and you need to implement doWork.

Inside the suspend doWork function, call refreshTitle() from the repository and return the appropriate result!

After you've completed the TODO, the code will look like this:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Note that CoroutineWorker.doWork() is a suspending function. Unlike the simpler Worker class, this code does NOT run on the Executor specified in your WorkManager configuration, but instead uses Dispatchers.Default. You can switch to other dispatchers by using withContext().

Testing our CoroutineWorker

No codebase should be complete without testing.

WorkManager makes available a couple of different ways to test your Worker classes, to learn more about the original testing infrastructure, you can read the documentation.

WorkManager v2.1 introduces a new set of APIs to support a simpler way to test ListenableWorker classes and, as a consequence, CoroutineWorker. In our code we're going to use one of these new API: TestListenableWorkerBuilder.

To add our new test, update the RefreshMainDataWorkTest file under the androidTest folder.

The content of the file is:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Before we get to the test, we tell WorkManager about the factory so we can inject the fake network.

The test itself uses the TestListenableWorkerBuilder to create our worker that we can then run calling the startWork() method.

WorkManager is just one example of how coroutines can be used to simplify APIs design.

13. Congratulations!

In this codelab we have covered the basics you'll need to start using coroutines in your app!

We covered:

  • How to integrate coroutines to Android apps from both the UI and WorkManager jobs to simplify asynchronous programming,
  • How to use coroutines inside a ViewModel to fetch data from the network and save it to a database without blocking the main thread.
  • And how to cancel all coroutines when the ViewModel is finished.

For testing coroutine based code, we covered both by testing behavior as well as directly calling suspend functions from tests.

Learn more

Check out the " Advanced Coroutines with Kotlin Flow and LiveData" codelab to learn more advanced coroutines usage on Android.

To learn more about cancellation and exceptions in coroutines, check out this article series: Part 1: Coroutines, Part 2: Cancellation in coroutines, and Part 3: Exceptions in coroutines.

Kotlin coroutines have many features that weren't covered by this codelab. If you're interested in learning more about Kotlin coroutines, read the coroutines guides published by JetBrains. Also check out " Improve app performance with Kotlin coroutines" for more usage patterns of coroutines on Android.