Blog para desarrollo de aplicaciones en Android, aprende paso a paso como crear aplicaciones.

Usamos cookies propias y de terceros que entre otras cosas recogen datos sobre sus hábitos de navegación para mostrarle publicidad personalizada y realizar análisis de uso de nuestro sitio.
Si continúa navegando consideramos que acepta su uso. OK Más información | Y más

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-safeNoSí
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.

No hay comentarios:

Publicar un comentario