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 nuestros ejemplos 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]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }
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.room)
}
Agrega la dependencia del tiempo de ejecución de Room y la biblioteca de SQLite incluida:
commonMain.dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room.sqlite.wrapper)
}
Agrega las dependencias de KSP al bloque dependencies
de root. Ten en cuenta que debes agregar todos los objetivos que usa tu app. Para obtener más información, consulta KSP con Kotlin Multiplataforma.
dependencies {
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.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 las clases de la base de datos
Debes crear una clase de base de datos anotada con @Database
junto con las DAO y las entidades dentro del conjunto de fuentes común de tu módulo compartido de KMP. 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 puede 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 el compilador de bases de datos específico de la plataforma
Debes definir un compilador de bases de datos para crear instancias de Room en cada plataforma. Esta es la única parte de la API que debe estar en conjuntos de fuentes específicos de la plataforma debido a las diferencias en las APIs del sistema de archivos.
Android
En Android, la ubicación de la base de datos suele obtenerse a través de la API de Context.getDatabasePath()
. Para crear la instancia de base de datos, especifica un Context
junto con la ruta de acceso a 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 a la base de datos con NSFileManager
, que suele ubicarse 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,
)
}
private fun documentDirectory(): String {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
return requireNotNull(documentDirectory?.path)
}
JVM (computadoras)
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, puedes configurar el resto de la base de datos de Room en código común junto con la 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
El fragmento de código anterior llama a la función de compilador setDriver
para definir qué controlador de SQLite debe usar la base de datos de Room. Estos controladores varían según la plataforma de destino. Los fragmentos de código anteriores usan BundledSQLiteDriver
.
Este 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 SQLite proporcionado por el SO, usa la API de setDriver
en los conjuntos de fuentes 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:
AndroidSQLiteDriver
enandroidMain
NativeSQLiteDriver
eniosMain
Para usar NativeSQLiteDriver
, debes proporcionar una opción de vinculador -lsqlite3
para que la app para iOS se vincule de forma dinámica con el 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")
}
}
}
Cómo establecer un contexto de corrutina (opcional)
Un objeto RoomDatabase
en Android se puede configurar de forma opcional con ejecutores de aplicaciones compartidos a través de 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 se puede establecer con RoomDatabase.Builder.setCoroutineContext()
. Si no se establece ningún contexto, el objeto RoomDatabase
usará Dispatchers.IO
de forma predeterminada.
Reducción y ofuscación
Si el proyecto está minificado o ofuscado, 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.room.RoomDatabase { <init>(); }
Migra a Kotlin Multiplatform
Originalmente, Room se desarrolló como una biblioteca de Android y, luego, se migró a KMP con un enfoque en la compatibilidad de la API. La versión de Room para KMP difiere un poco entre las plataformas y de la versión específica para 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
debe refactorizarse con las APIs del controlador de SQLite, ya que las APIs en androidx.sqlite.db
son solo para Android (ten en cuenta el paquete diferente del paquete de KMP).
Para garantizar la retrocompatibilidad, y siempre que RoomDatabase
esté configurado con un SupportSQLiteOpenHelper.Factory
(por ejemplo, no se haya establecido ningún SQLiteDriver
), Room se comporta en "modo de compatibilidad", en el que las APIs de Support SQLite y SQLite Driver funcionan según lo esperado. Esto habilita las migraciones incrementales para que no necesites convertir todos tus usos de SQLite de Support a SQLite Driver en un solo cambio.
Cómo convertir subclases de migraciones
Las subclases de migraciones deben migrarse a las contrapartes del controlador de SQLite:
Kotlin multiplataforma
Subclases de migración
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// …
}
}
Subclases de especificación de migración automática
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// …
}
}
Solo para Android
Subclases de migración
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 la 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 fun onCreate(connection: SQLiteConnection) {
// …
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// …
}
override 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 funciones DAO de @RawQuery
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 tiempo 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 todoDao().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 tiempo 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 kotlinx.coroutines
asíncrona y rica en funciones que Kotlin ofrece para múltiples plataformas. Para una funcionalidad óptima, se aplican 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 se usa Room para KMP, todas las funciones de DAO compiladas para plataformas que no son de Android deben ser funciones suspend
.
Kotlin multiplataforma
Cómo suspender consultas
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Cómo suspender transacciones
@Transaction
suspend fun transaction() { … }
Solo para Android
Bloqueo de consultas
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Cómo bloquear transacciones
@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 se deben convertir en funciones de suspensión. Sin embargo, algunos tipos, como LiveData
, no son compatibles con el KMP. Las funciones de DAO con tipos de datos que se muestran 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>>
APIs de Convert transaction
Las APIs de transacciones de bases de datos para Room KMP pueden diferenciar entre las transacciones de escritura (useWriterConnection
) y las de lectura (useReaderConnection
).
Kotlin multiplataforma
val database: RoomDatabase = …
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 manera coherente. Puedes hacerlo con useWriterConnection
y cualquiera de los tres tipos de transacciones:
immediateTransaction
: En el modo de registro de escritura anticipada (WAL) (predeterminado), este tipo de transacción adquiere un bloqueo cuando comienza, pero los lectores pueden seguir leyendo. Esta es la opción preferida en la mayoría de los casos.deferredTransaction
: La transacción no adquirirá un bloqueo hasta la primera instrucción de escritura. Usa este tipo de transacción como una optimización cuando no sepas 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 aimmediateTransaction
en el modo WAL. En otros modos de registro, evita que otras conexiones de bases de datos lean la base de datos mientras se realiza la transacción.
Transacciones de lectura
Usa transacciones de lectura para leer de forma coherente la base de datos varias veces. Por ejemplo, cuando tienes dos o más consultas separadas y no usas una cláusula JOIN
. En las conexiones de lectores, solo se permiten transacciones diferidas. Si intentas iniciar una transacción inmediata o exclusiva en una conexión de lector, se arrojará una excepción, ya que estas se consideran operaciones de "escritura".
val database: RoomDatabase = …
database.useReaderConnection { transactor ->
transactor.deferredTransaction {
// perform database operations in transaction
}
}
No disponible en Kotlin Multiplatform
Algunas de las APIs que estaban disponibles para Android no lo están 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.setQueryCallback
RoomDatabase.QueryCallback
Tenemos la intención de agregar compatibilidad con la devolución de llamada de consultas en una versión futura de Room.
La API para configurar un RoomDatabase
con una devolución de llamada de consulta RoomDatabase.Builder.setQueryCallback
junto con la interfaz de devolución de llamada RoomDatabase.QueryCallback
no están disponibles en común y, por lo tanto, no están disponibles en otras plataformas que no sean Android.
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 y no en otras plataformas.
Base de datos preempaquetada
Las siguientes APIs para crear un RoomDatabase
con una base de datos existente (es decir, una base de datos empaquetada previamente) no están disponibles en común y, por lo tanto, no están disponibles en otras plataformas que no sean Android. Estas APIs son las siguientes:
RoomDatabase.Builder.createFromAsset
RoomDatabase.Builder.createFromFile
RoomDatabase.Builder.createFromInputStream
RoomDatabase.PrepackagedDatabaseCallback
Tenemos la intención de agregar compatibilidad con bases de datos preempaquetadas en una versión futura de Room.
Invalidación de instancias múltiples
La API para habilitar la invalidación de varias instancias, RoomDatabase.Builder.enableMultiInstanceInvalidation
, solo está disponible en Android y no en plataformas comunes o de otro tipo.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Codelab para migrar apps existentes a Room KMP
- Codelab de introducción a KMP
- Cómo guardar contenido en una base de datos local con Room