Licence CC BY-SA

PreferenceFragment

Les paramètres ont une place à part dans une application Android. Une bonne pratique de design est de les intégrer à l’apparence du système. Même si ceci peut constraster avec une application au design élaboré, l’utilisateur Android est habitué à ce pattern et en sera moins déstabilisé. Le framework Android fournit une solution pour faciliter la vie du développeur en lui permettant de confectionner rapidement ce type d’écran.

Mais il persiste une restriction non négligeable. La bibliothèque de compatibilité, développée par Google, est constamment utilisée à travers ce tutoriel mais elle ne compte pas parmi ses classes les préférences. Le framework Android propose deux solutions pour implémenter cette fonctionnalité. Elles seront toutes les deux expliquées mais la rétro-compatibilité sera plus complexe que les concepts vus précédemment.

Définir ses préférences

Paramétriser une application n’est pas une obligation. Certaines applications n’ont pas forcément besoin de sauvegarder quelque chose. De plus, utiliser l’affichage des paramètres du système n’est pas toujours souhaité même si c’est vivement recommandé par les guidelines Android.

Un utilisateur Android a l’habitude de se rendre dans les paramètres de son téléphone pour activer le Wi-Fi, le Bluetooth, le NFC, gérer le son, la luminosité, etc. Toutes ces choses sont « standardisées » (dans le sens où c’est l’affichage voulu par Google). L’utilisateur est familier avec ce type d’écran. Il sait instinctivement comment l’utiliser. Il n’est alors pas nécessaire d’entamer une phase d’apprentissage qui pourrait en rebuter certains.

Créer des groupes de préférences

La définition des préférences se fait simplement grâce à un fichier XML. Ce fichier doit se trouver dans le dossier xml des ressources du projet et son contenu doit avoir comme racine l’élément PreferenceScreen. A partir de là, il existe trois possibilités pour confectionner les préférences :

La première est de catégoriser plusieurs préférences en les regroupant. Il rajoute un titre souligné sur toute la largeur de l’écran et liste les préférences de la catégorie en dessous. L’élément parent est PreferenceCategory et les préférences doivent se placer comme ses fils.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <PreferenceCategory
        android:key="pref_key_category_1"
        android:title="@string/title_category_1" >
        <CheckBoxPreference
            android:defaultValue="false"
            android:key="pref_key_pref_1"
            android:summary="@string/summary_pref_checkbox"
            android:title="@string/title_pref_checkbox" />

        <Preference
            android:dependency="pref_key_pref_1"
            android:key="pref_key_pref_2"
            android:summary="@string/summary_pref_simple"
            android:title="@string/title_pref_simple" />

        <SwitchPreference
            android:key="pref_key_pref_3"
            android:summary="@string/summary_pref_switch"
            android:switchTextOff="@string/switch_off_switch"
            android:switchTextOn="@string/switch_on_switch"
            android:title="@string/title_pref_switch" />
    </PreferenceCategory>
</PreferenceScreen>

La seconde est de placer les préférences dans un nouvel écran. Une préférence sera créée sur le premier écran et lorsque l’utilisateur cliquera dessus, un second écran affichera les nouvelles préférences. L’élément parent est PreferenceScreen et les préférences doivent se placer comme ses fils.

1
2
3
4
5
6
7
8
<PreferenceScreen
    android:key="pref_key_screen"
    android:persistent="false"
    android:title="@string/title_screen" >
    <Preference
        android:summary="@string/summary_pref_simple"
        android:title="@string/title_pref_simple" />
</PreferenceScreen>

La dernière consiste à déclarer les préférences directement, sans catégorisation ou sous écran. L’inconvénient est le risque d’avoir son seul et unique écran assez brouillon s’il y a trop de préférences.

Les différentes préférences

Il existe plusieurs types de préférence. Elles sont disponibles dans des classes java mais elles peuvent être utilisées directement dans le fichier XML destiné aux préférences en spécifiant un nom identique à l’une de ces classes. Les préférences disponibles sont les suivantes :

  • CheckBoxPreference peut retenir un booléen en cochant une case ;
  • SwitchPreference peut retenir un booléen avec un switcher On/Off ;
  • EditTextPreference peut sauvegarder une chaîne de caractères ;
  • RingtonePreference peut saisir une sonnerie à partir du système ou d'une application proposant de la musique ;
  • Preference affiche une valeur non éditable. Elle est souvent couplée avec une autre préférence pour mettre à jour sa valeur. Raison pour laquelle elle possède un attribut supplémentaire, android:dependency, pour référencer la préférence dont elle est dépendante ;
  • ListPreference est la plus complexe des préférences. Elle définie une liste de valeurs que l’utilisateur peut sélectionner. Pour l'initialiser, il faut renseigner plusieurs nouveaux attributs : android:entries pour donner le tableau des valeurs qui seront affichés à l'utilisateur et android:entryValues pour afficher les identifiants de chaque valeur des lignes du tableau ;
  • MultiSelectListPreference est identique à ListPreference mais permet de sélectionner plusieurs lignes dans la liste.

Parmi les attributs, plusieurs sont communs à toutes les préférences, voire certains essentiels pour pouvoir afficher quelque chose sur la ligne de la préférence ou pour récupérer sa valeur par après dans l’application :

  • android:key permet de spécifier un identifiant pour la préférence et permettre de récupérer sa valeur dans le projet ;
  • android:title permet de donner un nom à afficher pour la préférence ou la catégorie ;
  • android:summary pour donner des précisions supplémentaires sur la préférence ;
  • android:defaultValue pour donner une valeur par défaut à la préférence ;
  • android:persistent pour indiquer si la préférence devra être sauvegardée ou non.

Intégrer ses paramètres dans une activité

L’intégration dans une activité dépendra de la version d’Android. Le framework fournit un fragment, PreferenceFragment, utilisable comme tous les fragments rencontrés jusqu’à présent mais son utilisation est limité à l’API 11. Il existe deux méthodes pour intégrer les préférences. L’une pour les versions antérieures à la version 3 et l’autre pour les versions supérieures. Sachant que ces deux méthodes peuvent cohabiter pour être compatibles pour toutes les versions.

Antérieur à la version 3

Malgré le fait que toutes les méthodes, qui seront présentées dans ce point, soient marquées « dépréciées » par l’environnement de travail Eclipse, il n’en reste pas moins le seul moyen actuel pour intégrer un panneau de préférences pour les versions antérieures à 3.0.

Sans la bibliothèque de compatibilité, les versions 2.x ne peuvent pas utiliser les fragments. C’est pourquoi le framework fournit une activité supplémentaire uniquement destinée aux préférences, PreferenceActivity. Cette activité étend ListActivity pour fournir une liste qu’il suffit de remplir par les préférences définies dans le fichier XML.

Pour peupler cette liste, il n’est pas question de désérialiser le fichier XML mais d’appeler la méthode, public void addPreferencesFromResource(int preferencesResId), en renseignant le fichier XML par son identifiant. Elle sera appelée dans la méthode public void onCreate(Bundle savedInstanceState) de l’activité. Son implémentation se rapprochera donc de l’exemple suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.siteduzero.android.settings;

import android.preference.PreferenceActivity;

import com.siteduzero.android.R;

public class SettingsActivity extends PreferenceActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.preferences);
    }
}

Postérieur à la version 3

En revanche, pour les versions plus récentes, la solution est bien plus élégante puisqu’elle utilise pleinement la puissance des fragments. La classe PreferenceFragment est conçu pour accueillir les préférences d’une application. La méthode est exactement la même qu’avec une activité. Il suffit d’appeler la méthode public void addPreferencesFromResource(int preferencesResId) dans la méthode public void onCreate(Bundle savedInstanceState) du fragment.

Quant à l’activité hôte, comme il n’est pas question d’utiliser la bibliothèque de compatibilité, une simple activité suffit pour le gérer. A noter que le framework supporte aussi l’intégration du fragment dans la même activité que pour les versions antérieures à la 3, PreferenceActivity.

Malgré le fait qu’il soit possible de gérer les préférences dans la classe java, le fragment est rarement plus complexe que l’exemple ci-présent.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.siteduzero.android.settings;

import android.os.Bundle;
import android.preference.PreferenceFragment;

import com.siteduzero.android.R;

public class SettingsFragment extends PreferenceFragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        addPreferencesFromResource(R.xml.preferences);
    }
}

Exécution des préférences sur une version supérieure à Android 3.0

Affiner ses paramètres avec les en-têtes

Tout comme les catégories ou les sous écrans, les en-têtes ont pour objectif de rassembler des préférences et de les structurer mais à un niveau supérieur. Contrairement aux catégories (qui sont recommandées), une petite application aura rarement besoin d'utiliser ce nouveau concept. Cela reste tout de même une bonne chose à connaître.

D’autant plus qu’ils sont largement utilisés, notamment dans l’application des préférences du système à partir d’Android 3.0. Structurer une telle application est nécessaire au vu de la quantité des préférences. Elle utilise à la fois les en-têtes, les catégories, les sous écrans et les préférences. A noter qu'autant de préférences pourraient décourager certains utilisateurs. Il faut tenter de rester le plus simple possible tout en restant complet.

Supérieur à la version 3

Comme la définition des préférences, les en-têtes se définissent dans un fichier XML contenu dans le même dossier ressource, res/xml. L’élément racine est preference-headers. Ses fils seront obligatoirement des éléments header qui comportent une série d’attributs obligatoires pour référencer le fragment des préférences et ses informations personnelles.

  • android:fragment renseigne le chemin vers le fragment représentant l'écran des préférences ;
  • android:title renseigne le titre de l'en-tête ;
  • android:summary renseigne les précisions sur l'en-tête ;
  • android:icon renseigne l’icône à ajouter à gauche de l’en-tête.

La définition des en-têtes n’est pas bien complexe. L’exemple ci-dessous illustre les attributs présentés en déclarant deux en-têtes qui pointent vers deux fragments différents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<preference-headers 
xmlns:android="http://schemas.android.com/apk/res/android" >

    <header
        android:fragment="com.siteduzero.android.settings.EditFragment"
        android:icon="@android:drawable/ic_menu_edit"
        android:summary="@string/summary_header_edit"
        android:title="@string/title_header_edit" >
    </header>
    <header
        android:fragment="com.siteduzero.android.settings.AgendaFragment"
        android:icon="@android:drawable/ic_menu_agenda"
        android:summary="@string/summary_header_agenda"
        android:title="@string/title_header_agenda" >
    </header>

</preference-headers>

En ce qui concerne l’activité hôte, il n’est plus question de gérer son fragment avec l’API de gestion des fragments. L’activité PreferenceActivity permet de redéfinir la méthode public void onBuildHeaders(List<Header> target) qui sera appelée uniquement si l’application est exécutée sur un terminal dont la version d’Android est supérieure à 3.0. Dans cette méthode, il faut charger les en-têtes grâce à public void loadHeadersFromResource (int resid, List<PreferenceActivity.Header> target) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.siteduzero.android.settings;

import java.util.List;

import android.preference.PreferenceActivity;

import com.siteduzero.android.R;

public class SettingsActivity extends PreferenceActivity {
    // This methods is called with Android 3.0 and higher.
    @Override
    public void onBuildHeaders(List<Header> target) {
        loadHeadersFromResource(R.xml.settings_headers, target);
    }
}

L’utilisation des en-têtes avec une PreferenceActivity propose automatique un affichage multi-fragment en mode paysage sur les tablettes. Les en-têtes s’affichent sur une colonne à gauche et les préférences, de l’en-tête sélectionné sur une colonne plus grande à droite. Cela peut simplifier la vie du développeur.

L’exécution sur un terminal avec une version récente d’Android donne le résultat suivant pour l’affichage des en-têtes.

Résultat de l'exécution des en-têtes

Antérieur à la version 3

La cohabitation reste possible avec les en-têtes mais demande une légère duplication dans les ressources en créant un nouveau fichier XML et d’ajouter des informations dans l’activité hôte. Du côté de la ressource, les vieilles versions ne connaissent pas les en-têtes. Il faut donc ruser en utilisant les éléments existants pour émuler des en-têtes.

En regardant de plus près, les en-têtes ressemblent étrangement à de simples préférences qui ouvrent un sous écran. Bien que cela soit une solution envisageable, ce n’est pas la meilleure. S’il est possible de réutiliser les fichiers XML existants, définissant les écrans, cela rendrait le fichier des en-têtes moins lourd. Raison pour laquelle, il va falloir déclarer des Preference avec comme fils un élément Intent qui va indiquer l’écran à charger.

Ce nouvel élément déclare trois attributs nécessaires pour indiquer la bonne préférence :

  • android:action indique l’action assignée ;
  • android:targetClass indique la classe qui va recevoir l’Intent ;
  • android:targetPackage indique le paquetage de la classe cible.

L’exemple démontre l’utilisation de préférences avec les Intent pour émuler les en-têtes pour être parfaitement compatible avec les vieilles versions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >

    <Preference
        android:icon="@android:drawable/ic_menu_edit"
        android:summary="@string/summary_header_edit"
        android:title="@string/title_header_edit" >
        <intent
            android:action="com.siteduzero.android.settings.EDIT"
            android:targetClass="com.siteduzero.android.settings.SettingsActivity"
            android:targetPackage="com.siteduzero.android.settings" />
    </Preference>
    <Preference
        android:icon="@android:drawable/ic_menu_agenda"
        android:summary="@string/summary_header_agenda"
        android:title="@string/title_header_edit" >
        <intent
            android:action="com.siteduzero.android.settings.AGENDA"
            android:targetClass="com.siteduzero.android.settings.SettingsActivity"
            android:targetPackage="com.siteduzero.android.settings" />
    </Preference>

</PreferenceScreen>

Du côté de l’activité hôte, c’est un petit plus complexe. Il faut pouvoir ajouter les en-têtes des deux versions et, dans le cas des vieilles versions, traité l’Intent en affichant le bon écran de préférences. La solution pour les versions récentes reste indépendante. Elle ne gène pas la cohabitation. En revanche, l’inverse n’est pas aussi simple.

L’ajout des en-têtes doit se faire uniquement si la version d’Android est plus petite qu’Honeycomb et l’écran adéquat doit être affiché en fonction de l’action de l’Intent. Dans la méthode public void onCreate(Bundle savedInstanceState) de l’activité, il faut récupérer l’action de l’Intent et tester sa valeur avec les différentes actions des en-têtes. Si aucune action n’est disponible dans l’Intent, cela veut dire qu’il faut afficher les en-têtes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.siteduzero.android.settings;

import java.util.List;

import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;

import com.siteduzero.android.R;

public class SettingsActivity extends PreferenceActivity {
    private static final String ACTION_PREF_EDIT = "com.siteduzero.android.settings.EDIT";
    private static final String ACTION_PREF_AGENDA = "com.siteduzero.android.settings.AGENDA";

    @SuppressWarnings("deprecation")
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Preferences for Android 2.3 and lower.
        final String settings = getIntent().getAction();
        // Show screen if a preference if header send an
        if (ACTION_PREF_EDIT.equals(settings)) {
            addPreferencesFromResource(R.xml.settings_edit);
        } else if (ACTION_PREF_AGENDA.equals(settings)) {
            addPreferencesFromResource(R.xml.settings_agenda);
        } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
            // Show header if there aren't intent.
            addPreferencesFromResource(R.xml.settings_headers_legacy);
        }
    }

    // This methods is called with Android 3.0 and higher.
    @Override
    public void onBuildHeaders(List<Header> target) {
        loadHeadersFromResource(R.xml.settings_headers, target);
    }
}

Passer un bundle

Avec l’utilisation des PreferenceFragment, cela peut paraître lourd de devoir créer un fragment par fichier XML. Les en-têtes peuvent palier à ce problème en prenant un Extra comme fils. Cet élément se place dans les arguments du fragment cible en rajoutant la valeur donnée dans son attribut android:value à la clé donnée par l’attribut android:name.

Le fragment cible reste identique pour chaque en-tête mais la valeur, pour la même clé, dans les extras est différente pour savoir quel écran des paramètres afficher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >

    <header
        android:fragment="com.siteduzero.android.settings.SettingsFragment"
        android:icon="@android:drawable/ic_menu_edit"
        android:summary="@string/summary_header_edit"
        android:title="@string/title_header_edit" >
        <extra
            android:name="settings"
            android:value="header_edit" />
    </header>
    <header
        android:fragment="com.siteduzero.android.settings.SettingsFragment"
        android:icon="@android:drawable/ic_menu_agenda"
        android:summary="@string/summary_header_agenda"
        android:title="@string/title_header_agenda" >
        <extra
            android:name="settings"
            android:value="header_agenda" />
    </header>

</preference-headers>

L’unique fragment doit simplement gérer l’argument et ajouter les préférences à partir de sa ressource suivant la valeur donnée.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.siteduzero.android.settings;

import android.os.Bundle;
import android.preference.PreferenceFragment;

import com.siteduzero.android.R;

public class SettingsFragment extends PreferenceFragment {
    private static final String KEY_SETTINGS = "settings";
    private static final String HEADER_EDIT = "header_edit";
    private static final String HEADER_AGENDA = "header_agenda";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Show right preference due to arguments in the fragment.
        final String settings = getArguments().getString(KEY_SETTINGS);
        if (HEADER_EDIT.equals(settings)) {
            addPreferencesFromResource(R.xml.settings_edit);
        } else if (HEADER_AGENDA.equals(settings)) {
            addPreferencesFromResource(R.xml.settings_agenda);
        }
    }
}

Lire les préférences

La lecture des préférences se fait par l’intermédiaire des SharedPreference, utilisables en dehors des préférences. Il suffit de récupérer les préférences partagées par défaut via la méthode statique public static SharedPreferences getDefaultSharedPreferences(Context context) de la classe PreferenceManager.

Une fois l’instance des préférences partagées, il faut récupérer les valeurs des différentes préférences via l’identifiant renseigné dans le fichier XML. Le code de cet exemple pourrait être intégré dans la méthode onResume de l’activité ou du fragment pour recharger les données des préférences après une mise en pause de l’activité hôte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean pref1 = prefs.getBoolean("pref_key_pref_1", false);
mTextViewPref1.setText("" + pref1);
String pref2 = prefs.getString("pref_key_pref_2", "Nothing");
mTextViewPref2.setText("" + pref2);
boolean pref3 = prefs.getBoolean("pref_key_pref_3", false);
mTextViewPref3.setText("" + pref3);
String pref4 = prefs.getString("pref_key_pref_4", "Nothing");
mTextViewPref4.setText("" + pref4);
String pref5 = prefs.getString("pref_key_pref_5", "Nothing");
mTextViewPref5.setText("" + pref5);
String pref6 = prefs.getString("pref_key_pref_6", "Nothing");
mTextViewPref6.setText("" + pref6);

En affichant simplement ses préférences et en éditant quelques valeurs parmi elles, cela donne l’exécution suivante.

Affichage des valeurs des paramètres


En résumé

  • PreferenceFragment n’est pas disponible dans le projet de compatibilité mais une cohabitation est possible avec les plus vieilles versions d’Android ;
  • Les préférences et les en-têtes se définissent dans un fichier XML et sont ajoutés dans une activité ou un fragment à la place d’être désérialisé ;
  • PrefrenceActivity est destiné à la compatibilité avec les vieilles versions mais elle peut être utilisée avec les plus récentes ;
  • Toutes les préférences possèdent un identifiant unique pour pouvoir en récupérer sa valeur à partir de la mémoire partagée par défaut ;
  • Il est possible de rajouter des valeurs dans un bundle pour les préférences et dans les arguments d’un fragment pour les en-têtes.