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

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

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.

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

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

    // Kotlin Coroutines Android
    implementation ’org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0’
}
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)
    }
}
// Setup FAB
fab.setOnClickListener {
    // 1
    presenter?.getData()

    // 2
    // 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.

private val coroutineJob = Job()

private val uiScope = CoroutineScope(Dispatchers.Main + coroutineJob)
coroutineJob.cancel()
class MainActivityPresenter(var view: ViewContract?, var repository: DataRepositoryContract?) :
    PresenterContract, CoroutineScope {
}
override val coroutineContext: CoroutineContext = Dispatchers.Main + coroutineJob
coroutineScope.launch {
    ...
}
launch {
    ...
}
override fun cleanup() {
    // Cancel all coroutines running in this context
    coroutineJob.cancel()

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

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

    // Fetch Data
    fetchData()
}

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

        // 2
        ProcessUsing.Coroutines -> fetchUsingCoroutines()
    }
}
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()
  }
}
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)
      }
    }
}

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.

dependencies {
    // Other dependencies

    //region Lifecycle
    final lifecycleVersion = "2.0.0"
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycleVersion"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
    //endregion
}
class MainActivityPresenter(var view: ViewContract?, var repository: DataRepositoryContract?) :
    PresenterContract, CoroutineScope, DefaultLifecycleObserver {
    ...
}
override fun onResume(owner: LifecycleOwner) {
    super.onResume(owner)
    getData()
}

override fun onDestroy(owner: LifecycleOwner) {
    cleanup()
    super.onDestroy(owner)
}
 override fun onCreate(savedInstanceState: Bundle?) {
    ...

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

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

    ...
  }

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

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

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.

dependencies {
    // Other dependencies

    final workManagerVersion = "2.1.0"
    implementation "androidx.work:work-runtime-ktx:$workManagerVersion"
}
// 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()
        }
}
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)
  }
}
<application
      android:name=".StarSyncApp"
      ...
      >
    ...
</application> 
Final App
Vulub Ohn

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.

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