Bienvenido a Universo Android

Estamos actualizando el sitio web! Pronto subiremos el nuevo diseño de la web! Gracias por su paciencia!

Mostrando entradas con la etiqueta almacenamiento Android. Mostrar todas las entradas
Mostrando entradas con la etiqueta almacenamiento Android. Mostrar todas las entradas

DataStore: Preferences y Proto en Android

DataStore: Preferences y Proto en Android

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ísticaSharedPreferencesDataStore
APISíncronaAsíncrona (Flow/suspend)
Seguridad threadNo garantizadaThread-safe
ErroresExcepciones runtimeManejo con Flow
Tipos complejosNoSí (Proto)
MigraciónManualAutomática

🟩 3. Crear el Proyecto

Creamos un nuevo proyecto en Android Studio con Empty Activity.

🟩 4. Agregar Dependencias

📄 build.gradle (Module: app)

gradle
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

kotlin
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
<?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

kotlin
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

gradle
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

protobuf
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

kotlin
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

kotlin
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

kotlin
val dataStore: DataStore<Preferences> = context.createDataStore(
    name = "settings",
    migrations = listOf(
        SharedPreferencesMigration(
            context,
            "old_shared_prefs_name"
        )
    )
)

🟩 13. Comparación Preferences vs Proto

CaracterísticaPreferencesProto
Tipos de datosPrimitivosPersonalizados
Type-safeNo
SchemaNoSí (definido)
ComplejidadBajaMedia
RendimientoBuenoExcelente
UsoConfiguraciones simplesDatos complejos

▶️ Cómo Ejecutar

  1. Abre Android Studio
  2. Copia el código
  3. Sync Gradle
  4. Presiona Run
  5. 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.