Hola amigos 👋 Bienvenidos a un nuevo tutorial de Universo Android. Hoy aprenderemos a usar DataStore, el reemplazo moderno de SharedPreferences que ofrece almacenamiento asÃncrono, seguro y con soporte para tipos complejos usando Protocol Buffers.
Al finalizar este tutorial tendrás:
- DataStore Preferences (clave-valor simple)
- DataStore Proto (datos tipados)
- Migración desde SharedPreferences
- Operaciones asÃncronas con Flow
- Manejo de errores
- Ejemplos prácticos
🟩 1. ¿Qué es DataStore?
DataStore es la solución moderna de Jetpack para almacenamiento de datos que reemplaza SharedPreferences. Ofrece dos implementaciones: Preferences (clave-valor) y Proto (datos tipados con Protocol Buffers).
🟩 2. Diferencias con SharedPreferences
| CaracterÃstica | SharedPreferences | DataStore |
|---|---|---|
| API | SÃncrona | AsÃncrona (Flow/suspend) |
| Seguridad thread | No garantizada | Thread-safe |
| Errores | Excepciones runtime | Manejo con Flow |
| Tipos complejos | No | SÃ (Proto) |
| Migración | Manual | Automática |
🟩 3. Crear el Proyecto
Creamos un nuevo proyecto en Android Studio con Empty Activity.
🟩 4. Agregar Dependencias
📄 build.gradle (Module: app)
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// DataStore Preferences
implementation 'androidx.datastore:datastore-preferences:1.0.0'
// DataStore Proto
implementation 'androidx.datastore:datastore:1.0.0'
// Protocol Buffers
implementation 'com.google.protobuf:protobuf-javalite:3.21.12'
// Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
}🟩 5. DataStore Preferences (Básico)
📄 PreferencesManager.kt
package com.example.datastoreapp
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
class PreferencesManager(private val context: Context) {
// Definir las claves
companion object {
private val USER_NAME = stringPreferencesKey("user_name")
private val USER_EMAIL = stringPreferencesKey("user_email")
private val USER_AGE = intPreferencesKey("user_age")
private val IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
private val DARK_MODE = booleanPreferencesKey("dark_mode")
private val LANGUAGE = stringPreferencesKey("language")
private val NOTIFICATIONS = booleanPreferencesKey("notifications")
}
// Guardar datos
suspend fun saveUserData(name: String, email: String, age: Int) {
context.dataStore.edit { preferences ->
preferences[USER_NAME] = name
preferences[USER_EMAIL] = email
preferences[USER_AGE] = age
preferences[IS_LOGGED_IN] = true
}
}
// Leer datos como Flow
val userName: Flow<String> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[USER_NAME] ?: ""
}
val userEmail: Flow<String> = context.dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[USER_EMAIL] ?: ""
}
val userAge: Flow<Int> = context.dataStore.data
.map { preferences ->
preferences[USER_AGE] ?: 0
}
val isLoggedIn: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[IS_LOGGED_IN] ?: false
}
// Guardar configuración
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[DARK_MODE] = enabled
}
}
val darkMode: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[DARK_MODE] ?: false
}
suspend fun setNotifications(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[NOTIFICATIONS] = enabled
}
}
val notifications: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[NOTIFICATIONS] ?: true
}
suspend fun setLanguage(language: String) {
context.dataStore.edit { preferences ->
preferences[LANGUAGE] = language
}
}
val language: Flow<String> = context.dataStore.data
.map { preferences ->
preferences[LANGUAGE] ?: "es"
}
// Cerrar sesión
suspend fun logout() {
context.dataStore.edit { preferences ->
preferences[IS_LOGGED_IN] = false
preferences.remove(USER_NAME)
preferences.remove(USER_EMAIL)
preferences.remove(USER_AGE)
}
}
// Limpiar todo
suspend fun clearAll() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}🟩 6. Diseño XML
📄 activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="DataStore Demo"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#2196F3"
android:layout_marginBottom="24dp" />
<!-- Formulario -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Nombre"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txtName"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Email"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txtEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Edad"
android:layout_marginTop="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/txtAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Guardar Datos"
android:layout_marginTop="16dp" />
<!-- Configuraciones -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Configuraciones"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Modo Oscuro"
android:textSize="16sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchDarkMode"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Notificaciones"
android:textSize="16sp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchNotifications"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- Información -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#CCCCCC"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/txtStoredData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:background="#F5F5F5"
android:text="No hay datos almacenados" />
<!-- Acciones -->
<Button
android:id="@+id/btnLogout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Cerrar Sesión"
android:backgroundTint="#FF9800"
android:layout_marginTop="16dp" />
<Button
android:id="@+id/btnClearAll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Limpiar Todo"
android:backgroundTint="#F44336"
android:layout_marginTop="8dp" />
</LinearLayout>
</ScrollView>🟩 7. MainActivity
📄 MainActivity.kt
package com.example.datastoreapp
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var prefsManager: PreferencesManager
private lateinit var txtName: TextInputEditText
private lateinit var txtEmail: TextInputEditText
private lateinit var txtAge: TextInputEditText
private lateinit var switchDarkMode: SwitchMaterial
private lateinit var switchNotifications: SwitchMaterial
private lateinit var txtStoredData: TextView
private lateinit var btnSave: Button
private lateinit var btnLogout: Button
private lateinit var btnClearAll: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
prefsManager = PreferencesManager(this)
initializeViews()
observeData()
setupListeners()
}
private fun initializeViews() {
txtName = findViewById(R.id.txtName)
txtEmail = findViewById(R.id.txtEmail)
txtAge = findViewById(R.id.txtAge)
switchDarkMode = findViewById(R.id.switchDarkMode)
switchNotifications = findViewById(R.id.switchNotifications)
txtStoredData = findViewById(R.id.txtStoredData)
btnSave = findViewById(R.id.btnSave)
btnLogout = findViewById(R.id.btnLogout)
btnClearAll = findViewById(R.id.btnClearAll)
}
private fun observeData() {
// Observar nombre
lifecycleScope.launch {
prefsManager.userName.collect { name ->
updateStoredDataDisplay()
}
}
// Observar modo oscuro
lifecycleScope.launch {
prefsManager.darkMode.collect { enabled ->
switchDarkMode.isChecked = enabled
}
}
// Observar notificaciones
lifecycleScope.launch {
prefsManager.notifications.collect { enabled ->
switchNotifications.isChecked = enabled
}
}
}
private fun setupListeners() {
btnSave.setOnClickListener {
saveUserData()
}
switchDarkMode.setOnCheckedChangeListener { _, isChecked ->
lifecycleScope.launch {
prefsManager.setDarkMode(isChecked)
Toast.makeText(
this@MainActivity,
"Modo oscuro: ${if (isChecked) "ON" else "OFF"}",
Toast.LENGTH_SHORT
).show()
}
}
switchNotifications.setOnCheckedChangeListener { _, isChecked ->
lifecycleScope.launch {
prefsManager.setNotifications(isChecked)
}
}
btnLogout.setOnClickListener {
lifecycleScope.launch {
prefsManager.logout()
clearFields()
Toast.makeText(this@MainActivity, "Sesión cerrada", Toast.LENGTH_SHORT).show()
}
}
btnClearAll.setOnClickListener {
lifecycleScope.launch {
prefsManager.clearAll()
clearFields()
switchDarkMode.isChecked = false
switchNotifications.isChecked = true
Toast.makeText(this@MainActivity, "Datos eliminados", Toast.LENGTH_SHORT).show()
}
}
}
private fun saveUserData() {
val name = txtName.text.toString().trim()
val email = txtEmail.text.toString().trim()
val ageStr = txtAge.text.toString().trim()
if (name.isEmpty() || email.isEmpty() || ageStr.isEmpty()) {
Toast.makeText(this, "Completa todos los campos", Toast.LENGTH_SHORT).show()
return
}
val age = ageStr.toIntOrNull() ?: 0
lifecycleScope.launch {
prefsManager.saveUserData(name, email, age)
clearFields()
Toast.makeText(this@MainActivity, "Datos guardados", Toast.LENGTH_SHORT).show()
}
}
private fun updateStoredDataDisplay() {
lifecycleScope.launch {
val name = prefsManager.userName.first()
val email = prefsManager.userEmail.first()
val age = prefsManager.userAge.first()
val isLoggedIn = prefsManager.isLoggedIn.first()
val darkMode = prefsManager.darkMode.first()
val notifications = prefsManager.notifications.first()
val language = prefsManager.language.first()
val data = buildString {
if (isLoggedIn) {
append("👤 Usuario:\n")
append("Nombre: $name\n")
append("Email: $email\n")
append("Edad: $age\n\n")
} else {
append("❌ No hay sesión activa\n\n")
}
append("⚙️ Configuraciones:\n")
append("Modo Oscuro: ${if (darkMode) "✓" else "✗"}\n")
append("Notificaciones: ${if (notifications) "✓" else "✗"}\n")
append("Idioma: $language")
}
txtStoredData.text = data
}
}
private fun clearFields() {
txtName.text?.clear()
txtEmail.text?.clear()
txtAge.text?.clear()
}
}🟩 8. Configurar Proto DataStore
📄 build.gradle (Module: app) - Agregar plugin
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.protobuf' version '0.9.4'
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.21.12"
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}🟩 9. Definir Proto Schema
📄 app/src/main/proto/user_settings.proto
syntax = "proto3";
option java_package = "com.example.datastoreapp";
option java_multiple_files = true;
message UserSettings {
string name = 1;
string email = 2;
int32 age = 3;
bool is_logged_in = 4;
bool dark_mode = 5;
bool notifications = 6;
string language = 7;
}🟩 10. Serializer Proto
📄 UserSettingsSerializer.kt
package com.example.datastoreapp
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream
object UserSettingsSerializer : Serializer<UserSettings> {
override val defaultValue: UserSettings = UserSettings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserSettings {
try {
return UserSettings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) = t.writeTo(output)
}🟩 11. Proto DataStore Manager
📄 ProtoDataStoreManager.kt
package com.example.datastoreapp
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.dataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import java.io.IOException
private val Context.userSettingsDataStore: DataStore<UserSettings> by dataStore(
fileName = "user_settings.pb",
serializer = UserSettingsSerializer
)
class ProtoDataStoreManager(private val context: Context) {
val userSettingsFlow: Flow<UserSettings> = context.userSettingsDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(UserSettings.getDefaultInstance())
} else {
throw exception
}
}
suspend fun updateUserData(name: String, email: String, age: Int) {
context.userSettingsDataStore.updateData { settings ->
settings.toBuilder()
.setName(name)
.setEmail(email)
.setAge(age)
.setIsLoggedIn(true)
.build()
}
}
suspend fun updateDarkMode(enabled: Boolean) {
context.userSettingsDataStore.updateData { settings ->
settings.toBuilder()
.setDarkMode(enabled)
.build()
}
}
suspend fun logout() {
context.userSettingsDataStore.updateData { settings ->
settings.toBuilder()
.setIsLoggedIn(false)
.clearName()
.clearEmail()
.clearAge()
.build()
}
}
}🟩 12. Migración desde SharedPreferences
val dataStore: DataStore<Preferences> = context.createDataStore(
name = "settings",
migrations = listOf(
SharedPreferencesMigration(
context,
"old_shared_prefs_name"
)
)
)🟩 13. Comparación Preferences vs Proto
| CaracterÃstica | Preferences | Proto |
|---|---|---|
| Tipos de datos | Primitivos | Personalizados |
| Type-safe | No | SÃ |
| Schema | No | SÃ (definido) |
| Complejidad | Baja | Media |
| Rendimiento | Bueno | Excelente |
| Uso | Configuraciones simples | Datos complejos |
▶️ Cómo Ejecutar
- Abre Android Studio
- Copia el código
- Sync Gradle
- Presiona Run
- Guarda datos y reinicia la app
🧪 Resultado Final
Aplicación con DataStore que persiste datos de forma asÃncrona y segura, superior a SharedPreferences.
📥 Descargar Proyecto
👉
🙌 Gracias por Visitar mi Blog
✔️ Compártelo
✔️ Déjame un comentario
✔️ SÃgueme para más contenido
❓ Preguntas Frecuentes
1. ¿Cuándo usar Preferences vs Proto?
Preferences para datos simples clave-valor. Proto para estructuras complejas y type-safety.
2. ¿DataStore reemplaza SharedPreferences?
SÃ, Google recomienda migrar a DataStore por ser más seguro y moderno.
3. ¿Es thread-safe?
SÃ, DataStore es completamente thread-safe y usa coroutines.
4. ¿Cómo migro de SharedPreferences?
Usa SharedPreferencesMigration para migración automática.

No hay comentarios:
Publicar un comentario