Persistencia de Datos en Android Usando Realm
Cuando hablamos de persistencia de datos local en Android, Room sobre SQLite es la solución estándar de Jetpack. Sin embargo, existen alternativas poderosas con enfoques diferentes. Una de las más destacadas es Realm, una base de datos móvil diseñada desde cero para ser rápida, fácil de usar y orientada a objetos.
A diferencia de Room/SQLite que son relacionales, Realm te permite trabajar directamente con tus objetos Kotlin, eliminando la necesidad de mapeo objeto-relacional (ORM). Con el moderno Realm Kotlin SDK, diseñado para Kotlin Multiplatform y Android, integrar Realm en tus proyectos es más sencillo que nunca.
Si buscas una base de datos reactiva, con una API intuitiva y un enfoque centrado en objetos, ¡Realm merece tu atención!
¿Por qué elegir Realm?
Realm se diferencia de las soluciones basadas en SQLite en varios aspectos clave:
- Verdaderamente Orientado a Objetos: Define tus modelos como simples clases Kotlin que heredan de
RealmObject
. Almacenas y recuperas objetos directamente, sin ORMs ni SQL. - Reactividad Nativa: Los objetos y resultados de consultas de Realm son "vivos". Cualquier cambio en la base de datos se refleja automáticamente en los objetos y colecciones que tienes en memoria, simplificando enormemente la actualización de la UI, especialmente al combinarse con Kotlin Flow.
- API Sencilla e Intuitiva: Las operaciones CRUD (Crear, Leer, Actualizar, Borrar) se realizan a través de una API fluida y fácil de aprender.
- Rendimiento: Realm está optimizado para dispositivos móviles y a menudo muestra un excelente rendimiento, especialmente en operaciones complejas o con grandes volúmenes de datos.
- Kotlin Multiplatform (KMP): El SDK está diseñado pensando en KMP, lo que facilita compartir tu lógica de base de datos entre Android, iOS, Desktop y otras plataformas soportadas por Kotlin.
- Características Avanzadas Integradas: Soporte nativo para migraciones de esquema, encriptación de base de datos y relaciones entre objetos.
- Sincronización Opcional (Atlas Device Sync): Permite sincronizar datos de forma transparente entre dispositivos y la nube (MongoDB Atlas).
Conceptos Clave de Realm Kotlin SDK
Para trabajar con Realm, necesitas entender algunos conceptos:
- Objeto Realm (
RealmObject
): Es la clase base para todos los modelos que quieras persistir en Realm. Define las propiedades de tu objeto dentro de una clase que herede deRealmObject
. - Configuración de Realm (
RealmConfiguration
): Define cómo se abrirá una instancia de Realm. Incluye el esquema (la lista de clasesRealmObject
), el nombre del archivo de la base de datos, configuraciones de migración, clave de encriptación, etc. - Instancia de Realm (
Realm
): Es el punto de entrada principal para interactuar con la base de datos. Se obtiene abriendo una configuración (Realm.open(config)
). Importante: Las instancias de Realm deben cerrarse cuando ya no se necesiten para liberar recursos (realm.close()
). - Consultas (
realm.query<T>(...)
): Realm proporciona una API fluida y segura para buscar objetos, usando un lenguaje de consulta similar a SQL pero adaptado a objetos (Realm Query Language - RQL). - Escrituras (
realm.write { ... }
/realm.writeBlocking { ... }
): Todas las operaciones que modifican la base de datos (crear, actualizar, borrar) deben realizarse dentro de un bloque de transacción de escritura. - Objetos y Colecciones Vivas / Flows: Los resultados de las consultas pueden ser observados como Kotlin Flows (usando
.asFlow()
), que emiten automáticamente nuevas versiones cuando los datos subyacentes cambian.
Configurando Realm en tu Proyecto Android
Empezar con Realm implica configurar el plugin de Gradle y añadir dependencias:
Paso 1: Añadir Plugin y Dependencias de Gradle
1. Archivo build.gradle.kts
(Nivel de Proyecto):
// Top-level build file
plugins {
id("com.android.application") version "8.X.X" apply false // Tu versión
id("org.jetbrains.kotlin.android") version "1.9.XX" apply false // Tu versión
// Plugin de Realm Kotlin
id("io.realm.kotlin") version "1.15.0" apply false // Usa la última versión estable de Realm
}
2. Archivo build.gradle.kts
(Nivel de App):
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
// Aplicar el plugin de Realm Kotlin
id("io.realm.kotlin")
}
android {
// ... otras configuraciones
}
dependencies {
// Dependencia base de Realm Kotlin (para uso local)
implementation("io.realm.kotlin:library-base:1.15.0") // Usa la misma versión que el plugin
// Si planeas usar Atlas Device Sync:
// implementation("io.realm.kotlin:library-sync:1.15.0")
// Dependencias necesarias para Coroutines y Flow si las usas
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") // O versión más reciente
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") // Para viewModelScope
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") // Para lifecycleScope
// ... otras dependencias
}
Nota: Reemplaza 1.15.0
y otras versiones con las últimas estables disponibles. Consulta la documentación oficial de Realm Kotlin SDK. Sincroniza Gradle.
Paso 2: Definir el Modelo de Objeto Realm
Crea una clase Kotlin que herede de RealmObject
. Las propiedades deben tener valores por defecto o ser nulables.
package com.example.myapp.data.realm.model
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.annotations.PrimaryKey
import org.mongodb.kbson.ObjectId // Para IDs únicos tipo MongoDB
// Hereda de RealmObject
class Task : RealmObject {
@PrimaryKey // Define la clave primaria
var _id: ObjectId = ObjectId() // Tipo común para IDs en Realm/MongoDB
var summary: String = "" // Debe tener valor inicial
var isComplete: Boolean = false
var owner: String? = null // Propiedad nulable
}
Paso 3: Configurar y Abrir una Instancia de Realm
Necesitas definir una RealmConfiguration
y luego abrir la instancia de Realm
. Esto a menudo se hace en una capa de repositorio o se gestiona con DI, asegurándose de que la instancia se cierre correctamente.
package com.example.myapp.data.realm
import com.example.myapp.data.realm.model.Task
import io.realm.kotlin.Realm
import io.realm.kotlin.RealmConfiguration
object RealmProvider {
// Configuración de la base de datos Realm local
val config: RealmConfiguration by lazy {
RealmConfiguration.Builder(
schema = setOf(Task::class) // Lista todas tus clases RealmObject
)
.name("myApp.realm") // Nombre del archivo de la base de datos
// .schemaVersion(1) // Versión del esquema (para migraciones)
// .migration(...) // Añadir migraciones si es necesario
// .encryptionKey(...) // Añadir clave de encriptación si se requiere
.build()
}
// Función para abrir una instancia (podría ser suspend si es necesario)
// Nota: ¡La gestión del ciclo de vida (abrir/cerrar) es crucial!
// Esto es un ejemplo simple, en una app real usarías DI o scopes.
// fun provideRealm(): Realm {
// return Realm.open(config)
// }
}
// En un Repositorio/ViewModel, la instancia se abriría y cerraría típicamente
// asociada a un ciclo de vida o scope. Ejemplo conceptual:
/*
class TaskRepository {
private val realmConfig = RealmProvider.config
private var realmInstance: Realm? = null // Gestión manual (propensa a errores)
suspend fun openRealm() {
realmInstance = Realm.open(realmConfig)
}
fun closeRealm() {
realmInstance?.close()
realmInstance = null
}
// ... métodos para usar realmInstance ...
}
*/
Importante sobre el ciclo de vida: Abrir y cerrar instancias de Realm es crucial. En una app real, esto se maneja mejor con inyección de dependencias (como Hilt o Koin) proveyendo instancias con scope (ej. ViewModelScope
) o usando Coroutine Scopes que aseguren que realm.close()
se llame cuando el scope termine.
Usando Realm: Realizando Operaciones
Interactuar con Realm es directo, especialmente dentro de coroutines.
Ejemplo en un ViewModel/Repositorio:
package com.example.myapp.ui // O data layer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.myapp.data.realm.RealmProvider
import com.example.myapp.data.realm.model.Task
import io.realm.kotlin.Realm
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.query
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.mongodb.kbson.ObjectId
// Asumiendo una gestión de instancia de Realm simplificada para el ejemplo
class TaskViewModel : ViewModel() {
private lateinit var realm: Realm // ¡Manejar apertura/cierre correctamente en una app real!
private val _tasks = MutableStateFlow>(emptyList())
val tasks: StateFlow> = _tasks.asStateFlow()
init {
// Abrir Realm al iniciar el ViewModel (¡simplificado!)
viewModelScope.launch {
realm = Realm.open(RealmProvider.config)
observeTasks()
}
}
// Observar cambios en las tareas usando Flow
private fun observeTasks() {
viewModelScope.launch {
// Query para todas las tareas, observadas como Flow
realm.query().asFlow()
.collect { changes ->
// changes.list contiene la lista actualizada
_tasks.value = changes.list
}
}
}
// Añadir una nueva tarea
fun addTask(summary: String, owner: String?) {
viewModelScope.launch(Dispatchers.IO) { // Usar IO dispatcher para escrituras
realm.write { // Transacción de escritura
this.copyToRealm(Task().apply {
this.summary = summary
this.isComplete = false
this.owner = owner
})
}
}
}
// Marcar una tarea como completa/incompleta
fun toggleTaskStatus(taskId: ObjectId) {
viewModelScope.launch(Dispatchers.IO) {
realm.write {
// Encontrar la tarea más reciente con ese ID
val task = this.query("_id == $0", taskId).first().find()
// Modificar el objeto dentro de la transacción
task?.isComplete = !(task?.isComplete ?: false)
}
}
}
// Borrar una tarea
fun deleteTask(task: Task) {
viewModelScope.launch(Dispatchers.IO) {
realm.write {
// Encontrar la versión más reciente del objeto en esta transacción y borrarla
findLatest(task)?.also { liveTask ->
delete(liveTask)
}
}
}
}
// ¡MUY IMPORTANTE! Cerrar Realm cuando el ViewModel se destruye
override fun onCleared() {
super.onCleared()
if (this::realm.isInitialized && !realm.isClosed()) {
realm.close()
}
}
}
En la UI (Ejemplo con Compose):
@Composable
fun TaskListScreen(taskViewModel: TaskViewModel /* Obtenido con viewModel(), etc. */) {
val tasks by taskViewModel.tasks.collectAsStateWithLifecycle()
Column {
// Botón para añadir tarea (llamaría a taskViewModel.addTask(...))
LazyColumn {
items(tasks, key = { it._id }) { task ->
TaskItem(
task = task,
onToggleComplete = { taskViewModel.toggleTaskStatus(task._id) },
onDelete = { taskViewModel.deleteTask(task) }
)
}
}
}
}
@Composable
fun TaskItem(task: Task, onToggleComplete: () -> Unit, onDelete: () -> Unit) {
Row(/*...*/) {
Checkbox(checked = task.isComplete, onCheckedChange = { onToggleComplete() })
Text(text = task.summary)
Button(onClick = onDelete) { Text("Borrar") }
}
}
Conceptos Avanzados (Brevemente)
- Atlas Device Sync: Permite sincronizar datos de Realm local con MongoDB Atlas en la nube, habilitando funcionalidades offline-first y colaboración en tiempo real.
- Migraciones: Define cómo actualizar el esquema de la base de datos cuando lanzas nuevas versiones de tu app con cambios en los modelos
RealmObject
. - Relaciones: Realm soporta relaciones a uno (
Task?
) y a muchos (RealmList
) entre objetos.
Conclusión
Realm ofrece un enfoque fresco y potente para la persistencia de datos en Android, centrado en objetos y la reactividad. Su API intuitiva, combinada con el rendimiento y las capacidades multiplataforma del Realm Kotlin SDK, lo convierten en una alternativa muy atractiva a Room/SQLite, especialmente si buscas una experiencia de desarrollo más orientada a objetos o planeas llevar tu lógica de datos a otras plataformas.
¡Considera Realm para tu próximo proyecto y experimenta una forma diferente de manejar tus datos locales!
Para profundizar, explora la documentación oficial de Realm Kotlin SDK.
Comentarios
Publicar un comentario