Écrire des requêtes DAO asynchrones

Pour empêcher les requêtes de bloquer l'interface utilisateur, Room ne permet pas d'accéder à la base de données sur le thread principal. Cette restriction signifie que vous devez rendre vos requêtes DAO asynchrones. La bibliothèque Room inclut des intégrations avec plusieurs frameworks pour permettre l'exécution de requêtes asynchrones.

Les requêtes DAO appartiennent à trois catégories :

  • Requêtes d'écriture unique qui insèrent, mettent à jour ou suppriment des données dans la base de données.
  • Requêtes de lecture unique qui lisent les données de votre base de données une seule fois et renvoient un résultat avec l'instantané de la base de données à ce moment-là.
  • Requêtes de lecture observables qui lisent les données de votre base de données chaque fois que les tables de base de données sous-jacentes changent et émettent de nouvelles valeurs reflétant ces modifications.

Options de langage et de framework

Room permet l'interopérabilité avec des bibliothèques et des fonctionnalités de langage spécifiques. Le tableau suivant présente les types renvoyés applicables en fonction du type de requête et du framework :

Type de requête Fonctionnalités du langage Kotlin (natif) RxJava Guava Jetpack Lifecycle*
Écriture unique Coroutines (suspend) Single<T>, Maybe<T>, Completable ListenableFuture<T> N/A
Lecture unique Coroutines (suspend) Single<T>, Maybe<T> ListenableFuture<T> N/A
Lecture observable Flow<T> Flowable<T>, Publisher<T>, Observable<T> N/A LiveData<T>

Ce guide présente trois façons d'utiliser ces intégrations pour implémenter des requêtes asynchrones dans vos DAO.

Kotlin avec Flow et coroutines

Kotlin fournit des fonctionnalités de langage intégrées qui vous permettent d'écrire des requêtes asynchrones sans frameworks tiers :

  • Room est directement compatible avec Flow de Kotlin pour écrire des requêtes observables.
  • Room nécessite le mot clé suspend pour rendre vos requêtes DAO uniques asynchrones avec les coroutines Kotlin.

La compatibilité avec les coroutines et Flow est intégrée directement dans l'environnement d'exécution Room de base. Aucun artefact supplémentaire n'est donc requis.

RxJava pour Kotlin et Java

Room 3.0 est compatible avec les types renvoyés RxJava 3. Pour utiliser les types renvoyés RxJava, vous devez enregistrer les convertisseurs de types renvoyés RxJava dans votre base de données ou votre DAO :

  1. Incluez l'artefact androidx.room3:room3-rxjava3 dans votre configuration de compilation.
  2. Annotez votre @Database ou @Dao déclaration avec @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class).

Room est compatible avec les types renvoyés RxJava 3 suivants :

LiveData et Guava

Room 3.0 est compatible avec les types renvoyés LiveData et Guava ListenableFuture à l'aide de convertisseurs :

  • LiveData : incluez l'artefact androidx.room3:room3-livedata et annotez votre base de données ou votre DAO avec @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class).
  • Guava : incluez l'artefact androidx.room3:room3-guava et annotez votre base de données ou votre DAO avec @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class).

Écrire des requêtes uniques asynchrones

Les requêtes uniques sont des opérations de base de données qui ne s'exécutent qu'une seule fois et récupèrent un instantané des données au moment de l'exécution. Voici quelques exemples de requêtes uniques asynchrones :

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

Écrire des requêtes observables

Les requêtes observables sont des opérations de lecture qui émettent de nouvelles valeurs chaque fois que les tables référencées changent. Vous pouvez, par exemple, utiliser ce comportement pour maintenir à jour la liste des éléments affichés à mesure que la base de données change. Voici quelques exemples de requêtes observables :

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

Effectuer manuellement le suivi de l'invalidation de la base de données

Lorsque vous devez créer manuellement des opérations de base de données observables, vous pouvez utiliser l' createFlow API de InvalidationTracker. Cette API vous permet de créer un Flow qui suit les modifications apportées à des tables spécifiques et émet une notification chaque fois que ces tables changent.

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

Par défaut, le Flow renvoyé émet une valeur initiale contenant toutes les tables enregistrées pour lancer le flux. Vous pouvez désactiver ce comportement en définissant le paramètre emitInitialState sur false.

Convertisseurs de types renvoyés DAO personnalisés

Pour les types qui ne sont pas directement compatibles avec Room ou ses bibliothèques d'extension, vous pouvez définir des convertisseurs de types renvoyés DAO personnalisés pour prendre en charge des types renvoyés supplémentaires. Pour transformer le résultat d'une fonction DAO en type personnalisé, annotez une fonction de convertisseur avec @DaoReturnTypeConverter.

Par exemple, vous pouvez définir un convertisseur qui utilise androidx.tracing pour ajouter des sections de trace autour de l'exécution d'une requête afin de surveiller les requêtes sensibles aux performances en encapsulant l'exécution dans un type TracedQuery personnalisé :

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

Pour utiliser le convertisseur, annotez votre base de données ou votre DAO avec @DaoReturnTypeConverters :

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

Contrôler l'initialisation du convertisseur de type renvoyé DAO

Généralement, Room gère l'instanciation des convertisseurs de types renvoyés DAO. Toutefois, si vous devez transmettre des dépendances supplémentaires à vos classes de convertisseur, votre application doit contrôler directement leur initialisation. Dans ce cas, annotez la classe de vos convertisseurs avec @ProvidedDaoReturnTypeConverter :

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

Ensuite, en plus de déclarer la classe de vos convertisseurs dans @DaoReturnTypeConverters, utilisez la RoomDatabase.Builder.addDaoReturnTypeConverter fonction pour transmettre une instance de cette classe à RoomDatabase :

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

Conditions requises pour la fonction de convertisseur

Une fonction @DaoReturnTypeConverter doit répondre à plusieurs exigences :

  • Elle doit avoir un paramètre fonctionnel comme dernier argument, généralement nommé executeAndConvert. Ce paramètre est un lambda suspend que Room génère pour exécuter la requête et analyser le résultat.
    • Si le convertisseur doit transformer la requête, par exemple la pagination, le lambda peut accepter un paramètre RoomRawQuery.
  • Il peut éventuellement accepter les paramètres suivants avant le lambda :
    • db: RoomDatabase: accède à l'instance de base de données, ce qui est utile pour obtenir le champ d'application de la coroutine ou effectuer des opérations supplémentaires.
    • tableNames: Array<String> ou List<String> : fournit les noms des tables auxquelles la requête accède, ce qui est utile pour les types observables.
    • rawQuery: RoomRawQuery: fournit l'instance d'exécution de la requête.
    • inTransaction: Boolean: indique si la requête s'exécute dans une transaction.