Pokój (wieloplatformowy Kotlin)

Biblioteka trwałości Room zapewnia warstwę abstrakcji nad SQLite, aby umożliwić bardziej niezawodny dostęp do bazy danych, jednocześnie wykorzystując pełną moc SQLite. Ta strona dotyczy korzystania z Room w projektach Kotlin Multiplatform (KMP). Więcej informacji o używaniu Room znajdziesz w artykule Zapisywanie danych w lokalnej bazie danych za pomocą Room lub w naszych oficjalnych przykładach.

Konfigurowanie zależności

Obecna wersja Room, która obsługuje KMP, to 2.7.0-alpha01 lub nowsza.

Aby skonfigurować Room w projekcie KMP, dodaj zależności artefaktów w pliku build.gradle.kts swojego modułu:

  • androidx.room:room-gradle-plugin – wtyczka Gradle do konfigurowania schematów Room
  • androidx.room:room-compiler – procesor KSP, który generuje kod.
  • androidx.room:room-runtime – część biblioteki działająca w czasie wykonywania,
  • androidx.sqlite:sqlite-bundled – (opcjonalnie) biblioteka SQLite dołączona do pakietu

Dodatkowo musisz skonfigurować sterownik SQLite w Room. Te sterowniki różnią się w zależności od platformy docelowej. Opisy dostępnych implementacji sterowników znajdziesz w sekcji Implementacje sterowników.

Więcej informacji o konfigurowaniu znajdziesz w tych artykułach:

Definiowanie klas bazy danych

Musisz utworzyć klasę bazy danych z adnotacjami @Database, a także obiekty DAO i encje w ramach wspólnego zestawu źródeł w udostępnionym module KMP. Umieszczenie tych zajęć w powszechnie dostępnych źródłach umożliwi ich udostępnianie na wszystkich platformach docelowych.

Gdy zadeklarujesz obiekt expect za pomocą interfejsu RoomDatabaseConstructor, kompilator Room wygeneruje implementacje actual. Android Studio może wyświetlić ostrzeżenie "Expected object 'AppDatabaseConstructor' has no actual declaration in module"; możesz je zignorować, używając @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> {
    override fun initialize(): 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
)

Pamiętaj, że możesz opcjonalnie użyć deklaracji actual / expect, aby utworzyć implementacje Room na konkretne platformy. Możesz na przykład dodać DAO dla konkretnej platformy zdefiniowane w kodzie wspólnym za pomocą funkcji expect, a potem określić definicje actual za pomocą dodatkowych zapytań w zbiorach źródeł dla poszczególnych platform.

Tworzenie kreatora bazy danych

Aby utworzyć instancję Room na każdej platformie, musisz zdefiniować kreatora bazy danych. To jedyna część interfejsu API, która musi znajdować się w zbiorach źródeł specyficznych dla platformy z powodu różnic w interfejsach API systemu plików. Na przykład w przypadku Androida lokalizacja bazy danych jest zwykle uzyskiwana za pomocą interfejsu API Context.getDatabasePath(), a w przypadku iOS – za pomocą interfejsu NSFileManager.

Android

Aby utworzyć instancję bazy danych, podaj kontekst oraz ścieżkę do bazy danych.

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

Aby utworzyć instancję bazy danych, podaj ścieżkę do bazy danych za pomocą parametru NSFileManager, który zwykle znajduje się w katalogu 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 (komputera stacjonarnego)

Aby utworzyć instancję bazy danych, podaj ścieżkę do bazy danych za pomocą interfejsów API Java lub Kotlin.

// 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,
    )
}

Minifikacja i zaciemnianie

Jeśli projekt jest skompresowany lub zaciemniony, musisz uwzględnić tę regułę Proguard, aby Room mógł znaleźć wygenerowaną implementację definicji bazy danych:

-keep class * extends androidx.room.RoomDatabase { <init>(); }

Tworzenie bazy danych

Po uzyskaniu obiektu RoomDatabase.Builder z jednego z konstruktorów specyficznych dla platformy możesz skonfigurować pozostałą część bazy danych Room w kodzie wspólnym wraz z rzeczywistą instancją bazy danych.

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

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

Wybieranie obiektu SQLiteDriver

Poprzednie fragmenty kodu używają funkcji BundledSQLiteDriver. Jest to zalecany sterownik, który zawiera skompilowaną z źródła bazę danych SQLite. Zapewnia ona najbardziej spójną i aktualną wersję SQLite na wszystkich platformach. Jeśli chcesz używać SQLite udostępnianej przez system operacyjny, użyj interfejsu API setDriver w zbiorach źródeł przeznaczonych dla konkretnej platformy, które określają sterownik dla konkretnej platformy. W przypadku Androida możesz użyć AndroidSQLiteDriver, a w przypadku iOS – NativeSQLiteDriver. Aby używać NativeSQLiteDriver, musisz podać opcję linkera, aby aplikacja na iOS mogła dynamicznie łączyć się z systemem SQLite.

// 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")
        }
    }
}

Różnice

Biblioteka Room została pierwotnie opracowana jako biblioteka na Androida, a później przeniesiona do KMP z naciskiem na zgodność z interfejsem API. Wersja Room na KMP różni się nieco w zależności od platformy i od wersji na Androida. Te różnice zostały wymienione i opisane poniżej.

Blokowanie funkcji DAO

Jeśli używasz Room w KMP, wszystkie funkcje DAO skompilowane na platformy inne niż Android muszą być funkcjami suspend z wyjątkiem reaktywnej funkcji zwracającej, takiej jak 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 korzysta z bogatej biblioteki asynchronicznej kotlinx.coroutines, którą Kotlin udostępnia na wiele platform. Aby zapewnić optymalną funkcjonalność, funkcje suspend są wymuszane w przypadku DAO skompilowanych w projekcie KMP, z wyjątkiem DAO przeznaczonych do Androida, co ma na celu zachowanie zgodności wstecznej z dotychczasową bazą kodu.

Różnice w funkcjach w porównaniu z kampaniami z dodatkowym płatnym ruchem

W tej sekcji opisano różnice między wersjami aplikacji Room na platformy KMP i Android.

Funkcje DAO @RawQuery

Funkcje z adnotacjami @RawQuery skompilowane na platformy inne niż Android muszą deklarować parametr typu RoomRawQuery zamiast SupportSQLiteQuery.

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

Następnie możesz użyć RoomRawQuery do utworzenia zapytania w czasie wykonywania:

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)
}

Zapytanie o połączenie zwrotne

Te interfejsy API do konfigurowania wywołań zwrotnych zapytań nie są dostępne w ogóle, dlatego nie są dostępne na platformach innych niż Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

W przyszłej wersji Room zamierzamy dodać obsługę wywołania zwrotnego zapytania.

Interfejs API do konfigurowania RoomDatabase z wywołaniem zwrotnym zapytania RoomDatabase.Builder.setQueryCallback oraz interfejs wywołania zwrotnego RoomDatabase.QueryCallback nie są dostępne na innych platformach niż Android.

Automatycznie zamykająca się baza danych

Interfejs API umożliwiający automatyczne zamykanie po upływie limitu czasu, RoomDatabase.Builder.setAutoCloseTimeout, jest dostępny tylko na Androidzie i nie jest dostępny na innych platformach.

Baza danych w pakiecie

Te interfejsy API do tworzenia RoomDatabase za pomocą istniejącej bazy danych (czyli skompilowanej bazy danych) nie są dostępne na innych platformach niż Android. Te interfejsy API:

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

W przyszłej wersji usługi pokoju zamierzamy dodać obsługę gotowych baz danych.

Unieważnianie wielu instancji

Interfejs API umożliwiający unieważnienie wielu instancjiRoomDatabase.Builder.enableMultiInstanceInvalidation jest dostępny tylko na Androidzie i nie jest dostępny na innych platformach.