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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Coroutines With Room Persistence Library
25 mins
- Getting Started
- Pre-Populating the Database
- Creating the RoomDatabase.Callback
- Exploring CoroutineScope
- Loading the Players in the Background
- Providing CoroutineScope
- Suspending Functions
- Observing Changes to Data
- Getting a Single Player
- Updating a Player
- Adding Update to the Dao
- Adding Update to the Repository
- Adding Update to the ViewModel
- Adding Update to the UI
- Deleting a Player
- Adding Delete to the DAO
- Adding Delete to the Repository
- Adding Delete to the ViewModel
- Adding Delete to the UI
- Where to Go From Here?
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.
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 calljob = Job()
from your activity’sonCreate
andjob.cancel()
fromonDestroy()
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:
- Reading the players.json raw resource file into a
String
. - Converting it to a
List
using Gson. - 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:
- Calling the
launch
coroutine builder on the CoroutineScope passed to PlayerDatabaseCallback named asscope
- Accessing the
playerDao
. - 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.