Criar consultas DAO assíncronas

Para impedir que as consultas bloqueiem a interface, o Room não oferece suporte ao acesso ao banco de dados na linha de execução principal. Devido a essa restrição, é necessário fazer com que as consultas DAO assíncronas. A biblioteca do Room inclui integrações com vários frameworks para oferecer a execução de consulta assíncrona.

As consultas DAO se enquadram em três categorias:

  • Consultas de gravação única, que inserem, atualizam ou excluem dados do banco de dados.
  • Consultas de leitura única, que leem dados do banco de dados apenas uma vez e retornam um resultado com um snapshot do banco de dados naquele momento.
  • Consultas de leitura observável, que leem dados do banco de dados sempre que as tabelas subjacentes mudam e emitem novos valores para refletir essas mudanças.

Opções de linguagem e framework

O Room oferece suporte de integração para interoperabilidade com bibliotecas e recursos de linguagem específicos. A tabela abaixo mostra os tipos de retorno aplicáveis de acordo com o tipo de consulta e framework:

Tipo de consulta Recursos da linguagem Kotlin (nativo) RxJava Goiaba Ciclo de vida do Jetpack*
Gravação única Corrotinas (suspend) Single<T>, Maybe<T>, Completable ListenableFuture<T> N/A
Leitura única Corrotinas (suspend) Single<T>, Maybe<T> ListenableFuture<T> N/A
Leitura observável Flow<T> Flowable<T>, Publisher<T>, Observable<T> N/A LiveData<T>

Este guia demonstra três maneiras de usar essas integrações para implementar consultas assíncronas nos DAOs.

Kotlin com fluxo e corrotinas

O Kotlin oferece recursos de linguagem integrados que permitem criar consultas assíncronas sem frameworks de terceiros:

O suporte a corrotinas e fluxo é integrado diretamente ao ambiente de execução principal do Room, portanto, nenhum artefato adicional é necessário.

RxJava para Kotlin e Java

O Room 3.0 oferece suporte a tipos de retorno RxJava 3. Para usar os tipos de retorno RxJava, registre os conversores de tipo de retorno RxJava no banco de dados ou DAO:

  1. Inclua o artefato androidx.room3:room3-rxjava3 na configuração de build.
  2. Adicione a anotação @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class) à declaração @Database ou @Dao.

O Room oferece suporte a estes tipos de retorno RxJava 3:

LiveData e Guava

O Room 3.0 oferece suporte a tipos de retorno LiveData e Guava ListenableFuture usando conversores:

  • LiveData: inclua o artefato androidx.room3:room3-livedata e adicione a anotação @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class) ao banco de dados ou DAO.
  • Guava: inclua o artefato androidx.room3:room3-guava e adicione a anotação ao banco de dados ou DAO com @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class).

Criar consultas assíncronas únicas

Consultas únicas são operações de banco de dados que são executadas apenas uma vez e capturam um snapshot dos dados durante a execução. Veja abaixo alguns exemplos de consultas assíncronas únicas:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    suspend fun loadUserById(id: Int): User

    @Query("SELECT * from user WHERE region IN (:regions)")
    suspend fun loadUsersByRegion(regions: List<String>): List<User>
}

Criar consultas observáveis

Consultas observáveis são operações de leitura que emitem novos valores sempre que as tabelas referenciadas mudam. Por exemplo, é possível usar esse comportamento para manter uma lista de itens exibida atualizada à medida que o banco de dados muda. Veja abaixo alguns exemplos de consultas observáveis:

@Dao
interface ObservableUserDao {
    @Query("SELECT * FROM user WHERE id = :id")
    fun loadUserById(id: Int): Flow<User>

    @Query("SELECT * from user WHERE region IN (:regions)")
    fun loadUsersByRegion(regions: List<String>): Flow<List<User>>
}

Rastrear a invalidação do banco de dados manualmente

Quando você precisa criar operações de banco de dados observáveis manualmente, é possível usar a createFlow API de InvalidationTracker. Essa API permite criar um Flow que rastreia modificações em tabelas específicas e emite uma notificação sempre que essas tabelas mudam.

fun getArtistTours(db: RoomDatabase, from: Date, to: Date): Flow<Map<Artist, TourState>> {
    return db.invalidationTracker.createFlow("Artist").map { _ ->
        val artists = artistsDao.getAllArtists()
        val tours = tourService.fetchStates(artists.map { it.id })
        associateTours(artists, tours, from, to)
    }
}

Por padrão, o Flow retornado emite um valor inicial contendo todas as tabelas registradas para iniciar o fluxo. É possível desativar esse comportamento definindo o parâmetro emitInitialState como false.

Conversores de tipo de retorno DAO personalizados

Para tipos que não são compatíveis diretamente com o Room ou as bibliotecas de extensão, é possível definir conversores de tipo de retorno DAO personalizados para oferecer suporte a outros tipos de retorno. Para transformar o resultado de uma função DAO no tipo personalizado, adicione a anotação @DaoReturnTypeConvertera uma função de conversor.

Por exemplo, é possível definir um conversor que usa androidx.tracing para adicionar seções de rastreamento em torno da execução de uma consulta para monitorar consultas sensíveis ao desempenho, encapsulando a execução em um tipo TracedQuery personalizado:

class TracedQuery<T>(val result: T)

object TracingDaoReturnTypeConverter {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

Para usar o conversor, adicione a anotação ao banco de dados ou DAO com @DaoReturnTypeConverters:

@Dao
@DaoReturnTypeConverters(TracingDaoReturnTypeConverter::class)
interface MusicDao {
    @Query("SELECT * FROM Song")
    suspend fun getAllSongs(): TracedQuery<List<Song>>
}

Controlar a inicialização do conversor de tipo de retorno DAO

Geralmente, o Room processa a instanciação de conversores de tipo de retorno DAO. No entanto, se você precisar transmitir outras dependências para as classes de conversores, o app precisará controlar diretamente a inicialização delas. Nesse caso, adicione a anotação @ProvidedDaoReturnTypeConverter à classe de conversor:

@ProvidedDaoReturnTypeConverter
class TracingDaoReturnTypeConverter(val tracer: Tracer) {
    @DaoReturnTypeConverter([OperationType.READ])
    suspend fun <T> convert(
        rawQuery: RoomRawQuery,
        executeAndConvert: suspend () -> T
    ): TracedQuery<T> {
        val result = tracer.trace("TracedQuery: ${rawQuery.sql}") {
            executeAndConvert()
        }
        return TracedQuery(result)
    }
}

Em seguida, além de declarar a classe de conversor em @DaoReturnTypeConverters, use a RoomDatabase.Builder.addDaoReturnTypeConverter função para transmitir uma instância da classe de conversor ao RoomDatabase builder:

val db = Room.databaseBuilder<MyDatabase>(applicationContext, "database-name")
    .addDaoReturnTypeConverter(TracingDaoReturnTypeConverter(myLoggerInstance))
    .build()

Requisitos da função de conversor

Uma função @DaoReturnTypeConverter precisa atender a vários requisitos:

  • Ela precisa ter um parâmetro funcional como último argumento, geralmente chamado de executeAndConvert. Esse parâmetro é uma lambda suspend que o Room gera para executar a consulta e analisar o resultado.
    • Se o conversor precisar transformar a consulta, como a paginação, a lambda poderá usar um parâmetro RoomRawQuery.
  • Ela pode aceitar opcionalmente os seguintes parâmetros antes da lambda:
    • db: RoomDatabase: acessa a instância do banco de dados, o que é útil para receber o escopo da corrotina ou realizar outras operações.
    • tableNames: Array<String> ou List<String>: fornece os nomes das tabelas acessadas pela consulta, o que é útil para tipos observáveis.
    • rawQuery: RoomRawQuery: fornece a instância de execução da consulta.
    • inTransaction: Boolean: indica se a consulta está sendo executada em uma transação.