Valores De Preferencias Con SharedPreferences

Ahora es el turno de usar los valores almacenados de nuestras preferencias con SharedPreferences, a partir de la jerarquía de ajustes que hemos construido con la librería androidx.preference.

Valores de preferencias con SharedPreferences

Por lo que en este tutorial aprenderás a leer valores almacenados de preferencias y a escuchar eventos de cambios de los mismos.

Nota: Este tutorial es la cuarta parte de la guía de la librería de preferencias, por lo que asumiré que leíste la parte tres Categorías De Preferencias y sus antecesores.


Ejemplo De Preferencias Con SharedPreferences

Para incluir en nuestra App de ejemplo la lectura de los valores de las preferencias, añadiremos varios TextViews que permitan mostrar el estado actual del almacenamiento para los ajustes del usuario:

App de ejemplo para mostrar valores de preferencias con SharedPreferences

También ejemplificaremos como escuchar el cambio de una preferencia, antes de que se guarde para procesar su contenido:

Ejemplo de OnPreferenceChangeListener en Android

Y finalizaremos escuchando el cambio de cualquier valor luego de que fue confirmado.

Ejemplo de OnSharePreferenceChangeListener en Android

Puedes descargar el proyecto Android Studio desde el siguiente enlace. Abre el módulo P4_Preferencias_Con_SharedPreferences que será el correspondiente al desarrollo de estos ejemplos.

Con esto en mente, comencemos por la lectura de preferencias.


1. Leer Valores De Preferencias

La librería de preferencias maneja internamente el guardado de los valores en SharedPreferences. Por esta razón es posible pasar directamente a la lectura de dichos elementos.

SharedPreferences almacena a cada preferencia como un par clave-valor, donde la clave es el string que asignaste en el atributo app:key en preferences.xml o a la propiedad Preference.key si creaste la preferencia dinámicamente.

Por ejemplo, para nuestra preferencia del nombre del negocio:

<EditTextPreference
    app:defaultValue="No establecido"
    app:key="businessName"
    app:title="Nombre del negocio"
    app:useSimpleSummaryProvider="true" />

El par sería "businessName"-"TextoDelUsuario" dependiendo de qué entrada sea colectada por el diálogo.

Por lo que si quieres leer el valor almacenado, entonces obtienes la referencia al punto de acceso con PreferenceManager.getDefaultSharedPreferences() y luego invocas el método get*() asociado al tipo de la preferencia:

PreferenceManager.getDefaultSharedPreferences(this).getString("businessName", "")

Ya que businessName se relaciona con un valor de texto, el método a llamar es getString().

Como ves, la dinámica de lectura es muy sencilla. Así que repliquemos esta implementación para mostrar a los valores de nuestras preferencias en MainActivity.

Diseñar Layout

Layout para mostrar valores de preferencias

En primera instancia actualizaremos al archivo activity_main.xml para incluir un TextView para las siguientes preferencias:

  • Cuenta
    • Nombre del negocio
  • Ubicación
    • Moneda
    • Distancia mínima de ofertas
  • Notificaciones
    • Días en que se envía resumen de cuenta
  • Presentación
    • Mostrar sección de últimos cambios
    • Tema
    • Mostrar animaciones
    • Diseño de colecciones

Puedes simplificar el diseño usando un LinearLayout vertical de la siguiente forma:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/account"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:text="Cuenta"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" />

    <TextView
        android:id="@+id/business_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        tools:text="Nombre del negocio = Abogados PQA" />

    <TextView
        android:id="@+id/locale"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:text="Ubicación"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" />

    <TextView
        android:id="@+id/currency"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Moneda = COP" />

    <TextView
        android:id="@+id/distance"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Distancia mínima de ofertas = 75km." />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:text="Notificaciones"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" />

    <TextView
        android:id="@+id/account_summary"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Días en que se envía resumen de cuenta = [1, 3, 5]" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="16dp"
        android:text="Presentación"
        android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" />

    <TextView
        android:id="@+id/latest_events"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Mostrar sección de últimos cambios = True\nY Ordenar por = date" />

    <TextView
        android:id="@+id/theme"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Tema = Claro" />

    <TextView
        android:id="@+id/animations"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Mostrar animaciones = false" />

    <TextView
        android:id="@+id/collections_design"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:text="Diseño de colecciones = Grilla" />
</LinearLayout>

Mostrar Valores De Preferencias

Ahora desde onCreate() de MainActivity, vamos a obtener la referencia de cada TextView y le asignamos a su propiedad text el valor de cada preferencia.

class MainActivity : AppCompatActivity() {
    private lateinit var businessName: TextView
    private lateinit var currency: TextView
    private lateinit var distance: TextView
    private lateinit var accountSummary: TextView
    private lateinit var latestEvents: TextView
    private lateinit var themeText: TextView
    private lateinit var animations: TextView
    private lateinit var collectionsDesign: TextView


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setUpUI()
        loadPrefs()
    }

    private fun setUpUI() {
        businessName = findViewById(R.id.business_name)
        currency = findViewById(R.id.currency)
        distance = findViewById(R.id.distance)
        accountSummary = findViewById(R.id.account_summary)
        latestEvents = findViewById(R.id.latest_events)
        themeText = findViewById(R.id.theme)
        animations = findViewById(R.id.animations)
        collectionsDesign = findViewById(R.id.collections_design)
    }

    private fun loadPrefs() {
        PreferenceManager.getDefaultSharedPreferences(this).let { store ->
            businessName.text = getString(
                R.string.business_name,
                store.getString("businessName", "")
            )
            currency.text = getString(
                R.string.currency,
                store.getString("currency", "")
            )
            distance.text = getString(
                R.string.distance,
                store.getInt("distance", 0)
            )
            accountSummary.text =
                getString(
                    R.string.account_summary,
                    store.getStringSet("accountSummary", emptySet())
                )

            val eventsActive = store.getBoolean("latestEvents", false)
            var string = getString(R.string.latest_event, eventsActive)
            if (eventsActive)
                string += getString(
                    R.string.latest_events_order,
                    store.getString("latestEventsOrder", "")
                )
            latestEvents.text = string

            themeText.text = getString(
                R.string.theme,
                store.getString("theme", "Claro")
            )
            animations.text = getString(
                R.string.animations,
                store.getBoolean("animations", false)
            )
            collectionsDesign.text = getString(
                R.string.collections_design,
                store.getString("collectionsDesign", "Lista")
            )
        }
    }

    //...
}

La lectura es una tarea repetitiva, por lo que podemos cubrirla con let() para mapear la población de views. Es importante destacar el método get*() usado para cada subclase:

  • Preference, EditTextPreference, ListPreference -> getString()
  • SwitchPreferenceCompat, CheckBoxPreference -> getBoolean()
  • SeekBarPreference -> getInt()
  • MultiSelectListPreference -> getStringSet()

Al desplegar los valores en SharedPreferences verías algo así:

Preferencias con SharedPreferences

Recuerda que el método Context.getString() obtiene un recurso string del archivo strings.xml. Debido a que usamos elementos con argumentos, pasamos como segundo argumento el valor obtenido de las preferencias para reemplazar el placeholder:

<!-- Strings para lectura de valores de preferencias-->
<string name="business_name">Nombre del negocio =  %1$s </string>
<string name="currency">Moneda = %1$s</string>
<string name="distance">Distancia mínima de ofertas = %1$dkm.</string>
<string name="account_summary">Días en que se envía resumen de cuenta = %1$s</string>
<string name="latest_event">Mostrar sección de últimos cambios = %1$b</string>
<string name="latest_events_order">\nY ordenar por = %1$s</string>
<string name="theme">Tema = %1$s</string>
<string name="animations">Animations = %1$b</string>
<string name="collections_design">Diseño de colecciones = %1$s</string>

2. Escuchar Cambios Antes De Guardar Preferencia

Si deseas obtener el control en el momento en que se intenta cambiar el valor de una preferencia específica, entonces usa la interfaz OnPreferenceChangeListener y su controlador onPreferenceChange().

Este método recibe la preferencia que se cambiará y el nuevo valor para dicho cambio. Ya habíamos usado este observador en Tipos de preferencias, cuando actualizábamos el texto secundario de una MultiSelectListPreference:

private fun setUpAccountSummary() {
    val accountSummary: MultiSelectListPreference? = findPreference("accountSummary")
    accountSummary?.run {
        setSummary(values) // Texto secundario inicial
        setOnPreferenceChangeListener { _, newValue ->
            val newValues = newValue as Set<String>
            accountSummary.setSummary(newValues) // Texto secundario al cambiar
            true
        }
    }
}

Pero veamos otro ejemplo donde validamos que el nombre del negocio tenga por lo menos diez caracteres.

Esta acción consiste en comparar la propiedad String.lenght con el literal 10. Si cumple con la precondición, entonces retornamos true, de lo contrario false:

private fun setUpBusinessName() {
    val businessName = findPreference<EditTextPreference>("businessName")
    businessName?.setOnPreferenceChangeListener { _, newValue ->
        if (newValue.toString().length >= 10)
            true
        else {
            Toast.makeText(
                requireActivity(),
                "El nombre debe tener más de 10 caracteres",
                Toast.LENGTH_SHORT
            ).show()
            false
        }
    }
}

true indica que el valor se almacena y false evita que se guarde el nuevo valor entrante.

Toast mostrado antes de intentar cambiar valor de preferencia

Nota: Asignar la anterior escucha protege a la preferencia de valores que no deseamos, pero no avisa al usuario del por qué. Una de las soluciones para ello es que implementes tu propia EditTextPreference para mostrar el error en el EditText con setError().


3. Escuchar Cambios Después De Guardar Preferencia

Por otro lado, es posible que requieras manejar el cambio pero luego de que ha sido guardado el valor de alguna preferencia.

Para ello existe la interfaz SharedPreferences.OnSharedPreferenceChangeListener. A diferencia de OnPreferenceChangeListener, esta percibe los cambios en todas las preferencias.

Veamos un ejemplo simplificado para ilustrar la implementación de esta escucha. Registremos un nuevo observador que muestre un Toast que presente la preferencia que cambió y su valor asentado:

Toast después de cambiar valor de preferencia

Ve a SettingsFragment e implementale a OnSharedPreferenceChangeListener y luego sobrescribe al método onSharedPreferenceChanged():

class SettingsFragment : PreferenceFragmentCompat(),
    SharedPreferences.OnSharedPreferenceChangeListener {

    //...

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        TODO("Not yet implemented")
    }
}

Luego vincula al fragmento como observador a las preferencias a través de registerOnSharedPreferenceChangeListener() en onResume() para que perciba los eventos cuando el fragmento está listo para interactuar con el usuario.

override fun onResume() {
    super.onResume()
    preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}

Equivalentemente, cancela el recibimiento de cambios cuando el fragmento entra en pausa:

override fun onPause() {
    super.onPause()
    preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
}

Y por último escribimos la lógica de onSharedPreferenceChanged():

override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
    Toast.makeText(
        requireActivity(),
        "Preferencia cambiada: $key",
        Toast.LENGTH_SHORT
    ).show()
}

Al recibir la instancia de SharedPreferences y la clave de la preferencia, es posible realizar una comparación con la clave que desees procesar. De esta forma puedes obtener el valor nuevo con algún método get*() e iniciar el cambio en la capa respectiva de tu App.


Valores De Preferencias Con PreferenceDataStore

En este tutorial aprendiste a leer los valores de tus preferencias a partir de la API SharedPreferences. Además, viste ejemplos para escuchar los cambios en los valores antes y después de la confirmación del usuario.

Aunque este almacén es la fuente de datos por defecto de la librería de preferencias de AndroidX, existe una clase llamada PreferenceDataStore que te permite personalizar el lugar donde almacenarás los valores de tus preferencias.

Por esta razón, en el siguiente tutorial veremos cómo implementar una fuente de preferencias personalizada (todo) que sea de utilidad por si SharedPreferences no cumple con nuestras necesidades.


Más Contenidos Android

Únete Al Discord De Develou

Si tienes problemas con el código de este tutorial, preguntas, recomendaciones o solo deseas discutir sobre desarrollo Android conmigo y otros desarrolladores, únete a la comunidad de Discord de Develou y siéntete libre de participar como gustes. ¡Te espero!