Переход с Room 2.x на Room 3.0

Room 3.0 — это обновление основной версии, которое переводит библиотеку на Kotlin-first. Она поддерживает Kotlin Multiplatform (KMP), требует Kotlin Symbol Processing (KSP) и использует сопрограммы для асинхронных операций.

Чтобы избежать проблем совместимости с существующими приложениями Room 2.x и транзитивными зависимостями, Room 3.0 находится в новом пакете: androidx.room3.

В этом руководстве описаны шаги, необходимые для переноса существующей реализации Room 2.x в Room 3.0.

Основные изменения в Room 3.0

Прежде чем начать перенос, ознакомьтесь с основными различиями:

  • Новый пакет и артефакты. Все классы находятся в androidx.room3. Для артефактов используется префикс room3, например androidx.room3:room3-runtime.
  • Только Kotlin и KSP. Room 3.0 не поддерживает генерацию кода Java. Используйте KSP вместо KAPT или инструментов обработки аннотаций Java. Room 3.0 по-прежнему поддерживает исходники Java в качестве входных данных.
  • Используйте сопрограммы. Функции DAO должны быть функциями suspend, за исключением наблюдаемых типов. CoroutineContext заменяет исполнителей.
  • Нет SupportSQLite. API SQLiteDriver поддерживают Room. Room удаляет SupportSQLiteDatabase из основных API.
  • Изменения API. При переносе и обратных вызовах базы данных используется SQLiteConnection вместо SupportSQLiteDatabase.
  • Конвертеры для реактивных типов. Для типов возвращаемых значений RxJava, LiveData, Guava и Paging требуется зарегистрировать @DaoReturnTypeConverters.

Мы рекомендуем выполнить миграцию в два этапа: сначала подготовить и модернизировать базу кода в Room 2.x, а затем перейти на Room 3.0.


Подготовка и модернизация в Room 2.x

Прежде чем переходить на Room 3.0, вы можете выполнить большую часть работы по модернизации, обновив Room до текущей версии 2.x, например Room 2.8. Room 2.8 поддерживает Kotlin Multiplatform (KMP) и включает множество API драйверов, которые используются в Room 3.0.

Обновите Room до версии 2.8 или более поздней

Обновите конфигурацию сборки, чтобы использовать текущую версию 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" }

Переход с KAPT на KSP

Room 3.0 не поддерживает инструменты обработки аннотаций Java и KAPT. Вы должны использовать Kotlin Symbol Processing (KSP). Вы можете выполнить этот переход, продолжая использовать Room 2.x.

  1. В файле build.gradle.kts модуля примените плагин KSP:

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

    Убедитесь, что версия KSP совместима с вашей версией Kotlin.

  2. Замените kapt или annotationProcessor на ksp для зависимости компилятора Room:

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

Используйте сопрограммы

Для асинхронных операций в Room 3.0 требуются сопрограммы.

  • Обновите объекты доступа к данным. Если они не возвращают наблюдаемый реактивный тип, например Flow или типы RxJava, все функции объектов доступа к данным должны быть функциями 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>
}
  • Если вы настроили RoomDatabase с помощью пользовательского Executor для выполнения операций с базой данных, перейдите на CoroutineContext, используя setQueryCoroutineContext в конструкторе:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Используйте API драйверов и избегайте Support SQLite

Room 3.0 полностью поддерживается SQLiteDriver и больше не поддерживает SupportSQLiteDatabase в своих основных API.

Если вы не вызовете setDriver, чтобы задать SQLiteDriver в построителе базы данных, Room 2.8 будет работать в режиме совместимости, в котором функционируют API Support SQLite и Driver. Этот режим совместимости позволяет постепенно преобразовывать базу кода перед включением драйвера.

  • Преобразование переносов. Перенесите подклассы Migration и AutoMigrationSpec, чтобы использовать SQLiteConnection вместо 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"
        )
    }
}
  • Преобразуйте обратные вызовы базы данных. Измените реализации RoomDatabase.Callback, чтобы использовать 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) {
        // ...
    }
}
  • Преобразуйте функции DAO @RawQuery. Для функций, аннотированных с помощью @RawQuery, используйте RoomRawQuery вместо SupportSQLiteQuery:
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

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

Вы можете создать RoomRawQuery во время выполнения:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Преобразуйте API транзакций. Замените блоки withTransaction и runInTransaction, предназначенные только для Android, на useWriterConnection и immediateTransaction.
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

// After (useWriterConnection - Room 2.8)
import androidx.room.useWriterConnection
import androidx.room.immediateTransaction

db.useWriterConnection { connection ->
    connection.immediateTransaction {
        // perform database operations
    }
}
  • Избегайте прямого использования SupportSQLiteDatabase. Если у вас есть большой объем устаревшего кода, который по-прежнему требует SupportSQLiteDatabase, и вы пока не можете его перенести, используйте артефакт совместимости androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Затем используйте getSupportWrapper, чтобы получить SupportSQLiteDatabase из экземпляра базы данных Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Настройте драйвер SQLite. После того как вы перенесете все вызовы Room API в Driver API, настройте драйвер, например BundledSQLiteDriver или AndroidSQLiteDriver, вызвав setDriver в построителе RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

Используйте отслеживание аннулирований на основе потоков

В Room 2.8 добавлен API InvalidationTracker.createFlow. Используйте этот API, чтобы перейти с устаревших реализаций InvalidationTracker.Observer, если вы все ещё используете Room 2.x. Это подготовит вашу базу кода к Room 3.0, в которой 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()
}

Переход на Room 3.0

После модернизации приложения в Room 2.x переход на Room 3.0 включает обновление зависимостей, импорт пакетов и обратные вызовы базы данных.

Обновите зависимости и импорт пакетов

  • В конфигурации сборки замените зависимости androidx.room на 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" }
  • Обновите блок зависимостей:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Обновите импорт пакетов. Замените import androidx.room.* на import androidx.room3.*.

Обновление API конвертера типов

В Room 3.0 переименованы API преобразователей типов, чтобы уточнить их использование для преобразования значений столбцов и избежать путаницы с преобразователями типов возвращаемых значений DAO.

Обновите следующие аннотации и функции в базе кода:

  • Переименовать @TypeConverter в @ColumnTypeConverter.
  • Переименовать @TypeConverters в @ColumnTypeConverters.
  • Переименовать @ProvidedTypeConverter в @ProvidedColumnTypeConverter.
  • Переименовать RoomDatabase.Builder.addTypeConverter в addColumnTypeConverter.

Пример:

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

Обновите обратные вызовы, чтобы приостановить функции

В Room 3.0 обратные вызовы и переносы баз данных используют SQLiteConnection и являются функциями suspend.

  • Обновите курсы, созданные вручную в Migration:
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"
        )
    }
}
  • Обновите реализации RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Регистрация конвертеров типов возвращаемых значений DAO

В Room 3.0 реактивные типы возвращаемых значений, такие как RxJava, LiveData, Guava и Paging, требуют регистрации конвертеров типов возвращаемых значений DAO с помощью @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • Paging (PagingSource): зарегистрируйте PagingSourceDaoReturnTypeConverter из артефакта androidx.room3:room3-paging.
  • RxJava (Observable, Flowable, Single, Maybe, Completable). Зарегистрируйте RxDaoReturnTypeConverters из артефакта androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture). Зарегистрируйте GuavaDaoReturnTypeConverter из артефакта androidx.room3:room3-guava.
  • LiveData (LiveData). Зарегистрируйте LiveDataDaoReturnTypeConverter из артефакта androidx.room3:room3-livedata.

Проверьте удаление наблюдателя InvalidationTracker

В Room 3.0 полностью удален InvalidationTracker.Observer и связанные с ним методы регистрации, такие как addObserver и removeObserver.

Если вы ещё не перешли на потоки сопрограмм на этапе 1, вам нужно перенести все экземпляры Observer в createFlow.

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