Migrer de Room 2.x vers Room 3.0

Room 3.0 est une mise à jour de version majeure qui fait passer la bibliothèque à une approche Kotlin-first. Elle est compatible avec Kotlin Multiplatform (KMP), nécessite le traitement des symboles Kotlin (KSP) et applique les coroutines pour les opérations asynchrones.

Pour éviter les problèmes de compatibilité avec les applications Room 2.x existantes et les dépendances transitives, Room 3.0 réside dans un nouveau package : androidx.room3.

Ce guide décrit les étapes nécessaires pour migrer votre implémentation Room 2.x existante vers Room 3.0.

Principaux changements dans Room 3.0

Avant de commencer la migration, familiarisez-vous avec les principales différences :

  • Nouveau package et nouveaux artefacts : toutes les classes résident dans androidx.room3. Les artefacts utilisent le room3 préfixe, tel que androidx.room3:room3-runtime.
  • Kotlin et KSP uniquement : Room 3.0 n'est pas compatible avec la génération de code Java. Utilisez KSP au lieu de KAPT ou de processeurs d'annotations Java. Room 3.0 est toujours compatible avec les sources Java en tant qu'entrées.
  • Coroutines en premier : les fonctions DAO doivent être des fonctions suspend, à l'exception des types observables. CoroutineContext remplace les exécutants.
  • Pas de SupportSQLite : les API SQLiteDriver sont compatibles avec Room. Room supprime SupportSQLiteDatabase des API de base.
  • Modifications de l'API : les migrations et les rappels de base de données utilisent SQLiteConnection au lieu de SupportSQLiteDatabase.
  • Convertisseurs pour les types réactifs : les types renvoyés RxJava, LiveData, Guava et Paging nécessitent l'enregistrement de @DaoReturnTypeConverters.

Nous vous recommandons de migrer en deux phases distinctes : préparez et modernisez d'abord votre codebase dans Room 2.x, puis passez à Room 3.0.


Préparer et moderniser dans Room 2.x

Avant de migrer vers Room 3.0, vous pouvez effectuer la plupart des tâches de modernisation en passant à la version actuelle de Room 2.x, comme Room 2.8. Room 2.8 est compatible avec Kotlin Multiplatform (KMP) et inclut de nombreuses API de pilote utilisées par Room 3.0.

Passer à Room 2.8 ou version ultérieure

Mettez à jour votre configuration de compilation pour utiliser la version actuelle de 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" }

Migrer de KAPT vers KSP

Room 3.0 n'est pas compatible avec les processeurs d'annotations Java ni avec KAPT. Vous devez utiliser le traitement des symboles Kotlin (KSP). Vous pouvez effectuer cette transition tout en restant sur Room 2.x.

  1. Dans le fichier build.gradle.kts de votre module, appliquez le plug-in KSP :

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

    Assurez-vous que la version de KSP est compatible avec votre version de Kotlin.

  2. Remplacez kapt ou annotationProcessor par ksp pour la dépendance du compilateur Room :

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

Adopter les coroutines

Room 3.0 nécessite des coroutines pour les opérations asynchrones.

  • Mettez à jour vos DAO : à moins qu'ils ne renvoient un type réactif observable, tel que Flow ou des types RxJava, toutes les fonctions DAO doivent être des fonctions 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>
}
  • Si vous avez configuré votre RoomDatabase avec un Executor personnalisé pour effectuer des opérations de base de données, migrez vers CoroutineContext à l'aide de setQueryCoroutineContext sur le compilateur :
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Adopter les API de pilote et éviter Support SQLite

Room 3.0 est entièrement compatible avec SQLiteDriver et n'est plus compatible avec SupportSQLiteDatabase dans ses API de base.

Si vous n'appelez pas setDriver pour définir un SQLiteDriver sur votre compilateur de base de données, Room 2.8 fonctionne dans un mode de compatibilité où les API Support SQLite et Driver fonctionnent toutes les deux. Ce mode de compatibilité vous permet de convertir progressivement votre codebase avant d'activer le pilote.

  • Convertir les migrations : migrez vos Migration et AutoMigrationSpec sous-classes pour utiliser SQLiteConnection au lieu de 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"
        )
    }
}
  • Convertir les rappels de base de données : mettez à jour les RoomDatabase.Callback implémentations pour utiliser 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) {
        // ...
    }
}
.
  • Convertir les fonctions DAO @RawQuery : pour les fonctions annotées avec @RawQuery, utilisez RoomRawQuery au lieu de SupportSQLiteQuery :
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

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

Vous pouvez construire un RoomRawQuery au moment de l'exécution :

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Convertir les API de transaction : remplacez les blocs withTransaction et runInTransaction propres à Android par withWriteTransaction ou withReadTransaction :
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

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

db.withWriteTransaction {
    // perform database operations
}

Si vous avez besoin d'un accès direct de bas niveau à la connexion de transaction, vous pouvez également utiliser useWriterConnection avec immediateTransaction.

  • Éviter l'utilisation directe de SupportSQLiteDatabase : si vous disposez d'un code hérité étendu qui nécessite toujours SupportSQLiteDatabase et que vous ne pouvez pas encore le migrer, utilisez l'artefact de compatibilité androidx.room:room-sqlite-wrapper :
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Ensuite, utilisez getSupportWrapper pour obtenir un SupportSQLiteDatabase à partir de votre instance de base de données Room :

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Définir le pilote SQLite : une fois que vous avez migré toutes les utilisations de l'API Room vers les API de pilote, configurez un pilote, tel que BundledSQLiteDriver ou AndroidSQLiteDriver, en appelant setDriver dans votre compilateur RoomDatabase :
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

Adopter le suivi des invalidations basé sur le flux

Room 2.8 introduit l'API InvalidationTracker.createFlow. Utilisez cette API pour migrer des implémentations InvalidationTracker.Observer héritées tout en restant sur Room 2.x. Cela prépare votre codebase pour Room 3.0, qui supprime complètement 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()
}

Migrer vers Room 3.0

Une fois que vous avez modernisé votre application sur Room 2.x, la transition vers Room 3.0 implique la mise à jour des dépendances, des importations de packages et des rappels de base de données.

Mettre à jour les dépendances et les importations de packages

  • Dans votre configuration de compilation, remplacez les dépendances androidx.room par 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" }
  • Mettez à jour votre bloc de dépendances :
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Mettez à jour vos importations de packages. Remplacez import androidx.room.* par import androidx.room3.*.

Mettre à jour les API de convertisseur de type

Room 3.0 renomme les API de convertisseur de type pour clarifier leur utilisation dans la conversion des valeurs de colonne et éviter toute confusion avec les convertisseurs de type renvoyé DAO.

Mettez à jour les annotations et fonctions suivantes dans votre codebase :

  • Renommez @TypeConverter en @ColumnTypeConverter.
  • Renommez @TypeConverters en @ColumnTypeConverters.
  • Renommez @ProvidedTypeConverter en @ProvidedColumnTypeConverter.
  • Renommez RoomDatabase.Builder.addTypeConverter en addColumnTypeConverter.

Exemple :

// 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()

Mettre à jour les rappels pour suspendre les fonctions

Dans Room 3.0, les rappels et les migrations de base de données utilisent SQLiteConnection et sont des fonctions suspend.

  • Mettez à jour vos classes Migration manuelles :
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"
        )
    }
}
  • Mettez à jour vos implémentations RoomDatabase.Callback :
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Enregistrer les convertisseurs de type renvoyé DAO

Dans Room 3.0, les types renvoyés réactifs, tels que RxJava, LiveData, Guava et Paging, nécessitent l'enregistrement des convertisseurs de type renvoyé DAO à l'aide de @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

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

Vérifier la suppression de l'observateur InvalidationTracker

Room 3.0 supprime complètement InvalidationTracker.Observer et les méthodes d'enregistrement associées, telles que addObserver et removeObserver.

Si vous n'êtes pas encore passé aux flux de coroutines en Phase 1, vous devez migrer toutes les utilisations Observer vers createFlow :

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