Migrar do Room 2.x para o Room 3.0

A Room 3.0 é uma atualização de versão principal que faz a transição da biblioteca para priorizar o Kotlin. Ela oferece suporte ao Kotlin Multiplatform (KMP), exige o Kotlin Symbol Processing (KSP) e aplica corrotinas para operações assíncronas.

Para evitar problemas de compatibilidade com apps Room 2.x e dependências transitivas, a Room 3.0 reside em um novo pacote: androidx.room3.

Este guia descreve as etapas necessárias para migrar sua implementação atual da Room 2.x para a Room 3.0.

Principais mudanças na Room 3.0

Antes de iniciar a migração, familiarize-se com as principais diferenças:

  • Novo pacote e artefatos: todas as classes residem em androidx.room3. Os artefatos usam o room3 prefixo, como androidx.room3:room3-runtime.
  • Somente Kotlin e KSP: a Room 3.0 não oferece suporte à geração de código Java. Use o KSP em vez de processadores de anotações KAPT ou Java. A Room 3.0 ainda oferece suporte a fontes Java como entradas.
  • Corrotinas primeiro: as funções DAO precisam ser suspend funções, exceto para tipos observáveis. CoroutineContext substitui executores.
  • Sem SupportSQLite: SQLiteDriver as APIs fazem o backup da Room. A Room remove SupportSQLiteDatabase das APIs principais.
  • Mudanças na API: as migrações e os callbacks de banco de dados usam SQLiteConnection em vez de SupportSQLiteDatabase.
  • Conversores para tipos reativos: os tipos de retorno RxJava, LiveData, Guava e Paging exigem que você registre @DaoReturnTypeConverters.

Recomendamos migrar em duas fases distintas: primeiro, preparar e modernizar sua base de código na Room 2.x e, em seguida, mudar para a Room 3.0.


Preparar e modernizar na Room 2.x

Antes de migrar para a Room 3.0, você pode realizar a maior parte do trabalho de modernização atualizando para a versão atual da Room 2.x, como a Room 2.8. A Room 2.8 oferece suporte ao Kotlin Multiplatform (KMP) e inclui muitas APIs de driver que a Room 3.0 usa.

Atualizar para a Room 2.8 e versões mais recentes

Atualize a configuração de build para usar a versão atual da 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" }

Migrar do KAPT para o KSP

A Room 3.0 não oferece suporte a processadores de anotações Java ou KAPT. Você precisa usar o Kotlin Symbol Processing (KSP). É possível fazer essa transição enquanto ainda estiver na Room 2.x.

  1. No build.gradle.kts do módulo, aplique o plug-in KSP:

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

    Verifique se a versão do KSP é compatível com a versão do Kotlin.

  2. Substitua kapt ou annotationProcessor por ksp para a dependência do compilador da Room:

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

Adotar corrotinas

A Room 3.0 exige corrotinas para operações assíncronas.

  • Atualize seus DAOs: a menos que retornem um tipo reativo observável, como Flow ou tipos RxJava, todas as funções DAO precisam ser funções 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 você configurou seu RoomDatabase com um Executor personalizado para realizar operações de banco de dados, migre para CoroutineContext usando setQueryCoroutineContext no builder:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Adotar APIs de driver e evitar o Support SQLite

A Room 3.0 tem suporte total do SQLiteDriver e não oferece mais suporte ao SupportSQLiteDatabase nas APIs principais.

Se você não chamar setDriver para definir um SQLiteDriver no builder do banco de dados, a Room 2.8 vai operar em um modo de compatibilidade em que as APIs do Support SQLite e do Driver funcionam. Esse modo de compatibilidade permite converter sua base de código de forma incremental antes de ativar o driver.

  • Converter migrações: migre suas Migration e AutoMigrationSpec subclasses para usar SQLiteConnection em vez 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"
        )
    }
}
  • Converter callbacks de banco de dados: atualize as RoomDatabase.Callback implementações 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) {
        // ...
    }
}
  • Converter funções DAO @RawQuery: para funções anotadas com @RawQuery, use RoomRawQuery em vez 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
}

É possível criar um RoomRawQuery no ambiente de execução:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Converter APIs de transação: substitua os blocos somente para Android withTransaction e runInTransaction por withWriteTransaction ou withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

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

db.withWriteTransaction {
    // perform database operations
}

Se você precisar de acesso direto de baixo nível à conexão de transação, também poderá usar useWriterConnection com immediateTransaction.

  • Evitar o uso direto de SupportSQLiteDatabase: se você tiver um código legado extenso que ainda exige SupportSQLiteDatabase e não puder migrá-lo ainda, use o artefato de compatibilidade androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Em seguida, use getSupportWrapper para receber um SupportSQLiteDatabase da instância do banco de dados da Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Definir o driver SQLite: depois de migrar todos os usos da API Room para APIs de driver, configure um driver, como BundledSQLiteDriver ou AndroidSQLiteDriver, chamando setDriver no builder RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

Adotar o rastreamento de invalidação baseado em fluxo

A Room 2.8 apresenta a API InvalidationTracker.createFlow. Use essa API para migrar das implementações legadas de InvalidationTracker.Observer enquanto ainda estiver na Room 2.x. Isso prepara sua base de código para a Room 3.0, que remove completamente o 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()
}

Migrar para a Room 3.0

Depois de modernizar o aplicativo na Room 2.x, a transição para a Room 3.0 envolve a atualização de dependências, importações de pacotes e callbacks de banco de dados.

Atualizar dependências e importações de pacotes

  • Na configuração do build, substitua as dependências androidx.room por 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" }
  • Atualize o bloco de dependências:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Atualize as importações de pacotes. Substitua import androidx.room.* por import androidx.room3.*.

Atualizar APIs de conversor de tipo

A Room 3.0 renomeia as APIs do conversor de tipo para esclarecer o uso delas na conversão de valores de coluna e evitar confusão com os conversores de tipo de retorno DAO.

Atualize as seguintes anotações e funções na sua base de código:

  • Renomeie @TypeConverter para @ColumnTypeConverter.
  • Renomeie @TypeConverters para @ColumnTypeConverters.
  • Renomeie @ProvidedTypeConverter para @ProvidedColumnTypeConverter.
  • Renomeie RoomDatabase.Builder.addTypeConverter para addColumnTypeConverter.

Exemplo:

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

Atualizar callbacks para suspender funções

Na Room 3.0, os callbacks e as migrações de banco de dados usam SQLiteConnection e são funções suspend.

  • Atualize suas classes Migration manuais:
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"
        )
    }
}
  • Atualize suas implementações de RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Registrar conversores de tipo de retorno DAO

Na Room 3.0, os tipos de retorno reativos, como RxJava, LiveData, Guava e Paging, exigem que você registre conversores de tipo de retorno DAO usando @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

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

Verificar a remoção do observador InvalidationTracker

A Room 3.0 remove completamente InvalidationTracker.Observer e métodos de registro relacionados, como addObserver e removeObserver.

Se você ainda não fez a transição para fluxos de corrotina em Fase 1, migre todos os usos de Observer para createFlow:

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