Cómo migrar de Room 2.x a Room 3.0

Room 3.0 es una actualización de versión principal que hace que la biblioteca priorice a Kotlin. Es compatible con Kotlin Multiplataforma (KMP), requiere el Procesamiento de Símbolos de Kotlin (KSP) y aplica corrutinas para operaciones asíncronas.

Para evitar problemas de compatibilidad con las apps existentes de Room 2.x y las dependencias transitivas, Room 3.0 reside en un paquete nuevo: androidx.room3.

En esta guía, se describen los pasos necesarios para migrar tu implementación existente de Room 2.x a Room 3.0.

Cambios clave en Room 3.0

Antes de comenzar la migración, familiarízate con las principales diferencias:

  • Paquete y artefactos nuevos: Todas las clases residen en androidx.room3. Los artefactos usan el room3 prefijo, como androidx.room3:room3-runtime.
  • Solo Kotlin y KSP: Room 3.0 no admite la generación de código Java. Usa KSP en lugar de KAPT o procesadores de anotaciones de Java. Room 3.0 aún admite fuentes Java como entradas.
  • Prioridad para las corrutinas: Las funciones DAO deben ser funciones suspend excepto para los tipos observables. CoroutineContext reemplaza a los ejecutores.
  • Sin SupportSQLite: SQLiteDriver Las APIs respaldan Room. Room quita SupportSQLiteDatabase de las APIs principales.
  • Cambios en la API: Las migraciones y las devoluciones de llamada de la base de datos usan SQLiteConnection en lugar de SupportSQLiteDatabase.
  • Convertidores para tipos reactivos: Los tipos de datos que se muestran de RxJava, LiveData, Guava y Paging requieren que registres @DaoReturnTypeConverters.

Te recomendamos que realices la migración en dos fases distintas: primero, prepara y moderniza tu base de código en Room 2.x y, luego, cambia a Room 3.0.


Prepara y moderniza en Room 2.x

Antes de migrar a Room 3.0, puedes realizar la mayor parte del trabajo de modernización actualizando a la versión actual de Room 2.x, como Room 2.8. Room 2.8 admite Kotlin Multiplataforma, o KMP, e incluye muchas APIs de controlador que usa Room 3.0.

Actualiza a Room 2.8 y versiones posteriores

Actualiza tu configuración de compilación para usar la versión actual 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" }

Migra de KAPT a KSP

Room 3.0 no admite procesadores de anotaciones de Java ni KAPT. Debes usar el Procesamiento de Símbolos de Kotlin (KSP). Puedes realizar esta transición mientras usas Room 2.x.

  1. En el archivo build.gradle.kts de tu módulo, aplica el complemento KSP:

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

    Asegúrate de que la versión de KSP sea compatible con tu versión de Kotlin.

  2. Reemplaza kapt o annotationProcessor por ksp para la dependencia del compilador de Room:

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

Adopta corrutinas

Room 3.0 requiere corrutinas para operaciones asíncronas.

  • Actualiza tus DAO: A menos que muestren un tipo reactivo observable, como Flow o tipos RxJava, todas las funciones DAO deben ser funciones 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 configuraste tu RoomDatabase con un Executor personalizado para realizar operaciones de bases de datos, migra a CoroutineContext con setQueryCoroutineContext en el compilador:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Adopta las APIs de controlador y evita Support SQLite

Room 3.0 está completamente respaldado por SQLiteDriver y ya no admite SupportSQLiteDatabase en sus APIs principales.

Si no llamas a setDriver para establecer un SQLiteDriver en el compilador de tu base de datos, Room 2.8 opera en un modo de compatibilidad en el que funcionan tanto Support SQLite como las APIs de controlador. Este modo de compatibilidad te permite convertir tu base de código de forma incremental antes de habilitar el controlador.

  • Convierte migraciones: Migra tus Migration y AutoMigrationSpec subclases para usar SQLiteConnection en lugar 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"
        )
    }
}
  • Convierte devoluciones de llamada de la base de datos: Actualiza las implementaciones de RoomDatabase.Callback para usar 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) {
        // ...
    }
}
  • Convierte funciones DAO @RawQuery: Para las funciones anotadas con @RawQuery, usa RoomRawQuery en lugar 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
}

Puedes construir un RoomRawQuery en el tiempo de ejecución:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Convierte APIs de transacción: Reemplaza los bloques withTransaction y runInTransaction solo para Android por withWriteTransaction o withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

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

db.withWriteTransaction {
    // perform database operations
}

Si necesitas acceso directo de bajo nivel a la conexión de transacción, también puedes usar useWriterConnection con immediateTransaction.

  • Evita el uso directo de SupportSQLiteDatabase: Si tienes un código heredado extenso que aún requiere SupportSQLiteDatabase y aún no puedes migrarlo, usa el artefacto de compatibilidad androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Luego, usa getSupportWrapper para obtener un SupportSQLiteDatabase de tu instancia de base de datos de Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Establece el controlador de SQLite: Después de migrar todos los usos de la API de Room a las APIs de controlador, configura un controlador, como BundledSQLiteDriver o AndroidSQLiteDriver, llamando a setDriver en tu compilador RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

Adopta el seguimiento de invalidación basado en flujo

Room 2.8 presenta la API de InvalidationTracker.createFlow. Usa esta API para migrar de las implementaciones heredadas de InvalidationTracker.Observer mientras usas Room 2.x. Esto prepara tu base de código para Room 3.0, que quita por completo 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()
}

Migra a Room 3.0

Una vez que modernices tu aplicación en Room 2.x, la transición a Room 3.0 implicará actualizar las dependencias, las importaciones de paquetes y las devoluciones de llamada de la base de datos.

Actualiza las dependencias y las importaciones de paquetes

  • En tu configuración de compilación, reemplaza las dependencias de 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" }
  • Actualiza tu bloque de dependencias:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Actualiza las importaciones de paquetes. Reemplaza import androidx.room.* por import androidx.room3.*.

Actualiza las APIs del convertidor de tipos

Room 3.0 cambia el nombre de las APIs del convertidor de tipos para aclarar su uso para convertir valores de columna y evitar confusiones con los convertidores de tipos de datos que se muestran de DAO.

Actualiza las siguientes anotaciones y funciones en tu base de código:

  • Cambia el nombre de @TypeConverter a @ColumnTypeConverter.
  • Cambia el nombre de @TypeConverters a @ColumnTypeConverters.
  • Cambia el nombre de @ProvidedTypeConverter a @ProvidedColumnTypeConverter.
  • Cambia el nombre de RoomDatabase.Builder.addTypeConverter a addColumnTypeConverter.

Ejemplo:

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

Actualiza las devoluciones de llamada para suspender funciones

En Room 3.0, las devoluciones de llamada y las migraciones de la base de datos usan SQLiteConnection y son funciones suspend.

  • Actualiza tus clases Migration manuales:
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"
        )
    }
}
  • Actualiza tus implementaciones de RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Registra los convertidores de tipos de datos que se devuelve de DAO

En Room 3.0, los tipos de datos que se devuelven reactivos, como RxJava, LiveData, Guava y Paging, requieren que registres los convertidores de tipos de datos que se devuelven de DAO con @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 desde el artefacto androidx.room3:room3-paging.
  • RxJava (Observable, Flowable, Single, Maybe, Completable): Registra RxDaoReturnTypeConverters desde el artefacto androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture): Registra GuavaDaoReturnTypeConverter desde el artefacto androidx.room3:room3-guava.
  • LiveData (LiveData): Registra LiveDataDaoReturnTypeConverter desde el androidx.room3:room3-livedata artefacto.

Verifica la eliminación del observador InvalidationTracker

Room 3.0 quita por completo InvalidationTracker.Observer y los métodos de registro relacionados, como addObserver y removeObserver.

Si aún no hiciste la transición a los flujos de corrutinas en la Fase 1, debes migrar todos los usos de Observer a createFlow:

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