Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Second Edition · Android 10 · Kotlin 1.3 · Android Studio 3.5

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

17. Coroutines on Android - Part 1
Written by Nishant Srivastava

Most Android apps are data-consuming apps, meaning that, most of the time, these apps are requesting data from some other source, usually a web service. Android apps run by default on the main thread. When it comes to consuming data either from local or remote locations, they use multiple approaches to switch context from the main (or UI) thread to a background thread in order to offload heavy processing and/or long-running tasks, and then back to the main thread to convey the result in the UI (you read about many of these approaches in the previous chapter).

Of those many approaches, coroutines stand out as a completely different approach to handling async operations. As was made clear in the previous chapter, coroutines turn out to be the simplest of them all. They make context switching clear, easy and sequential, which, in turn, leads to a lean and readable implementation.

In this chapter, you will learn how to use Kotlin Coroutines in an Android app. Also, you will learn about coroutine concepts such as dispatchers, coroutine scopes and how they enable working with various lifecycle events in an Android app.

Getting started

Coroutines on Android: Part 2
Coroutines on Android: Part 2

Android apps mainly involve CRUD operations on information (i.e., Create, Read, Update, Delete). The information can be accessed either from the local database or from a remote server via network calls, which can be a long-running task. Since the Android OS executes tasks by default on the main/UI thread, executing such long-running tasks can freeze your app, or crash the app and show an ANR (Application Not Responding) error.

Coroutines are a Kotlin feature that allows you to write asynchronous code in a sequential manner while still ensuring that long-running operations, such as database or network access, are properly dispatched to run in the background, which keeps the UI thread from being blocked. Once the long-running or high-processing task complete, the result is dispatched to the main/UI thread in an obvious manner.

For this chapter, you will use a simple Android app called StarSync, which is an offline first MVP (Model-View-Presenter) app. There is a repository that takes care of fetching data from the SWAPI API, which is a public Star Wars API. You can access the documentation for the same at https://swapi.co. The SWAPI API is pretty straight forward and completely public. You don’t even need to set up a token. Once fetched, the data is saved to the local database using the Room architecture components library.

The starter app uses a callback style for long-running tasks. The app uses the MVP architecture to separate the UI code in MainActivity from the app logic in MainActivityPresenter. Take a moment to familiarize yourself with the structure of the project.

If you have already downloaded the starter project, open it in Android Studio.

The project consists of pre-setup MVP architecture, with classes under their respective packages:

  1. contract: This package consists of contracts/interfaces defining the methods concrete implementations should be following.
  2. repository: This package contains the local and remote repository sub packages. It also contains the model sub package, which, in turn, contains the POJO (Plain Old Java Object) model classes.
  3. ui: This package contains the main and splash screen sub packages, which, in turn, contain the Activity and the Presenter associated with them.
  4. utils: This package contains some helper classes in order to help in writing clean code.

Some important classes to look at include:

  1. RemoteRepo: This class takes care of defining methods used for fetching data from the remote server using the Retrofit library.

  2. LocalRepo: This class takes care of defining methods used for fetching from and saving to a local database using the Room library.

  3. DataRepository: This class implements the repository pattern to implement logic around fetching from a remote server or local database.

  4. RemoteApi: This is a singleton class, which defines the base URL for SWAPI API, the people’s route path and the retrofit service with pre-setup with the Moshi Converter and Coroutine Adapter.

  5. RetrofitService: This interface defines the retrofit service routes used for making the GET requests to the SWAPI API.

  6. MainActivityPresenter: This class is the presenter for the MainActivity.kt file implementing the main business logic. This is where you will be working mostly. Notice that the presenter is initialized in the onCreate() and uses the getData() method to fetch data when the FloatingActionButton is clicked, and in onResume(). Later in onDestroy() the presenter calls cleanup() to avoid memory leak.

  7. RemoteRepo: This class takes care of defining methods used for fetching data from the remote server using Retrofit library.

  8. LocalRepo: This class takes care of defining methods used for fetching from and saving to local database using Room library.

  9. DataRepository: This class implements the repository pattern to implement logic around fetching from a remote server or local database.

  10. RemoteApi: This is a singleton class, which defines the base URL for SWAPI API, the people’s route path and the retrofit service with pre-setup Moshi Converter and Coroutine Adapter.

Note: The starter app includes both a callback and a coroutine-based implementation. To use the right kind, inside the MainActivityPresenter, a value is passed to the property processingUsing as ProcessUsing.BackgroundThread by default. This means that the app uses callback based implementation. To switch to the coroutine-based implementation, simply change the value of processingUsing to ProcessUsing.Coroutines.

Run the starter app now and you will see the following:

Starter App
Starter App

When the app loads for the first time, because it is an offline-first Android app, it tries to load data from the local database first. It then goes on to fetch from the remote server via a GET call to the SWAPI API.

Fetch from Local and Remote database
Fetch from Local and Remote database

After the first remote fetch, data is saved to a local database. To verify the offline-first approach, simply switch to Airplane Mode and re-launch the app. Data will be fetched from the local database and populated in the list on the screen.

To simplify and focus on coroutines, you will notice everything is mostly wired up. You will, however, be implementing the important parts. So get ready to get your feet wet!

Note: It is expected that you know about the usage of Retrofit and Room libraries, as well as the implementation of the MVP architecture. The parts covered here will focus mostly on the implementation of coroutines in a practical real-world Android app.

What’s in the context?

When talking about Android apps, one cannot ignore the pain around multi-threading. Android apps are limited to a single main thread for all processing and this makes it difficult to build highly responsive and performant apps. If a lot of processing is done on the main thread, the UI can become non-responsive and eventually lead to an app crash. To avoid that, do all heavy processing on a background thread. This is easy to achieve because one can simply start a thread or a pool of threads to offload the heavy processing.

It becomes tricky when the heavy processing completes and the result needs to be updated in the UI — i.e., back in the main thread. Switching back to the main thread isn’t as easy it sounds. Passing values across threads is very painful and this is the reason Android has constructs like AsyncTask, which switch from background thread to the main thread automatically.

CoroutineDispatcher

The CoroutineDispatcher determines what thread or threads the corresponding coroutine uses for its execution. It can confine (restrict) coroutine execution to a specific thread, dispatch it to a thread pool, or let it run unconfined (unrestricted).

In the simplest terms, it defines where your piece of code executes — i.e., on the main thread, background thread or a pool of threads.

The standard coroutine library provides the following dispatchers:

  1. Dispatchers.Default: Used by all standard builders if no dispatcher is specified. It uses a common pool of shared background threads. This is an appropriate choice for compute-intensive coroutines that consume CPU resources.
  2. Dispatchers.IO: Uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive blocking operations (like file I/O and blocking socket I/O).
  3. Dispatchers.Unconfined: Unrestricted to any specific thread or pool and should not be used normally in code. There will be certain use-cases in which you might need to use this.
  4. newSingleThreadContext: Use to create a new private single-threaded coroutine context.
  5. newFixedThreadPoolContext: To create a private thread pool of fixed size.
  6. asCoroutineDispatcher: Extension function to convert an Executor to a dispatcher.
  7. Dispatchers.IO: Uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive blocking operations (like file I/O and blocking socket I/O).
  8. Dispatchers.Unconfined: Unrestricted to any specific thread or pool and should not be used normally in code. There will be certain use-cases in which you might need to use this.
  9. newSingleThreadContext: To create new private single-threaded coroutine context.

All coroutines builders like launch and async accept an optional CoroutineContext parameter that can be used to explicitly specify the dispatcher for new coroutine and other context elements. There is, however, another coroutine builder called withContext that takes in a CoroutineContext parameter, which is not optional. The usage, however, is pretty similar to the async coroutines builder:

withContext(Dispatchers.IO) {
    // Code to execute
}

Note: launch(Dispatchers.Default) { … } uses the same dispatcher as GlobalScope.launch { … }.

A coroutine can switch dispatchers any time after it is started. For example, a coroutine can start on the main dispatcher then use another dispatcher to process a long-running task off the main thread. This is typically called context switching in coroutines. Kotlin Coroutines typically make this very easy to achieve.

In Android, we need a coroutine dispatcher context that restricts the coroutine execution to the main UI thread. The core coroutines library provides the Dispatchers.Main context that uses a service loader behind the scenes to pick the correct main-thread dispatcher implementation. The dispatcher implementation you want is provided via an Android specific kotlinx-coroutines-android library dependency, which should be added alongside the core coroutines library. The kotlinx-coroutines-android library provides a concrete implementation of a Dispatchers.Main context for Android applications, which allows you to start coroutines confined to the main thread.

Coroutines, during execution, can easily switch context from Dispatcher.IO or Dispatcher.Default to Dispatchers.Main, thereby enabling processing in a background thread without blocking the main thread and, when the result is ready, switching back to the main thread to display it in the UI.

The dispatcher implementation you want is provided via an Android specific kotlinx-coroutines-android library dependency, which should be added alongside the core coroutines library.

The kotlinx-coroutines-android library provides a concrete implementation of a Dispatchers.Main context for Android apps, which allows you to start coroutines confined to the main thread. A coroutine started on the main won’t block the main thread while suspended.

To add kotlinx-coroutines-android library, simply add the library below to your app’s build.gradle file in the dependencies section, replacing //TODO: add Kotlin Coroutines Android dependency here and sync your project:

dependencies {
    // Other dependencies

    // Kotlin Coroutines Android
    implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0’
}

Now, that the dependency is added to the project, calls like demonstrateUsingMainDispatcher, shown below, are possible:

private fun demonstrateUsingMainDispatcher() {
    GlobalScope.launch(Dispatchers.Main) {
        
        //1 Runs on Main Thread
        var stringToShow = "Luke"
        prompt(stringToShow)
        
        //2 Runs on Background Thread
        withContext(Dispatchers.IO) {
            delay(5000)
            stringToShow = "Darth Vader"
        }
        
        //3 Runs on Main Thread
        prompt(stringToShow)
    }
}

Here’s what this code does:

  1. When demonstrateUsingMainDispatcher() is called, the prompt method is called on the main thread with the string "Luke". Next, the context is switched to IO thread (i.e., background thread) and the coroutine now suspends. After a delay of five seconds (using the delay call), the value for stringToShow is updated to "Darth Vader" and then finishes.
  2. Next, the context is switched to IO thread (i.e., background thread) and the coroutine now suspends. After a delay of five seconds, the value for stringToShow is updated to "Darth Vader" and then finishes.
  3. Finally, once done, the coroutine switches back to the main thread and calls the prompt method with the updated value for stringToShow, which is now "Darth Vader".

Navigate to MainActivity.kt file. The above method is pre-written and exists inside the MainActivity.kt file. If you check the code under the onClickListener on the FloatingActionButton called fab, you will see:

// Setup FAB
fab.setOnClickListener {
    // 1
    presenter?.getData()

    // 2
    // demonstrateUsingMainDispatcher()
}

Uncomment demonstrateUsingMainDispatcher() and comment out presenter?.getData(). Now, run the app.

When you press the FloatingActionButton, you will see that at first a snackbar with the text “Luke” shows up. Then, after a delay of five seconds, another snackbar shows with the text “Darth Vader.” You will also notice that, during the delay of five seconds, you can scroll the list of items on the screen because the main thread is not blocked at all.

Note: You will not be using this code anymore in the future, so you can delete demonstrateUsingMainDispatcher() called inside the onClickListener for the fab. Make sure you uncomment presenter?.getData() after removing demonstrateUsingMainDispatcher().

CoroutineScope

Each coroutine runs inside a scope defined by you, so you can make it app-wide or specific for an Android component with a well-defined life cycle, such as an Activity or Fragment. The scope here is represented by the class name CoroutineScope. Each coroutine waits for all the coroutines inside their block/scope to complete before completing themselves. A scope controls the lifetime of coroutines through its job. When you cancel the scope’s job, it cancels all coroutines started in that scope, i.e. when the user navigates away from an Activity or Fragment.

Note: Every coroutine builder is an extension of CoroutineScope and inherits its coroutineContext to automatically propagate both context elements and cancellation.

CoroutineScope provides properties like coroutineContext, and it is a set of various elements like the Job of the coroutine and its dispatcher. You can also check whether a coroutine is active or not using the isActive property of Job.

Note: GlobalScope is used to launch a coroutine that corresponds to the lifetime of the whole app.

Setting up a CoroutineScope is pretty straightforward in Android because you almost always want to update the UI on the main thread. Starting coroutines on the main thread is a reasonable default. To create a scope that is dispatched to the UI thread, you first define a job and then pass it along with Dispatchers.Main to CoroutineScope constructor as shown below:

private val coroutineJob = Job()

private val uiScope = CoroutineScope(Dispatchers.Main + coroutineJob)

Since coroutineJob is passed as the job to uiScope, when coroutineJob is canceled, every coroutine started with uiScope will be canceled as well.

To cancel coroutines on a job object, simply call cancel() on the job instance:

coroutineJob.cancel()

The same code is being called inside the MainActivityPresenter class under the method cleanup(). It means that, when the presenter calls the cleanup() method, it will cancel all ongoing coroutine jobs.

Note: You must pass CoroutineScope a Job in order to cancel all coroutines started in the scope. If you don’t, the cancel function will throw an IllegalStateException when you try to cancel the scope.

Scopes created with the CoroutineScope constructor add an implicit job, which you can cancel using uiScope.coroutineContext.cancel(), which is another way of canceling coroutines running inside a CoroutineScope.

Another step that is required for creating Scopes: implement the CoroutineScope interface. When you do that, you need to override the coroutineContext and assign it the right dispatchers.

Implement CoroutineScope on MainActivityPresenter, as shown below:

class MainActivityPresenter(var view: ViewContract?, var repository: DataRepositoryContract?) :
    PresenterContract, CoroutineScope {
}

Next, replace coroutineScope with the below snippet:

override val coroutineContext: CoroutineContext = Dispatchers.Main + coroutineJob

Here, by default, the scope is set on the main thread. When required, the coroutines will switch to a different dispatcher such as Dispatcher.IO. You can find an example under the saveDataUsingCoroutines() method inside the MainActivityPresenter class.

Now that you have implemented the CoroutineScope interface, you do not need to use the coroutineScope property that is defined in MainActivityPresenter directly, so you can remove all calls to it.

For all calls like:

coroutineScope.launch {
    ...
}

Simply becomes:

launch {
    ...
}

Delete the coroutineScope property and update your MainActivityPresenter class with the above changes to fix the errors created after deletion of the property.

Note that you can directly call cancel() on the coroutineContext inside the cleanup() method instead of calling it on the coroutineJob.

Inside MainActivityPresenter class under cleanup() method, the call:

override fun cleanup() {
    // Cancel all coroutines running in this context
    coroutineJob.cancel()

    ...
}

Now becomes:

override fun cleanup() {
    // Cancel all coroutines running in this context
    coroutineContext.cancel()

    ...
}

Converting existing API call to use coroutines

On Android, to guarantee a great and smooth user experience, the app needs to function without any visible pauses. Most pauses are usually noticeable when the device cannot refresh the screen at 60 frames per second. On Android, the main thread is a single thread responsible for handling all updates to the UI, calls to all click handlers and other UI callbacks. Common tasks, such as writing data to a database or fetching data from the network, usually take longer than 16ms to do and this long processing time makes it hard to keep screen refresh rates at 60 frames per second. Therefore, calling code like this from the main thread can cause the app to pause, stutter, or even freeze. Moreover, if you block the main thread for too long, the app may even crash and present an Application Not Responding dialog.

For performing long-running tasks without blocking the main thread, callbacks are a common pattern you can use. 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.

If you look at the implementation of the getData() method inside the MainActivityPresenter class, you will find:

override fun getData() {
    // Start loading animation
    view?.showLoading()

    // Fetch Data
    fetchData()
}

private fun fetchData() {
    when (processingUsing) {
        // 1
        ProcessUsing.BackgroundThread -> fetchUsingBackgroundThreads()

        // 2
        ProcessUsing.Coroutines -> fetchUsingCoroutines()
    }
}

Inside the getData() method, a call to fetchData() is made, and based on the value of processingUsing, calls the corresponding method. In the current case, since processingUsing is equal to ProcessUsing.BackgroundThread, the fetchUsingBackgroundThreads() method is called.

Diving deeper inside the method implementation of fetchUsingBackgroundThreads(), you can find the implementation calls an AsyncTask called FetchFromLocalDbTask and passes a callback called ItemListCallback to the AsyncTask in the constructor as shown below:

class FetchFromLocalDbTask(val repository: DataRepositoryContract?,
    private val itemListCallback: ItemListCallback) : AsyncTask<Void, Void, List<People>>() {

  override fun onPostExecute(result: List<People>?) {
    super.onPostExecute(result)

    // Return callback
    itemListCallback.onSuccess(result)

    // Stop the task
    cancel(true)
  }

  override fun doInBackground(vararg params: Void?): List<People> {
    return repository?.getDataFromLocal() ?: emptyList()
  }
}

The call itemListCallback.onSuccess(result) is called when the AsyncTask has finished and is called on the main thread while doInBackground executes on the background thread.

Back inside the fetchUsingBackgroundThreads() method implementation, inside the MainActivityPresenter class, under onSuccess the view is updated when AsyncTask has finished on the main thread.

This is the usual way of using a callback pattern to pass the data around. However, it eventually leads to Callback Hell and is not very efficient nor readable. A lot of jumping back and forth is required to follow the code flow.

Using coroutines, you can avoid this callback hell and make the code more readable. Kotlin coroutines let you convert callback-based code to sequential code. To do this, change the value of processingUsing to ProcessUsing.Coroutines inside the MainActivityPresenter class.

Now, whenever the getData() method is called, it will call fetchData(), which will then call fetchUsingCoroutines() method.

Take a look at the implementation of the fetchUsingCoroutines() method:

private fun fetchUsingCoroutines() {
    launch {
      try {
        //1
        var itemList = withContext(Dispatchers.IO) {
          repository?.getDataFromLocal()
        }

        //2
        updateData(itemList, "Local DB")

        //3 
        itemList = withContext(Dispatchers.IO) {
          repository?.getDataFromRemoteUsingCoroutines()
        }

        //4
        updateData(itemList, "Remote Server")
      } catch (e: Exception) {
        handleError(e)
      }
    }
}

Notice, how the code flow is defined, here:

  1. Fetch from local first, using a background thread — i.e., Dispatcher.IO.
  2. When done, update the UI (set to Dispatcher.Main in the coroutineScope).
  3. Try fetching from remote next, using a background thread — i.e., Dispatcher.IO.
  4. When done, update the UI (set to Dispatcher.Main in the coroutineScope).

As is visible, the code flow is pretty straightforward and can be read sequentially.

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

Coroutines and Android lifecycle

Android apps consists of various components, which have a lifecycle of their own such as Activities, Fragments, Services, etc. Processing done outside the lifecycle of these components can lead to memory leaks or crashes in general. For example, if the Activity is destroyed and an async processing task — after finishing its work — tries to update the UI of the Activity, it will lead to a crash. This is a serious problem when it comes to configuration changes, such as when the phone is rotated.

Coroutines are not free from such issues; however, they are well prepared to handle them. One of the important concepts that can be used to combat these lifecycle issues is to confine the CoroutineScope to the lifecycle of the Android component.

To do that, you will need to observe the lifecycle of the Android component. Such functionality is available via the Architecture Components Libraries. These libraries consist of a LifecycleObserver and DefaultLifecycleObserver interfaces to enable observing the lifecycle of a lifecycle owner.

You already set up a CoroutineScope for the presenter. All you need to do now is to make sure the presenter adheres to the lifecycle of the Activity. When the Activity goes to the onDestroy() state, it will call the presenters cleanup() method, thus canceling all the coroutines within the presenters CoroutineScope and its siblings. This process makes sure there are no coroutines running once the Activity is destroyed. This concurrency mechanism is called Structured Concurrency.

To implement this functionality, go to your app’s build.gradle file and replace //TODO: add lifecycle dependencies here under the dependencies section with the following, and sync your project:

dependencies {
    // Other dependencies

    //region Lifecycle
    final lifecycleVersion = "2.0.0"
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
    //endregion
}

Now, simply implement the interface DefaultLifecycleObserver on your MainActivityPresenter class, as below:

class MainActivityPresenter(var view: ViewContract?, var repository: DataRepositoryContract?) :
    PresenterContract, CoroutineScope, DefaultLifecycleObserver {
    ...
}

Once implemented, you can override the onResume() and onDestroy() methods inside MainActivityPresenter class:

override fun onResume(owner: LifecycleOwner) {
    super.onResume(owner)
    getData()
}

override fun onDestroy(owner: LifecycleOwner) {
    cleanup()
    super.onDestroy(owner)
}

Notice the call to cleanup() inside the onDestroy() and the call to getData() inside onResume(). Now, all that is required is to wire this presenter to the lifecycle of the MainActivity.

To do that, navigate to MainActivity.kt and, inside the onCreate() method, add the presenter as the observer on the lifecycle by replacing the //TODO: Observe the lifecycle line as shown below:

 override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Setup the presenter
    val presenter = MainActivityPresenter(this, repository)

    //TODO: Observe the lifecycle
    lifecycle.addObserver(presenter)

    ...
  }

Notice that the presenter is now a local val inside the onCreate(). Because you already react to the lifecycle of the Activity and call getData() when the Activity goes through onResume(), and you also react to the call cleanup() when the Activity goes through onDestroy(), you no longer need those methods inside the MainActivity.kt file itself. So you can safely delete those methods inside the MainActivity.kt file:

// Delete the below from within MainActivity.kt file

private var presenter: PresenterContract? = null
...
override fun onResume() {
    super.onResume()
    presenter?.getData()
}

override fun onDestroy() {
    presenter?.cleanup()
    super.onDestroy()
}

Another way to handle the lifecycle is to make your CoroutineScope lifecycle aware. The implementation of such a CoroutineScope would look like below:

class LifecycleScope : DefaultLifecycleObserver, CoroutineScope {
  private val job = Job()

  override val coroutineContext: CoroutineContext = job + Dispatchers.Main

  override fun onDestroy(owner: LifecycleOwner) {
    coroutineContext.cancel()
    super.onDestroy(owner)
  }
}

In such a case, the CoroutineScope, in itself, calls cancel on its coroutineContext when the lifecycle owner goes through the onDestroy() state.

Note: You can find the implementation of such a lifecycle aware CoroutineScope in the final app, under the utils package in the file named LifecycleScope.kt.

Coroutines and WorkManager

WorkManager is a simple library that is a part of Android Jetpack, used for deferrable background work. It enables a combination of opportunistic and guaranteed executions. 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.

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

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

To enable coroutine support in WorkManager, you need the dependency shown below, which is already added to the starter project.

dependencies {
    // Other dependencies

    final workManagerVersion = "2.1.0"
    implementation "androidx.work:work-runtime-ktx:$workManagerVersion"
}

To define background work in WorkManager, you extend Worker and implement doWork(). However, when dealing with Coroutines, you would extend from CoroutineWorker and implement the doWork() method, which is a suspend marked method; thus, it will not block the main thread when called.

You can find the existing implementation for the same inside the file named RefreshRemoteRepo.kt. Under the doWork() method implementation, a call to refreshData() is made, which is another suspend coroutine.

Here, data is fetched from the network using the repository, and the local database is updated with the latest information fetched:

// Refresh data from the network using [DataRepository]
@WorkerThread
suspend fun refreshData(): Result {

    // 1
    val localRepo = LocalRepo(applicationContext)
    val remoteRepo = RemoteRepo(applicationContext)
    val repository = DataRepository(localRepo, remoteRepo)

    return try {
            //2 
            val itemLists = repository.getDataFromRemoteUsingCoroutines()

            //3 
            repository.saveData(itemLists)

            //4 
            Result.success()
        } catch (error: Exception) {

            //5
            Result.failure()
        }
}

The code flow is as follows:

  1. Initialize the repository.
  2. Fetch from remote first using coroutines.
  3. Once the updated data is fetched, update the local database with the refreshed data.
  4. Signal successful completion of the work to WorkManager.
  5. Signal failed work to WorkManager.

WorkManager is typically wired inside a custom Application class, extending from Application class. The implementation can be checked out inside the StarSyncApp.kt file:

class StarSyncApp : Application() {

  override fun onCreate() {
    super.onCreate()

    setupWorkManagerJob()
  }

  private fun setupWorkManagerJob() {
    // 1
    val constraints = Constraints.Builder()
        .setRequiresCharging(true)
        .setRequiredNetworkType(UNMETERED)
        .build()

    //2
    val work = PeriodicWorkRequest
        .Builder(RefreshRemoteRepo::class.java, 1, TimeUnit.DAYS)
        .setConstraints(constraints)
        .build()

    //3 Enqueue it work WorkManager, keeping any previously scheduled jobs for the same work.
    WorkManager.getInstance()
        .enqueueUniquePeriodicWork(RefreshRemoteRepo::class.java.name, KEEP, work)
  }
}

The code flow is pretty straightforward:

  1. Set up conditions/constraints on the WorkManager to execute the worker jobs, such as the phone needs to be plugged in and charging while being on the unmetered (wifi) network before executing the worker jobs.
  2. Set up a periodic job, which will be attempted to run every day.
  3. Enqueue the work to WorkManager, keeping any previously scheduled jobs for the same work.

This custom Application class needs to be wired in the AndroidManifest.xml too for this to work. Navigate to AndroidManifest.xml and you will see it is pre-wired in the <application> as a value to the name attribute:

<application
      android:name=".StarSyncApp"
      ...
      >
    ...
</application> 

Run the app. The worker jobs will be scheduled to run in the background every day and refresh the data in the local database with updated data fetched from the remote server, i.e., SWAPI API.

Final App
Final App

Key points

  1. CoroutineDispatcher determines what thread or threads the corresponding coroutine uses for its execution.
  2. A coroutine can switch dispatchers any time after it is started.
  3. Dispatchers.Main context for Android apps, allows starting coroutines confined to the main thread.
  4. Each coroutine runs inside a defined scope.
  5. A Job must be passed to CoroutineScope in order to cancel all coroutines started in the scope.
  6. Coroutines can replace callbacks for more readable and clear code implementation.
  7. Making CoroutineScope lifecycle aware helps to adhere to the lifecycle of android components and avoid memory leaks.
  8. Coroutines seamlessly integrate with WorkManager to run background jobs efficiently.

Where to go from here?

This chapter introduced the concept of using coroutines in an Android app. The concept of various contexts was also covered and how to switch between them when required, all while being able to react to lifecycle events in an Android app.

In the next chapters, you will be working your way through the topics testing, debugging and logging with Coroutines, which are the most important aspects of building a solid Android app.

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.
© 2026 Kodeco Inc.