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
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. PlayerDao methods to getPlayerCount() and insertAllPlayers(players: List<Player>) are still accessing the database on the main thread.
  2. 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.

Note: Since you’re returning a LiveData object, there’s no need to use 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!

list restored with coroutines

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.

player details broken

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:

  1. Adding an observer to the getPlayer(playerListItem).
  2. Updating the local Player with the observer player item it.
  3. 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.

player details working

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.