内容简介:In this tutorial, you’ll learn how to: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
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.
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.
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!
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.
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) }
-
Here, you added a new method to update the
-
viewModelScope
to calllaunch
, a coroutine builder method -
This, in turn, calls
updatePlayer(player: Player)
within a coroutine.
Player
. This method uses 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:
-
Attaching
OnCheckedChangeListener
to the checkbox starMenuItem
. - Assigning the player-favorite property to the checkbox checked value.
-
Calling
updatePlayer(player)
from ViewModel. -
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.
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.
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!
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Java并发编程实战
Brian Goetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes、Doug Lea / 童云兰 / 机械工业出版社华章公司 / 2012-2 / 69.00元
本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。书中从并发性和线程安全性的基本概念出发,介绍了如何使用类库提供的基本并发构建块,用于避免并发危险、构造线程安全的类及验证线程安全的规则,如何将小的线程安全类组合成更大的线程安全类,如何利用线程来提高并发应用程序的吞吐量,如何识别可并行执行的任务,如何提高单线程子系统的响应性,如何确保并发程序执行预期任务,如何提高并发代码的性......一起来看看 《Java并发编程实战》 这本书的介绍吧!