Room (multiplataforma de Kotlin)

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. Esta página se enfoca en el uso de Room en proyectos multiplataforma de Kotlin (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

La versión actual de Room que admite KMP es la 2.7.0-alpha01 o una posterior.

Para configurar Room en tu proyecto de KMP, agrega las dependencias de los artefactos en el archivo build.gradle.kts de tu módulo:

  • androidx.room:room-gradle-plugin: Es el complemento de Gradle para configurar esquemas de Room.
  • androidx.room:room-compiler: Es el procesador KSP que genera código.
  • androidx.room:room-runtime: Es la parte del tiempo de ejecución de la biblioteca.
  • androidx.sqlite:sqlite-bundled: Es la biblioteca de SQLite integrada (opcional).

Además, debes configurar el controlador SQLite de Room. Estos controladores difieren según la plataforma de segmentación. Consulta Implementaciones de controladores para obtener descripciones de las implementaciones de controladores disponibles.

Para obtener más información sobre la configuración, consulta los siguientes vínculos:

Define las clases de la base de datos

Debes crear una clase de base de datos anotada con @Database junto con DAO y entidades dentro del conjunto de orígenes común de tu módulo KMP compartido. Si colocas estas clases en fuentes comunes, podrás compartirlas en todas las plataformas de destino.

// shared/src/commonMain/kotlin/Database.kt

@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
}

@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>>
}

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

Ten en cuenta que puedes usar declaraciones reales y previsibles para crear implementaciones de Room específicas para la plataforma. Por ejemplo, puedes agregar un DAO específico de la plataforma que esté definido en el código común con expect y, luego, especificar las definiciones de actual con consultas adicionales en conjuntos de orígenes específicos de la plataforma.

Crea el compilador de bases de datos

Debes definir un creador de bases de datos para crear una instancia de Room en cada plataforma. Esta es la única parte de la API que debe estar en conjuntos de orígenes específicos de la plataforma debido a las diferencias en las APIs del sistema de archivos. Por ejemplo, en Android, la ubicación de la base de datos suele obtenerse a través de la API de Context.getDatabasePath(), mientras que, para iOS, la ubicación de la base de datos se obtiene con NSHomeDirectory.

Android

Para crear la instancia de base de datos, especifica un Contexto junto con la ruta de la base de datos. No es necesario que especifiques una fábrica de base de datos.

// shared/src/androidMain/kotlin/Database.kt

fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder<AppDatabase> {
    val appContext = ctx.applicationContext
    val dbFile = appContext.getDatabasePath("my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        context = appContext,
        name = dbFile.absolutePath
    )
}

iOS

Para crear la instancia de base de datos, proporciona una fábrica de base de datos junto con la ruta de la base de datos. La fábrica de base de datos es una función lambda que invoca una función de extensión generada cuyo nombre es instantiateImpl con un receptor del tipo KClass<T>, en el que T es el tipo de la clase con anotaciones @Database.

// shared/src/iosMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFilePath = NSHomeDirectory() + "/my_room.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFilePath,
        factory =  { AppDatabase::class.instantiateImpl() }
    )
}

JVM (computadoras de escritorio)

Para crear la instancia de base de datos, especifica solo la ruta de acceso de la base de datos. No necesitas proporcionar una fábrica de bases de datos.

// shared/src/commonMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

Creación de instancias 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 código común junto con la creación de instancias de la base de datos real.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
    return builder
        .addMigrations(MIGRATIONS)
        .fallbackToDestructiveMigrationOnDowngrade()
        .setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}

Cómo seleccionar un SQLiteDriver

Los fragmentos de código anteriores usan BundledSQLiteDriver. Este es el controlador recomendado que incluye SQLite compilado desde la fuente, 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 conjuntos de orígenes específicos de la plataforma que especifiquen un controlador específico de esta. En Android, puedes usar AndroidSQLiteDriver, mientras que, en iOS, puedes usar NativeSQLiteDriver. Si quieres usar NativeSQLiteDriver, debes proporcionar una opción de vinculador para que la app para iOS se vincule de forma dinámica con el sistema SQLite.

// 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")
        }
    }
}

Diferencias

Originalmente, Room se desarrolló como una biblioteca de Android y, luego, se migró a KMP con un enfoque en la compatibilidad de las APIs. 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 de la siguiente manera.

Bloquea funciones DAO

Cuando se usa Room para KMP, todas las funciones DAO compiladas para plataformas que no son de Android deben ser suspend, excepto los tipos de datos que se muestran reactivos, como Flow.

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
    // ERROR: Blocking function not valid for non-Android targets
    @Query("SELECT * FROM Entity")
    fun blockingQuery(): List<Entity>

    // OK
    @Query("SELECT * FROM Entity")
    suspend fun query(): List<Entity>

    // OK
    @Query("SELECT * FROM Entity")
    fun queryFlow(): Flow<List<Entity>>

    // ERROR: Blocking function not valid for non-Android targets
    @Transaction
    fun blockingTransaction() { // … }

    // OK
    @Transaction
    suspend fun transaction() { // … }
}

Room se beneficia de la biblioteca kotlinx.coroutines asíncrona y con abundantes funciones que Kotlin ofrece para varias plataformas. Para obtener una funcionalidad óptima, se aplican las funciones suspend a los DAOs compilados en un proyecto de KMP, a excepción de los DAO específicos de Android, a fin de mantener la retrocompatibilidad con la base de código existente.

Diferencias entre funciones con KMP

En esta sección, se describe en qué se diferencian las funciones de Room entre las versiones de la plataforma de KMP y Android.

Funciones DAO @RawQuery

Las funciones anotadas con @RawQuery que se compilen para plataformas que no son de Android producirán un error. Tenemos la intención de agregar compatibilidad con @RawQuery en una versión futura de Room.

Devolución de llamada de consulta

Las siguientes APIs para configurar devoluciones de llamada de consulta no están disponibles en común y, por lo tanto, tampoco lo están en plataformas distintas de 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.

Las APIs para configurar un objeto 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.

Base de datos de cierre automático

La API que permite 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 previa al paquete

Las siguientes APIs para crear un RoomDatabase usando 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 son las APIs:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Tenemos la intención de agregar compatibilidad con bases de datos empaquetadas previamente 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 está disponible en plataformas comunes ni en otras plataformas.