Almacenamiento de datos persistente en Android Studio

- Andrés Cruz

EN In english

Almacenamiento de datos persistente en Android Studio

Casi todas las aplicaciones que usamos o desarrollamos en Android necesitan almacenar datos de forma persistente: configuraciones del usuario, sesiones, archivos, información estructurada o estados de la interfaz. Aunque el concepto de persistencia no ha cambiado con los años, las herramientas, APIs y buenas prácticas sí lo han hecho radicalmente.

En términos simples, persistir datos significa que la información sobrevive al cierre de la aplicación, a cambios de configuración e incluso al reinicio del dispositivo. En Android moderno, elegir mal el mecanismo de almacenamiento puede provocar pérdida de datos, problemas de rendimiento o incluso el rechazo de la app en Google Play.

En este artículo se analizan todas las técnicas actuales de almacenamiento de datos persistente en Android Studio, partiendo de los conceptos clásicos y llevándolos al estado actual de Android (Jetpack, Kotlin y Compose).

En versiones modernas de Android, elegir incorrectamente el mecanismo de almacenamiento puede provocar:

  • Problemas de rendimiento (bloqueos del hilo principal)
  • Riesgos de seguridad y privacidad
  • Pérdida de datos ante cambios de configuración
  • Rechazo de la aplicación en Google Play

En términos simples, persistir datos significa que la información sobrevive al cierre de la app, a cambios de configuración y, en muchos casos, al reinicio del dispositivo. Android actual ya no apuesta por una única solución, sino por una estrategia combinada de persistencia.

En esta guía ampliada y actualizada veremos todas las opciones actuales de almacenamiento de datos persistente en Android Studio, cuándo usar cada una, ejemplos prácticos y qué enfoques han quedado obsoletos.


¿Qué es la persistencia en Android?

En informática, la persistencia consiste en lograr que los datos manipulados por una aplicación sobrevivan en el tiempo, independientemente del proceso que los creó. Coloquialmente hablando: que los datos no se borren cuando la app se cierra.

En Android moderno, la persistencia se divide en tres grandes niveles, cada uno con un propósito distinto:

  1. Estado de la IU (memoria): datos temporales necesarios para que la interfaz no se reinicie en rotaciones o cambios de configuración.
  2. Persistencia local (disco): datos que deben sobrevivir al cierre de la app.
  3. Persistencia estructurada: datos relacionales o consultables.

Una aplicación bien diseñada combina varios mecanismos, no depende de uno solo.

Android recomienda explícitamente no mezclar responsabilidades. Un error común en artículos antiguos es usar bases de datos o archivos para guardar estado de IU, lo cual hoy se considera una mala práctica.


1. Preferencias compartidas (SharedPreferences)

Las SharedPreferences permiten almacenar pares clave–valor simples como booleanos, números o texto. Históricamente fueron el mecanismo más utilizado para guardar configuraciones del usuario.

Características

  • Almacenamiento en archivo XML privado
  • No requiere permisos
  • Ideal para pequeñas configuraciones

Durante muchos años, SharedPreferences fue la solución estándar para guardar pares clave–valor simples.

Qué permite almacenar:

  • Boolean
  • Int, Long, Float
  • String
  • Set

Ejemplo clásico:

 
SharedPreferences prefs = getSharedPreferences("MiPreferencia", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("modo_oscuro", true);
editor.apply();

Problemas detectados con el tiempo:

  • Escrituras síncronas si se usa commit()
  • Riesgo de corrupción
  • Sin control de concurrencia
  • Sin tipado fuerte

Limitaciones actuales

  • Acceso síncrono potencial
  • Riesgo de inconsistencias
  • Sin seguridad de tipos

Por estos motivos, ya no se recomienda como solución principal en apps nuevas.

1.2 DataStore (reemplazo moderno y recomendado)

DataStore forma parte de Jetpack y es el reemplazo oficial de SharedPreferences.

Existen dos variantes:

DataStore Preferences

Ideal para configuraciones simples.

 
val Context.dataStore by preferencesDataStore(name = "settings")
 
suspend fun saveTheme(context: Context, dark: Boolean) {
context.dataStore.edit { prefs ->
prefs[booleanPreferencesKey("dark_mode")] = dark
}
}

Lectura reactiva:

 
val darkModeFlow: Flow<Boolean> = context.dataStore.data
.map { prefs -> prefs[booleanPreferencesKey("dark_mode")] ?: false }

Proto DataStore

Usado cuando necesitas esquema fuerte y validación.

  • Ideal para apps grandes
  • Usa Protobuf
  • Evita errores por claves incorrectas

Cuándo usar DataStore:

  • Preferencias de usuario
  • Flags de configuración
  • Datos pequeños pero persistentes

Ventajas

  • Asíncrono
  • Seguro frente a corrupción
  • Integración con corrutinas
  • Observación reactiva con Flow

Cuándo usar DataStore

  • Preferencias de usuario
  • Flags de configuración
  • Estados simples persistentes

2. Almacenamiento de archivos

2.1 Memoria interna (privada y segura)

La memoria interna sigue siendo una de las opciones más seguras y simples.

val file = File(filesDir, "config.txt")
file.writeText("Hola Mundo")

Características:

  • Solo accesible por la app
  • Se elimina al desinstalar
  • Ideal para datos sensibles

Características:

  • No requiere permisos
  • Solo accesible por la app
  • Se elimina al desinstalar

Ejemplo en Kotlin:

 
val file = File(context.filesDir, "config.txt")
file.writeText("Configuración inicial")

Lectura:

 
val content = file.readText()

Casos de uso típicos:

  • Archivos de configuración
  • Tokens cifrados
  • Datos privados de la app

2.2 Memoria externa y Scoped Storage

Desde Android 10 (API 29), Google introdujo Scoped Storage.

Qué cambió:

  • Acceso libre a la SD eliminado
  • Permisos WRITE/READ_EXTERNAL_STORAGE obsoletos
  • Acceso limitado por sandbox

Ejemplo recomendado:

 
val file = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),
"reporte.pdf"
)

Para acceso a archivos creados por otras apps se debe usar:

  • Storage Access Framework (SAF)
  • Intent ACTION_OPEN_DOCUMENT

Importante: el uso incorrecto de storage es una de las causas más comunes de rechazo en Google Play.


3. Bases de datos: SQLite vs Room

3.1 SQLite directo (bajo nivel)

SQLite sigue siendo el motor base, pero manejarlo manualmente implica:

  • Código repetitivo
  • Errores en runtime
  • Difícil mantenimiento

Hoy se considera una opción de bajo nivel.

Características

  • Base de datos en un solo archivo
  • No cliente-servidor
  • Rápida y ligera

3.2 Room (estándar actual)

Room es la capa de abstracción oficial de Android Jetpack sobre SQLite.

Componentes:

  • Entity
  • DAO
  • Database

Ejemplo completo:

 
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
 
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
 
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<User>>
}
 
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

Ventajas reales:

  • Verificación en compilación
  • Integración con Flow y LiveData
  • Fácil testing

4. Caché: datos temporales

La caché permite almacenar datos reconstruibles.

Ejemplo:

 
val cacheFile = File(context.cacheDir, "response.json")

Buenas prácticas:

  • Nunca datos críticos
  • Limitar tamaño
  • El sistema puede eliminarla

Buenas prácticas:

  • Nunca guardar datos críticos
  • Limitar tamaño
  • Limpiar periódicamente

5. Estado de la IU: ViewModel y SavedState

5.1 ViewModel

Diseñado para:

  • Sobrevivir a rotaciones
  • Evitar recargas innecesarias
 
class MainViewModel : ViewModel() {
val counter = MutableLiveData(0)
}

5.2 SavedStateHandle

Permite restaurar estado tras cierre del proceso.

 
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
 
var query: String
get() = savedStateHandle["query"] ?: ""
set(value) {
savedStateHandle["query"] = value
}
}

Regla clave:

Estado de IU no debe guardarse en base de datos.

ViewModel

  • Mantiene datos en memoria
  • Sobrevive a rotaciones
  • No sobrevive al kill del proceso

SavedState / rememberSaveable

  • Backup ligero
  • Solo datos pequeños

6. Persistencia avanzada: Ink API y serialización

En apps de dibujo:

  • Serializar StrokeInputBatch
  • Guardar Brush por separado
  • Exportar a imagen

Este enfoque es esencial para:

  • Apps de notas
  • Whiteboards
  • Colaboración en tiempo real

¿Qué mecanismo debo usar?

NecesidadSolución recomendada
PreferenciasDataStore
Estado de IUViewModel + SavedState
Datos relacionalesRoom
Archivos privadosMemoria interna
Archivos compartidosSAF / Scoped Storage
Datos temporalesCaché

Conclusión

La persistencia en Android moderno no se basa en una única técnica, sino en una arquitectura bien pensada:

  • ViewModel para IU
  • DataStore para preferencias
  • Room para datos complejos
  • Scoped Storage para archivos

Actualizar estos conceptos es esencial para crear aplicaciones seguras, eficientes y alineadas con los estándares actuales de Android Studio.


7. Ejemplo end-to-end: App completa con ViewModel + DataStore + Room + UI

Escenario

Aplicación sencilla de tareas (To‑Do) que:

  • Guarda preferencias (modo oscuro)
  • Persiste tareas en base de datos
  • Mantiene estado de IU ante rotaciones

Arquitectura

  • UI: Jetpack Compose
  • Estado: ViewModel + StateFlow
  • Preferencias: DataStore
  • Datos: Room

DataStore – Preferencias

 
val Context.settingsDataStore by preferencesDataStore("settings")
 
class SettingsRepository(private val context: Context) {
val darkMode: Flow<Boolean> = context.settingsDataStore.data
.map { it[booleanPreferencesKey("dark_mode")] ?: false }
 
suspend fun setDarkMode(enabled: Boolean) {
context.settingsDataStore.edit {
it[booleanPreferencesKey("dark_mode")] = enabled
}
}
}

Room – Base de datos

 
@Entity
data class Task(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val completed: Boolean = false
)
 
@Dao
interface TaskDao {
@Query("SELECT * FROM Task")
fun getTasks(): Flow<List<Task>>
 
@Insert
suspend fun insert(task: Task)
}

ViewModel – Lógica de estado

 
class TaskViewModel(
private val dao: TaskDao,
private val settings: SettingsRepository
) : ViewModel() {
 
val tasks = dao.getTasks().stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyList()
)
 
val darkMode = settings.darkMode.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false
)
}

UI – Jetpack Compose

 
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsState()
val darkMode by viewModel.darkMode.collectAsState()
 
LazyColumn {
items(tasks) { task ->
Text(task.title)
}
}
}

8. Versión 100% Jetpack Compose

rememberSaveable

 
var text by rememberSaveable { mutableStateOf("") }

Se usa para:

  • Texto de inputs
  • Scroll
  • Selecciones temporales

StateFlow + Compose

 
val uiState: StateFlow<UiState>

Ventajas:

  • Reactivo
  • Lifecycle‑aware
  • Fácil testing

Room + Flow + Compose

  • Room expone Flow
  • ViewModel lo transforma
  • Compose lo observa

Esto evita:

  • Callbacks
  • Estados inconsistentes

9. Tabla comparativa avanzada

MecanismoRendimientoSobrevive kill procesoCasos realesErrores comunes
ViewModelMuy altoEstado UIGuardar datos persistentes
SavedStateMedio✔️Inputs, scro 

Aprende cómo implementar almacenamiento de datos persistente en Android Studio usando DataStore, Room, SQLite y buenas prácticas modernas paso a paso.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english