Esegui la migrazione di Room a Kotlin Multiplatform

Questo documento descrive come eseguire la migrazione di un'implementazione di stanze esistente a uno che utilizza Kotlin Multiplatform (KMP).

Migrazione degli utilizzi delle stanze in un codebase Android esistente a un KMP condiviso comune modulo può variare notevolmente in termini di difficoltà a seconda delle API Room utilizzate o il codebase utilizza già Coroutines. Questa sezione offre indicazioni e suggerimenti durante la migrazione degli utilizzi di Room a un modulo comune.

È importante prima familiarizzare con le differenze e i mancati di funzionalità tra la versione per Android di Room e la versione KMP, oltre a la configurazione prevista. In sostanza, una migrazione di successo prevede il refactoring di utilizzo delle API SupportSQLite* e la loro sostituzione con le API SQLite Driver oltre allo spostamento delle dichiarazioni della stanza (@Database classe annotata, DAO, entità e così via) in codice comune.

Prima di continuare, rivedi le seguenti informazioni:

Nelle sezioni successive vengono descritti i vari passaggi necessari per creare migrazione.

Esegui la migrazione da Support SQLite a SQLite Driver

Le API in androidx.sqlite.db sono solo Android e tutti gli utilizzi devono essere con il refactoring delle API SQLite Driver. Per la compatibilità con le versioni precedenti e a condizione che RoomDatabase è configurato con un SupportSQLiteOpenHelper.Factory (ad es. nessun SQLiteDriver è impostato), la stanza si comporta in "modalità di compatibilità" dove entrambe le API Support SQLite e SQLite Driver funzionano come previsto. Ciò consente migrazioni incrementali in modo da non dover convertire tutte le tue richieste SQLite di utilizzo di SQLite Driver in una singola modifica.

I seguenti esempi sono utilizzi comuni di Support SQLite e delle relative SQLite Contrassegni dei conducenti:

Supporta SQLite (da)

Esegui una query senza risultato

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

Esegui una query con risultato ma senza argomenti

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

Esegui una query con risultato e argomenti

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

Driver SQLite (a)

Esegui una query senza risultato

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

Esegui una query con risultato ma senza argomenti

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

Esegui una query con risultato e argomenti

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

Le API per le transazioni dei database sono disponibili direttamente in SupportSQLiteDatabase con beginTransaction(), setTransactionSuccessful() e endTransaction(). Sono disponibili anche tramite la Stanza virtuale con runInTransaction(). Esegui la migrazione di questi di utilizzo delle API SQLite Driver.

Supporta SQLite (da)

Eseguire una transazione (utilizzando RoomDatabase)

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

Eseguire una transazione (utilizzando SupportSQLiteDatabase)

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

Driver SQLite (a)

Eseguire una transazione (utilizzando RoomDatabase)

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

Eseguire una transazione (utilizzando 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")
}

È necessario eseguire anche la migrazione di vari override di callback alle rispettive controparti del driver:

Supporta SQLite (da)

Sottoclassi di migrazione

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

Sottoclassi delle specifiche della migrazione automatica

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

Sottoclassi di callback del database

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

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

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

Driver SQLite (a)

Sottoclassi di migrazione

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

Sottoclassi delle specifiche della migrazione automatica

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

Sottoclassi di callback del database

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

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

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

Ricapitolando, sostituisci gli utilizzi di SQLiteDatabase, con SQLiteConnection quando RoomDatabase non è disponibile, ad esempio negli override dei callback (onMigrate, onCreate e così via). Se è disponibile un RoomDatabase, accedi all'elemento sottostante connessione al database utilizzando RoomDatabase.useReaderConnection e RoomDatabase.useWriterConnection invece di RoomDatabase.openHelper.writtableDatabase.

Converti le funzioni DAO di blocco in funzioni di sospensione

La versione KMP di Room si basa sulle coroutine per eseguire l'I/O operazioni sul criterio CoroutineContext configurato. Ciò significa che devono eseguire la migrazione delle funzioni DAO di blocco per sospendere le funzioni.

Blocco funzione DAO (da)

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

Sospensione della funzione DAO (to)

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

È possibile eseguire la migrazione delle funzioni di blocco DAO esistenti alle funzioni di sospensione complicata se il codebase esistente non include già le coroutine. Consulta l'articolo Coroutine in Android per iniziare a utilizzare le coroutine nel tuo codebase.

Converti i tipi di reso reattivi in Flow

Non tutte le funzioni DAO devono essere funzioni di sospensione. Funzioni DAO che restituiscono tipi reattivi come LiveData o Flowable di RxJava non devono essere convertiti per sospendere le funzioni. Alcuni tipi, tuttavia, come LiveData non sono KMP compatibili. È necessario eseguire la migrazione delle funzioni DAO con tipi restituiti reattivi in fluisce della coroutina.

Tipo KMP incompatibile (da)

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

Tipo KMP compatibile (to)

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

Consulta Flows in Android per iniziare a utilizzare Flows nelle tue codebase.

(Facoltativo) Imposta un contesto per la Coroutine

Facoltativamente, un RoomDatabase può essere configurato con un'applicazione condivisa esecutori che usano RoomDatabase.Builder.setQueryExecutor() per eseguire il database operazioni. Poiché gli esecutori non sono compatibili con KMP, il valore setQueryExecutor() della stanza L'API non è disponibile per le origini comuni. RoomDatabase deve invece deve essere configurata con un CoroutineContext. È possibile impostare un contesto utilizzando RoomDatabase.Builder.setCoroutineContext(), se non è impostato nulla, allora RoomDatabase utilizzerà Dispatchers.IO per impostazione predefinita.

Imposta un driver SQLite

Una volta eseguita la migrazione degli utilizzi Support SQLite alle API SQLite Driver, viene il driver deve essere configurato utilizzando RoomDatabase.Builder.setDriver. La il conducente consigliato è BundledSQLiteDriver. Consulta Implementazioni dei driver per le descrizioni delle implementazioni disponibili dei driver.

SupportSQLiteOpenHelper.Factory personalizzata configurata con RoomDatabase.Builder.openHelperFactory() non sono supportati in KMP, le funzionalità fornite dall'helper aperto personalizzato dovranno essere implementate nuovamente Interfacce del driver SQLite.

Sposta dichiarazioni della stanza

Una volta completata la maggior parte dei passaggi della migrazione, è possibile spostare la stanza definizioni di un insieme di origini comune. Tieni presente che expect strategie su actual possono per spostare in modo incrementale le definizioni relative alle stanze. Ad esempio, se non tutti di blocco delle funzioni DAO, è possibile eseguire la migrazione delle funzioni di sospensione, dichiarare un'interfaccia annotata @Dao expect vuota nel codice comune, ma contiene funzioni di blocco 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
}