Coroutines With Room Persistence Library

In this tutorial, you’ll learn how to use coroutines with the Room persistence library to allow for asynchronous database operations. By Kyle Jablonski.

4.5 (11) · 2 Reviews

Download materials
Save for later
Share

Room is Google’s architecture components library for working with SQLite on Android. With the release of Version 2.1, the library added support for database transactions using coroutines.

In this tutorial, you’ll learn how to:

  • Implement suspension functions on Data Access Objects (or DAO) in Room.
  • Call them using Kotlin’s coroutines for database transactions.

As you progress, you’ll take an app listing the top 20 tennis players and refactor it to use suspension functions and coroutines. You’ll also add a few new features including viewing player details, selecting favorite players and deleting players.

This tutorial assumes a basic understanding of how to build Android applications, work with the Room persistence library and use the Android framework threading model with the Kotlin programming language. Experience with the Kotlin Gradle DSL is useful but not required.

If you don’t have experience with Room, please check out the Data Persistence With Room article for an introduction. If you don’t have experience with coroutines then, read the Kotlin Coroutines Tutorial for Android: Getting Started first. Then swing back to this tutorial. Otherwise, proceed at your own risk. :]

Getting Started

To get started, download the project resources from the Download Materials button at the top or bottom of this tutorial.

Import the TennisPlayers-starter project into Android Studio and let Gradle sync the project.

Build and run the application.

If everything compiles, you’ll see a list of the top tennis players in the world.

list of top tennis players

Great job! You’re up and running.

The list gets loaded from a Room database that is implemented in PlayersDatabase.kt file.
Look closely though, and you’ll see a problem with the implementation. Inside the getDatabase() function, under the synchronized block you will notice RoomDatabase.Builder has a call to allowMainThreadQueries() method, which means all database operations will run on the main thread.

Executing database transactions on the MainThread is actually bad, since it would lead UI freeze and/or application crash.

Time to fix this problem with the power of coroutines.

Pre-Populating the Database

Locate players.json in res/raw inside app module. Parsing that file and placing it in the database can be a costly operation, though. It’s certainly not something that should be on the main thread.

Ideally, you want to insert the data while the database is being created. Room provides this mechanism in the form of RoomDatabase.Callback. This callback lets you intercept the database as it’s being opened or created. It also allows you to hook your own code into the process. You will setup the callback next.

Creating the RoomDatabase.Callback

Replace // TODO: Add PlayerDatabaseCallback here in PlayersDatabase.kt with code provided below:

private class PlayerDatabaseCallback(
      private val scope: CoroutineScope,
      private val resources: Resources
  ) : RoomDatabase.Callback() {

  override fun onCreate(db: SupportSQLiteDatabase) {
    super.onCreate(db)
    INSTANCE?.let { database ->
      // TODO: dispatch some background process to load our data from Resources
    }
  }
  // TODO: Add prePopulateDatabase() here
}

Here, you define a concrete class of RoomDatabase.Callback. Notice that the class constructor accepts Resources as argument. This is required in order to load the JSON file from res/raw. The other argument passed is the CoroutineScope, which is used to dispatch background work. This will be discussed more in the next section.

getDatabase() in the Companion Object eventually needs to set an instance of your callback in the builder. To do that, you will need to modify the signature to pass in CoroutineScope and Resources as arguments.

Update getDatabase(context: Context) with the following signature:

fun getDatabase(
    context: Context,
    coroutineScope: CoroutineScope, // 1
    resources: Resources // 2
): PlayersDatabase { /* ...ommitted for brevity */}

Next, replace allowMainThreadQueries() inside Room.databaseBuilder with the addCallback as shown below:

val instance = Room.databaseBuilder(context.applicationContext,
            PlayersDatabase::class.java,
            "players_database")
            .addCallback(PlayerDatabaseCallback(coroutineScope, resources)) 
            .build()

The callback is all hooked up. Time to launch a coroutine from your callback to do some heavy lifting.

Exploring CoroutineScope

CoroutineScope defines a new scope for coroutines. This means that context elements and cancellations are propagated automatically to the child coroutines running within. Various types of scopes can be used when considering the design of your application. Scopes usually bind internally to a Job to ensure structured concurrency.

Since coroutine builder functions are extensions on CoroutineScope, starting a coroutine is as simple as calling launch and async among other builder methods right inside the Coroutine-Scoped class.

A few scope types:

  • GlobalScope: A scope bound to the application. Use this when the component running doesn’t get destroyed easily. For example, in Android using this scope from the application class should be OK. Using it from an activity, however, is not recommended. Imagine you launch a coroutine from the global scope. The activity is destroyed, but the request is not finished beforehand. This may cause either a crash or memory leak within your app.
  • ViewModel Scope: A scope bound to a view model. Use this when including the architecture components ktx library. This scope binds coroutines to the view model. When it is destroyed, the coroutines running within the ViewModel’s context will be cancelled automatically.
  • Custom Scope: A scope bound to an object extending Coroutine scope. When you extend CoroutineScope from your object and tie it to an associated Job, you can manage the coroutines running within this scope. For example, you call job = Job() from your activity’s onCreate and job.cancel() from onDestroy() to cancel any coroutines running within this component’s custom scope.

Next up, you will use this knowledge about CoroutineScope when you start loading Player data in the background using coroutines to keep them under check.

Loading the Players in the Background

Before worrying about where the work will run, you must first define the work to be done.

To do that, navigate to PlayersDatabase.kt file. Right below the onCreate() override inside PlayerDatabaseCallback, replace // TODO: Add prePopulateDatabase() here with code shown below: :

private fun prePopulateDatabase(playerDao: PlayerDao){
  // 1
  val jsonString = resources.openRawResource(R.raw.players).bufferedReader().use {
    it.readText()
  }
  // 2
  val typeToken = object : TypeToken<List<Player>>() {}.type
  val tennisPlayers = Gson().fromJson<List<Player>>(jsonString, typeToken)
  // 3
  playerDao.insertAllPlayers(tennisPlayers)
}

Here you are:

  1. Reading the players.json raw resource file into a String.
  2. Converting it to a List using Gson.
  3. Inserting it into the Room database using the playerDao.

CoroutineScopes provide several coroutine builders for starting background work. When you just want to fire and forget about some background work, while not caring about a return value, then the appropriate choice is to use launch coroutine builder.

Copy the following code, replacing // TODO: dispatch some background process to load our data from Resources in onCreate() of PlayerDatabaseCallback:

//1
scope.launch{
   val playerDao = database.playerDao() // 2
   prePopulateDatabase(playerDao) // 3
}

Here you are:

  1. Calling the launch coroutine builder on the CoroutineScope passed to PlayerDatabaseCallback named as scope
  2. Accessing the playerDao.
  3. Calling the prePopulateDatabase(playerDoa) function you defined earlier.

Nice Work! Build and run the app now. Did it work?

You’ll notice the app no longer run because you updated getDatabase() signature. Time to fix this.