Las categorías de preferencias en Android te permiten agrupar configuraciones asociadas a un mismo contexto, con el fin de reducir la complejidad de la pantalla de ajustes y aumentar la legibilidad para el usuario.
La agrupación se enfatiza con la inclusión de un divisor en la parte superior de la categoría y un título de sección.
En este tutorial aprenderás a:
- Definir categorías de preferencias desde XML y Kotlin
- Crear categorías expandibles
- Navegar a subpantallas de ajustes cuando las categorías no son suficientes
Nota: Es necesario que leas el tutorial anterior Tipos De Preferencias para comprender el estado previo del ejemplo que actualizaremos.
Ejemplo De Categorías De Preferencias En Android
Esta vez nuestro objetivo será dividir las seis preferencias que tenemos actualmente en las siguientes categorías: Cuenta, Ubicación, Notificaciones, Presentación y Ayuda:
Además de eso, añadiremos tres preferencias más asociadas a los datos acerca de la aplicación, con el fin de crear una subpantalla. Descarga el proyecto Android Studio del ejemplo desde el siguiente enlace (módulo P3_Categorias_De_Preferencias
):
Empecemos estableciendo como se definen las categorías con la librería de preferencias de AndroidX.
1. Agrupar Preferencias Por Categorías
Una vez que hayas definido qué categorías se relacionan entre sí para proveer la configuración de un aspecto similar, entonces, solo queda ir a la jerarquía preferences.xml
y crear su representación:
La clase a usar será PreferenceCategory
. Para implementarla, tan solo envuelve a todas las preferencias pertenecientes al grupo:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="Cuenta">
<EditTextPreference
app:defaultValue="No establecido"
app:key="businessName"
app:title="Nombre del negocio"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<PreferenceCategory android:title="Ubicación">
<ListPreference
app:defaultValue="1"
app:entries="@array/currency_entries"
app:entryValues="@array/currency_values"
app:key="currency"
app:title="Moneda"
app:useSimpleSummaryProvider="true" />
<SeekBarPreference
app:defaultValue="50"
app:showSeekBarValue="true"
app:title="Distancia (KM)" />
</PreferenceCategory>
<PreferenceCategory android:title="Notificaciones">
<MultiSelectListPreference
android:defaultValue="@array/account_summary_default"
android:entries="@array/account_summary_entries"
android:entryValues="@array/account_summary_values"
android:key="accountSummary"
android:title="Frecuencia resumen de cuenta" />
</PreferenceCategory>
<PreferenceCategory
android:title="Presentación">
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="latestEvents"
android:title="Mostrar últimos cambios"
app:icon="@drawable/ic_timeline"
app:summaryOff="No aparecerán los últimos cambios ocurridos en la pantalla principal"
app:summaryOn="Aparecerán los últimos cambios ocurridos en la pantalla principal" />
<ListPreference
android:defaultValue="date"
android:entries="@array/latest_events_order_entries"
android:entryValues="@array/latest_events_order_values"
android:key="latestEventsOrder"
android:title="Ordenar por"
app:dependency="latestEvents"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
<Preference
app:icon="@drawable/ic_help"
app:allowDividerAbove="true"
app:title="Ayuda" />
</PreferenceScreen>
El título y la clave de la categoría son establecidos por app:title
y app:key
, al igual que las preferencias. Esto se debe a que es descendiente de PreferenceGroup
, que a su vez es hija de Preference
.
Crear Categorías Desde Kotlin
Crear los objetos PreferenceCategory
en tiempo de ejecución solo requiere la invocación de su constructor público y el método addPreference()
como vimos en la sección Crear Preferencias Desde Kotlin en el primer tutorial.
Esto quiere decir que crearemos primero los contenedores y luego añadiremos las preferencias a cada grupo según su nivel en la jerarquía:
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
val accountCategory = PreferenceCategory(context).apply {
key = "account"
title = "Cuenta"
}
screen.addPreference(accountCategory)
val businessName = EditTextPreference(context).apply {
key = "buildVersion"
setTitle(R.string.business_name_pref)
setDefaultValue(getString(R.string.business_name_default))
summaryProvider = EditTextPreference.SimpleSummaryProvider.getInstance()
setDialogTitle(R.string.business_name_pref)
}
accountCategory.addPreference(businessName)
preferenceScreen = screen
}
En el código anterior creamos la categoría de Cuenta. Como ves, usamos el constructor de PreferenceCategory
y luego con la función de alcance apply()
configuramos sus atributos base.
Luego la añadimos a la pantalla para que se le asigne un ID. De esa forma, será posible añadirle preferencias como businessName
.
Y al igual que siempre, terminamos asignando el objeto PreferenceScreen
conformado al atributo preferenceScreen
.
2. Categoría Desplegable
Si posees un grupo de categorías que no son usadas frecuentemente, puedes ocultarlas para reducir el espacio que consumen en la pantalla de configuración:
En nuestro ejemplo usaremos un conjunto de tres nuevos ajustes asociados a la categoría de presentación: Tema, Animaciones y Diseño por defecto.
Así que para ocultar estas preferencias y mostrar un botón de expansión, solo crearemos una nueva categoría que las contenga y luego asignaremos el atributo initialExpandedChildrenCount
con el valor de 2
:
<PreferenceCategory
android:title="Presentación"
app:initialExpandedChildrenCount="2">
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="latestEvents"
android:title="Mostrar últimos cambios"
app:icon="@drawable/ic_timeline"
app:summaryOff="No aparecerán los últimos cambios ocurridos en la pantalla principal"
app:summaryOn="Aparecerán los últimos cambios ocurridos en la pantalla principal" />
<ListPreference
android:defaultValue="date"
android:entries="@array/latest_events_order_entries"
android:entryValues="@array/latest_events_order_values"
android:key="latestEventsOrder"
android:title="Ordenar por"
app:dependency="latestEvents"
app:useSimpleSummaryProvider="true" />
<Preference
app:summary="Claro"
app:title="Tema" />
<SwitchPreferenceCompat
app:summaryOff="Ocultar animaciones"
app:summaryOn="Mostrar animaciones"
app:title="Animaciones" />
<Preference
app:summary="Lista"
app:title="Diseño por defecto" />
</PreferenceCategory>
El valor asignado a este atributo representa la cantidad de preferencias que serán visibles. El resto será compactado en una opción llamada «Opciones avanzadas».
Nota: A este comportamiento también se le conoce como «Progressive Disclosure» en la guía de lineamientos de ajustes de Android.
3. Subpantallas Con Listas De Preferencias
Cuando existe un gran número de preferencias relacionadas o gran variedad de categorías, lo mejor es usar subpantallas que contengan a dichos grupos:
Por ejemplo, la captura anterior muestra la adición de tres preferencias nuevas relacionadas a la sección de Ayuda:
- FAQ
- Contacto de soporte
- Políticas de privacidad
Crear Fragmento Para Subpantalla
Ahora supongamos que deseamos aislar estas preferencias en otra subpantalla de ajustes como la siguiente:
La forma de lograr este resultado es usando el atributo app:fragment
. Al cual asignaremos el nombre completamente calificado de un nuevo PreferenceFragmenCompat
que represente a la subpantalla:
<Preference
app:allowDividerAbove="true"
app:fragment="com.develou.p3_categorias_de_preferencias.HelpSettingsFragment"
app:icon="@drawable/ic_help"
app:title="Ayuda" />
Si la referencia del fragmento es larga como la del ejemplo, ve al fragmento, presiona click derecho y selecciona Copy>Copy Reference. Con ello copiarás al portapapeles el nombre calificado.
Luego, añade un nuevo fragmento llamado HelpSettingsFragment
como sigue:
class HelpSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.help_preferences, rootKey)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if(android.R.id.home == item.itemId){
requireActivity().setTitle(R.string.settings)
requireActivity().onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
}
Sobrescribiremos a onOptionsItemSelected()
para definir un nuevo comportamiento del Up button: Cambiar el título de la actividad y luego invocar onBackPressed()
de la misma, para regresar al primer fragmento de ajustes en la back stack.
Definir UI De Subpantalla
Lo siguiente será construir la interfaz a partir de un nuevo archivo de jerarquía llamado help_preferences.xml
que contenga los ajustes nombrados anteriormente:
<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:key="buildVersion"
app:summary="1.0.0"
app:title="@string/build_version_preference" />
<Preference
app:key="faq"
app:title="FAQ" />
<Preference
app:key="contact"
app:title="Contacto de soporte" />
<Preference
app:key="privacity"
app:title="Políticas de privacidad" />
</PreferenceScreen>
Si corres la App verás como la librería maneja por ti la transacción para navegar al nuevo fragmento:
Manejar Inicio Del Fragmento
Ahora bien, si deseas manejar el evento en el que el fragmento es iniciado al presionar la preferencia, entonces usa al observador OnPreferenceStartFragmentCallback
y su controlador onPreferenceStartFragment()
.
Esto significa ir a la actividad contenedora de ambos fragmentos e implementar la interfaz, con el objetivo de que cumpla el rol de administradora del inicio:
class SettingsActivity : AppCompatActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
if (savedInstanceState == null) {
supportFragmentManager.commit {
replace(R.id.settings_container, SettingsFragment())
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onPreferenceStartFragment(
caller: PreferenceFragmentCompat,
pref: Preference
): Boolean {
// Fabricar a HelpSettingsFragment
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
pref.fragment
)
// Instalarlo en SettingsActivity
supportFragmentManager.commit {
replace(R.id.settings_container, fragment)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
addToBackStack(null)
}
// Cambiar título de actividad por "Ayuda"
setTitle(R.string.help_settings_title)
return true
}
}
El método onPreferenceStartFragment()
recibe la instancia del fragmento que solicita la navegación (SettingsFragment
) y la preferencia asociada (Ayuda
).
La preferencia nos brindará acceso a Preference.fragment
para obtener la clase asociada y así fabricar el fragmento con fragmentFactory
.
Con esta información puedes usar el supportFragmentManager
desde la actividad para crear una transacción de reemplazo con la función de extensión commit()
.
Al final terminamos cambiando el título de la actividad y luego retornamos true
para especificar que manejamos la creación del fragmento, por lo que la ejecución por defecto no debería tener lugar.
Nota
: Si usas el Navigation Component, onPreferenceStartFragment()
será un buen lugar para invocar a navigate()
. Aunque también podrías usar la escucha OnPreferenceClickListener
directamente en la preferencia para registrar la navegación.
Obtener Valores De Preferencias
En este tutorial viste como agrupar tus preferencias por categorías o el uso de subpantallas. El siguiente aspecto a tratar será el acceso a los valores de estas preferencias a través de SharedPreferences (todo).
Usar este almacenamiento te permitirá obtener todos los ajustes guardados por el usuario desde cualquier parte de tu App.