Cómo migrar Room a Kotlin multiplataforma

En este documento, se describe cómo migrar una implementación de Room existente a una que usa Kotlin multiplataforma (KMP).

Migración de los usos de Room en una base de código de Android existente a un KMP compartido común puede variar mucho en dificultad según las APIs de Room que se usen o si la base de código ya usa corrutinas. En esta sección, se ofrecen orientación y sugerencias cuando intentas migrar los usos de Room a un módulo común.

Es importante que primero te familiarices con las diferencias y los elementos faltantes entre la versión para Android de Room y la versión de KMP, junto con la configuración involucrada. En términos simples, una migración exitosa implica refactorizar usos de las APIs de SupportSQLite* y reemplazarlas por las APIs del controlador de SQLite además de mover declaraciones de Room (clase con anotaciones @Database, DAOs, entidades, etc.) en un código común.

Revisa la siguiente información antes de continuar:

En las siguientes secciones, se describen los diversos pasos necesarios para un proceso migración.

Cómo migrar del controlador de compatibilidad con SQLite al controlador de SQLite

Las APIs de androidx.sqlite.db son solo para Android, y todos los usos deben o refactorización con las APIs de SQLite Driver. Para la retrocompatibilidad y siempre que RoomDatabase se configura con un SupportSQLiteOpenHelper.Factory (es decir, no se configura SQLiteDriver), Room se comportará en "modo de compatibilidad". dónde admiten que las APIs de Driver de SQLite y SQLite funcionen como se espera. Esto permite de modo que no necesites convertir todas tus instancias de Support SQLite al controlador de SQLite en un solo cambio.

Los siguientes ejemplos son usos comunes de la compatibilidad con SQLite y su SQLite Contrapartes del conductor:

Compatibilidad con SQLite (de)

Ejecuta una consulta sin resultados

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

Ejecuta una consulta con resultado, pero sin argumentos

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

Ejecuta una consulta con el resultado y los argumentos

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

Controlador de SQLite (para)

Ejecuta una consulta sin resultados

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

Ejecuta una consulta con resultado, pero sin argumentos

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

Ejecuta una consulta con el resultado y los argumentos

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

Las APIs de transacción de Database están disponibles directamente en SupportSQLiteDatabase con beginTransaction(), setTransactionSuccessful() y endTransaction(). También están disponibles a través de Room con runInTransaction(). Migrar estas a las APIs de Driver de SQLite.

Compatibilidad con SQLite (de)

Cómo realizar una transacción (mediante RoomDatabase)

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

Cómo realizar una transacción (mediante SupportSQLiteDatabase)

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

Controlador de SQLite (para)

Cómo realizar una transacción (mediante RoomDatabase)

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

Cómo realizar una transacción (mediante 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")
}

También se deben migrar varias anulaciones de devolución de llamada a sus equivalentes del controlador:

Compatibilidad con SQLite (de)

Subclases de migración

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

Subcategorías de especificación de migración automática

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

Subclases de devolución de llamada de la base de datos

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

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

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

Controlador de SQLite (para)

Subclases de migración

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

Subcategorías de especificación de migración automática

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

Subclases de devolución de llamada de la base de datos

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

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

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

Para resumir, reemplaza los usos de SQLiteDatabase por SQLiteConnection cuando un elemento RoomDatabase no está disponible, como en anulaciones de devolución de llamada (onMigrate, onCreate, etcétera). Si hay un RoomDatabase disponible, accederás al a la base de datos con RoomDatabase.useReaderConnection y RoomDatabase.useWriterConnection en lugar de RoomDatabase.openHelper.writtableDatabase

Cómo convertir funciones DAO de bloqueo en funciones de suspensión

La versión de KMP de Room se basa en corrutinas para realizar E/S. operaciones en el CoroutineContext configurado. Esto significa que deberá migrar cualquier función DAO de bloqueo para suspender funciones.

Función DAO de bloqueo (de)

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

Suspende la función DAO (para)

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

Se puede migrar funciones de bloqueo de DAO existentes para funciones de suspensión se complica si la base de código existente no incorpora corrutinas. Consulta Corrutinas en Android para comenzar a usar corrutinas. en tu base de código.

Convertir tipos de datos reactivos que se devuelven en Flow

No todas las funciones DAO deben ser funciones de suspensión. Funciones DAO que muestran los tipos reactivos, como LiveData o Flowable de RxJava, no se deben convertir suspender funciones. Sin embargo, algunos tipos, como LiveData, no son KMP. compatibles. Se deben migrar las funciones DAO con tipos de datos reactivos que se muestran de corrutinas.

Tipo de KMP incompatible (de)

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

Tipo de KMP compatible (a)

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

Consulta Flujos en Android para comenzar a usar flujos en tu base de código.

Cómo establecer un contexto de corrutina (opcional)

De manera opcional, se puede configurar una RoomDatabase con una aplicación compartida ejecutores que usan RoomDatabase.Builder.setQueryExecutor() para realizar una base de datos las operaciones. Como los ejecutores no son compatibles con KMP, setQueryExecutor() de Room La API no está disponible para fuentes comunes. En cambio, RoomDatabase debe configurar con un CoroutineContext. Se puede establecer un contexto usando RoomDatabase.Builder.setCoroutineContext(). Si no hay ninguno establecido, el RoomDatabase usará Dispatchers.IO de forma predeterminada.

Cómo establecer un controlador SQLite

Una vez que se hayan migrado los usos de SQLite Driver a las APIs del controlador de SQLite, se el controlador debe configurarse con RoomDatabase.Builder.setDriver. El El controlador recomendado es BundledSQLiteDriver. Consulta Implementaciones de controladores para descripciones de las implementaciones de controladores disponibles.

SupportSQLiteOpenHelper.Factory personalizado configurado con RoomDatabase.Builder.openHelperFactory() no son compatibles con KMP, el las funciones proporcionadas por el asistente de apertura personalizado deberán volver a implementarse con Interfaces del controlador de SQLite.

Declaraciones de Move Room

Cuando se completan la mayoría de los pasos de la migración, se puede mover Room. definiciones a un conjunto de orígenes común. Ten en cuenta que las estrategias expect de actual sí pueden Se usará para trasladar de forma incremental las definiciones relacionadas con Room. Por ejemplo, si no todas las funciones DAO de bloqueo se pueden migrar a funciones de suspensión, es posible declara una interfaz con anotaciones expect @Dao que está vacía en código común, pero contiene funciones de bloqueo en 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
}