Migrer Room vers la multiplateforme Kotlin

Ce document explique comment migrer une implémentation Room existante vers une autre qui utilise la multiplateforme Kotlin (KMP).

Migrer les utilisations de Room dans un codebase Android existant vers un KMP partagé commun la difficulté du module peut varier considérablement selon les API Room utilisées ou si le codebase utilise déjà des coroutines. Cette section fournit des conseils et des astuces lorsque vous essayez de migrer des utilisations de Room vers un module commun.

Il est important de vous familiariser d'abord avec les différences et les entre la version Android de Room et la version KMP, ainsi que la configuration impliquée. Concrètement, une migration réussie implique la refactorisation utilisations des API SupportSQLite* et les remplacer par les API SQLite Driver ainsi que les déclarations de Room (classe annotée @Database, DAO, entités, etc.) en code commun.

Relisez les informations suivantes avant de continuer:

Les sections suivantes décrivent les différentes étapes requises pour réussir la migration.

Passer du pilote SQLite au pilote SQLite

Les API dans androidx.sqlite.db sont disponibles uniquement sur Android, et toutes les utilisations doivent être refactorisé avec les API SQLite Driver. Pour assurer la rétrocompatibilité, et tant que le RoomDatabase est configuré avec un SupportSQLiteOpenHelper.Factory (par exemple, qu'aucun SQLiteDriver n'est défini), Room se comporte alors en "mode de compatibilité" où Les API Support SQLite et SQLite Driver fonctionnent comme prévu. Cela permet des migrations incrémentielles afin de ne pas avoir à convertir au pilote SQLite en une seule modification.

Les exemples suivants illustrent des utilisations courantes de l'assistance SQLite et de leur SQLite Équivalents conducteurs:

Compatibilité avec SQLite (à partir de)

Exécuter une requête sans résultat

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

Exécuter une requête avec un résultat mais sans argument

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

Exécuter une requête avec un résultat et des arguments

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

Pilote SQLite (vers)

Exécuter une requête sans résultat

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

Exécuter une requête avec un résultat mais sans argument

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

Exécuter une requête avec un résultat et des arguments

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

Les API de transaction de base de données sont disponibles directement dans SupportSQLiteDatabase avec beginTransaction(), setTransactionSuccessful() et endTransaction(). Ils sont également disponibles dans Room avec runInTransaction(). Migrer ces éléments vers les API SQLite Driver.

Compatibilité avec SQLite (à partir de)

Effectuer une transaction (avec RoomDatabase)

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

Effectuer une transaction (avec SupportSQLiteDatabase)

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

Pilote SQLite (vers)

Effectuer une transaction (avec RoomDatabase)

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

Effectuer une transaction (avec 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")
}

Différents remplacements de rappel doivent également être migrés vers leurs équivalents pilotes:

Compatibilité avec SQLite (à partir de)

Sous-classes de migration

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

Sous-classes de spécification de migration automatique

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

Sous-classes de rappel de base de données

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

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

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

Pilote SQLite (vers)

Sous-classes de migration

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

Sous-classes de spécification de migration automatique

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

Sous-classes de rappel de base de données

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

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

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

Pour résumer, remplacez les utilisations de SQLiteDatabase par SQLiteConnection lorsqu'un RoomDatabase n'est pas disponible, comme dans les remplacements de rappel (onMigrate, onCreate, etc.). Si un RoomDatabase est disponible, accédez à l'instance connexion à la base de données à l'aide de RoomDatabase.useReaderConnection et RoomDatabase.useWriterConnection au lieu de RoomDatabase.openHelper.writtableDatabase

Convertir les fonctions DAO bloquantes en fonctions de suspension

La version KMP de Room repose sur des coroutines pour effectuer des E/S. des opérations sur le CoroutineContext configuré. Cela signifie que vous n'avez pas besoin de migrer toutes les fonctions DAO bloquantes vers les fonctions de suspension.

Blocage de la fonction DAO (de)

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

Suspension de la fonction DAO (vers)

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

La migration des fonctions de blocage DAO existantes vers des fonctions de suspension peut être compliquée si le codebase existant n'intègre pas déjà des coroutines. Consultez Coroutines sur Android pour découvrir comment utiliser des coroutines. dans votre codebase.

Convertir les types renvoyés réactifs en flux

Toutes les fonctions DAO n'ont pas besoin d'être des fonctions de suspension. Les fonctions DAO qui renvoient Les types réactifs tels que LiveData ou Flowable de RxJava ne doivent pas être convertis des fonctions de suspension. Toutefois, certains types, comme LiveData, ne sont pas des KMP compatibles. Les fonctions DAO avec des types renvoyés réactifs doivent être migrées vers flux de coroutine.

Type de KMP incompatible (à partir de)

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

Type de KMP compatible (vers)

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

Reportez-vous à la section Flux dans Android pour découvrir comment utiliser les flux dans votre de votre codebase.

Définir un contexte de coroutine (facultatif)

Vous pouvez éventuellement configurer un RoomDatabase avec une application partagée exécuteurs utilisant RoomDatabase.Builder.setQueryExecutor() pour exécuter la base de données opérations. Comme les exécuteurs ne sont pas compatibles avec KMP, le setQueryExecutor() de Room L'API n'est pas disponible pour les sources courantes. À la place, RoomDatabase doit être configuré avec un CoroutineContext. Un contexte peut être défini à l'aide de RoomDatabase.Builder.setCoroutineContext(), si aucune valeur n'est définie, le RoomDatabase utilisera Dispatchers.IO par défaut.

Configurer un pilote SQLite

Une fois que les utilisations de Support SQLite ont été migrées vers les API SQLite Driver, le pilote doit être configuré avec RoomDatabase.Builder.setDriver. La Le conducteur recommandé est BundledSQLiteDriver. Consultez Implémentations de pilotes pour des implémentations de pilotes disponibles.

SupportSQLiteOpenHelper.Factory personnalisé configuré avec Les RoomDatabase.Builder.openHelperFactory() ne sont pas compatibles avec KMP, les fonctionnalités fournies par l'assistant ouvert personnalisé devront être réimplémentées avec Interfaces du pilote SQLite.

Déplacer les déclarations Room

Une fois la plupart des étapes de migration terminées, vous pouvez déplacer Room à un ensemble de sources commun. Notez que les stratégies expect / actual peuvent être utilisée pour déplacer progressivement les définitions liées à Room. Par exemple, si les les fonctions DAO bloquantes peuvent être migrées vers des fonctions de suspension, il est possible de déclarer une interface annotée expect @Dao vide dans le code commun, mais contient des fonctions de blocage dans 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
}