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

Principios SOLID del Tio Bob

  Principios SOLID del Tio Bob Los principios sólidos son un conjunto de pautas de diseño de software que se centran en lograr código limpio, modular y mantenible. Estos principios fueron propuestos por Robert C. Martin (Uncle Bob) y se consideran fundamentales en el desarrollo de software orientado a objetos. Los cinco principios sólidos son los siguientes:   1. Principio de Responsabilidad Única (Single Responsibility Principle, SRP): Una clase debería tener una única responsabilidad. Esto significa que una clase debe tener una única razón para cambiar. Al tener una responsabilidad única, se logra un código más cohesivo y fácil de mantener.   2. Principio de Abierto/Cerrado (Open/Closed Principle, OCP): Las entidades de software (clases, módulos, etc.) deben estar abiertas para su extensión pero cerradas para su modificación. Esto significa que el comportamiento de una entidad puede ser extendido sin necesidad de modificar su código fuente original.  ...

Desarrollador junior C#

Habilidades que se esperan de un desarrollador junior (C#)   1.      Conocimientos básicos de programación: Debes tener una comprensión sólida de los conceptos fundamentales de programación, como variables, estructuras de control, bucles, funciones, etc. 2.      Dominio del lenguaje C#: Debes tener conocimientos sólidos del lenguaje C# y su sintaxis. Debes estar familiarizado con los conceptos orientados a objetos, como clases, herencia, polimorfismo, etc. 3.      Conocimientos de .NET Framework: C# se utiliza principalmente para el desarrollo en el entorno de .NET Framework, por lo que debes tener un conocimiento básico de esta plataforma, incluyendo las bibliotecas y clases comunes que se utilizan en el desarrollo de aplicaciones. 4.      Experiencia con Visual Studio: Visual Studio es el entorno de desarrollo integrado (IDE) más popular para C#. Debes estar familiarizado con su uso y ser capa...

Programación asíncrona comparación Kotlin y C#

La programación asíncrona es un enfoque en la programación que permite que las tareas se ejecuten de manera independiente y no bloqueante. En lugar de esperar a que una tarea se complete antes de pasar a la siguiente, las tareas se pueden ejecutar en paralelo o de manera secuencial, lo que mejora la eficiencia y la capacidad de respuesta de las aplicaciones. ¿Como implementar programación asíncrona en Kotlin? Usando Coroutines. Las coroutines en Kotlin son una forma de escribir código asíncrono de manera más concisa y legible. En lugar de bloquear el hilo principal mientras esperamos a que se complete una tarea, las coroutines permiten que el hilo siga ejecutándose mientras esperamos que una tarea asincrónica termine. Esto mejora la eficiencia y la capacidad de respuesta de las aplicaciones. Un ejemplo básico: KOTLIN import kotlinx.coroutines.* fun main() {     println("Inicio")     // Lanzar una coroutine     GlobalScope.launch {       ...