Room (Multiplatform Kotlin)

Library persistensi Room menyediakan lapisan abstraksi atas SQLite untuk memungkinkan untuk akses database yang lebih tangguh sambil memanfaatkan kemampuan penuh SQLite. Ini berfokus pada penggunaan Room di project Kotlin Multiplatform (KMP). Untuk selengkapnya informasi tentang cara menggunakan Room, lihat Menyimpan data di database lokal menggunakan Room atau contoh resmi kami.

Menyiapkan dependensi

Versi Room saat ini yang mendukung KMP adalah 2.7.0-alpha01 atau yang lebih tinggi.

Untuk menyiapkan Room di project KMP Anda, tambahkan dependensi untuk artefak di File build.gradle.kts untuk modul Anda:

  • androidx.room:room-gradle-plugin - Plugin Gradle untuk mengonfigurasi skema Room
  • androidx.room:room-compiler - Prosesor KSP yang membuat kode
  • androidx.room:room-runtime - Bagian runtime library
  • androidx.sqlite:sqlite-bundled - (Opsional) Library SQLite yang dipaketkan

Selain itu, Anda perlu mengonfigurasi driver SQLite Room. Penggerak ini berbeda berdasarkan platform target. Lihat Penerapan driver untuk deskripsi implementasi driver yang tersedia.

Untuk informasi penyiapan tambahan, lihat referensi berikut:

Menentukan class database

Anda perlu membuat class database yang dianotasi dengan @Database beserta DAO dan entity di dalam set sumber umum pada modul KMP bersama. Menempatkan kelas-kelas dalam sumber umum ini akan memungkinkan kelas-kelas itu dibagikan di semua target di berbagai platform Google.

Saat Anda mendeklarasikan objek expect dengan antarmuka RoomDatabaseConstructor, compiler Room akan menghasilkan actual implementasi yang tepat. Android Studio mungkin memberikan peringatan "Expected object 'AppDatabaseConstructor' has no actual declaration in module"; you can suppress the warning with@Suppress("NO_ACTUAL_FOR_EXPECT")`.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
}

// The Room compiler generates the `actual` implementations.
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase>

@Dao
interface TodoDao {
  @Insert
  suspend fun insert(item: TodoEntity)

  @Query("SELECT count(*) FROM TodoEntity")
  suspend fun count(): Int

  @Query("SELECT * FROM TodoEntity")
  fun getAllAsFlow(): Flow<List<TodoEntity>>
}

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

Perhatikan bahwa Anda dapat secara opsional menggunakan deklarasi sebenarnya / yang diharapkan untuk membuat implementasi Room khusus platform. Misalnya, Anda dapat menambahkan DAO khusus platform yang ditentukan dalam kode umum menggunakan expect, lalu menentukan definisi actual dengan kueri tambahan dalam khusus platform set sumber.

Membuat builder database

Anda perlu menentukan builder database untuk membuat instance Room di setiap platform. Ini adalah satu-satunya bagian API yang harus ada di sumber khusus platform karena perbedaan dalam API sistem file. Misalnya, di Android, lokasi database biasanya diperoleh melalui Context.getDatabasePath() API, sedangkan untuk iOS, lokasi database adalah diperoleh menggunakan NSFileManager.

Android

Untuk membuat instance database, tentukan Context bersama dengan database .

// shared/src/androidMain/kotlin/Database.kt

fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = ctx.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

Untuk membuat instance database, sediakan jalur database menggunakan metode NSFileManager, biasanya berada di NSDocumentDirectory.

// shared/src/iosMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = documentDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
    )
}

private fun documentDirectory(): String {
  val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
    directory = NSDocumentDirectory,
    inDomain = NSUserDomainMask,
    appropriateForURL = null,
    create = false,
    error = null,
  )
  return requireNotNull(documentDirectory?.path)
}

JVM (Desktop)

Untuk membuat instance database, sediakan jalur database menggunakan Java atau Kotlin Google Cloud Platform.

// shared/src/jvmMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

Pembuatan instance database

Setelah Anda mendapatkan RoomDatabase.Builder dari salah satu platform Anda, Anda dapat mengonfigurasi sisa database Room dalam kode umum beserta pembuatan instance database yang sebenarnya.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .addMigrations(MIGRATIONS)
      .fallbackToDestructiveMigrationOnDowngrade()
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

Memilih SQLiteDriver

Cuplikan kode sebelumnya menggunakan BundledSQLiteDriver. Ini adalah driver yang direkomendasikan yang menyertakan kompilasi SQLite dari sumber, yang memberikan yang paling konsisten dan terbaru dari SQLite pada semua platform. Jika Anda ingin menggunakan SQLite yang disediakan OS, gunakan setDriver API di platform khusus set sumber yang menentukan {i>driver<i} khusus platform. Untuk Android, Anda dapat menggunakan AndroidSQLiteDriver, sedangkan untuk iOS Anda dapat menggunakan NativeSQLiteDriver. Kepada menggunakan NativeSQLiteDriver, Anda harus menyediakan opsi penaut sehingga sistem secara dinamis menautkan ke SQLite sistem.

// shared/build.gradle.kts

kotlin {
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "TodoApp"
            isStatic = true
            // Required when using NativeSQLiteDriver
            linkerOpts.add("-lsqlite3")
        }
    }
}

Perbedaan

Room awalnya dikembangkan sebagai library Android dan kemudian dimigrasikan ke KMP dengan fokus pada kompatibilitas API. Versi KMP Room agak berbeda antar platform dan dari versi khusus Android. Perbedaan ini adalah tercantum dan dijelaskan sebagai berikut.

Memblokir fungsi DAO

Saat menggunakan Room untuk KMP, semua fungsi DAO dikompilasi untuk platform non-Android harus berupa fungsi suspend dengan pengecualian untuk jenis nilai yang ditampilkan reaktif, seperti sebagai Flow.

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
  // ERROR: Blocking function not valid for non-Android targets
  @Query("SELECT * FROM Entity")
  fun blockingQuery(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  suspend fun query(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  fun queryFlow(): Flow<List<Entity>>

  // ERROR: Blocking function not valid for non-Android targets
  @Transaction
  fun blockingTransaction() { // … }

  // OK
  @Transaction
  suspend fun transaction() { // … }
}

Room memanfaatkan library kotlinx.coroutines asinkron yang kaya fitur yang ditawarkan Kotlin untuk beberapa platform. Untuk fungsionalitas yang optimal, suspend fungsi diberlakukan untuk DAO yang dikompilasi dalam project KMP, dengan pengecualian DAO khusus Android untuk mempertahankan kompatibilitas mundur dengan saat ini.

Perbedaan fitur dengan KMP

Bagian ini menjelaskan perbedaan fitur antara KMP dan platform Android Room.

Fungsi DAO @RawQuery

Fungsi yang dianotasi dengan @RawQuery yang dikompilasi untuk platform non-Android harus mendeklarasikan parameter jenis RoomRawQuery, bukan SupportSQLiteQuery.

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query RoomRawQuery): List<TodoEntity>
}

Kemudian, RoomRawQuery dapat digunakan untuk membuat kueri saat runtime:

suspend fun getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = RoomRawQuery(
    sql = "SELECT * FROM TodoEntity WHERE title = ?"
    onBindStatement = {
      it.bindText(1, title.lowercase())
    }
  )
  return todosDao.getTodos(query)
}

Callback Kueri

API berikut untuk mengonfigurasi callback kueri tidak tersedia dan karenanya tidak tersedia di platform selain Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Kita bermaksud menambahkan dukungan untuk callback kueri di Room versi mendatang.

API untuk mengonfigurasi RoomDatabase dengan callback kueri RoomDatabase.Builder.setQueryCallback bersama dengan antarmuka callback RoomDatabase.QueryCallback tidak tersedia bersama sehingga tidak tersedia di platform lain selain Android.

Menutup Database Otomatis

API untuk mengaktifkan penutupan otomatis setelah waktu tunggu, RoomDatabase.Builder.setAutoCloseTimeout, hanya tersedia di Android dan merupakan tidak tersedia di platform lain.

Database dalam bentuk paket

API berikut untuk membuat RoomDatabase menggunakan database yang sudah ada (yaitu dalam bentuk paket) tidak tersedia bersama dan dengan demikian tidak tersedia di platform lain selain Android. API tersebut adalah:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Kami bermaksud untuk menambahkan dukungan untuk {i>database<i} dalam bentuk paket di versi Ruang.

Pembatalan Multi-Instance

API untuk memungkinkan pembatalan validasi multi-instance, RoomDatabase.Builder.enableMultiInstanceInvalidation hanya tersedia di Android dan tidak tersedia di platform umum atau platform lainnya.