Kotlin coroutines meeting Room

前言

本文翻譯自 Florina MuntenescuRoom 🔗 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 時經過調用 setTransactionExecutorsetQueryExecutor 傳入指定的 Executor 來控制這些 suspend 函數的協程調度器。若是不設置,它們默認都會在 query 操做執行的子線程中。ide

注意:suspend 不能與 RxJavaLiveData 共用。 所以下面的寫法會在編譯期報錯。函數

@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
複製代碼

查看源碼可知:

  1. 當數據庫被打開並正在執行事務時:會直接調用 Callable#call() 函數執行 insert 操做。
  2. 非上述狀況時:Room 要確保 Callable#call() 中的操做要在子線程中執行。Room 會使用不一樣的協程調度器執行 transactionquery。咱們能夠在構建 Database 時使用 setTransactionExecutorsetQueryExecutor 配置;若不配置默認會使用 Architecture Components 提供的 IO 線程。這個線程也是 LiveData 執行後臺任務的線程。

完整的源碼參見:CoroutinesRoom.javaRoomDatabase.kt

盡情的在 Room 中使用 Coroutines 吧,如今已是 Release 版本了,它能夠內部保證數據庫操做運行在非 UI 調度器,使用 suspend 能夠像同步調用同樣完成數據庫的讀寫。

Reference

聯繫

我是 xiaobailong24,您能夠經過如下平臺找到我:

相關文章
相關標籤/搜索