Categorías De Preferencias En Android

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.

Apariencia de categorías de preferencias en Android

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:

Ejemplo de categorías de preferencias en AndroidX

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:

Ejemplo de initialExpandedChildrenCount en categoría de preferencias

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».

Ejemplo de categoría de preferencias expandible en Android

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:

Ejemplo de categoría de preferencias de ayuda

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:

Subscreen de preferencias en AndroidX

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:

Ejemplo de navegación a subpantalla de ajustes de ayuda

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.


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!