Room e Coroutines

Tempo de leitura: 3 minutes

A Room 2.1 adiciona suporte para Coroutines Kotlin. Os métodos DAO agora podem ser marcados como suspensos para garantir que não sejam executados no thread principal. Continue lendo para ver como usar isso, como funciona nos bastidores e como testar essa nova funcionalidade.

 

Adicione um pouco de suspense ao seu banco de dados

Para usar coroutines e Room em seu aplicativo, atualize para Room 2.1 e adicione a nova dependência ao seu arquivo build.gradle:

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

Você também precisará do Kotlin 1.3.0 e do Coroutines 1.0.0 ou mais recente.

Agora você pode atualizar seus métodos DAO para usar funções de suspensão:

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

}

Os métodos @Transaction também podem ser suspensos e podem chamar outras funções DAO suspensas:

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

Você também pode chamar funções de suspensão de diferentes DAOs dentro de uma transação:

class Repository(val database: MyDatabase) {

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

Você pode fornecer executores (chamando setTransactionExecutor ou setQueryExecutor quando estiver construindo seu banco de dados) para controlar os threads em que são executados. Por padrão, este será o mesmo executor usado para executar consultas no thread de segundo plano.

 

Testando funções de suspensão DAO

Testar uma função de suspensão DAO não é diferente de testar qualquer outra função de suspensão. Por exemplo, para verificar se, após inserir um usuário, podemos recuperá-lo, envolvemos o teste em um bloco 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)
}

 

Sob o capô

Para ver o que está por baixo do capô, vamos dar uma olhada na implementação da classe DAO que a Room gera para uma inserção síncrona e suspensa:

@Insert
fun insertUserSync(user: User)

@Insert
suspend fun insertUser(user: User)

Para a inserção síncrona, o código gerado inicia uma transação, executa a inserção, marca a transação como bem-sucedida e a termina. O método síncrono apenas executará a inserção em qualquer thread de onde for chamado.

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

Agora vamos ver como adicionar o modificador de suspensão muda as coisas:

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

O código gerado garante que a inserção aconteça fora do thread de IU. Em nossa implementação da função de suspensão, a mesma lógica do método de inserção síncrona é envolvida em um `Callable`. Room chama a função de suspensão `CoroutinesRoom.execute`, que muda para um despachante em segundo plano, dependendo se o banco de dados está aberto e estamos em uma transação ou não. Aqui está a implementação da função:

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

 

Caso 1. O banco de dados é aberto e estamos em uma transação

Aqui, apenas executamos imediatamente o chamável – ou seja, a inserção real do usuário no banco de dados

Caso 2. Caso contrário

A Room certifica-se de que o trabalho feito no método Callable # call seja executado em um thread de segundo plano.

A Room usará diferentes despachantes para transações e consultas. Eles são derivados dos executores que você fornece ao construir seu banco de dados ou, por padrão, usarão o executor de E / S dos componentes de arquitetura. Este é o mesmo executor que seria usado pelo LiveData para fazer o trabalho em segundo plano.

Se você estiver interessado em verificar a implementação, consulte CoroutinesRoom.java e RoomDatabase.kt

 

Comece a usar Room e Coroutines em seu aplicativo, o trabalho de banco de dados será executado em um Dispatcher não-UI. Marque seu método DAO com o modificador de suspend e chame-o de outras funções de suspensão ou Coroutines!