前言
本文翻译自 Florina Muntenescu 的 Room 🔗 Coroutines 。介绍了 Google 官方对 Room 提供了原生的 Coroutines 支持。
从 Room v2.1.0 开始,我们可以使用 suspend
标记 DAO 中的函数,确保在非主线程中操作数据库。
如何使用?
在 build.gradle
中添加依赖库:(最新版本可以在官方更新文档中查看)
implementation "androidx.room:room-ktx:${versions.room}"
需要使用 Kotlin 1.3.0+ 和 Coroutines 1.0.0+。
然后我们可以在 DAO 中使用 suspend
函数:
@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
函数。
@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
操作执行的子线程中。
注意: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
操作。 - 非上述情况时:Room 要确保
Callable#call()
中的操作要在子线程中执行。Room 会使用不同的协程调度器执行transaction
和query
。我们可以在构建 Database 时使用 setTransactionExecutor 或 setQueryExecutor 配置;若不配置默认会使用Architecture Components
提供的 IO 线程。这个线程也是LiveData
执行后台任务的线程。
完整的源码参见:CoroutinesRoom.java 和 RoomDatabase.kt
尽情的在 Room 中使用 Coroutines 吧,现在已经是 Release 版本了,它可以内部保证数据库操作运行在非 UI 调度器,使用 suspend
可以像同步调用一样完成数据库的读写。
Reference
- 【Official】Room Release Notes
- 【Official】Room
- 【Medium】Room 🔗 Coroutines
- 【GitHub】Kotlin/kotlinx.coroutines
联系
我是 xiaobailong24,您可以通过以下平台找到我: