Scrivi query DAO asincrone

Per evitare che le query blocchino l'interfaccia utente, Room non supporta l'accesso al database sul thread principale. Questa limitazione significa che devi rendere asincrone le query DAO. La libreria Room include integrazioni con diversi framework per fornire l'esecuzione di query asincrone.

Le query DAO rientrano in tre categorie:

  • Query di scrittura one-shot che inseriscono, aggiornano o eliminano dati nel database.
  • Query di lettura one-shot che leggono i dati dal database una sola volta e restituiscono un risultato con lo snapshot del database in quel momento.
  • Query di lettura osservabile che leggono i dati dal database ogni volta che le tabelle di database sottostanti cambiano ed emettono nuovi valori per riflettere queste modifiche.

Opzioni di linguaggio e framework

Room fornisce il supporto per l'integrazione per l'interoperabilità con funzionalità e librerie di linguaggi specifici. La tabella seguente mostra i tipi di restituzione applicabili in base al tipo di query e al framework:

Tipo di query Funzionalità del linguaggio Kotlin (nativo) RxJava Guava Jetpack Lifecycle*
Scrittura one-shot Coroutine (suspend) Single<T>, Maybe<T>, Completable ListenableFuture<T> N/D
Lettura one-shot Coroutine (suspend) Single<T>, Maybe<T> ListenableFuture<T> N/D
Lettura osservabile Flow<T> Flowable<T>, Publisher<T>, Observable<T> N/D LiveData<T>

Questa guida illustra tre modi per utilizzare queste integrazioni per implementare query asincrone nei DAO.

Kotlin con Flow e coroutine

Kotlin fornisce funzionalità di linguaggio integrate che consentono di scrivere query asincrone senza framework di terze parti:

Il supporto per coroutine e Flow è integrato direttamente nel runtime principale di Room, quindi non sono necessari artefatti aggiuntivi.

RxJava per Kotlin e Java

Room 3.0 supporta i tipi di restituzione RxJava 3. Per utilizzare i tipi restituiti RxJava, devi registrare i convertitori di tipi restituiti RxJava nel database o nel DAO:

  1. Includi l'artefatto androidx.room3:room3-rxjava3 nella configurazione della build.
  2. Annota la dichiarazione @Database o @Dao con @DaoReturnTypeConverters(RxDaoReturnTypeConverters::class).

Room supporta i seguenti tipi di restituzione RxJava 3:

LiveData e Guava

Room 3.0 supporta i tipi di restituzione LiveData e Guava ListenableFuture utilizzando i convertitori:

  • LiveData: includi l'artefatto androidx.room3:room3-livedata e annota il database o il DAO con @DaoReturnTypeConverters(LiveDataDaoReturnTypeConverter::class).
  • Guava: includi l'artefatto androidx.room3:room3-guava e annota il database o il DAO con @DaoReturnTypeConverters(GuavaDaoReturnTypeConverter::class).

Scrivere query one-shot asincrone

Le query one-shot sono operazioni di database che vengono eseguite una sola volta e acquisiscono uno snapshot dei dati al momento dell'esecuzione. Ecco alcuni esempi di query one-shot asincrone:

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

Scrivere query osservabili

Le query osservabili sono operazioni di lettura che emettono nuovi valori ogni volta che le tabelle a cui fanno riferimento cambiano. Ad esempio, puoi utilizzare questo comportamento per mantenere aggiornato un elenco di elementi visualizzato man mano che il database cambia. Ecco alcuni esempi di query osservabili:

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

Monitorare manualmente l'invalidazione del database

Quando devi creare manualmente operazioni di database osservabili, puoi utilizzare l' createFlow API di InvalidationTracker. Questa API consente di creare un Flow che monitora le modifiche a tabelle specifiche ed emette una notifica ogni volta che queste tabelle cambiano.

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

Per impostazione predefinita, il Flow restituito emette un valore iniziale contenente tutte le tabelle registrate per avviare lo stream. Puoi disattivare questo comportamento impostando il parametro emitInitialState su false.

Convertitori di tipi restituiti DAO personalizzati

Per i tipi non supportati direttamente da Room o dalle relative librerie di estensione, puoi definire convertitori di tipi restituiti DAO personalizzati per supportare tipi restituiti aggiuntivi. Per trasformare il risultato di una funzione DAO nel tuo tipo personalizzato, annota una funzione di conversione con @DaoReturnTypeConverter.

Ad esempio, puoi definire un convertitore che utilizza androidx.tracing per aggiungere sezioni di traccia intorno all'esecuzione di una query per monitorare le query sensibili alle prestazioni eseguendo il wrapping dell'esecuzione in un tipo TracedQuery personalizzato:

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

Per utilizzare il convertitore, annota il database o il DAO con @DaoReturnTypeConverters:

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

Controllare l'inizializzazione del convertitore di tipi di restituzione DAO

In genere, Room gestisce l'istanziazione dei convertitori di tipi restituiti DAO. Tuttavia, se devi passare dipendenze aggiuntive alle classi di conversione, la tua app deve controllare direttamente la loro inizializzazione. In questo caso, annota la classe di conversione con @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)
    }
}

Poi, oltre a dichiarare la classe di conversione in @DaoReturnTypeConverters, utilizza la RoomDatabase.Builder.addDaoReturnTypeConverter funzione per passare un'istanza della classe di conversione al RoomDatabase builder:

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

Requisiti della funzione di conversione

Una funzione @DaoReturnTypeConverter deve soddisfare diversi requisiti:

  • Deve avere un parametro funzionale come ultimo argomento, in genere denominato executeAndConvert. Questo parametro è una lambda suspend che Room genera per eseguire la query e analizzare il risultato.
    • Se il convertitore deve trasformare la query, ad esempio Paging, la lambda può accettare un parametro RoomRawQuery.
  • Può accettare facoltativamente i seguenti parametri prima della lambda:
    • db: RoomDatabase: accede all'istanza del database, utile per ottenere l'ambito della coroutine o eseguire operazioni aggiuntive.
    • tableNames: Array<String> o List<String>: fornisce i nomi delle tabelle a cui accede la query, utile per i tipi osservabili.
    • rawQuery: RoomRawQuery: fornisce l'istanza di runtime della query.
    • inTransaction: Boolean: indica se la query viene eseguita all'interno di una transazione.