Jetpack DataStore en 2026: migrar desde SharedPreferences sin perder un solo ajuste
Una guía práctica para pasar de SharedPreferences a Jetpack DataStore en Android — las trampas asíncronas, la ruta de migración que preserva los valores existentes y cómo probarla.
SharedPreferences todavía funciona. Pero también hace una lectura de disco síncrona en el primer acceso, no ofrece seguridad de tipos en tiempo de compilación, y no da ninguna forma de observar un cambio de valor sin conectar un listener a mano. Jetpack DataStore corrige los tres problemas, y en 2026 es la opción por defecto que recomienda Google para cualquier cosa más allá de una bandera desechable. Lo que detiene la mayoría de las migraciones no es aprender la nueva API — es mover a los usuarios existentes sin reiniciar sus ajustes en la próxima actualización.
Qué falla realmente en SharedPreferences
getSharedPreferences().getString(...) parece síncrono y barato, pero en el primer acceso de un proceso puede bloquear el hilo que lo llama con una lectura de archivo real — a menudo el hilo principal, durante el arranque de la app. No hay una forma integrada de recolectar cambios como un flujo; se registra un OnSharedPreferenceChangeListener y se gestiona su ciclo de vida a mano. Y cada lectura está tipada por cadenas: un error de tipeo en el nombre de una clave compila sin problemas y falla silenciosamente en tiempo de ejecución devolviendo el valor por defecto.
Nada de esto es un caso límite exótico. Es el comportamiento normal de SharedPreferences, y Preferences DataStore se construyó específicamente para corregirlo sin cambiar demasiado el modelo mental.
Preferences DataStore: la misma forma, hecha con seguridad
Una instancia de Preferences DataStore se construye una vez, típicamente como una propiedad de extensión sobre Context:
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
Las lecturas llegan como un Flow, así que obtienes notificaciones de cambio gratis en lugar de construirlas tú mismo:
val TIMER_MINUTES = intPreferencesKey("timer_minutes")
val timerMinutes: Flow<Int> = context.settingsDataStore.data
.map { prefs -> prefs[TIMER_MINUTES] ?: 25 }
Las escrituras pasan por una función suspend, así que nunca se llaman accidentalmente desde el hilo principal:
suspend fun setTimerMinutes(context: Context, minutes: Int) {
context.settingsDataStore.edit { prefs ->
prefs[TIMER_MINUTES] = minutes
}
}
Esto es lo bastante parecido a SharedPreferences como para que la reescritura en sí sea mecánica. El riesgo está enteramente en qué le pasa a los datos que ya existen en el dispositivo de un usuario.
La parte que la gente se salta: migrar los valores existentes
DataStore trae un SharedPreferencesMigration pensado exactamente para esto, y es fácil pasarlo por alto porque nada te obliga a usarlo — tu app compilará y funcionará bien sin él, y luego reiniciará silenciosamente cada ajuste a su valor por defecto en la actualización donde cambias el backend de almacenamiento.
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(SharedPreferencesMigration(context, "settings_prefs"))
}
)
La migración se ejecuta una sola vez, la primera vez que se abre el DataStore después de publicar este código: lee el antiguo archivo SharedPreferences, copia cada entrada al nuevo almacén Preferences, y deja el archivo antiguo en su sitio (no lo borra — esa es una decisión tuya, aparte, una vez que confíes en que la migración se ha ejecutado en todas partes). Si el tipo de una clave no se corresponde limpiamente — un Set<String> donde ahora quieres una List, por ejemplo — lo filtras en el shouldRunMigration de la migración o lo transformas explícitamente en lugar de dejar que un desajuste de tipo lance una excepción al leer.
Equivocarse en el nombre del archivo de preferencias antiguo — un error común si se creó con un nombre personalizado en vez del nombre por defecto del paquete — hace que la migración no encuentre nada que copiar, y cada usuario se reinicia a los valores por defecto en la actualización. Antes de publicar, comprueba el nombre exacto con las llamadas context.getSharedPreferences("name", MODE_PRIVATE) ya presentes en el código en lugar de adivinarlo.
Cuando Preferences DataStore no basta
Preferences DataStore mantiene un espacio de claves plano y tipado por cadenas — resolvió los problemas de hilos y observabilidad pero no el de seguridad de tipos. Proto DataStore reemplaza la bolsa de clave-valor por un esquema que defines una vez en un archivo .proto, de modo que un objeto de ajustes o coincide con el esquema o no compila — no hay un resultado null por una clave mal escrita en tiempo de ejecución. Vale la pena la configuración adicional en cuanto una pantalla de ajustes crece más allá de un puñado de banderas, o en cuanto objetos anidados (una preferencia de notificación con sus propios campos de sonido, vibración y horas silenciosas) empiezan a aparecer en el espacio de claves como tres o cuatro claves en espacios de nombres separados en vez de un solo valor estructurado. En Mintly, los ajustes de sonido, vibración y reinicio automático del temporizador se movieron a un pequeño mensaje Proto DataStore exactamente por esta razón — en cuanto los ajustes relacionados necesitan leerse y escribirse juntos, un almacén clave-valor plano empieza a jugar en tu contra.
Probar la migración, no solo la nueva API
El nuevo código de lectura/escritura es lo bastante sencillo como para tentar a saltarse las pruebas. La migración no lo es — es la única pieza de este cambio que se ejecuta exactamente una vez, en silencio, contra datos reales de usuarios, sin oportunidad de reintentar si sale mal. Una prueba mínima crea un archivo SharedPreferences real, abre un DataStore con la migración conectada, y verifica que los valores sobrevivieron:
@Test
fun migration_preservesExistingTimerSetting() = runTest {
val prefs = context.getSharedPreferences("settings_prefs", Context.MODE_PRIVATE)
prefs.edit().putInt("timer_minutes", 45).commit()
val dataStore = PreferenceDataStoreFactory.create(
migrations = listOf(SharedPreferencesMigration(context, "settings_prefs")),
produceFile = { File(context.filesDir, "test_settings.preferences_pb") }
)
val minutes = dataStore.data.first()[intPreferencesKey("timer_minutes")]
assertEquals(45, minutes)
}
Ejecuta esto una vez por cada clave que esté actualmente en producción, no solo las de una rama nueva — la migración tiene que trasladar todo lo que un dispositivo real ha acumulado, incluyendo ajustes de funciones publicadas años antes de esta reescritura.
La lista de comprobación
Antes de fusionar una migración de SharedPreferences a DataStore: el nombre del archivo de preferencias antiguo está confirmado contra la llamada real a getSharedPreferences() en el código, SharedPreferencesMigration está conectado para cada clave actualmente en uso, una prueba crea el formato antiguo y verifica que cada valor sobrevive, y el archivo antiguo se deja intacto hasta que la telemetría confirme que la migración se ha ejecutado en la base instalada. Es un poco de cuidado extra para un cambio que los usuarios nunca deberían notar — que, para una migración de ajustes, es exactamente el objetivo.
// Lecturas relacionadas
Más del diario
Baseline Profiles en Android: qué mueve realmente tu tiempo de arranque en frío en 2026
Una guía práctica de los Baseline Profiles de Android: cómo generarlos con Macrobenchmark, conectarlos en Gradle, medir la ganancia real y las otras tres cosas que mueven el arranque en frío.
Construyendo Mintly: mantener un temporizador de enfoque preciso cuando Android quiere matar el proceso
Un temporizador Pomodoro en ejecución tiene un problema de fiabilidad más difícil que un recordatorio puntual. Así es como Mintly sobrevive al modo Doze, a la muerte del proceso y al desfase con la pantalla apagada, con un servicio en primer plano y una hora de fin en tiempo real.
Escaneo de códigos de barras con ML Kit en Android en 2026: cómo Stocky añade un artículo a la despensa en menos de un segundo
Una guía práctica 2026 del escaneo de códigos de barras en el dispositivo con ML Kit y CameraX: ajuste de formatos, búsqueda de producto sin conexión, y las matemáticas del uso parcial.