انتقال از 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 پشتیبانی نمی‌کند. به‌جای KAPT یا پردازشگرهای گزارمان Java، از KSP استفاده کنید. ‫Room 3.0 همچنان از منابع Java به‌عنوان ورودی پشتیبانی می‌کند.
  • Coroutines first: DAO functions must be suspend functions, except for observable types. CoroutineContext جایگزین مجریان می‌شود.
  • No SupportSQLite: SQLiteDriver APIs back Room. Room SupportSQLiteDatabase را از میاناهای اصلی برمی‌دارد.
  • تغییرات میانای برنامه‌سازی کاربردی: انتقال‌ها و فراخوانی‌های پایگاه داده از 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) پشتیبانی می‌کند و شامل بسیاری از میاناهای برنامه‌سازی کاربردی درایور است که Room 3.0 از آن‌ها استفاده می‌کند.

به‌روزرسانی به Room نسخه ۲.۸ و بالاتر

پیکربندی ساخت خود را به‌روز کنید تا از نسخه فعلی 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 برای عملیات ناهمگام به کوروتین نیاز دارد.

  • DAO خود را به‌روز کنید: همه عملکردهای DAO باید عملکرد suspend باشند، مگراینکه نوع واکنشی قابل‌مشاهده‌ای را برگردانند، مانند Flow یا انواع RxJava.
// 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 سفارشی برای انجام عملیات پایگاه داده پیکربندی کرده‌اید، بااستفاده از setQueryCoroutineContext در سازنده به CoroutineContext منتقل شوید:
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

از میاناهای برنامه‌سازی کاربردی درایور استفاده کنید و از Support SQLite استفاده نکنید

‫Room 3.0 کاملاً توسط SQLiteDriver پشتیبانی می‌شود و دیگر از SupportSQLiteDatabase در میاناهای برنامه‌سازی کاربردی اصلی خود پشتیبانی نمی‌کند.

اگر setDriver را برای تنظیم SQLiteDriver در سازنده پایگاه داده‌تان فراخوانی نکنید، Room 2.8 در حالت سازگاری عمل می‌کند که در آن هم Support SQLite و هم Driver APIs کار می‌کنند. این حالت سازگاری به شما امکان می‌دهد پایگاه کدتان را قبل‌از فعال کردن درایور به‌صورت افزایشی تبدیل کنید.

  • انتقال‌های تبدیل: زیرکلاس‌های Migration و AutoMigrationSpec خود را انتقال دهید تا به‌جای SupportSQLiteDatabase از SQLiteConnection استفاده کنند.
// 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)
    }
)
  • تبدیل کردن میاناهای برنامه‌سازی کاربردی تراکنش: بلوک‌های 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 را به میاناهای برنامه‌سازی کاربردی درایور منتقل کردید، درایوری مثل BundledSQLiteDriver یا AndroidSQLiteDriver را با فراخوانی setDriver در سازنده RoomDatabase پیکربندی کنید:
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

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

اتخاذ ردگیری ابطال مبتنی بر جریان

Room 2.8 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.* جایگزین کنید.

به‌روزرسانی میاناهای برنامه‌سازی کاربردی تبدیل‌کننده نوع

Room 3.0 نام میاناهای برنامه‌سازی کاربردی تبدیل‌کننده نوع را تغییر می‌دهد تا کاربرد آن‌ها را برای تبدیل مقادیر ستون شفاف‌سازی کند و از سردرگمی با تبدیل‌کننده‌های نوع بازگشتی 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>
}
  • صفحه‌بندی (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 Observer

‫Room 3.0 به‌طور کامل InvalidationTracker.Observer و روش‌های ثبت‌نام مرتبط، مانند addObserver و removeObserver را حذف می‌کند.

اگر قبلاً در فاز ۱ به جریان‌های هم‌روال منتقل نشده‌اید، باید همه موارد استفاده از Observer را به createFlow منتقل کنید:

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