Eseguire la migrazione da Room 2.x a Room 3.0

Room 3.0 è un aggiornamento della versione principale che porta la libreria a essere Kotlin-first. Supporta Kotlin Multiplatform (KMP), richiede Kotlin Symbol Processing (KSP) e applica le coroutine per le operazioni asincrone.

Per evitare problemi di compatibilità con le app Room 2.x esistenti e le dipendenze transitive, Room 3.0 si trova in un nuovo pacchetto: androidx.room3.

Questa guida illustra i passaggi necessari per eseguire la migrazione dell'implementazione Room 2.x esistente a Room 3.0.

Modifiche principali in Room 3.0

Prima di iniziare la migrazione, prendi confidenza con le principali differenze:

  • Nuovo pacchetto e nuovi artefatti: tutte le classi si trovano in androidx.room3. Gli artefatti utilizzano il prefisso room3, ad esempio androidx.room3:room3-runtime.
  • Solo Kotlin e KSP: Room 3.0 non supporta la generazione di codice Java. Utilizza KSP anziché KAPT o i processori di annotazione Java. Room 3.0 supporta ancora le origini Java come input.
  • Coroutines first: le funzioni DAO devono essere funzioni suspend, ad eccezione dei tipi osservabili. CoroutineContext sostituisce gli esecutori.
  • Nessun SupportSQLite: le API supportano Room.SQLiteDriver Room rimuove SupportSQLiteDatabase dalle API principali.
  • Modifiche alle API: le migrazioni e i callback del database utilizzano SQLiteConnection anziché SupportSQLiteDatabase.
  • Convertitori per tipi reattivi: i tipi restituiti RxJava, LiveData, Guava e Paging richiedono la registrazione di @DaoReturnTypeConverters.

Ti consigliamo di eseguire la migrazione in due fasi distinte: prima prepara e modernizza il codebase in Room 2.x, poi passa a Room 3.0.


Preparazione e modernizzazione in Room 2.x

Prima di eseguire la migrazione a Room 3.0, puoi eseguire la maggior parte del lavoro di modernizzazione aggiornando alla versione corrente di Room 2.x, ad esempio Room 2.8. Room 2.8 supporta Kotlin Multiplatform, o KMP, e include molte API driver utilizzate da Room 3.0.

Esegui l'aggiornamento a Room 2.8 e versioni successive

Aggiorna la configurazione di compilazione per utilizzare la versione corrente di Room 2.x:

[versions]
room2 = "2.8.4" # Use the current Room 2.8 version

[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room2" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room2" }

Esegui la migrazione da KAPT a KSP

Room 3.0 non supporta i processori di annotazione Java o KAPT. Devi utilizzare Kotlin Symbol Processing (KSP). Puoi eseguire questa transizione mentre utilizzi ancora Room 2.x.

  1. In build.gradle.kts del modulo, applica il plug-in KSP:

    plugins {
        id("com.google.devtools.ksp") version "<ksp_version>"
    }
    

    Assicurati che la versione KSP sia compatibile con la tua versione Kotlin.

  2. Sostituisci kapt o annotationProcessor con ksp per la dipendenza del compilatore Room:

    dependencies {
        implementation(libs.androidx.room.runtime)
        ksp(libs.androidx.room.compiler)
    }
    

Adotta le coroutine

Room 3.0 richiede le coroutine per le operazioni asincrone.

  • Aggiorna i DAO: a meno che non restituiscano un tipo reattivo osservabile, come i tipi Flow o RxJava, tutte le funzioni DAO devono essere funzioni suspend.
// Before (Blocking)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAll(): List<User>
}

// After (Suspend)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    suspend fun getAll(): List<User>
}
  • Se hai configurato RoomDatabase con un Executor personalizzato per eseguire le operazioni del database, esegui la migrazione a CoroutineContext utilizzando setQueryCoroutineContext nel builder:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Adotta le API driver ed evita Support SQLite

Room 3.0 è completamente supportato da SQLiteDriver e non supporta più SupportSQLiteDatabase nelle sue API principali.

Se non chiami setDriver per impostare un SQLiteDriver nel builder del database, Room 2.8 funziona in modalità di compatibilità in cui funzionano sia Support SQLite sia le API driver. Questa modalità di compatibilità ti consente di convertire gradualmente il codebase prima di abilitare il driver.

  • Converti le migrazioni: esegui la migrazione delle sottoclassi Migration e AutoMigrationSpec per utilizzare SQLiteConnection anziché SupportSQLiteDatabase.
// Before (SupportSQLiteDatabase)
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL")
    }
}

// After (SQLiteConnection - Room 2.8)
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(connection: SQLiteConnection) {
        connection.execSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • Converti i callback del database: aggiorna le implementazioni RoomDatabase.Callback per utilizzare SQLiteConnection:
// Before (SupportSQLiteDatabase)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        // ...
    }
}

// After (SQLiteConnection - Room 2.8)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}
  • Converti le funzioni DAO @RawQuery: per le funzioni annotate con @RawQuery, utilizza RoomRawQuery anziché SupportSQLiteQuery:
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

// After (RoomRawQuery)
@Dao
interface UserDao {
    @RawQuery
    suspend fun getUser(query: RoomRawQuery): User
}

Puoi creare un RoomRawQuery in fase di runtime:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Converti le API delle transazioni: sostituisci i blocchi solo per Android withTransaction e runInTransaction con withWriteTransaction o withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

// After (withWriteTransaction - Room 2.8)
import androidx.room.withWriteTransaction

db.withWriteTransaction {
    // perform database operations
}

Se hai bisogno di un accesso diretto di basso livello alla connessione della transazione, puoi anche utilizzare useWriterConnection con immediateTransaction.

  • Evita l'utilizzo diretto di SupportSQLiteDatabase: se hai un codice legacy esteso che richiede ancora SupportSQLiteDatabase e non puoi ancora eseguirne la migrazione, utilizza l'artefatto di compatibilità androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Poi, utilizza getSupportWrapper per ottenere un SupportSQLiteDatabase dall'istanza del database Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Imposta il driver SQLite: dopo aver eseguito la migrazione di tutti gli utilizzi dell'API Room alle API driver, configura un driver, ad esempio BundledSQLiteDriver o AndroidSQLiteDriver, chiamando setDriver nel builder RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

val db = Room.databaseBuilder<AppDatabase>(context, "db")
    .setDriver(BundledSQLiteDriver())
    .build()

Adotta il monitoraggio delle invalidazioni basato su flussi

Room 2.8 introduce l'API InvalidationTracker.createFlow. Utilizza questa API per eseguire la migrazione dalle implementazioni InvalidationTracker.Observer legacy mentre utilizzi ancora Room 2.x. In questo modo, il codebase viene preparato per Room 3.0, che rimuove completamente Observer.

// Before (InvalidationTracker.Observer)
val observer = object : InvalidationTracker.Observer("User") {
    override fun onInvalidated(tables: Set<String>) {
        // reload user data
    }
}
db.invalidationTracker.addObserver(observer)

// After (createFlow - Room 2.8)
val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}

Esegui la migrazione a Room 3.0

Una volta modernizzata l'applicazione in Room 2.x, la transizione a Room 3.0 comporta l'aggiornamento delle dipendenze, delle importazioni dei pacchetti e dei callback del database.

Aggiorna le dipendenze e le importazioni dei pacchetti

  • Nella configurazione di compilazione, sostituisci le dipendenze androidx.room con androidx.room3:
[versions]
room3 = "3.0.0" # Use the current Room 3.0 version

[libraries]
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
  • Aggiorna il blocco delle dipendenze:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Aggiorna le importazioni dei pacchetti. Sostituisci import androidx.room.* con import androidx.room3.*.

Aggiorna le API del convertitore di tipi

Room 3.0 rinomina le API del convertitore di tipi per chiarirne l'utilizzo per la conversione dei valori delle colonne ed evitare confusione con i convertitori di tipi restituiti DAO.

Aggiorna le seguenti annotazioni e funzioni nel codebase:

  • Rinomina @TypeConverter in @ColumnTypeConverter.
  • Rinomina @TypeConverters in @ColumnTypeConverters.
  • Rinomina @ProvidedTypeConverter in @ProvidedColumnTypeConverter.
  • Rinomina RoomDatabase.Builder.addTypeConverter in addColumnTypeConverter.

Esempio:

// Before
@ProvidedTypeConverter
class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addTypeConverter(convertersInstance)
  .build()
// After
import androidx.room3.ColumnTypeConverter
import androidx.room3.ColumnTypeConverters
import androidx.room3.ProvidedColumnTypeConverter

@ProvidedColumnTypeConverter
class Converters {
  @ColumnTypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addColumnTypeConverter(convertersInstance)
  .build()

Aggiorna i callback alle funzioni di sospensione

In Room 3.0, i callback e le migrazioni del database utilizzano SQLiteConnection e sono funzioni suspend.

  • Aggiorna le classi Migration manuali:
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.async.executeSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override suspend fun migrate(connection: SQLiteConnection) {
        connection.executeSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • Aggiorna le implementazioni RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Registra i convertitori di tipi restituiti DAO

In Room 3.0, i tipi restituiti reattivi, come RxJava, LiveData, Guava e Paging, richiedono la registrazione dei convertitori di tipi restituiti DAO utilizzando @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • Paging (PagingSource): registra PagingSourceDaoReturnTypeConverter dall'artefatto androidx.room3:room3-paging.
  • RxJava (Observable, Flowable, Single, Maybe, Completable): registra RxDaoReturnTypeConverters dall'artefatto androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture): registra GuavaDaoReturnTypeConverter dall'artefatto androidx.room3:room3-guava.
  • LiveData (LiveData): registra LiveDataDaoReturnTypeConverter dall' androidx.room3:room3-livedata artefatto.

Verifica la rimozione di InvalidationTracker Observer

Room 3.0 rimuove completamente InvalidationTracker.Observer e i metodi di registrazione correlati, come addObserver e removeObserver.

Se non hai ancora eseguito la transizione ai flussi di coroutine in Fase 1, devi eseguire la migrazione di tutti gli utilizzi di Observer a createFlow:

val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}