天天看點

【譯】Room Coroutines

原文連結:https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5

作者:Florina Muntenescu

Room

從2.1版本(目前已更新到2.2.0-alpha03版本)開始添加了對

kotlin

協程的支援。現在我們可以使用

suspend

關鍵字将

DAO

中的方法聲明為挂起函數,進而保證這些方法不在主線程中執行。請繼續閱讀以了解如何在

Room

中使用協程,它的工作原理,以及如何測試這個新功能。

給你的資料庫添加suspend特性

如果想在你的APP中使用協程來操作

Room資料庫

,那麼必須将項目中的

Room

版本更新為2.1版本,同時在

build.gradle

檔案添加如下依賴:

implementation "androidx.room:room-coroutines:${versions.room}"
           

另外你的

kotlin

版本至少為

1.3.0

Coroutines

版本至少為

1.0.0

現在你可以将

DAO

中的方法使用

suspend

關鍵字将其定義為挂起函數了

//具有suspend方法的DAO代碼示例
@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

聲明的方法也可以定義為挂起函數,同時它也可以調用

DAO

中其他的挂起函數

//具有事務挂起功能的DAO代碼示例
@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)
}
           

你也可以在事務中調用不同

DAO

的挂起函數

//在一個事務中調用兩個不同DAO的挂起函數
class Repository(val database: MyDatabase) {

    suspend fun clearData(){
        database.withTransaction {
            database.userDao().deleteLoggedInUser() // suspend function
            database.commentsDao().deleteComments() // suspend function
        }
    }    
}
           

預設情況

Room

會使用架構元件的

IO Executor

來執行SQL語句,但我們也可以在建構

Room

資料庫的時候,通過調用

setTransactionExecutor

setQueryExecutor

這兩個方法來自定義執行SQL語句的

Executor

測試DAO中的挂起函數

測試

DAO

中的挂起函數和測試其他普通的挂起函數沒有什麼不同。

例如我們要測試使用者資訊被插入後能夠被檢索到,我們可以将測試代碼塊放在

runBlock

塊中:

@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

支援協程的實作原理,我們來看看

Room

為同步方法和挂起函數生成的

DAO

方法實作

//DAO中同步和挂起函數的定義插入操作

@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)
           

對于同步插入,生成的代碼首先開啟了一個事務,接着執行資料插入操作,然後标記事務成功,最後結束事務。生成的代碼如下:

@Override
public void insertUserSync(final User user) {
  __db.beginTransaction();
  try {
    __insertionAdapterOfUser.insert(user);
    __db.setTransactionSuccessful();
  } finally {
    __db.endTransaction();
  }
}
           

從上面的代碼也可以看出來在任何線程中調用同步插入方法都會在該線程中直接執行插入操作。

接下來讓我們來看看使用了

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);
}
           

上面的生成代碼確定了插入操作不會在

UI線程

中執行。

在生成的

suspend

函數的中,傳入了一個

Continuation

和待插入的資料,同時它的插入邏輯和同步插入邏輯相同,隻是它的插入邏輯被封裝在

Callable

中。

另外我們可以看到,生成的函數一開始會調用

CoroutinesRoom.execute

函數,實際上該函數會根據資料庫是否打開,是否處于事務來決定如何切換上下文。

CoroutinesRoom.execute

方法實作如下:

@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()
   }
}
           

情況1:資料庫打開且處于事務中

這種情況會直接執行

Callable.call()

方法,也就是資料庫的實際插入操作。

這種情況下

Room

不會對操作資料庫的協程上下文做任何處理,是以調用者需要自己確定調用

Room

操作的協程上下文環境不是

Dispatcher.Main

情況2:非事務

這種情況下

Room

會確定

Callable.call()

操作是在背景線程中完成的。

對于事務和查詢

Room

會采用不同的

Dispatchers

,這些

Dispatchers

來源于建構

Room資料庫

時我們自己自定義的

Dispatchers

,或者是系統預設提供的架構元件

IO Executor

,這和使

LiveData

處于背景運作的

Dispatchers

是一樣的。

如果有興趣研究具體的實作原理,可以檢視CoroutinesRoom.kt和RoomDatabase.kt的源碼

在你的APP中使用

Room

和協程吧,保證資料庫的操作在

non-UI Dispatcher

中執行。使用

suspend

DAO

中的方法定義為挂起函數,并從其他挂起函數或協程中調用他們。

下面是我的個人公衆号,歡迎關注交流

【譯】Room Coroutines