Licence CC BY-SA

ListFragment

Les listes sont, sans aucun doute, les composants graphiques les plus utilisés dans les applications mobiles, qu’elles soient sur Android, iOS ou autres. Elles permettent d’afficher facilement des informations les unes à la suite des autres verticalement avec des vues personnalisées ou non pour chaque ligne.

Plusieurs choses seront abordées dans ce chapitre : comment intégrer une liste mêlée à la puissance des fragments, personnaliser ses vues et la rendre dynamique en indiquant des vues spécifiques pour chaque ligne qui compose une liste.

Utilisation simple des listes

Il existes deux possibilités pour intégrer une liste dans une application : la renseigner dans un fichier XML d’affichage ou étendre la classe ListActivity. L’équivalent avec les fragments est similaire à celle des activités à l’exception près qu’il faut tenir compte qu’une liste ne peut pas être initialisée sans un contexte et qu’elle n’est créée qu’une fois l’activité hôte du fragment créée.

Déclarer sa liste dans des fichiers XML

Une petite astuce méconnue consiste à coupler un fichier XML d’affichage avec une classe qui étend ListFragment pour indiquer une vue à afficher si aucune ligne n’est contenue dans la liste. Cet exemple illustrera les deux possibilités décrites précédemment.

Sa mise en place est assez simple. Elle nécessite simplement l’utilisation d’identifiants déclarés dans le framework Android. Parmi tous les identifiants disponibles dans @android:id/, seulement list et empty sont nécessaires pour les listes. Dans le fichier XML d’affichage destiné au ListFragment, il va falloir déclarer une ListView avec l’identifiant @android:id/list et, dans le même fichier XML, au même niveau que la ListView, une autre vue dont l’élément racine possèdera un identifiant @android:id/empty. Ainsi, lors de la désérialisation, le framework Android saura ce qu’il devra charger lorsqu’il y a des éléments dans la liste ou non.

Dans un exemple où un simple texte est affiché au centre de l’écran s’il n’y a aucun élément dans la liste, le fichier XML ressemblerait à :

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

    <ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true" />

    <TextView
        android:id="@android:id/empty"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="@string/text_empty" />

</RelativeLayout>

Création du ListFragment

Le fragment ne sera plus aussi générique que celui du chapitre précédent. Afin de profiter de la création automatique d’une ListView pour le fragment, la classe étendra ListFragment, parfait équivalent de la classe ListActivity. De la même manière que les fragments basiques, un ficher XML d’affichage sera désérialisé dans sa méthode public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) (chose qui n’est pas obligatoire si aucune vue ne doit être affichée s’il n’y a aucun élément dans la liste).

Comme avec une ListActivity, un adaptateur doit être attaché à la liste (adaptateur qui ne sera pas personnalisé pour le moment). L’unique subtilité dans ce cas présent est la méthode à redéfinir. En effet, comme il faut un contexte pour initialiser un adaptateur, il faudra le créer, l’initialiser et l’attacher uniquement lorsque l’activité hôte sera créée. Cela se fera dans la méthode public void onActivityCreated(Bundle savedInstanceState).

Ceci donne le code et le résultat suivant :

 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
package com.siteduzero.android.lists.simple;

import com.siteduzero.android.R;

import android.os.Bundle;
import android.support.v4.app.ListFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;

public class SimpleListViewFragment extends ListFragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_listview, container, false);
    }

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

        final String[] items = getResources().getStringArray(R.array.list_examples);
        final ArrayAdapter<String> aa = new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_1, items);

        setListAdapter(aa);
    }
}

Résultat de l'exécution d'une liste simple

Intégrer une vue personnalisée

La confection de vues personnalisées dans un ListFragment ne diffère pas de son équivalent avec une ListActivity. Cette personnalisation est complètement indépendante de son hôte. Par conséquent, certains aspects seront moins expliqués que d’autres mais une piqûre de rappelle sera donnée, notamment sur l’architecture à adoptée.

Créer une vue personnalisée

Il faut créer plusieurs fichiers pour confectionner sa vue : son fichier XML d’affichage et la classe qui le désérialise. Dans cet exemple, chaque ligne de la liste affichera un texte contenu dans un bloc avec un ombrage. Normalement, les ombrages dans le développement Android sont possibles uniquement grâce à la technologie 9-patch. Elle permet d’étendre horizontalement ou verticalement certaines images sans les pixeliser. Cependant, le framework Android met à disposition des développeurs une série de ressources dont des ombrages.

Le fichier XML d’affichage pourra utiliser ces ressources pour afficher l’ombrage et ressemblera alors à :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:drawable/dialog_holo_light_frame"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="15dp"
        android:text="@string/default_lorem" />

</LinearLayout>

Quant à la classe qui désérialise ce fichier XML, il doit étendre un conteneur, LinearLayout, redéfinir ses constructeurs et initialiser le texte avec la valeur souhaitée. Ce dernier point est possible grâce à une méthode publique qui prend en paramètre l’identifiant d’une chaîne de caractères présente dans les ressources du projet.

 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
package com.siteduzero.android.lists.custom;

import com.siteduzero.android.R;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;

public class CustomListViewView extends LinearLayout {
    private TextView mTextView;

    public CustomListViewView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    public CustomListViewView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public CustomListViewView(Context context) {
        super(context);
        init();
    }

    private void init() {
        inflate(getContext(), R.layout.view_custom_listview, this);
        mTextView = (TextView) findViewById(R.id.textView);
    }

    public void bind(int text) {
        mTextView.setText(getResources().getString(text));
    }
}

Créer son adaptateur

La création d’un adaptateur prend tout son sens à partir du moment où une ListView est constituée de vues personnalisées. Un adaptateur doit étendre la classe BaseAdapter, redéfinir les méthodes obligatoires (il existe des méthodes facultatives pour des listes dynamiques, cela fera l’objet du prochain point de ce chapitre) et permettre de rajouter les données à injecter dans la liste. Ces données peuvent être aussi simples que complexes. Il en revient au développeur d’injecter l’ « intelligence » nécessaire dans son adaptateur.

La chose à savoir dans la création d’un adaptateur est la façon d’implémenter la méthode public View getView(int position, View convertView, ViewGroup parent). Dans les applications mobiles, il est nécessaire d’économiser la moindre zone mémoire possible. Ainsi, lorsque l’utilisateur parcourt une liste, si la vue d’une ligne est encore en zone mémoire, il est préférable de la réutiliser plutôt que d’en créer une nouvelle. Raison pour laquelle cette méthode renvoie une vue en second paramètre.

L’adaptateur s’implémente donc de la manière suivante :

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.siteduzero.android.lists.custom;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class CustomListViewAdapter extends BaseAdapter {
    private List<Integer> mModel = new ArrayList<Integer>();
    private Context mContext;

    public CustomListViewAdapter(Context context) {
        mContext = context;
    }

    @Override
    public int getCount() {
        return mModel.size();
    }

    @Override
    public Integer getItem(int position) {
        return mModel.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        CustomListViewView v = null;
        // Notre vue n'a pas encore été construite, nous le faisons
        if (convertView == null) {
            v = new CustomListViewView(mContext);
        } // Notre vue peut être récupérée, nous le faisons
        else {
            v = (CustomListViewView) convertView;
        }
        v.bind(getItem(position));
        return v;
    }

    public void bind(List<Integer> model) {
        mModel = model;
    }
}

Puisque l’objectif est d’utiliser les fragments, le fragment hôte s’occupera d’envoyer les données via la méthode public void bind(List<Integer> model) de l’adaptateur après son initialisation. L’utilisation d’un adaptateur personnalisé est toute aussi simple qu’un des adaptateurs donnés à la disposition des développeurs par défaut dans le framework. Le résultat correspond à la capture suivante :

Résultat de l'exécution d'une liste avec une vue personnalisée

Vers des listes dynamiques

Une liste dynamique réside dans les différentes vues possibles à appliquer pour ses lignes. A la place de s’arranger avec une ScrollView et d’ajouter des vues dynamiquement à ce conteneur grâce à d’autres conteneurs, le framework Android fournit une solution élégante juste en redéfinissant deux autres méthodes dans un adaptateur personnalisé.

Nouvelles méthodes de l’adaptateur

L’exemple qui illustrera cette liste dynamique consistera à afficher un « header » avec une image à gauche et du texte à droite. Une seconde vue sera un « body » avec simplement du texte (comme la vue du point précédent de ce chapitre). La création de ces vues ne sera pas abordée dans la suite de ce chapitre vu sa simplicité.

Concernant l’adaptateur, pour indiquer qu’il va devoir gérer plusieurs vues différentes dans sa liste, deux méthodes, liées au type de la vue courante à instancier, doivent être redéfinies :

  • public int getViewTypeCount() : Indique combien de vues différentes comportent la liste ;
  • public int getItemViewType(int position) : Indique le type de la vue à instancier pour la ligne courante.

Pour implémenter ces méthodes, il faudra plusieurs constantes et une liste représentant les types. Cette manière de faire est la plus simple et pas du tout obligatoire. Il en revient au développeur de l’adapter pour son propre problème.

L’adaptateur dynamique pourrait ressembler au code cité ci-présent :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class DynamicListViewAdapter extends BaseAdapter {
    private static final int TYPE_HEADER = 0;
    private static final int TYPE_BODY = 1;
    private static final int TYPE_MAX = 2;
    private List<Integer> mTypes = new ArrayList<Integer>();

    // Autres attributs

    @Override
    public int getViewTypeCount() {
        return TYPE_MAX;
    }

    @Override
    public int getItemViewType(int position) {
        return mTypes.get(position);
    }

    // Autres méthodes
}

La mise en place est aussi simple que cela. Il faut aussi permettre de remplir la liste des types en les faisant correspondre avec les données. Comme le point précédent sur les listes personnalisées, il faut définir des méthodes pour attacher une liste de données à celle de l’adaptateur et s’occuper de la liste des types par la même occasion. Il en faudra deux :

  • L’une pour l’en-tête et une seconde pour le corps de la liste. La première méthode prend en paramètre un modèle qui se trouve être une classe confectionnée spécialement pour cet exemple avec seulement deux attributs représentant une image et un texte ;
  • La seconde est identique à la méthode du point précédent, elle prendra une liste d’identifiants de chaînes de caractères.

L’implémentation de ces méthodes pourraient ressembler à :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class DynamicListViewAdapter extends BaseAdapter {
    // Autres attributs et méthodes

    public void bindHeader(DynamicListViewModel model) {
        mModelHeader = model;
        mTypes.add(TYPE_HEADER);
    }

    public void bindBody(List<Integer> model) {
        mModelBody = model;
        for (int i = 0; i < model.size(); i++) {
            mTypes.add(TYPE_BODY);
        }
    }
}

Modification de l’existant

Ces ajouts ont des répercussions sur l’implémentation des méthodes existantes. Le nombre de lignes et la ligne courante de la liste ne correspondent plus à une seule liste de données. Tout comme la méthode d’instanciation de la vue courante qui doit se charger d’instancier la bonne vue. Les deux premières méthodes s’implémentent assez facilement pour cet exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class DynamicListViewAdapter extends BaseAdapter {
    // Autres attributs et méthodes

    @Override
    public int getCount() {
        if (mModelHeader == null)
            return mModelBody.size();
        return 1 + mModelBody.size();
    }

    @Override
    public Object getItem(int position) {
        int type = getItemViewType(position);
        return type == TYPE_HEADER ? mModelHeader : mModelBody
                .get(position - 1);
    }

    // Autres méthodes
}

La méthode public View getView(int position, View convertView, ViewGroup parent) est un poil plus complexe mais, au final, ne fait qu’utiliser les autres méthodes de l’adaptateur. Il faut récupérer le type de la ligne courante au début de la méthode pour pouvoir instancier la vue adéquate. Son implémentation est très similaire à la précédente, mise à part qu’elle effectue une série de condition en fonction de ce type.

 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
39
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    View v = null;
    int type = getItemViewType(position);

    if (convertView == null) {
        switch (type) {
        case TYPE_HEADER:
            v = new DynamicHeaderListViewView(mContext);
            break;
        case TYPE_BODY:
            v = new DynamicBodyListViewView(mContext);
            break;
        }
    } else {
        switch (type) {
        case TYPE_HEADER:
            v = (DynamicHeaderListViewView) convertView;
            break;
        case TYPE_BODY:
            v = (DynamicBodyListViewView) convertView;
            break;
        }
    }

    switch (type) {
    case TYPE_HEADER:
        DynamicListViewModel model1 = (DynamicListViewModel) getItem(position);
        ((DynamicHeaderListViewView) v).bind(model1.getImageRessource(),
                model1.getTextRessource());
        break;
    case TYPE_BODY:
        Integer model2 = (Integer) getItem(position);
        ((DynamicBodyListViewView) v).bind(model2);
        break;
    }

    return v;
}

Il n’en faut pas plus pour concevoir une liste dynamique. Tous les changements se situent au niveau de l’adaptateur. Il suffit alors d’intégrer ce nouvel adaptateur à l’activité ou le fragment hôte en n’oubliant pas d’appeler les méthodes pour lier les données à la liste. Le résultat de l’exemple ressemble à ceci :

Résultat de l'exécution d'une liste avec plusieurs vues personnalisées


En résumé

ListFragment est une sous classe de Fragment qui contient automatiquement une ListView ;

  • Il est possible d’afficher une vue à l’écran lorsqu’il n’y a aucun élément dans la liste. Cela grâce à un fichier XML d’affichage attaché à une ListFragment ;
  • Il est nécessaire de développer son propre adaptateur lorsque les vues, qui composent une liste, deviennent complexes ;
  • Une liste dynamique oblige la redéfinition de deux méthodes supplémentaires dans son adaptateur : public int getViewTypeCount() et public int getItemViewType(int position).