Комната (мультиплатформенность Kotlin)

Библиотека персистентности Room предоставляет уровень абстракции поверх SQLite, чтобы обеспечить более надежный доступ к базе данных, используя при этом всю мощь SQLite. На этой странице основное внимание уделяется использованию Room в многоплатформенных проектах Kotlin (KMP). Дополнительные сведения об использовании Room см. в разделе Сохранение данных в локальной базе данных с помощью Room или наших официальных образцах .

Настройка зависимостей

Текущая версия Room, поддерживающая KMP, — 2.7.0-alpha01 или выше.

Чтобы настроить Room в проекте KMP, добавьте зависимости для артефактов в файл build.gradle.kts для вашего модуля:

  • androidx.room:room-gradle-plugin — плагин Gradle для настройки схем комнат.
  • androidx.room:room-compiler — процессор KSP, генерирующий код.
  • androidx.room:room-runtime — часть библиотеки времени выполнения.
  • androidx.sqlite:sqlite-bundled — (необязательно) встроенная библиотека SQLite.

Кроме того, вам необходимо настроить драйвер SQLite для Room. Эти драйверы различаются в зависимости от целевой платформы. См. Реализации драйверов для описания доступных реализаций драйверов.

Дополнительную информацию о настройке см. в следующих разделах:

Определение классов базы данных

Вам необходимо создать класс базы данных, аннотированный @Database , вместе с DAO и объектами внутри общего исходного набора вашего общего модуля KMP. Размещение этих классов в общих источниках позволит использовать их на всех целевых платформах.

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

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

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
)

Обратите внимание, что вы можете использовать объявления фактического/ожидаемого значения для создания реализаций комнаты для конкретной платформы. Например, вы можете добавить DAO для конкретной платформы, которая определена в общем коде с помощью expect , а затем указать actual определения с помощью дополнительных запросов в наборах исходных кодов для конкретной платформы.

Создание построителя базы данных

Вам необходимо определить построитель базы данных для создания экземпляра Room на каждой платформе. Это единственная часть API, которая должна присутствовать в исходных наборах для конкретной платформы из-за различий в API файловых систем. Например, в Android расположение базы данных обычно получается через API Context.getDatabasePath() , а в iOS расположение базы данных получается с помощью NSFileManager .

Андроид

Чтобы создать экземпляр базы данных, укажите контекст вместе с путем к базе данных.

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

Чтобы создать экземпляр базы данных, укажите путь к базе данных с помощью NSFileManager , обычно расположенного в 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 (настольный компьютер)

Чтобы создать экземпляр базы данных, укажите путь к базе данных с помощью API Java или 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,
    )
}

Создание экземпляра базы данных

Получив RoomDatabase.Builder от одного из конструкторов, специфичных для платформы, вы можете настроить остальную часть базы данных Room в общем коде вместе с фактическим созданием экземпляра базы данных.

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

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

Выбор SQLiteDriver

В предыдущих фрагментах кода используется BundledSQLiteDriver . Это рекомендуемый драйвер, включающий SQLite, скомпилированный из исходного кода, который обеспечивает наиболее согласованную и актуальную версию SQLite на всех платформах. Если вы хотите использовать SQLite, предоставляемый ОС, используйте API setDriver в исходных наборах для конкретной платформы, в которых указан драйвер для конкретной платформы. Для Android вы можете использовать AndroidSQLiteDriver , а для iOS — NativeSQLiteDriver . Чтобы использовать NativeSQLiteDriver , вам необходимо предоставить опцию компоновщика, чтобы приложение iOS динамически связывалось с системным 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")
        }
    }
}

Различия

Первоначально Room разрабатывался как библиотека Android, а затем был перенесен на KMP с упором на совместимость API. Версия Room для KMP несколько отличается на разных платформах и от версии для Android. Эти различия перечислены и описаны следующим образом.

Блокировка функций DAO

При использовании Room для KMP все функции DAO, скомпилированные для платформ, отличных от Android, должны быть suspend функциями, за исключением реактивных типов возврата, таких как 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 использует многофункциональную асинхронную библиотеку kotlinx.coroutines , которую Kotlin предлагает для нескольких платформ. Для оптимальной функциональности функции suspend применяются для DAO, скомпилированных в проекте KMP, за исключением DAO, специфичных для Android, для обеспечения обратной совместимости с существующей кодовой базой.

Отличия функций от KMP

В этом разделе описывается, чем отличаются функции версий Room для KMP и Android.

@RawQuery функции DAO

Функции, помеченные @RawQuery и скомпилированные для платформ, отличных от Android, должны будут объявить параметр типа RoomRawQuery вместо SupportSQLiteQuery .

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

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

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

Запрос обратного вызова

Следующие API для настройки обратных вызовов запросов не являются общедоступными и поэтому недоступны на платформах, отличных от Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Мы намерены добавить поддержку обратного вызова запроса в будущей версии Room.

API для настройки RoomDatabase с обратным вызовом запроса RoomDatabase.Builder.setQueryCallback вместе с интерфейсом обратного вызова RoomDatabase.QueryCallback обычно не доступны и, следовательно, недоступны на других платформах, кроме Android.

Автоматическое закрытие базы данных

API для включения автоматического закрытия после тайм-аута RoomDatabase.Builder.setAutoCloseTimeout доступен только на Android и недоступен на других платформах.

База данных предварительных пакетов

Следующие API-интерфейсы для создания RoomDatabase с использованием существующей базы данных (т. е. предварительно упакованной базы данных) недоступны обычно и, следовательно, недоступны на других платформах, кроме Android. Эти API:

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

Мы намерены добавить поддержку предварительно упакованных баз данных в будущей версии Room.

Недействительность нескольких экземпляров

API для включения недействительности нескольких экземпляров RoomDatabase.Builder.enableMultiInstanceInvalidation доступен только на Android и недоступен на обычных или других платформах.