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.
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:
También ejemplificaremos como escuchar el cambio de una preferencia, antes de que se guarde para procesar su contenido:
Y finalizaremos escuchando el cambio de cualquier valor luego de que fue confirmado.
Puedes descargar el proyecto Android Studio desde el siguiente enlace. Abre el módulo P4_Preferencias_Con_SharedPreference
s 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
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í:
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.
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:
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.