Room 2.1(目前为 alpha 版本)添加了对 Kotlin 协程的支持。DAO 方法现在可以被标记为挂起以确保他们不会在主线程执行。默认情况下,Room 会使用架构组件 I/O Executor
作为 Dispatcher
来执行 SQL 语句,但在构建 RoomDatabase
的时候你也可以提供自己的 Executor
。请继续阅读以了解如何使用它、引擎内部的工作原理以及如何测试该项新功能。
目前,Coroutines 对 Room 的支持正在大力开发中,该库的未来版本中将会增加更多的特性。
为了在你的 app 中使用协程和 Room,需将 Room 升级为 2.1 版本并在 build.gradle
文件中添加新的依赖:
implementation "androidx.room:room-coroutines:${versions.room}"
复制代码
你还需要 Kotlin 1.3.0 和 Coroutines 1.0.0 及以上版本。
现在,你可以更新 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)
}
具有 suspend
方法的 DAO
@Transaction
方法也可以挂起,并且可以调用其他挂起的 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
Room 会根据是否在事务内调用挂起方法进行区别对待:
1. 事务内
Room 不会对触发数据库语句的协程上下文(CoroutineContext)做任何处理。方法调用者有责任确保当前不是在 UI 线程。由于 suspend
方法只能在其他 suspend
方法或协程中调用,因此需确保你使用的 Dispatcher
是 Dispatchers.IO
或自定义的,而不是 Dispatcher.Main
。
2. 事务外
Room 会确保数据库语句是在架构组件 I/O Dispatcher
上被触发。该 Dispatcher
是基于使处于后台工作的 LiveData
运行起来的同一 I/O Executor
而创建的。
测试 DAO 的挂起方法与测试其他挂起方法一般无二。例如,为了测试在插入一个用户后我们还可以取到它,我们将测试代码包含在一个 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)
}
测试 DAO 的挂起方法
为了能够了解原理,让我们看一下 Room 为同步的和挂起的插入方法生成的 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();
}
}
Room 对同步插入生成的实现代码
再看一下添加 suspend 修饰符后发生的变化:生成的代码会确保数据在非 UI 线程上被插入。
生成的代码传入了一个 continution 和待插入的数据。使用了和同步插入方法相同的逻辑,不同的是它在一个 Callable#call
方法中执行。
@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);
}
Room 对挂起插入生成的实现代码
不过有趣的是 CoroutinesRoom.execute
方法,这是一个根据数据库是否打开以及是否处于事务内来处理上下文切换的方法。
情形 1. 数据库被打开同时处于事务内
这种情况下只触发了 call 方法,即用户在数据库中的实际插入操作
情形 2. 非事务
Room 通过架构组件 IO Executor
来确保 Callable#call
中的操作是在后台线程中完成的。
suspend fun <R> execute(db: RoomDatabase, callable: Callable<R>): R {
if (db.isOpen && db.inTransaction()) {
return callable.call()
}
return withContext(db.queryExecutor.asCoroutineDispatcher()) {
callable.call()
}
}
CoroutinesRoom.execute 实现
现在就开始在你的 app 中使用 Room 和协程吧,保证数据库的操作在一个非 UI 分发器上执行。在 DAO 方法上添加 suspend
修饰符并在其他 supend 方法或者协程中调用。
感谢 Chris Banes 和 Jose Alcérreca。