Ir al contenido principal

Persistencia de Datos en Android Usando Realm

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:

  1. 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 de RealmObject.
  2. Configuración de Realm (RealmConfiguration): Define cómo se abrirá una instancia de Realm. Incluye el esquema (la lista de clases RealmObject), el nombre del archivo de la base de datos, configuraciones de migración, clave de encriptación, etc.
  3. 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()).
  4. 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).
  5. 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.
  6. 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

Entradas populares de este blog

De Coverlet a JaCoCo: Trayendo la magia de la cobertura a Android Studio

Todo comenzó un día cualquiera, mientras revisaba un reporte de cobertura de pruebas generado por Coverlet para un proyecto en .NET. Me quedé fascinado por el nivel de detalle y claridad que proporcionaba: saber exactamente qué partes del código estaban cubiertas por las pruebas, y cuáles no. Fue entonces cuando me pregunté: ¿Y si pudiera tener algo así en mi proyecto de Android con Kotlin y Jetpack Compose? La Chispa Inicial ✨ Como desarrollador, siempre busco mejorar la calidad de mi código, y contar con herramientas que me permitan medir la cobertura de pruebas es clave. Después de una rápida búsqueda, me topé con **JaCoCo**, una herramienta muy popular para medir cobertura de código en proyectos Java y Kotlin. ¡Lo mejor de todo es que es compatible con Android Studio! Mi objetivo estaba claro: debía integrar JaCoCo en mi proyecto de Android para tener reportes detallados de cobertura, tal como lo había visto en Coverlet. El Desafío: Configurar JaCoCo en un Proyecto Android 🛠️ El p...

Guía Definitiva de Estrategias de Branching en Git: Elige la Mejor para tu Equipo

Imagina esta situación: lideras un equipo de desarrollo y todos están enviando código al mismo tiempo. Sin una estrategia de ramas (branching) clara, tu base de código se convierte rápidamente en un caos de conflictos, compilaciones rotas y desarrolladores frustrados. ¿Te suena familiar? Si alguna vez te has encontrado en este lío, no estás solo. Una estrategia de branching en Git es el mapa que tu equipo necesita para gestionar los cambios de código, colaborar de manera efectiva y entregar software de calidad. Es, en esencia, un conjunto de reglas que define cómo los desarrolladores interactúan con el repositorio, cuándo crear ramas, cómo fusionar cambios y cómo mantener la estabilidad del código en todo el ciclo de vida del desarrollo. Pero es más que solo reglas. Una buena estrategia de branching se alinea con la forma en que trabaja tu equipo: tus ciclos de lanzamiento, los flujos de trabajo de QA, tus pipelines de CI/CD e incluso la frecuencia con la que necesitas aplicar correcc...

Inyección de Dependencias usando Hilt

Inyección de Dependencias usando Hilt La Inyección de Dependencias (DI) es fundamental para construir aplicaciones Android robustas, escalables y fáciles de testear. Mientras que existen varias librerías para lograrlo, Hilt se ha establecido como la solución recomendada y estándar de Google. Construido sobre la potencia de Dagger, Hilt simplifica enormemente la implementación de DI en Android. Si buscas una forma estandarizada, con menos boilerplate que Dagger puro y con excelente integración con los componentes de Android Jetpack, Hilt es la respuesta. ¡Descubramos cómo funciona! ¿Por qué elegir Hilt? Hilt ofrece ventajas significativas para el desarrollo Android: Estándar de Android: Es la librería DI recomendada por Google, lo que asegura buena documentación, soporte y alineación con las prácticas modernas de Android. Menos Boilerplate (vs. Dagger): Reduce drásticamente el código de configuración necesario en comparación con usar Dagger directamente en Andr...