Migrazione di stanze a Kotlin Multiplaform

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

La migrazione degli utilizzi delle stanze in un codebase Android esistente a un modulo KMP condiviso comune può variare notevolmente in base alle API Room utilizzate o se il codebase utilizza già Coroutine. Questa sezione offre alcune indicazioni e suggerimenti per la migrazione degli utilizzi di Room a un modulo comune.

È importante prima acquisire dimestichezza con le differenze e le funzionalità mancanti tra la versione Android di Room e la versione KMP, nonché con la configurazione richiesta. In sostanza, una migrazione riuscita comporta il refactoring degli utilizzi delle API SupportSQLite* e la loro sostituzione con API SQLite Driver, oltre allo spostamento delle dichiarazioni Room (classe annotata @Database, DAO, entità e così via) nel codice comune.

Prima di continuare, esamina le seguenti informazioni:

Le sezioni successive descrivono i vari passaggi necessari per una migrazione riuscita.

Esegui la migrazione da Support SQLite a SQLite Driver

Le API in androidx.sqlite.db sono solo Android e qualsiasi utilizzo deve essere sottoposto a refactoring con le API SQLite Driver. Per la compatibilità con le versioni precedenti e purché RoomDatabase sia configurato con un SupportSQLiteOpenHelper.Factory (ovvero non è impostato alcun SQLiteDriver), la stanza si comporta in "modalità di compatibilità", in cui entrambe le API Support SQLite e SQLite Driver funzionano come previsto. Ciò consente migrazioni incrementali in modo che non sia necessario convertire tutti gli utilizzi di SQLite di supporto in SQLite Driver in una singola modifica.

I seguenti esempi sono usi comuni di Support SQLite e le relative controparti dei driver SQLite:

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 (to)

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 di 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 utilizzi alle 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 (to)

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 anche eseguire la migrazione di vari override di callback alle controparti del conducente:

Supporta SQLite (da)

Sottoclassi di migrazione

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

Sottoclassi delle specifiche di 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 (to)

Sottoclassi di migrazione

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

Sottoclassi delle specifiche di 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) {
    // ...
  }
}

Per riepilogare, 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 elemento RoomDatabase, accedi alla connessione al database sottostante utilizzando RoomDatabase.useReaderConnection e RoomDatabase.useWriterConnection anziché RoomDatabase.openHelper.writtableDatabase.

Converti le funzioni di blocco DAO per sospendere le funzioni

La versione KMP della sala si basa sulle coroutine per eseguire operazioni di I/O sull'oggetto CoroutineContext configurato. Ciò significa che devi eseguire la migrazione di qualsiasi funzione DAO di blocco per sospendere le funzioni.

Blocco funzione DAO (da)

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

Sospensione della funzione DAO (a)

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

La migrazione delle funzioni di blocco DAO esistenti per sospendere le funzioni può essere complicata se il codebase esistente non incorpora già coroutine. Consulta la pagina relativa alle coroutine in Android per iniziare a utilizzare le coroutine nel codebase.

Converti i tipi di ritorno reattivi in Flow

Non tutte le funzioni DAO devono essere sospese. Le funzioni DAO che restituiscono tipi reattivi come LiveData o Flowable di RxJava non devono essere convertite in funzioni di sospensione. Tuttavia, alcuni tipi, come LiveData, non sono compatibili con KMP. È necessario eseguire la migrazione delle funzioni DAO con tipi restituiti reattivi ai flussi di coroutine.

Tipo KMP incompatibile (da)

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

Tipo di KMP compatibile (a)

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

Consulta Flows in Android per iniziare a utilizzare Flows nel codebase.

(Facoltativo) Imposta un contesto coroutine

Facoltativamente, puoi configurare RoomDatabase con esecutori di applicazioni condivisi utilizzando RoomDatabase.Builder.setQueryExecutor() per eseguire operazioni sul database. Poiché gli esecutori non sono compatibili con KMP, l'API setQueryExecutor() di Room non è disponibile per origini comuni. RoomDatabase deve invece essere configurato con un CoroutineContext. È possibile impostare un contesto utilizzando RoomDatabase.Builder.setCoroutineContext(). Se non ne è configurato nessuno, RoomDatabase utilizzerà Dispatchers.IO per impostazione predefinita.

Imposta un driver SQLite

Una volta eseguita la migrazione degli utilizzi dell'assistenza SQLite alle API SQLite Driver, è necessario configurare un driver utilizzando RoomDatabase.Builder.setDriver. Il driver consigliato è BundledSQLiteDriver. Per le descrizioni delle implementazioni disponibili, consulta Implementazioni dei driver.

I SupportSQLiteOpenHelper.Factory personalizzati configurati utilizzando RoomDatabase.Builder.openHelperFactory() non sono supportati in KMP, le funzionalità fornite dall'helper aperto personalizzato dovranno essere reimplementate con le interfacce del driver SQLite.

Dichiarazioni relative alla stanza virtuale

Una volta completata la maggior parte dei passaggi della migrazione, puoi spostare le definizioni delle stanze in un set di origine comune. Tieni presente che è possibile utilizzare le strategie expect / actual per spostare in modo incrementale le definizioni relative alle stanze. Ad esempio, se non è possibile eseguire la migrazione di tutte le funzioni DAO di blocco per sospendere le funzioni, è possibile dichiarare un'interfaccia annotata expect @Dao che è 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
}