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?
Providing CoroutineScope
Open both the PlayerViewModel.kt and DetailViewModel.kt files. Update the getDatabase()
function in playerDao
as shown below:
val playerDao = PlayersDatabase
.getDatabase(application, viewModelScope, application.resources)
.playerDao()
Here, you’re passing in the viewModelScope
CoroutineScope from the lifecycle-viewmodel-ktx library to allow the database to use this scope when running coroutines. By using viewModelScope
, any coroutine running will be cancelled when AndroidViewModel
is destroyed. Application’s resources are also passed to the getDatabase()
call as required by the new function signature.
At this point, you can build and run the application, but you’ll see an IllegalStateException
thrown in with the following message:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
This happens for two reasons:
-
PlayerDao
methods togetPlayerCount()
andinsertAllPlayers(players: List<Player>)
are still accessing the database on the main thread. - The old code in the MainActivity.kt file runs queries on the main thread.
But wait! CoroutineScope’s launch
coroutine builder pushes this work off to a coroutine, but Room doesn’t know this yet. The internal check inside the Room library fails even if you push the MainActivity
work off to a coroutine. This is because the DAO methods are missing something very important: suspend
keyword.
Suspending Functions
In Kotlin, a suspension function is a function that can suspend the execution of a coroutine. This means the coroutine can pause, resume or cancel. It also means the function can perform some long-running behavior and wait for its completion alongside other suspending function calls.
The app needs to check the number of players in the database before populating, so you want to call the methods one after the other with suspend
keyword. To leverage this behavior with Room, you will update PlayerDao
by adding the suspend
keyword to its method definitions.
First, open the PlayerDao.kt file and add suspend
keyword to insertAllPlayers(players: List<Player>)
.
Copy the following code and paste it in place of the existing definition:
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllPlayers(players: List<Player>)
Here, you added suspend
keyword to tell Room that this method can suspend its execution and run its operations using a coroutine.
Next, open the PlayersDatabase.kt file and add the suspend
keyword to prePopulateDatabase(playerDao)
:
private suspend fun prePopulateDatabase(playerDao: PlayerDao) {
//... omitted for brevity
}
You’ve updated prePopulateDatabase(playerDao: PlayerDao)
to run as a suspending function from the launch coroutine builder.
This alteration will change the call to insert all methods to run within a coroutine. During the creation of the database, the callback will call the prePopulateDatabase (playerDao: PlayerDao)
suspending function and insert all the players read from the raw JSON file.
Next, open the PlayerRepository.kt file. Highlight and delete fun insertAllPlayers(players: List<Player>)
function. You won’t need it any longer.
Since insertAllPlayers()
function is deleted, so any place in code where it is referenced will not be resolved anymore. This will cause compilation errors. You will need to get rid of that.
Open the PlayerViewModel.kt file. Highlight and delete populateDatabase()
.
Next, open the MainActivity.kt file. Highlight and delete playerViewModel.populateDatabase()
since the populateDatabase()
was deleted from the playerViewModel
.
At this point, you’ve almost completed the updates. However, MainActivity
still queries the database on the main thread. To fix this, you’ll need to observe changes in the database instead of query them. It’s time to update the PlayerDao.kt, PlayerRepository.kt, PlayerViewModel.kt and MainActivity.kt files to use LiveData.
Observing Changes to Data
Right now, you’re running the pre-populate functionality when MainActivity
instantiates PlayerViewModel
. As such, you can’t query the database right away, because Room won’t allow multiple connections to the database simultaneously.
To get around this restriction, you’ll need to add LiveData
around the return type of getAllPlayers()
and observe changes rather than query for the List<PlayerListItem>
.
First, open the PlayerDao.kt file and change getAllPlayers()
to have the following code:
@Query("SELECT id, firstName, lastName, country, favorite, imageUrl FROM players")
fun getAllPlayers(): LiveData<List<PlayerListItem>>
Here, you wrapped List<PlayerListItem>
in a LiveData
object.
suspend
on this method. In fact, Room won’t even allow it. The LiveData object relies on the observer pattern where the caller can subscribe to changes on the value it contains. Whenever new data are available from the database, this list will update and reflect that data within the UI. It won’t need to re-query the database.Next, open the PlayerRespository.kt file and update the getAllPlayers()
method’s signature as well:
fun getAllPlayers(): LiveData<List<PlayerListItem>> {
return playerDao.getAllPlayers()
}
Then, open the PlayerViewModel.kt file and do the same:
fun getAllPlayers(): LiveData<List<PlayerListItem>> {
return repository.getAllPlayers()
}
Finally, you need to fix the list. Open the MainActivity.kt file. Delete all the code below //TODO Replace below lines with viewmodel observation
as well as the Todo itself, and then attach Observer
to the playerViewModel.getAllPlayers()
as shown below:
playerViewModel.getAllPlayers().observe(this, Observer<List<PlayerListItem>> { players ->
adapter.swapData(players)
})
Build and run the application. The list restores!
Wow! You did a lot of work to get those changes implemented. Now that it’s all done, you can enhance the application by adding favorite and delete features to the player details screen.
Before you try this update, go ahead and tap on any player on the list. You’ll notice that the player details are missing.
Time to set their records straight. :]
Getting a Single Player
To retrieve a Player
from the database, a few things need to happen. First, DetailFragment
needs to access the PlayerListItem
from fragment’s arguments — this has already been implemented for you.
Then PlayerListItem
‘s id
needs to pass into a new method getPlayer(id: Int): Player
from your DAO. Remember: You have to wrap this return type again in LiveData
so the actual method signature will have a return type of LiveData
.
To begin navigate to PlayerDao.kt file and the below code:
@Query("SELECT * FROM players WHERE id = :id")
fun getPlayer(id: Int): LiveData<Player>
Here, you’re adding the ability to read a player from the database using LiveData
.
Next, you’ll update PlayerRepository
with a similar method that calls into the DAO. Open the PlayerRepository.kt file and add the following code:
fun getPlayer(id: Int): LiveData<Player> {
return playerDao.getPlayer(id)
}
Next, you will be calling the getPlayer()
from the repository
. To do this navigate to DetailViewModel.kt and add the following code:
fun getPlayer(player: PlayerListItem): LiveData<Player> {
return repository.getPlayer(player.id)
}
You can now attach an Observer
to this method call from the DetailsFragment.kt file. Inside onViewCreated()
, replace //TODO observe viewmodel changes
with below code block:
// 1
detailViewModel.getPlayer(playerListItem).observe(viewLifecycleOwner, Observer {
// 2
this.player = it
// 3
displayPlayer()
})
Here you are:
- Adding an observer to the
getPlayer(playerListItem)
. - Updating the local
Player
with the observer player itemit
. - Calling to display the player now that the observer’s data is up to date.
Build and run the app. Nice work! The player details are present, and the application is almost fully functional.
In the next section, you’ll start to gain a better understanding of coroutine support in Room by adding a favorite feature to the players’ details views.