在Room2.1版本中提供了对协程的支持。Dao层的方法可以被suspend标记来确保他们在主线程中被执行。接下来,我们就来看看如何使用并为它写一个简单的单元测试。
为你的数据库加点suspending
首先我们要为项目加上Room的依赖,并确保版本在2.1及以上。
implementation "androidx.room:room-coroutines:${versions.room}"
同时,我们的还需要Kotlin版本在1.3.0以上,以及Coroutines 1.0.0以上。
接下来我们可以编写如下的DAO层,使用suspend标记方法。
@Dao
interface UsersDao{
@Query
suspend fun getUsers(): List
@Query
suspend fun incrementUserAge(userId: String)
@Insert
suspend fun insertUser(user: User)
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
}
被@Transacition注解的方法也可以使用suspend关键字标记,并且可以调用其他被suspend标记的DAO层方法。
@Dao
abstract class UsersDao{
@Transaction
open suspend fun setLoggedInUser(loggerInUser: User){
deleteUser(loggedInUser)
insertUser(loggedInUser)
}
@Query("DELETE FROM users")
abstract fun deleteUser(user: User)
@Insert
abstract suspend fun insertUser(user: User)
}
同样,你也可以从不同的DAO中调用suspend方法。
class Repository(val database: MyDatabase) {
suspend fun clearData(){
database.withTransaction {
database.userDao().deleteLoggerInUser()
database.commentsDao().deleteComments()
}
}
}
你可以在创建数据库时通过调用setTransactionExecutor()方法或setQueryExecutor()方法来提供executors去控制它们运行的线程。默认情况下,这将是用于在后台线程上运行查询的相同执行程序。
编写单元测试
测试DAO suspend方法和测试其它的suspend方法是一样的。例如,要检查插入用户后我们能够检索它,我们将测试包装在runBlocking块中:
@Test
fun insertAndGetUser() = runBlocking {
//提供一个插入到数据库中的User
userDao.insertUser(user)
// 通过DAO获取Users
val usersFromDb = userDao.getUsers()
//验证
assertEquals(listOf(user), userFromDb)
}
再深入一点去看
进一步,让我们来看看为同步和暂停插入生成的DAO类实现:
@Insert
fun insertUserSync(user: User)
@Insert
suspend fun insertUser(user: User)
在同步插入的方式中,生成的代码启动事务,执行插入,将事务标记为成功并结束它。同步方法只在任何调用它的线程上的执行insert。
@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 p1){
return CoroutinesRoom.execute(_db,new Callable(){
@Override
public Unit call() throws Exception {
_db.beginTransaction();
try {
_insertionAdapterOfUser.insert(user);
_db.setTransactionSuccessful();
return kotlin.Unit.INSTANCE;
} finally {
_db.endTransaction();
}
}
}, p1);
}
生成的代码中确保了插入不发生在UI线程上。在我们的suspend函数实现中,同步insert方法中的相同逻辑包含在Callable
中。Room调用CoroutinesRoom.execute
挂起函数,该函数切换到后台调度程序,具体取决于数据库是否已打开且我们是否处于事务中。这是函数的实现:
@JvmStatic
suspend fun execute(
db: RoomDatabase,
inTransaction: Boolean,
callable: Callable
): R {
if (db.isOpen && db.inTransaction) {
return callable.call()
}
val context = coroutineContext[TransactionElement]?.transactionDispatcher
?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
return withContext(context) {
callable.call()
}
}
情形一:数据库打开,且我们在事务中
此时我们直接执行callable即可
情形二:其他
Room确保Callable中的工作已完成(call方法在后台执行)。
Room会使用不同的dispatcher来处理事务和查询。这些是从构建数据库时提供的执行程序派生的,或者默认情况下将使用系统组件IO执行程序,这和LiveData执行后台任务的executor是一样的。
在应用程序中开始使用Room和coroutines,保证数据库工作在非UI Dispatcher上运行。使用suspend修饰符标记您的DAO方法,并从其他挂起函数或协程中调用它们!