Room to Kotlin Multiplatform migrieren

In diesem Dokument wird beschrieben, wie Sie eine vorhandene Chatroom-Implementierung zu einer die Kotlin Multiplatform (KMP) verwendet.

Raumnutzungen in einer vorhandenen Android-Codebasis zu einem gemeinsamen gemeinsamen KMP migrieren kann je nach den verwendeten Room APIs oder davon, ob verwendet die Codebasis bereits Koroutinen. In diesem Abschnitt finden Sie Informationen und Tipps, wenn Sie versuchen, Raumnutzung zu einem gemeinsamen Modul zu migrieren.

Es ist wichtig, dass Sie sich zunächst mit den Unterschieden und fehlenden zwischen der Android-Version von Room und der KMP-Version sowie die damit einhergehende Einrichtung. Im Wesentlichen bedeutet eine erfolgreiche Migration eine Refaktorierung wie die SupportSQLite* APIs eingesetzt werden und durch SQLite-Treiber-APIs ersetzt werden zusammen mit den verschobenen Raumdeklarationen (@Database mit Anmerkungen versehene Klasse, DAOs, Entitäten usw.) in gemeinsamen Code umwandeln.

Lesen Sie sich die folgenden Informationen noch einmal durch, bevor Sie fortfahren:

In den nächsten Abschnitten werden die verschiedenen Schritte beschrieben, die für eine erfolgreiche Migration.

Von Support SQLite zu SQLite-Treiber migrieren

Die APIs in androidx.sqlite.db sind nur für Android verfügbar und die Nutzung muss mit SQLite-Treiber-APIs refaktoriert. Für die Abwärtskompatibilität und Das RoomDatabase ist mit einem SupportSQLiteOpenHelper.Factory konfiguriert (d.h. keine SQLiteDriver festgelegt ist), verhält sich der Raum im Kompatibilitätsmodus Wo? unterstützen sowohl die SQLite- als auch die SQLite-Treiber-APIs. Dies ermöglicht Inkrementelle Migrationen durchführen, sodass Sie nicht alle Ihre SQLite- Nutzungen in den SQLite-Treiber.

Die folgenden Beispiele zeigen häufige Verwendungen von Support SQLite und deren SQLite. Aussteller der Fahrer:

SQLite unterstützen (von)

Abfrage ohne Ergebnis ausführen

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

Abfrage mit Ergebnis, aber ohne Argumenten ausführen

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

Abfrage mit Ergebnis und Argumenten ausführen

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

SQLite-Treiber (nach)

Abfrage ohne Ergebnis ausführen

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

Abfrage mit Ergebnis, aber ohne Argumenten ausführen

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

Abfrage mit Ergebnis und Argumenten ausführen

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

Datenbanktransaktions-APIs sind direkt in SupportSQLiteDatabase verfügbar mit beginTransaction(), setTransactionSuccessful() und endTransaction(). Sie sind auch über den Chatroom (runInTransaction()) verfügbar. Diese migrieren Nutzung von SQLite-Treiber-APIs.

SQLite unterstützen (von)

Transaktion ausführen (mit RoomDatabase)

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

Transaktion ausführen (mit SupportSQLiteDatabase)

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

SQLite-Treiber (nach)

Transaktion ausführen (mit RoomDatabase)

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

Transaktion ausführen (mit 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")
}

Außerdem müssen verschiedene Callback-Überschreibungen zu ihren Treiber-Entsprechungen migriert werden:

SQLite unterstützen (von)

Abgeleitete Migrationsklassen

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

Abgeleitete Klassen der automatischen Migrationsspezifikation

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

Abgeleitete Datenbank-Callback-Klassen

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

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

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

SQLite-Treiber (nach)

Abgeleitete Migrationsklassen

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

Abgeleitete Klassen der automatischen Migrationsspezifikation

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

Abgeleitete Datenbank-Callback-Klassen

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

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

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

Fassen wir zusammen: Ersetzen Sie Verwendungen von SQLiteDatabase durch SQLiteConnection, wenn RoomDatabase ist nicht verfügbar, zum Beispiel in Callback-Überschreibungen (onMigrate, onCreate usw.). Wenn ein RoomDatabase verfügbar ist, greifen Sie auf die zugrunde liegende Datenbankverbindung über RoomDatabase.useReaderConnection und RoomDatabase.useWriterConnection statt RoomDatabase.openHelper.writtableDatabase.

Blockierende DAO-Funktionen in Sperrungsfunktionen umwandeln

Die KMP-Version von Room nutzt Koroutinen für die E/A-Ausführung Vorgänge für die konfigurierte CoroutineContext ausführen. Das bedeutet, dass Sie alle blockierenden DAO-Funktionen migrieren, um Funktionen auszusetzen.

DAO-Funktion blockieren (von)

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

DAO-Funktion anhalten (to)

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

Das Migrieren vorhandener DAO-Blockierfunktionen zu Sperrfunktionen komplizierter wäre, wenn die vorhandene Codebasis nicht bereits Koroutinen enthält. Weitere Informationen zur Verwendung von Koroutinen finden Sie unter Coroutines in Android. in Ihrer Codebasis.

Reaktive Rückgabetypen in Flow umwandeln

Nicht alle DAO-Funktionen müssen Anhaltefunktionen sein. DAO-Funktionen, die Reaktive Typen wie LiveData oder Flowable von RxJava sollten nicht konvertiert werden um Funktionen auszusetzen. Einige Typen, z. B. LiveData, sind jedoch keine KMP. kompatibel sind. DAO-Funktionen mit reaktiven Rückgabetypen müssen migriert werden zu Koroutineabläufe.

Inkompatibler KMP-Typ (von)

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

Kompatibler KMP-Typ (bis)

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

Weitere Informationen zur Verwendung von Abläufen in Ihrer App finden Sie unter Abläufe in Android. Codebasis.

Koroutine-Kontext festlegen (optional)

Eine RoomDatabase kann optional mit einer gemeinsam genutzten Anwendung konfiguriert werden Executors, die RoomDatabase.Builder.setQueryExecutor() zum Ausführen der Datenbank verwenden Geschäftsabläufe. Da Executors nicht KMP-kompatibel sind, wird setQueryExecutor() von Room Die API ist für allgemeine Quellen nicht verfügbar. Stattdessen muss der RoomDatabase mit einem CoroutineContext konfiguriert werden. Ein Kontext kann festgelegt werden mithilfe von RoomDatabase.Builder.setCoroutineContext(), wenn keiner festgelegt ist, RoomDatabase verwendet standardmäßig Dispatchers.IO.

SQLite-Treiber einrichten

Nachdem Support-SQLite-Nutzungen zu SQLite-Treiber-APIs migriert wurden, Treiber muss mit RoomDatabase.Builder.setDriver konfiguriert werden. Die empfohlener Treiber ist BundledSQLiteDriver. Weitere Informationen finden Sie unter Treiberimplementierungen für Beschreibungen der verfügbaren Treiberimplementierungen.

Benutzerdefinierte SupportSQLiteOpenHelper.Factory konfiguriert mit RoomDatabase.Builder.openHelperFactory() werden im KMP nicht unterstützt. die vom benutzerdefinierten Open Helper zur Verfügung gestellt werden, müssen mit SQLite-Treiberschnittstellen.

Deklarationen für den Raum verschieben

Sobald die meisten Migrationsschritte abgeschlossen sind, kann der Chatroom verschoben werden. mit einem gemeinsamen Quellsatz. Die Strategien expect / actual können um raumbezogene Definitionen schrittweise zu verschieben. Wenn beispielsweise nicht alle blockierende DAO-Funktionen in Aussetzungsfunktionen migriert werden, eine mit expect @Dao annotierte Schnittstelle deklarieren, die im allgemeinen Code leer ist, aber Blockierfunktionen in 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
}