6

Coroutines With Room Persistence Library [FREE]

 4 years ago
source link: https://www.raywenderlich.com/7414647-coroutines-with-room-persistence-library
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.

Screenshot_20200103-140038-243x500.png

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.

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

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-detail-empty-gif.gif

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-detail-gif.gif

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.

Updating a Player

To update a tennis player, use the same approach you took in the previous steps. There will again be a series of steps to complete the feature, but it won’t be so difficult this time. You already laid the groundwork after all.

Adding Update to the Dao

To begin, open the PlayerDoa.kt file and add the updatePlayer() as shown below:

@Update
suspend fun updatePlayer(player: Player)

Here, you annotate with @Update to tell Room that updatePlayer() DAO method will perform update operations. The suspend keyword in the method signature lets Room know this DAO method will suspend its execution.

Adding Update to the Repository

Next, open the PlayerRespository.kt file and add the following code:

suspend fun updatePlayer(player: Player) {
  playerDao.updatePlayer(player)
}

Here you wrapped the DAO’s functionality to update a Player with another suspension function.

Note : This step is necessary because suspending functions can only execute from other suspending functions or from within a coroutine.

Adding Update to the ViewModel

Time to update the DetailViewModel.kt file to run the repository’s updatePlayer(player: Player) within a coroutine. First, add the following method to the end of DetailViewModel.kt file:

// 1
fun updatePlayer(player: Player) = viewModelScope.launch {
  // 2
  repository.updatePlayer(player)
}
  1. Here, you added a new method to update the
Player . This method uses viewModelScope to call launch , a coroutine builder method This, in turn, calls updatePlayer(player: Player) within a coroutine.

Adding Update to the UI

Next up, you will setup the MenuItem in the Toolbar to update the selection of favorite player in the database. For simplicity, you will make changes only in the setupFavoriteToggle() function.

Open the DetailsFragment.kt file and replace the TODO in setupFavoriteToggle(checkBox: CheckBox, player : Player) with the following:

// 1
checkBox.setOnCheckedChangeListener { _, b ->
  // 2
  player.favorite = b
  // 3
  detailViewModel.updatePlayer(player)
}
// 4
checkBox.isChecked = player.favorite

Here you are:

  1. Attaching OnCheckedChangeListener to the checkbox star MenuItem .
  2. Assigning the player-favorite property to the checkbox checked value.
  3. Calling updatePlayer(player) from ViewModel.
  4. Handling the initial value of checkBox.isChecked .

Now, you need to call this setupFavoriteToggle method from the observer up in onViewCreated() .

Add the call to setupFavoriteToggle , just above displayPlayer() in the observer definition as shown below:

detailViewModel.getPlayer(playerListItem).observe(this, Observer {
  this.player = it

  setupFavoriteToggle(checkbox, it) // called the method here 

  displayPlayer()
})

Here, you’re calling the method that sets up the checkbox toggle for favorites.

Great Job! Now when you navigate away from the DetailsFragment you will see a filled-in star next to the Player within the list and when you navigate back to DetailsFragment the star menu item will highlight as well. Best of all, this is all happening by calling to update the player within the database using the DAO’s suspending function and the ViewModelScope ‘s launch method to run the operation in a coroutine.

Build and run the application to observe the new behavior. You can now favorite your favorite players.

favorite-gif.gif

Deleting a Player

Users may also want to remove players from the application. In this step, you’ll add a delete-player feature for just such an occasion.

Adding Delete to the DAO

To begin, open the PlayerDoa.kt file and add the deletePlayer() method call:

@Delete
suspend fun deletePlayer(player: Player)

This should look familiar. You’ve added another suspending function to the DAO and provided @Delete annotation so that Room knows this method will perform delete operations.

Adding Delete to the Repository

Now, it’s time to update the repository. Open the PlayerRepository.kt file and add the following code:

suspend fun deletePlayer(player: Player) {
  playerDao.deletePlayer(player)
}

Here, you added another suspending function that uses PlayerDao to call the deletePlayer() method from the previous step.

Adding Delete to the ViewModel

You’ll have to update DetailsViewModel again to call the deletePlayer() method from repository . Open the DetailsViewModel.kt file and add the following method:

// 1
fun deletePlayer(player: Player) = viewModelScope.launch {
  // 2
  repository.deletePlayer(player)
}

Here, you’re doing much the same as the update method call from the previous feature implementation. You use viewModelScope to call launch and run the operation in a coroutine.

Wow, you are becoming a pro at this!

Adding Delete to the UI

In this last step, you’ll add delete behavior to the UI. Open the DetailsFragment.kt file one more time and add the following code inside deleteCurrentPlayer() replacing the TODO:

detailViewModel.deletePlayer(player)
dismiss()

Here, you invoked deletePlayer(player) on the detailViewModel and dismissed DetailsFragment .

Build and run the application to observe the new behavior. You can now delete any players you’d like.

delete-gif.gif

Where to Go From Here?

Great Job! You’re now a Suspension-Function-Room-Database-Storing Pro! If you had any difficulty following along, no worries. Just import the TennisPlayers-final version of the application and compare your code to it.

If you enjoyed working with LiveData, you may discover you enjoy the new Flow addition from the release of 2.2. For more information on Flow, check out the developer’s release notes .

If you’re looking for Room or Coroutine content, read Room DB: Advanced Data Persistence or Kotlin Coroutines Tutorial for Android: Advanced to learn more.

Thank you for reading! If you have any questions or comments, please join the forum discussion below!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK