本文翻譯自 Florina Muntenescu 的 Room 🔗 Coroutines 。介紹了 Google 官方對 Room 提供了原生的 Coroutines 支持。 從 Room v2.1.0 開始,咱們可使用 suspend
標記 DAO 中的函數,確保在非主線程中操做數據庫。html
在 build.gradle
中添加依賴庫:(最新版本能夠在官方更新文檔中查看)java
implementation "androidx.room:room-ktx:${versions.room}"
複製代碼
須要使用 Kotlin 1.3.0+ 和 Coroutines 1.0.0+。android
而後咱們能夠在 DAO 中使用 suspend
函數:git
@Dao
interface UsersDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<User>
@Query("UPDATE users SET age = age + 1 WHERE userId = :userId")
suspend fun incrementUserAge(userId: String)
@Insert
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
複製代碼
Transaction 方法也能夠被 suspend
標記,而且能夠調用其它的 suspend
函數。github
@Dao
abstract class UsersDao {
@Transaction
open suspend fun setLoggedInUser(loggedInUser: User) {
deleteUser(loggedInUser)
insertUser(loggedInUser)
}
@Query("DELETE FROM users")
abstract fun deleteUser(user: User)
@Insert
abstract suspend fun insertUser(user: User)
}
複製代碼
咱們也能夠在一個 transaction 內調用不一樣 DAO 中的 suspend
函數。數據庫
class Repository(val database: MyDatabase) {
suspend fun clearData(){
database.withTransaction {
database.userDao().deleteLoggedInUser() // suspend function
database.commentsDao().deleteComments() // suspend function
}
}
}
複製代碼
此外,咱們能夠在構建 database 時經過調用 setTransactionExecutor 或 setQueryExecutor 傳入指定的 Executor
來控制這些 suspend
函數的協程調度器。若是不設置,它們默認都會在 query
操做執行的子線程中。ide
注意:suspend
不能與 RxJava
或 LiveData
共用。 所以下面的寫法會在編譯期報錯。函數
@Dao
interface UsersDao {
@Query("SELECT * FROM users")
suspend fun getUsersWithFlowable(): Flowable<List<User>>
@Query("SELECT * FROM users")
suspend fun getUsersWithLiveData(): LiveData<List<User>>
}
複製代碼
DAO 中的 suspend
函數的測試與其它的 suspend
測試沒什麼不一樣。舉個例子,下面咱們測試 insert
一條 User 數據,而後驗證是否能 query
相同的 User。咱們能夠藉助 runBlocking
進行測試:源碼分析
@Test fun insertAndGetUser() = runBlocking {
// Given a User that has been inserted into the DB
userDao.insertUser(user)
// When getting the Users via the DAO
val usersFromDb = userDao.getUsers()
// Then the retrieved Users matches the original user object
assertEquals(listOf(user), userFromDb)
}
複製代碼
咱們知道,Room 編譯器會爲咱們自動生成 DAO 的默認實現,下面就來看一下 suspend
函數和普通函數生成的代碼有什麼區別。咱們首先定義這兩個函數,以下:測試
@Insert
fun insertUserSync(user: User)
@Insert
suspend fun insertUser(user: User)
複製代碼
普通函數 insertUserSync
生成的代碼以下所示:
@Override
public void insertUserSync(final User user) {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
複製代碼
能夠看到,它的實現裏首先開啓了一個事物(transaction
),而後執行 insert
操做,並將事務置爲成功,最後關閉事務。
下面咱們再來看下 suspend
函數的實現:
@Override
public Object insertUserSuspend(final User user, final Continuation<? super Unit> p1) {
return CoroutinesRoom.execute(__db, new Callable<Unit>() {
@Override
public Unit call() throws Exception {
__db.beginTransaction();
try {
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
return kotlin.Unit.INSTANCE;
} finally {
__db.endTransaction();
}
}
}, p1);
}
複製代碼
如上所示,suspend
函數內部經過 Callable
包裝了與普通 insert
函數同樣的邏輯。不一樣的是,調用了一個 suspend
函數 -- CoroutinesRoom.execute
,它內部切換到子線程來執行。 咱們來看看 CoroutinesRoom
的源碼:
class CoroutinesRoom private constructor() {
companion object {
@JvmStatic
suspend fun <R> execute( db: RoomDatabase, inTransaction: Boolean, callable: Callable<R> ): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
// Use the transaction dispatcher if we are on a transaction coroutine, otherwise
// use the database dispatchers.
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
}
}
/** * Gets the query coroutine dispatcher. * * @hide */
internal val RoomDatabase.queryDispatcher: CoroutineDispatcher
get() = backingFieldMap.getOrPut("QueryDispatcher") {
queryExecutor.asCoroutineDispatcher()
} as CoroutineDispatcher
/** * Gets the transaction coroutine dispatcher. * * @hide */
internal val RoomDatabase.transactionDispatcher: CoroutineDispatcher
get() = backingFieldMap.getOrPut("TransactionDispatcher") {
queryExecutor.asCoroutineDispatcher()
} as CoroutineDispatcher
複製代碼
查看源碼可知:
Callable#call()
函數執行 insert
操做。Callable#call()
中的操做要在子線程中執行。Room 會使用不一樣的協程調度器執行 transaction
和 query
。咱們能夠在構建 Database 時使用 setTransactionExecutor 或 setQueryExecutor 配置;若不配置默認會使用 Architecture Components
提供的 IO 線程。這個線程也是 LiveData
執行後臺任務的線程。完整的源碼參見:CoroutinesRoom.java 和 RoomDatabase.kt
盡情的在 Room 中使用 Coroutines 吧,如今已是 Release 版本了,它能夠內部保證數據庫操做運行在非 UI 調度器,使用 suspend
能夠像同步調用同樣完成數據庫的讀寫。
我是 xiaobailong24,您能夠經過如下平臺找到我: