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
}