La biblioteca de persistencias Room brinda una capa de abstracción para SQLite que permite acceder a la base de datos sin problemas y, al mismo tiempo, aprovechar toda la potencia de SQLite. En esta página, se explica cómo usar Room en proyectos de Kotlin Multiplatform (KMP). Para obtener más información sobre el uso de Room, consulta Cómo guardar contenido en una base de datos local con Room o nuestras muestras oficiales.
Configura dependencias
Para configurar Room en tu proyecto de KMP, agrega las dependencias de los artefactos en el archivo build.gradle.kts de tu módulo de KMP.
Define las dependencias en el archivo libs.versions.toml:
[versions]
room3 = "3.1.0-alpha03"
sqlite = "2.7.1"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }
Agrega el complemento de Gradle de Room para configurar los esquemas de Room y el complemento de KSP.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room3)
}
Agrega la dependencia del entorno de ejecución de Room y la biblioteca de SQLite empaquetada:
commonMain.dependencies {
implementation(libs.androidx.room3.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room3.sqlite.wrapper)
}
Agrega las dependencias de KSP al bloque dependencies raíz. Debes agregar todos los destinos que usa tu app. Para obtener más información, consulta KSP con Kotlin Multiplatform.
dependencies {
add("kspAndroid", libs.androidx.room3.compiler)
add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
add("kspIosX64", libs.androidx.room3.compiler)
add("kspIosArm64", libs.androidx.room3.compiler)
// Add any other platform target you use in your project, for example kspDesktop
}
Define el directorio del esquema de Room. Para obtener más información, consulta Cómo establecer la ubicación del esquema con el complemento de Gradle de Room.
room {
schemaDirectory("$projectDir/schemas")
}
Define clases de base de datos
Debes crear una clase de base de datos anotada con @Database junto con DAOs y entidades dentro del conjunto de orígenes común de tu módulo de KMP compartido. Colocar estas clases en fuentes comunes permitirá que se compartan en todas las plataformas de destino.
// shared/src/commonMain/kotlin/Database.kt @Database(entities = [TodoEntity::class], version = 1) @ConstructedBy(AppDatabaseConstructor::class) abstract class AppDatabase : RoomDatabase() { abstract fun getDao(): TodoDao } // The Room compiler generates the `actual` implementations. @Suppress("KotlinNoActualForExpect") expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> { override fun initialize(): AppDatabase }
Cuando declaras un objeto expect con la interfaz RoomDatabaseConstructor, el compilador de Room genera las implementaciones de actual. Android Studio podría emitir la siguiente advertencia, que puedes suprimir con @Suppress("KotlinNoActualForExpect"):
Expected object 'AppDatabaseConstructor' has no actual declaration in module`
A continuación, define una nueva interfaz de DAO o mueve una existente a commonMain:
// shared/src/commonMain/kotlin/TodoDao.kt @Dao interface TodoDao { @Insert suspend fun insert(item: TodoEntity) @Query("SELECT count(*) FROM TodoEntity") suspend fun count(): Int @Query("SELECT * FROM TodoEntity") fun getAllAsFlow(): Flow<List<TodoEntity>> }
Define o mueve tus entidades a commonMain:
// shared/src/commonMain/kotlin/TodoEntity.kt @Entity data class TodoEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String )
Crea compiladores de bases de datos específicos de la plataforma
Debes definir un compilador de bases de datos para crear una instancia de Room en cada plataforma. Esta es la única parte de la API que debes definir en conjuntos de orígenes específicos de la plataforma, debido a las diferencias en las APIs del sistema de archivos.
Android
En Android, por lo general, obtienes la ubicación de la base de datos con la API de Context.getDatabasePath. Para crear la instancia de base de datos, especifica un
Context y la ruta de la base de datos.
// shared/src/androidMain/kotlin/Database.android.kt fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> { val appContext = context.applicationContext val dbFile = appContext.getDatabasePath("my_room.db") return Room.databaseBuilder<AppDatabase>( context = appContext, name = dbFile.absolutePath ) }
iOS
Para crear la instancia de la base de datos en iOS, proporciona una ruta de acceso de la base de datos con
NSFileManager, que suele encontrarse en NSDocumentDirectory.
// shared/src/iosMain/kotlin/Database.ios.kt fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> { val dbFilePath = documentDirectory() + "/my_room.db" return Room.databaseBuilder<AppDatabase>( name = dbFilePath, ) } @OptIn(ExperimentalForeignApi::class) private fun documentDirectory(): String { val documentDirectory = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, appropriateForURL = null, create = false, error = null, ) return requireNotNull(documentDirectory?.path) }
Escritorio de JVM
Para crear la instancia de la base de datos, proporciona una ruta de acceso a la base de datos con las APIs de Java o Kotlin.
// shared/src/jvmMain/kotlin/Database.desktop.kt
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
return Room.databaseBuilder<AppDatabase>(
name = dbFile.absolutePath,
)
}
Crea una instancia de la base de datos
Una vez que obtengas el RoomDatabase.Builder de uno de los constructores específicos de la plataforma, podrás configurar el resto de la base de datos de Room en el código común junto con la creación de una instancia de la base de datos real.
// shared/src/commonMain/kotlin/Database.kt fun getRoomDatabase( builder: RoomDatabase.Builder<AppDatabase> ): AppDatabase { return builder .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() }
Selecciona un controlador de SQLite
En el fragmento de código anterior, se llama a la función de compilador setDriver para definir qué controlador de SQLite debe usar la base de datos de Room. Estos controladores difieren según la plataforma de destino. En los fragmentos de código anteriores, se usa BundledSQLiteDriver.
Es el controlador recomendado que incluye SQLite compilado desde la fuente, lo que
proporciona la versión más coherente y actualizada de SQLite en todas las
plataformas.
Si quieres usar la versión de SQLite que proporciona el SO, usa la API de setDriver en los conjuntos de orígenes específicos de la plataforma que especifican un controlador específico de la plataforma. Consulta
Implementaciones de controladores para obtener descripciones de las implementaciones de controladores
disponibles. Puedes usar cualquiera de las siguientes opciones:
AndroidSQLiteDriverenandroidMainNativeSQLiteDrivereniosMain
Para usar NativeSQLiteDriver, debes proporcionar una opción de vinculador -lsqlite3 para que la app para iOS se vincule de forma dinámica con SQLite del sistema.
// shared/build.gradle.kts
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "TodoApp"
isStatic = true
// Required when using NativeSQLiteDriver
linkerOpts.add("-lsqlite3")
}
}
}
Establece un contexto de corrutina
Un objeto RoomDatabase en Android se puede configurar de forma opcional con ejecutores de aplicaciones compartidos con RoomDatabase.Builder.setQueryExecutor para realizar operaciones de bases de datos.
Debido a que los ejecutores no son compatibles con KMP, la API de setQueryExecutor de Room no está disponible en commonMain. En cambio, el objeto RoomDatabase debe configurarse con un CoroutineContext, que puedes establecer con RoomDatabase.Builder.setCoroutineContext. Si no estableces un contexto, el objeto RoomDatabase se establece de forma predeterminada en Dispatchers.IO.
Reducción y ofuscación
Si el proyecto está reducido o confuso, debes incluir la siguiente regla de ProGuard para que Room pueda encontrar la implementación generada de la definición de la base de datos:
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
Cómo migrar a Kotlin Multiplatform
Originalmente, Room se desarrolló como una biblioteca de Android y, luego, se migró a KMP con un enfoque en la compatibilidad con la API. La versión de KMP de Room difiere un poco entre las plataformas y de la versión específica de Android. Estas diferencias se enumeran y describen a continuación.
Migra de Support SQLite a SQLite Driver
Cualquier uso de SupportSQLiteDatabase y otras APIs en
androidx.sqlite.db se debe refactorizar con las APIs de SQLite Driver,
ya que las APIs de androidx.sqlite.db solo están disponibles para Android (ten en cuenta que
el paquete es diferente del paquete de KMP).
Para garantizar la retrocompatibilidad y siempre que RoomDatabase esté configurado con un SupportSQLiteOpenHelper.Factory (por ejemplo, no se establece ningún SQLiteDriver), Room se comporta en el "modo de compatibilidad", en el que las APIs de Support SQLite y SQLite Driver funcionan como se espera. Esto habilita las migraciones incrementales para que no necesites convertir todos tus usos de SQLite de compatibilidad en el controlador de SQLite en un solo cambio.
Usa el wrapper de SQLite de Room
El artefacto androidx.room3:room3-sqlite-wrapper proporciona APIs para conectar SQLiteDriver y SupportSQLiteDatabase durante la migración.
Para obtener un SupportSQLiteDatabase de un RoomDatabase configurado con un SQLiteDriver, usa la nueva función de extensión RoomDatabase.getSupportWrapper. Este wrapper de compatibilidad ayuda a mantener
los usos existentes de SupportSQLiteDatabase, que a menudo obtienes de
RoomDatabase.openHelper.writableDatabase, mientras adoptas SQLiteDriver,
especialmente para bases de código con usos extensos de la API de SupportSQLite que desean
usar BundledSQLiteDriver.
Convierte subclases de migración
Las subclases de Migrations deben migrarse a las contrapartes del controlador de SQLite:
Kotlin multiplataforma
Subclases de Migration
object Migration_1_2 : Migration(1, 2) { override suspend fun migrate(connection: SQLiteConnection) { // ... } }
Subclases de especificación de migración automática
class AutoMigrationSpec_1_2 : AutoMigrationSpec { override suspend fun onPostMigrate(connection: SQLiteConnection) { // ... } }
Solo para Android
Subclases de Migration
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// …
}
}
Subclases de especificación de migración automática
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// …
}
}
Devolución de llamada de conversión de base de datos
Las devoluciones de llamada de la base de datos deben migrarse a las contrapartes del controlador de SQLite:
Kotlin multiplataforma
object MyRoomCallback : RoomDatabase.Callback() { override suspend fun onCreate(connection: SQLiteConnection) { // ... } override suspend fun onDestructiveMigration(connection: SQLiteConnection) { // ... } override suspend fun onOpen(connection: SQLiteConnection) { // ... } }
Solo para Android
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
// …
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// …
}
override fun onOpen(db: SupportSQLiteDatabase) {
// …
}
}
Convierte @RawQuery funciones de DAO
Las funciones anotadas con @RawQuery que se compilan para plataformas que no son de Android
deberán declarar un parámetro de tipo RoomRawQuery en lugar de
SupportSQLiteQuery.
Kotlin multiplataforma
Define la consulta sin procesar
@Dao interface TodoDao { @RawQuery suspend fun getTodos(query: RoomRawQuery): List<TodoEntity> }
Luego, se puede usar un RoomRawQuery para crear una consulta en el entorno de ejecución:
suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> { val query = RoomRawQuery( sql = "SELECT * FROM TodoEntity WHERE title = ?", onBindStatement = { it.bindText(1, title.lowercase()) } ) return getDao().getTodos(query) }
Solo para Android
Define la consulta sin procesar
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}
Luego, se puede usar un SimpleSQLiteQuery para crear una consulta en el entorno de ejecución:
suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
val query = SimpleSQLiteQuery(
query = "SELECT * FROM TodoEntity WHERE title = ?",
bindArgs = arrayOf(title.lowercase())
)
return getTodos(query)
}
Convierte funciones DAO de bloqueo
Room se beneficia de la biblioteca asíncrona kotlinx.coroutines, que ofrece muchas funciones y que Kotlin proporciona para varias plataformas. Para obtener una funcionalidad óptima, se aplican de manera forzosa las funciones suspend para los DAO compilados en un proyecto de KMP, con la excepción de los DAO implementados en androidMain para mantener la retrocompatibilidad con la base de código existente. Cuando usas Room para KMP, todas las funciones de DAO compiladas para
plataformas que no son de Android deben ser funciones suspend.
Kotlin multiplataforma
Suspende consultas
@Query("SELECT * FROM Todo") suspend fun getAllTodos(): List<Todo>
Cómo suspender transacciones
@Transaction suspend fun transaction() { // ... }
Solo para Android
Consultas de bloqueo
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Transacciones de bloqueo
@Transaction
fun blockingTransaction() { … }
Cómo convertir tipos reactivos en Flow
No todas las funciones de DAO deben ser funciones de suspensión. Las funciones DAO que devuelven tipos reactivos, como LiveData o Flowable de RxJava, no deben convertirse en funciones de suspensión. Sin embargo, algunos tipos, como LiveData, no son compatibles con KMP. Las funciones DAO con tipos de datos que se devuelven reactivos deben migrarse a flujos de corrutinas.
Kotlin multiplataforma
Tipos reactivos Flows
@Query("SELECT * FROM Todo") fun getTodosFlow(): Flow<List<Todo>>
Solo para Android
Tipos reactivos, como LiveData o Flowable de RxJava
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Cómo convertir las APIs de transacciones
Las APIs de transacciones de bases de datos para Room KMP pueden diferenciar entre las transacciones de escritura (useWriterConnection) y lectura (useReaderConnection).
Kotlin multiplataforma
val database: RoomDatabase = getRoomDatabase(builder) database.useWriterConnection { transactor -> transactor.immediateTransaction { // perform database operations in transaction. } }
Solo para Android
val database: RoomDatabase = …
database.withTransaction {
// perform database operations in transaction
}
Transacciones de escritura
Usa transacciones de escritura para asegurarte de que varias consultas escriban datos de forma atómica,
de modo que los lectores puedan acceder a los datos de forma coherente. Puedes hacerlo con
useWriterConnection con cualquiera de los tres tipos de transacciones:
immediateTransaction: En el modo de almacenamiento de registros de escritura anticipada (WAL) (predeterminado), este tipo de transacción adquiere un bloqueo cuando se inicia, pero los lectores pueden seguir leyendo. Esta es la opción preferida para la mayoría de los casos.deferredTransaction: La transacción no adquirirá un bloqueo hasta la primera sentencia de escritura. Usa este tipo de transacción como una optimización cuando no estés seguro de si se necesitará una operación de escritura dentro de la transacción. Por ejemplo, si inicias una transacción para borrar canciones de una playlist con solo el nombre de la playlist y esta no existe, no se necesita ninguna operación de escritura (borrado).exclusiveTransaction: Este modo se comporta de manera idéntica aimmediateTransactionen el modo WAL. En otros modos de registro, evita que otras conexiones de base de datos lean la base de datos mientras la transacción está en curso.
Transacciones de lectura
Usa transacciones de lectura para leer de forma coherente de la base de datos varias veces. Por
ejemplo, cuando tienes dos o más consultas independientes y no usas una cláusula
JOIN. Solo se permiten transacciones diferidas en las conexiones de lector. Si intentas
iniciar una transacción inmediata o exclusiva en una conexión de lector, se arrojará
una excepción, ya que se consideran operaciones de "escritura".
val database: RoomDatabase = getRoomDatabase(builder) database.useReaderConnection { transactor -> transactor.deferredTransaction { // perform database operations in transaction. } }
No está disponible en Kotlin Multiplatform.
Algunas de las APIs que estaban disponibles para Android no están disponibles en Kotlin Multiplatform.
Devolución de llamada de consulta
Las siguientes APIs para configurar devoluciones de llamada de consultas no están disponibles en común y, por lo tanto, no están disponibles en plataformas que no sean Android.
RoomDatabase.Builder.setQueryCallbackRoomDatabase.QueryCallback
Cierre automático de la base de datos
La API para habilitar el cierre automático después de un tiempo de espera,
RoomDatabase.Builder.setAutoCloseTimeout, solo está disponible en Android.
Base de datos preempaquetada
Las siguientes APIs para crear una RoomDatabase con una base de datos existente (una
base de datos preempaquetada) no están disponibles en común y, por lo tanto, no están disponibles en
plataformas que no sean Android:
RoomDatabase.Builder.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.PrepackagedDatabaseCallback
Invalidación de instancias múltiples
La API para habilitar la invalidación de instancias múltiples, RoomDatabase.Builder.enableMultiInstanceInvalidation, solo está disponible en Android y no en plataformas comunes ni en otras plataformas.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Codelab para migrar apps existentes a Room en KMP
- Codelab para comenzar a usar KMP
- Cómo guardar datos en una base de datos local con Room