Cuando empiezas a desarrollar apps móviles, una de las primeras necesidades reales que aparece es guardar datos de forma rápida en el dispositivo: el correo del usuario, si ya vio el onboarding, el nivel de un juego, un token de sesión… Son datos pequeños, pero fundamentales para que la experiencia sea fluida y la app no parezca “amnésica” cada vez que se abre.
En Android, y también en otros entornos como Flutter, este tipo de almacenamiento ligero se suele resolver con mecanismos de pares clave-valor en almacenamiento local. Durante años el estándar han sido las SharedPreferences, y hoy en día su “evolución natural” en Android es DataStore. Vamos a ver en detalle cómo funcionan, cómo usarlas bien, cómo se integran alternativas como SharedPreferences en Flutter y en qué casos conviene dar el salto a soluciones más potentes.
¿Qué es el almacenamiento clave-valor en local y para qué sirve?
El patrón que comparten SharedPreferences en Android, DataStore y el paquete de SharedPreferences en Flutter es sencillo: se basan en almacenar información como pares clave-valor en disco, de forma persistente, sin necesidad de levantar una base de datos completa ni lidiar con SQL.
Este tipo de almacenamiento está pensado para cantidades pequeñas de datos, sin relaciones complejas y que se consultan con frecuencia: configuraciones de la app, preferencias del usuario, flags sobre si ya se mostró cierta pantalla, valores de formularios que queremos recordar, etc. En Android clásico se materializan como ficheros XML bajo el capó y, en iOS, como UserDefaults; Flutter abstrae esto para que el mismo código funcione en varias plataformas, integrando soluciones de memoria interna, externa y la nube.
A cambio de esta simplicidad, hay limitaciones claras: no hay consultas complejas, ni joins, ni índices; no es el sitio donde volcar grandes listas, historiales largos o información relacional. Para eso están bases de datos como Room/SQLite o soluciones tipo Hive, según la tecnología que uses.
SharedPreferences en Android: la herramienta clásica de almacenamiento rápido
En el framework de Android, SharedPreferences es la API histórica para leer y escribir pequeños datos persistentes en un archivo XML gestionado por el sistema. Cada archivo de preferencias está asociado a un nombre y contiene múltiples pares clave-valor que puedes consultar y modificar fácilmente desde tu código.
Un objeto SharedPreferences representa un archivo concreto de preferencias y proporciona métodos para obtener y guardar datos de tipo básico: cadenas de texto, enteros, booleanos, flotantes o conjuntos de strings. Es ideal para cosas como el email del usuario, el estado de una opción de configuración o el último nivel alcanzado en un juego.
Cuidado: DataStore es la solución moderna recomendada
Pese a que SharedPreferences sigue estando disponible y funcionando, Google ya indica claramente que DataStore es la opción recomendada para almacenamiento clave-valor moderno en Android. DataStore se apoya en coroutines y Flow de Kotlin, ofreciendo una API más segura en cuanto a concurrencia, operaciones atómicas y un modelo claro para observar cambios de datos.
Mientras SharedPreferences puede generar problemas en contextos con acceso concurrente o bloqueos en el hilo principal si se abusa de commit(), DataStore soluciona muchas de estas carencias con un diseño orientado a la asincronía y flujos reactivos. Más adelante veremos cómo encaja DataStore en la arquitectura de una app y en qué casos conviene incluso migrar.
Cómo obtener y gestionar un archivo de SharedPreferences en Android
Para trabajar con SharedPreferences en Android, primero necesitas acceder o crear el archivo de preferencias. El framework ofrece varias formas, según lo que te interese:
- getSharedPreferences(nombre, modo): se usa cuando quieres gestionar varios archivos de preferencias, cada uno identificado por un nombre. Puedes llamarlo desde cualquier instancia de Context.
- getPreferences(modo): pensado para usar un único archivo de preferencias por Activity. No necesitas especificar nombre; se asocia por defecto a esa actividad.
Un patrón habitual es construir un nombre de archivo que sea único para tu aplicación, por ejemplo combinando el applicationId con un sufijo, tipo «com.ejemplo.miapp.PREFERENCE_FILE_KEY». De esta forma evitas colisiones con otras apps y mantienes la estructura clara.
El modo habitual de apertura es MODE_PRIVATE, para que solo tu app tenga acceso a ese archivo. Los antiguos modos MODE_WORLD_READABLE y MODE_WORLD_WRITEABLE están obsoletos desde la API 17 y, desde Android 7.0, lanzarían un SecurityException si se usan. Si necesitas compartir archivos con otras apps, lo correcto es recurrir a un FileProvider con FLAG_GRANT_READ_URI_PERMISSION, no a preferencias globales.
En el caso concreto de que estés guardando configuración global de la app usando la librería de Preferences (pantallas de ajustes), lo más corriente es usar getDefaultSharedPreferences(), que te da acceso al archivo de preferencias por defecto para toda la aplicación, integrándose con los componentes de UI de configuración.
Escritura en SharedPreferences: Editor, apply() y commit()
Para modificar valores en un archivo de SharedPreferences, el flujo siempre pasa por un objeto SharedPreferences.Editor. Lo obtienes llamando a edit() sobre la instancia de SharedPreferences, añades o actualizas los pares clave-valor que te interesen y, finalmente, confirmas el cambio con apply() o commit().
Los métodos más usados del editor para escribir son putString(), putInt(), putBoolean(), putFloat() y variantes análogas. Cada uno recibe una clave (String) y un valor del tipo correspondiente. Una vez hechas todas las modificaciones en memoria, eliges cómo persistirlas:
- apply(): actualiza el objeto SharedPreferences en memoria de inmediato y guarda los cambios en disco de forma asíncrona. No bloquea el hilo principal y es la opción recomendada en la mayoría de casos.
- commit(): escribe en disco de forma síncrona y devuelve un booleano indicando si se pudo guardar. Como bloquea el hilo, no es buena idea invocarlo en el hilo principal si se usa con frecuencia.
Además de las operaciones de escritura básica, SharedPreferences admite eliminación de una clave concreta con remove() y borrado total del archivo con clear(), siempre a través del editor. Estas operaciones se consolidan igualmente con apply() o commit().
Lectura en SharedPreferences: acceso seguro con valores por defecto
Leer datos desde SharedPreferences es aún más directo: llamas a getString(), getInt(), getBoolean(), getFloat(), getLong() o al método equivalente según el tipo, pasando la clave y un valor por defecto. Si la clave no existe en el archivo, el método devuelve precisamente ese valor por defecto.
Este enfoque evita excepciones si consultas claves que todavía no se han creado, y te permite definir un estado inicial razonable (por ejemplo, cadena vacía para un texto, 0 para un entero, false para un booleano). También puedes recuperar todas las claves con getAll() o getStringSet() para conjuntos de strings.
Ejemplo práctico en Android: recordar el correo del usuario
Un caso muy típico de uso de SharedPreferences en Android es recordar el último dato ingresado por el usuario en un formulario. Imagina una pantalla sencilla con un EditText para el email y un botón que guarda y cierra la app. Cada vez que se abre de nuevo, quieres que el campo se rellene automáticamente con el último correo.
El flujo sería: en onCreate(), tras cargar el layout, obtienes la referencia al EditText y una instancia de SharedPreferences mediante getSharedPreferences(«datos», MODE_PRIVATE). Después, recuperas la cadena almacenada con getString(«mail», «») y se la asignas al EditText. Si el archivo aún no existe, Android lo crea y devuelve la cadena vacía como valor inicial.
Cuando se pulsa el botón, creas un Editor llamando a edit() sobre la misma instancia de SharedPreferences, guardas el contenido actual del EditText con putString(«mail», valorDelCampo), confirmas con commit() o apply() y finalizas la Activity. La próxima vez que arranques la app, el correo aparecerá ya escrito en el campo.
Otro ejemplo en Android: mini agenda con clave personalizada
SharedPreferences también puede servir para escenarios un poco más creativos, como montar una agenda sencilla donde la clave es el nombre de la persona y el valor un bloque de texto con sus datos.
La interfaz puede constar de dos EditText (nombre y datos) y dos botones (grabar y recuperar). En el método asociado al botón de grabar, obtienes los textos de ambos campos, abres el archivo de preferencias con getSharedPreferences(«agenda», MODE_PRIVATE), creas el Editor, llamas a putString(nombre, datos) y confirmas con commit(). Opcionalmente, muestras un Toast avisando de que los datos se han guardado.
Para recuperar, lees el nombre, abres de nuevo las preferencias y llamas a getString(nombre, «»). Si la cadena resultante está vacía, sabes que no hay entradas para esa persona, por lo que puedes notificarlo con un Toast. Si hay contenido, lo cargas en el segundo EditText para mostrarlo en pantalla. Es una solución muy básica, pero ilustra cómo SharedPreferences permite manejar datos clave-valor flexibles sin necesidad de base de datos.
Diseños más elaborados: encapsular SharedPreferences y usar patrones
A medida que una app crece, conviene dejar de acceder a SharedPreferences desde cualquier parte y encapsular la lógica de persistencia en clases dedicadas. Un enfoque típico en Android con Kotlin es crear una clase Prefs que reciba un Context y gestione internamente las operaciones de lectura y escritura.
En esa clase puedes definir constantes para el nombre del archivo y para cada clave (por ejemplo, PREFS_NAME y SHARED_NAME), así como propiedades con getters y setters personalizados. Un ejemplo muy práctico es definir una propiedad name que, en su get, consulte prefs.getString(SHARED_NAME, «») y, en su set, use prefs.edit().putString(SHARED_NAME, value).apply(). Si quisieras guardar un entero, usarías putInt y getInt de la misma forma.
Para no estar creando instancias de Prefs a lo loco, es habitual aprovechar una clase que extienda de Application. Desde ahí, en onCreate(), se puede instanciar Prefs con applicationContext y exponerla como un objeto accesible en todo el proyecto mediante companion object. Ese companion object actúa como un “singleton” accesible desde cualquier Activity o fragmento, a través de algo tipo SharedApp.prefs.name.
Una vez montada esta infraestructura, el código de las pantallas queda mucho más limpio: en una Activity solo te preocupas de leer y asignar valores desde SharedApp.prefs, sin repetir la lógica de creación de SharedPreferences ni las claves mágicas en cada sitio.
Visibilidad de vistas y lógica basada en preferencias
Un ejemplo muy didáctico de uso práctico consiste en crear una pantalla que muestre una vista de “invitado” o de “perfil” en función de si hay un nombre guardado en las preferencias. Para ello puedes tener dos métodos: showGuest() y showProfile().
En showProfile(), muestras un TextView con un saludo que incluye el nombre almacenado en SharedPreferences, haces visible un botón de borrar y ocultas el EditText y el botón de guardar. En showGuest(), haces lo contrario: enseñas el campo de texto y el botón de guardar, y ocultas el TextView y el botón de borrar.
Aquí es donde entra otro concepto importante de Android: la visibilidad de las vistas. Hay tres estados posibles:
- VISIBLE: el componente se ve y ocupa espacio en el layout.
- INVISIBLE: el componente no se ve, pero sigue ocupando su espacio, útil para mantener alineaciones.
- GONE: el componente se elimina del layout a efectos de espacio; no se ve ni deja hueco.
La lógica de la pantalla puede completarse con un método configView(), que decide si mostrar invitado o perfil según el resultado de isSavedName(), una función que comprueba si la preferencia de nombre está vacía o no. Los listeners de los botones simplemente actualizan el valor en SharedPreferences (guardan el nombre o lo vacían) y llaman a configView() de nuevo para refrescar la interfaz.
DataStore en Android: el siguiente paso en almacenamiento moderno
Como ya se ha comentado, SharedPreferences funciona, pero tiene algunas limitaciones importantes a nivel de concurrencia, atomicidad y rendimiento en el hilo principal. Para solventar esto, Google propone DataStore como nueva solución de almacenamiento clave-valor local.
DataStore ofrece dos variantes principales: Preferences DataStore, que mantiene el enfoque clave-valor sin esquema fijo, y Proto DataStore, que utiliza Protobuf para definir un esquema tipado más robusto, permitiendo validar y estructurar los datos de forma clara. Ambas se integran profundamente con las coroutines de Kotlin y con Flow, ofreciendo operaciones asíncronas seguras que no bloquean la interfaz.
Entre las ventajas de DataStore frente a SharedPreferences destacan las operaciones atómicas, la gestión nativa de concurrencia, la posibilidad de observar cambios en tiempo real con Flow y el soporte para migrar datos desde SharedPreferences. Para escenarios donde necesitas observar preferencias en directo desde la capa de UI o dominio, DataStore encaja de manera mucho más natural en arquitecturas reactivas.
Buenas prácticas con DataStore y migraciones desde SharedPreferences
Si tu app ya usa SharedPreferences pero quieres modernizarla, DataStore facilita migraciones programadas desde los archivos de preferencias existentes, evitando perder datos del usuario y complementándolas con estrategias de copia de seguridad. Es recomendable planificar estas migraciones para que se ejecuten una sola vez y de forma transparente.
Otras buenas prácticas con DataStore incluyen realizar siempre las escrituras en hilos adecuados (gracias a coroutines esto es sencillo), manejar errores y excepciones con políticas de retry razonables, y cifrar los datos sensibles antes de persistirlos. Para información relacional o de gran volumen, se sigue recomendando usar Room u otras bases de datos, reservando DataStore para configuraciones, preferencias y datos simples.
En arquitecturas limpias (“clean”), lo ideal es exponer el acceso a DataStore a través de repositorios que ofrezcan Flows a la capa de dominio, en lugar de acceder directamente desde las Activities. De este modo, el código se vuelve más sencillo de probar y mantener, y puedes sustituir la implementación de almacenamiento en tests sin tocar el resto de la app.
Cuándo elegir SharedPreferences/DataStore y cuándo usar base de datos
Una regla práctica para decidir es valorar el tamaño y la estructura de los datos. Usa SharedPreferences o DataStore cuando los datos sean simples, pequeños y sin relaciones complejas, como preferencias del usuario, configuración de la app, estados de flags o pequeños tokens temporales.
Si lo que quieres es almacenar listas extensas, historiales, relaciones entre entidades o datos que crecen sin parar, lo más razonable es apoyarse en una base de datos como Room/SQLite en Android o Hive/SQLite en Flutter. También es importante tener en cuenta la seguridad: ni SharedPreferences ni DataStore son el lugar apropiado para guardar contraseñas en texto plano o claves privadas sin cifrado adicional.
SharedPreferences en Flutter: persistencia ligera multi-plataforma
En el ecosistema Flutter, existe un paquete oficial llamado shared_preferences que ofrece un mecanismo de almacenamiento clave-valor persistente muy similar al de Android nativo, pero abstraído para funcionar en distintas plataformas (Android, iOS, web, escritorio…).
En Android, internamente suele usar XML; en iOS y macOS se integra con UserDefaults; en la web se apoya en localStorage, y en Windows/Linux recurre a almacenamiento local mediante FFI. Para el desarrollador de Flutter, todo esto es transparente, ya que trabaja siempre con la misma API en Dart.
Se trata de una solución perfecta para guardar datos como ID de usuario, email, estado de sesión, preferencias de configuración o pequeños flags sobre si se ha mostrado cierto contenido. Para cosas más pesadas, el propio ecosistema de Flutter recomienda usar bases de datos (por ejemplo, sqflite) u opciones como Hive.
Instalación y uso básico de SharedPreferences en Flutter
Para utilizar SharedPreferences en Flutter, basta con añadir la dependencia en el archivo pubspec.yaml dentro de la sección de dependencies, indicando la versión que corresponda, y ejecutar el comando de obtención de paquetes. No es necesaria una configuración extra.
Las operaciones de lectura y escritura son asíncronas, ya que se accede al sistema de almacenamiento del dispositivo. Primero obtienes una instancia de SharedPreferences con SharedPreferences.getInstance() usando await, y a partir de ahí tienes a tu disposición un conjunto de métodos get y set muy parecido al de Android.
Para leer datos existen métodos como getString(), getInt(), getBool(), getDouble(), getStringList() y un get() genérico, además de getKeys() para obtener todas las claves. Para escribir, se utilizan setString(), setInt(), setBool(), setDouble() y setStringList(). Si pasas null como valor en un setter, equivale a eliminar la clave con remove().
Ejemplos prácticos de get/set y manejo de objetos en Flutter
Con una instancia de SharedPreferences en Flutter puedes guardar datos simples con llamadas muy directas, como por ejemplo prefs.setString(‘nombre’, ‘Carlos’), prefs.setInt(‘edad’, 28) o prefs.setBool(‘logueado’, true). Para leerlos, simplemente invocas getString(‘nombre’), getInt(‘edad’) o getBool(‘logueado’).
Si necesitas guardar objetos más complejos (por ejemplo, una clase Person con nombre y edad), una táctica habitual es serializarlos a JSON y almacenarlos como String. Puedes tener una función que reciba un objeto, lo convierta a un mapa o directamente a JSON, y luego lo guarde con prefs.setString(clave, json.encode(valor)). Para recuperarlo, lees el string, haces json.decode y reconstruyes el objeto.
Es importante no abusar de este patrón para estructuras muy grandes. La escritura y lectura de JSON pesado en SharedPreferences no es eficiente ni cómoda. Para perfiles simples o pequeños datos de usuario, encaja bien; si quieres guardar colecciones largas, lo suyo es pasar a una base de datos o a soluciones diseñadas para ello.
Gestión de sesión y ejemplos reales en Flutter
Un caso muy habitual en apps hechas con Flutter es necesitar que el usuario permanezca autenticado incluso si cierra la app. Esto se puede resolver guardando un set mínimo de claves en SharedPreferences: identificador de usuario, email, un flag indicando si está logueado y, opcionalmente, un token de autenticación.
La lógica típica incluye tres funciones: una para guardar la sesión (almacenando las claves y poniendo is_logged_in en true), otra para comprobar al inicio de la app si el usuario sigue logueado (leyendo el booleano o devolviendo false si es null) y otra para cerrar sesión (eliminando user_id, user_email y poniendo is_logged_in en false). Con esto, la app puede saltarse la pantalla de login cuando arranca y llevar al usuario directamente a su contenido.
En proyectos de e‑learning o academias online desarrollados con Flutter, este enfoque es muy común: SharedPreferences se usa para recordar la identidad básica del usuario y su estado de sesión, mientras que para el contenido (cursos, progreso, etc.) se recurre a mecanismos más robustos y a veces sincronizados con un backend vía APIs REST o GraphQL.
Buenas prácticas y limitaciones de SharedPreferences en Flutter
Para evitar problemas y posibles cuellos de botella, conviene asumir que SharedPreferences en Flutter está pensado para datos pequeños y de acceso frecuente. No es buena idea guardar listas enormes o estructuras muy complejas en formato JSON dentro de un solo valor.
También hay que ser prudentes con la seguridad: aunque SharedPreferences no es accesible para el usuario de forma directa en producción, no está diseñado como almacenamiento seguro para contraseñas o claves sensibles. Para esos casos, es mejor utilizar soluciones como secure storage que se integran con el keystore del sistema operativo.
En la web, recuerda que el paquete se apoya en localStorage y tiene las limitaciones propias de este: tamaño máximo reducido, eliminación potencial por parte del navegador, etc. Conviene asumir que SharedPreferences no es un almacén infalible y que la información crítica debe estar también en el backend.
Servicios profesionales para diseñar y modernizar el almacenamiento local
En entornos profesionales, elegir correctamente cómo y dónde guardar datos es clave para que la app sea escalable, segura y fácil de mantener. Empresas especializadas en desarrollo a medida, con experiencia en Android nativo, Flutter, arquitecturas en la nube y ciberseguridad, pueden ayudarte a diseñar esa capa de persistencia teniendo en cuenta no solo el rendimiento, sino también la protección de la información.
Un soporte experto resulta especialmente útil al migrar de SharedPreferences a DataStore, definir qué datos deben ir a DataStore y cuáles a Room, o decidir cómo integrar la parte móvil con cuadros de mando BI (por ejemplo, usando Power BI) y agentes de inteligencia artificial que procesen eventos y métricas generadas en la app.
Si tu objetivo es modernizar el almacenamiento local en Android, aprovechar DataStore como base para preferencias y configuraciones, combinarlo con servicios cloud seguros, automatizar flujos con agentes de IA y obtener dashboards que exploten los datos de uso de tu aplicación, es muy recomendable abordar el diseño de esta capa con una visión global y no solo como un detalle técnico aislado.
En definitiva, SharedPreferences y sus variantes modernas como DataStore, así como el paquete de SharedPreferences en Flutter, ofrecen una forma muy ágil de implementar almacenamiento rápido en local para pares clave-valor; bien empleados, permiten recordar estados de usuario, gestionar sesiones ligeras, guardar preferencias y optimizar la experiencia de uso sin montar infraestructuras complejas, mientras que para datos relacionales o de gran volumen sigue siendo imprescindible apoyarse en bases de datos y servicios backend bien diseñados.
