Перенос комнаты на мультиплатформу Kotlin

В этом документе описывается, как перенести существующую реализацию Room на версию, использующую Kotlin Multiplatform (KMP).

Миграция использования Room из существующей кодовой базы Android в общий общий модуль KMP может сильно различаться по сложности в зависимости от используемых API Room или от того, использует ли кодовая база Coroutines. В этом разделе представлены некоторые рекомендации и советы при попытке перенести использование Room в общий модуль.

Важно сначала ознакомиться с различиями и недостающими функциями между версией Room для Android и версией KMP, а также с соответствующей настройкой. По сути, успешная миграция включает в себя рефакторинг использования API-интерфейсов SupportSQLite* и замену их API-интерфейсами драйверов SQLite, а также перемещение объявлений Room (аннотированных классов @Database , объектов DAO, сущностей и т. д.) в общий код.

Прежде чем продолжить, еще раз просмотрите следующую информацию:

В следующих разделах описаны различные шаги, необходимые для успешной миграции.

Переход с поддержки SQLite на драйвер SQLite

API-интерфейсы в androidx.sqlite.db предназначены только для Android, и любые варианты использования необходимо реорганизовать с помощью API-интерфейсов драйверов SQLite. В целях обратной совместимости, а также до тех пор, пока RoomDatabase настроена с помощью SupportSQLiteOpenHelper.Factory (т. е. SQLiteDriver не установлен), Room ведет себя в «режиме совместимости», где API-интерфейсы поддержки SQLite и драйвера SQLite работают должным образом. Это обеспечивает инкрементную миграцию, поэтому вам не нужно преобразовывать все используемые вами службы поддержки SQLite в драйвер SQLite за одно изменение.

Следующие примеры представляют собой распространенное использование поддержки SQLite и их аналогов драйвера SQLite:

Поддержка SQLite (от)

Выполнить запрос без результата

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

Выполнить запрос с результатом, но без аргументов

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

Выполнить запрос с результатом и аргументами

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Драйвер SQLite (для)

Выполнить запрос без результата

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

Выполнить запрос с результатом, но без аргументов

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

Выполнить запрос с результатом и аргументами

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

API-интерфейсы транзакций базы данных доступны непосредственно в SupportSQLiteDatabase с помощью beginTransaction() , setTransactionSuccessful() и endTransaction() . Они также доступны через Room с помощью runInTransaction() . Перенесите эти способы использования в API-интерфейсы драйверов SQLite.

Поддержка SQLite (от)

Выполнить транзакцию (используя RoomDatabase )

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

Выполнить транзакцию (используя SupportSQLiteDatabase )

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

Драйвер SQLite (для)

Выполнить транзакцию (используя RoomDatabase )

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Выполнить транзакцию (используя SQLiteConnection )

val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
  // perform database operations in transaction
  connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
  connection.execSQL("ROLLBACK TRANSACTION")
}

Различные переопределения обратного вызова также необходимо перенести на их аналоги в драйверах:

Поддержка SQLite (от)

Подклассы миграции

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Подклассы спецификации автоматической миграции

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Подклассы обратного вызова базы данных

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

Драйвер SQLite (для)

Подклассы миграции

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

Подклассы спецификации автоматической миграции

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

Подклассы обратного вызова базы данных

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

Подводя итог, замените использование SQLiteDatabase на SQLiteConnection , когда RoomDatabase недоступна, например, в переопределениях обратного вызова ( onMigrate , onCreate и т. д.). Если RoomDatabase доступна, получите доступ к базовому соединению с базой данных, используя RoomDatabase.useReaderConnection и RoomDatabase.useWriterConnection вместо RoomDatabase.openHelper.writtableDatabase .

Преобразование блокирующих функций DAO в функции приостановки

Версия Room для KMP использует сопрограммы для выполнения операций ввода-вывода в настроенном CoroutineContext . Это означает, что вам необходимо перенести все блокирующие функции DAO для приостановки функций.

Блокировка функции DAO (от)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

Приостановка функции DAO (чтобы)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

Миграция существующих функций блокировки DAO для приостановки функций может оказаться сложной, если существующая кодовая база еще не включает сопрограммы. Обратитесь к разделу «Сопрограммы в Android» , чтобы начать использовать сопрограммы в вашей кодовой базе.

Преобразование реактивных типов возврата в Flow

Не все функции DAO должны быть приостанавливаемыми. Функции DAO, которые возвращают реактивные типы, такие как LiveData или Flowable RxJava, не следует преобразовывать в функции приостановки. Однако некоторые типы, такие как LiveData , несовместимы с KMP. Функции DAO с реактивными типами возврата необходимо перенести в потоки сопрограмм.

Несовместимый тип KMP (от)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

Совместимый тип КМП (к)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Обратитесь к разделу «Потоки в Android» , чтобы начать использовать потоки в своей кодовой базе.

Установите контекст сопрограммы (необязательно)

RoomDatabase можно дополнительно настроить с использованием общих исполнителей приложений с помощью RoomDatabase.Builder.setQueryExecutor() для выполнения операций с базой данных. Поскольку исполнители несовместимы с KMP, API-интерфейс Room setQueryExecutor() недоступен для общих источников. Вместо этого RoomDatabase необходимо настроить с помощью CoroutineContext . Контекст можно установить с помощью RoomDatabase.Builder.setCoroutineContext() , если он не установлен, то RoomDatabase по умолчанию будет использовать Dispatchers.IO .

Установите драйвер SQLite

После переноса использования поддержки SQLite в API-интерфейсы драйверов SQLite необходимо настроить драйвер с помощью RoomDatabase.Builder.setDriver . Рекомендуемый драйвер — BundledSQLiteDriver . См. Реализации драйверов для описания доступных реализаций драйверов.

Пользовательский SupportSQLiteOpenHelper.Factory , настроенный с помощью RoomDatabase.Builder.openHelperFactory() не поддерживается в KMP. Функции, предоставляемые пользовательским открытым помощником, необходимо будет повторно реализовать с помощью интерфейсов драйвера SQLite.

Объявления о перемещении помещений

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

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

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

@Dao
interface TodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int
}

@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt

@Dao
actual interface BlockingTodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    fun count(): Int
}