Di chuyển từ Room 2.x sang Room 3.0

Room 3.0 là một bản cập nhật phiên bản chính, chuyển thư viện này sang Kotlin-first. Thư viện này hỗ trợ Kotlin Multiplatform (KMP), yêu cầu Kotlin Symbol Processing (KSP) và thực thi các coroutine cho các thao tác không đồng bộ.

Để ngăn các vấn đề về khả năng tương thích với các ứng dụng Room 2.x hiện có và các phần phụ thuộc bắc cầu, Room 3.0 nằm trong một gói mới: androidx.room3.

Hướng dẫn này trình bày các bước cần thiết để di chuyển hoạt động triển khai Room 2.x hiện có sang Room 3.0.

Các thay đổi chính trong Room 3.0

Trước khi bắt đầu di chuyển, hãy tìm hiểu những điểm khác biệt chính:

  • Gói và cấu phần phần mềm mới: Tất cả các lớp đều nằm trong androidx.room3. Các cấu phần phần mềm sử dụng tiền tố room3, chẳng hạn như androidx.room3:room3-runtime.
  • Chỉ Kotlin và KSP: Room 3.0 không hỗ trợ việc tạo mã Java. Sử dụng KSP thay vì KAPT hoặc trình xử lý chú giải Java. Room 3.0 vẫn hỗ trợ các nguồn Java làm dữ liệu đầu vào.
  • Ưu tiên coroutine: Các hàm DAO phải là hàm suspend, ngoại trừ các loại có thể quan sát. CoroutineContext thay thế các trình thực thi.
  • Không có SupportSQLite: Các API SQLiteDriver hỗ trợ Room. Room xoá SupportSQLiteDatabase khỏi các API cốt lõi.
  • Thay đổi về API: Các lệnh gọi lại cơ sở dữ liệu và hoạt động di chuyển sử dụng SQLiteConnection thay vì SupportSQLiteDatabase.
  • Trình chuyển đổi cho các loại phản ứng: Các loại phản hồi RxJava, LiveData, Guava và Phân trang yêu cầu bạn đăng ký @DaoReturnTypeConverters.

Bạn nên di chuyển theo 2 giai đoạn riêng biệt: đầu tiên là chuẩn bị và hiện đại hoá toàn bộ mã nguồn trong Room 2.x, sau đó chuyển sang Room 3.0.


Chuẩn bị và hiện đại hoá trong Room 2.x

Trước khi di chuyển sang Room 3.0, bạn có thể thực hiện hầu hết các công việc hiện đại hoá bằng cách cập nhật lên bản phát hành Room 2.x hiện tại, chẳng hạn như Room 2.8. Room 2.8 hỗ trợ Kotlin Multiplatform (KMP) và bao gồm nhiều API trình điều khiển mà Room 3.0 sử dụng.

Cập nhật lên Room 2.8 trở lên

Cập nhật cấu hình bản dựng để sử dụng bản phát hành Room 2.x hiện tại:

[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" }

Di chuyển từ KAPT sang KSP

Room 3.0 không hỗ trợ trình xử lý chú giải Java hoặc KAPT. Bạn phải sử dụng Kotlin Symbol Processing (KSP). Bạn có thể thực hiện quá trình chuyển đổi này trong khi vẫn sử dụng Room 2.x.

  1. Trong build.gradle.kts của mô-đun, hãy áp dụng trình bổ trợ KSP:

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

    Đảm bảo rằng phiên bản KSP tương thích với phiên bản Kotlin của bạn.

  2. Thay thế kapt hoặc annotationProcessor bằng ksp cho phần phụ thuộc trình biên dịch Room:

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

Sử dụng coroutine

Room 3.0 yêu cầu coroutine cho các thao tác không đồng bộ.

  • Cập nhật DAO: Trừ phi trả về một loại phản ứng có thể quan sát được, chẳng hạn như Flow hoặc các loại RxJava, tất cả các hàm DAO phải là hàm 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>
}
  • Nếu bạn đã định cấu hình RoomDatabase bằng Executor tuỳ chỉnh để thực hiện các thao tác trên cơ sở dữ liệu, hãy di chuyển đến CoroutineContext bằng cách sử dụng setQueryCoroutineContext trên trình tạo:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

Sử dụng các API trình điều khiển và tránh dùng Support SQLite

Room 3.0 được SQLiteDriver hỗ trợ đầy đủ và không còn hỗ trợ SupportSQLiteDatabase trong các API cốt lõi nữa.

Nếu bạn không gọi setDriver để đặt SQLiteDriver trên trình tạo cơ sở dữ liệu, thì Room 2.8 sẽ hoạt động ở chế độ tương thích, trong đó cả Support SQLite và Driver API đều hoạt động. Chế độ tương thích này cho phép bạn chuyển đổi dần toàn bộ mã nguồn trước khi bật trình điều khiển.

  • Chuyển đổi các hoạt động di chuyển: Di chuyển các lớp con MigrationAutoMigrationSpec để sử dụng SQLiteConnection thay vì 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"
        )
    }
}
  • Chuyển đổi lệnh gọi lại cơ sở dữ liệu: Cập nhật các hoạt động triển khai RoomDatabase.Callback để sử dụng 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) {
        // ...
    }
}
  • Chuyển đổi các hàm DAO @RawQuery: Đối với các hàm được chú thích bằng @RawQuery, hãy dùng RoomRawQuery thay vì SupportSQLiteQuery:
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

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

Bạn có thể tạo một RoomRawQuery trong thời gian chạy:

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • Chuyển đổi API giao dịch: Thay thế các khối withTransactionrunInTransaction chỉ dành cho Android bằng withWriteTransaction hoặc withReadTransaction:
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

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

db.withWriteTransaction {
    // perform database operations
}

Nếu cần quyền truy cập trực tiếp ở cấp thấp vào kết nối giao dịch, bạn cũng có thể sử dụng useWriterConnection với immediateTransaction.

  • Tránh sử dụng trực tiếp SupportSQLiteDatabase: Nếu bạn có nhiều mã cũ vẫn yêu cầu SupportSQLiteDatabase và bạn chưa thể di chuyển mã đó, hãy sử dụng cấu phần phần mềm tương thích androidx.room:room-sqlite-wrapper:
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

Sau đó, hãy dùng getSupportWrapper để lấy một SupportSQLiteDatabase từ phiên bản cơ sở dữ liệu Room:

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • Thiết lập trình điều khiển SQLite: Sau khi bạn di chuyển tất cả các cách sử dụng Room API sang driver API, hãy định cấu hình một trình điều khiển, chẳng hạn như BundledSQLiteDriver hoặc AndroidSQLiteDriver, bằng cách gọi setDriver trong trình tạo RoomDatabase:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

Sử dụng tính năng theo dõi việc vô hiệu hoá dựa trên luồng

Room 2.8 giới thiệu API InvalidationTracker.createFlow. Sử dụng API này để di chuyển khỏi các quy trình triển khai InvalidationTracker.Observer cũ trong khi vẫn dùng Room 2.x. Thao tác này sẽ chuẩn bị cơ sở mã của bạn cho Room 3.0, phiên bản này sẽ xoá hoàn toàn 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()
}

Di chuyển sang Room 3.0

Sau khi hiện đại hoá ứng dụng trên Room 2.x, việc chuyển đổi sang Room 3.0 sẽ liên quan đến việc cập nhật các phần phụ thuộc, lượt nhập gói và lệnh gọi lại cơ sở dữ liệu.

Cập nhật phần phụ thuộc và nhập gói

  • Trong cấu hình bản dựng, hãy thay thế các phần phụ thuộc androidx.room bằng 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" }
  • Cập nhật khối phần phụ thuộc:
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • Cập nhật các gói nhập. Thay thế import androidx.room.* bằng import androidx.room3.*.

Cập nhật API trình chuyển đổi loại

Room 3.0 đổi tên các API trình chuyển đổi loại để làm rõ cách sử dụng của chúng trong việc chuyển đổi các giá trị cột và tránh nhầm lẫn với các trình chuyển đổi loại trả về DAO.

Cập nhật các chú giải và hàm sau đây trong toàn bộ mã nguồn của bạn:

  • Đổi tên @TypeConverter thành @ColumnTypeConverter.
  • Đổi tên @TypeConverters thành @ColumnTypeConverters.
  • Đổi tên @ProvidedTypeConverter thành @ProvidedColumnTypeConverter.
  • Đổi tên RoomDatabase.Builder.addTypeConverter thành addColumnTypeConverter.

Ví dụ:

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

Cập nhật lệnh gọi lại thành các hàm tạm ngưng

Trong Room 3.0, các lệnh gọi lại và hoạt động di chuyển cơ sở dữ liệu sử dụng SQLiteConnection và là các hàm suspend.

  • Cập nhật các lớp học Migration theo cách thủ công:
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"
        )
    }
}
  • Cập nhật các hoạt động triển khai RoomDatabase.Callback:
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

Đăng ký trình chuyển đổi kiểu dữ liệu trả về DAO

Trong Room 3.0, các kiểu dữ liệu trả về phản ứng (chẳng hạn như RxJava, LiveData, Guava và Paging) yêu cầu bạn đăng ký các trình chuyển đổi kiểu dữ liệu trả về DAO bằng @DaoReturnTypeConverters.

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • Phân trang (PagingSource): Đăng ký PagingSourceDaoReturnTypeConverter từ cấu phần phần mềm androidx.room3:room3-paging.
  • RxJava (Observable, Flowable, Single, Maybe, Completable): Đăng ký RxDaoReturnTypeConverters từ cấu phần phần mềm androidx.room3:room3-rxjava3.
  • Guava (ListenableFuture): Đăng ký GuavaDaoReturnTypeConverter từ cấu phần phần mềm androidx.room3:room3-guava.
  • LiveData (LiveData): Đăng ký LiveDataDaoReturnTypeConverter từ cấu phần phần mềm androidx.room3:room3-livedata.

Xác minh việc xoá đối tượng theo dõi InvalidationTracker

Room 3.0 xoá hoàn toàn InvalidationTracker.Observer và các phương thức đăng ký liên quan, chẳng hạn như addObserverremoveObserver.

Nếu chưa chuyển sang luồng coroutine trong Giai đoạn 1, bạn phải di chuyển tất cả các cách sử dụng Observer sang createFlow:

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