Licence CC BY-SA

Interfaces dynamiques

La création des fragments est maintenant chose connue mais, jusqu’à présent, ils ont été assimilés à l’écran complet d’un smartphone. Le promesse derrière ce concept est toute autre puisque son but est de dynamiser les interfaces à travers les différents appareils (qu’ils soient smartphones, tablettes, télévisions ou autres). De brefs aperçus sur les possibilités, ont été donnés mais ni les bonnes pratiques ni la communication entre les fragments n’ont été expliquées.

Concevoir une interface dynamique

Concevoir une interface n’est pas une chose aisée, surtout sur Android. Au vu de la diversité des écrans sur tous les types d’appareils mobiles, il faut avant tout penser ses interfaces avant de les développer. Par exemple, un smartphone se contentera d’afficher un fragment à la fois (qu’il soit en paysage ou en portrait) alors qu’une tablette pourrait afficher deux fragments en portrait et trois en paysage (encore faut-il savoir s’il s’agit d’une tablette 7 ou 10 pouces). C’est encore une autre histoire avec les télévisions mais le problème reste le même.

En s’imposant une limite aux smartphones et aux tablettes, peu importe leurs tailles, une pratique d’ergonomie reconnue consiste a afficher un fragment sur smartphone et deux sur tablette en mode paysage. Cela permet de naviguer convenablement sur smartphone (comme toutes les applications) et de remplir complètement l’écran d’une tablette forcément plus grande.

Deux fragments affichés dans différentes configurations

L’objectif est d’ajouter un fragment à une activité pendant son exécution, dans les deux cas. Pour les smartphones, il faut changer le fragment visible à l’écran. Pour les tablettes, charger les données nécessaires si une communication est à faire entre le fragment A et le fragment B. Par exemple, le fragment A pourrait être une liste de pays et le fragment B pourrait vouloir afficher son drapeau. Les deux fragments doivent communiquer et rester affichés à l’écran.

Pour simplifier d’avantage, l’exemple d’illustration ne fera aucune distinction entre les appareils. Les interfaces seront pensées en mode portrait et paysage à la place des tailles minimales.

La manière de procéder est simple et requiert l’utilisation de l’API de gestion des fragments. Couplé à cela, la création des deux « layouts » du même nom pour déclarer l’interface en mode portrait et paysage. Le but est d’obtenir le résultat de l’illustration précédente sans tenir compte du type d’appareil.

Initialiser l'activité hôte

Pour illustrer la communication entre des fragments, il va falloir réviser quelques drapeaux européens. L’idée est d’afficher une liste de pays et son drapeau correspondant. Le projet nécessitera deux fragments : Un premier pour la liste des pays, un second pour le drapeau du pays sélectionné par l’utilisateur. Le mode portrait affichera les fragments en plein écran en les remplaçant à la demande de l’utilisateur alors que le mode paysage affichera la liste et le drapeau l’un à côté de l’autre. Dans ce dernier cas, la communication prendra un sens tout autre puisque la modification devra se faire directement sans charger un nouveau fragment.

La confection des fragments ne seront pas abordées. En cas de doute, revenez aux explications précédentes dans Fragment ou ListFragment.

Intégrer les deux fragments dans une activité hôte n’est pas plus difficile que l’utilisation des fragments dynamiques. Pour le mode paysage, il faut renseigner les deux fragments directement dans le fichier XML d’affichage du dossier des layouts correspondant, à savoir « layout-land ». Dans un souci de visibilité et d’ergonomie, il est préférable de privilégier le contenu à la liste. Dans cet exemple, il s’agit d’un drapeau mais cela aurait pu être un courrier, un site web, etc. Le fichier XML ressemblera à ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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="match_parent"
    android:baselineAligned="false"
    android:orientation="horizontal" >

    <fragment
        android:id="@+id/fragmentList"
        android:name="com.siteduzero.android.dynamicui.CountryListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="2" />

     <fragment
        android:id="@+id/fragmentDetails"
        android:name="com.siteduzero.android.dynamicui.CountryDetailsFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>

Quant au mode portrait, il est légèrement plus complexe puisqu’il faut rendre dynamique le fragment à afficher à l’écran. Dans le fichier XML d’affichage du dossier layout par défaut, il suffit de déclarer un simple conteneur FrameLayout avec un identifiant afin de pouvoir l’utiliser avec les APIs de gestion des fragments.

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frameLayoutDynamicUi"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Pour finir, dans la méthode public void onCreate(Bundle savedInstanceState) de l’activité, il faut rajouter le fragment contenant la liste, CountryListFragment, dans le FrameLayout. Dans le cas contraire, les fragments s’afficheront correctement en mode paysage mais ils ne s’afficheront pas en mode portrait. Pour y parvenir, il faut récupérer une instance d’une transaction, FragmentTransaction, via le manager, FragmentManager, et utiliser la méthode public FragmentTransaction add(int containerViewId, Fragment fragment) en lui passant une instance du fragment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_dynamic_ui);

    if (findViewById(R.id.frameLayoutDynamicUi) != null) {
        final CountryListFragment listFragment = new CountryListFragment();
        getSupportFragmentManager().beginTransaction()
            .add(R.id.frameLayoutDynamicUi, listFragment).commit();
    }
}

Communication entre les fragments

Au ce stade, l’application s’exécute correctement et affiche une liste en mode portrait et les deux fragments avec une liste à gauche et un drapeau à droite mais avec aucune interaction dans les deux cas en mode paysage. Pour remédier à ce problème, il faut utiliser des listeners pour permettre une communication entre un fragment A et un fragment B en passant par une activité hôte. En effet, l’activité va devoir gérer les messages entre les fragments qu’elle contient.

Dans le fragment CountryListFragment, il faut déclarer une interface, OnCountrySelectedListener, et un attribut du même type. Cette interface sera implémentée par l’activité afin de pouvoir transmettre le message du fragment contenant la liste vers celui contenant le drapeau pour savoir lequel il faut afficher à l’écran. Pour initialiser l’attribut, les fragments peuvent redéfinir la méthode public void onAttach(Activity activity) afin de connaitre son activité hôte et caster son instance dans le listener. Le fragment mettra en place le listener 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
package com.siteduzero.android.dynamicui;

import android.app.Activity;
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.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;

import com.siteduzero.android.R;

public class CountryListFragment extends ListFragment implements
        OnItemClickListener {
    private OnCountrySelectedListener mListener = null;

    // Autres méthodes

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            mListener = (OnCountrySelectedListener) activity;
        } catch (ClassCastException e) {
            // Unchecked exception.
            throw new ClassCastException(activity.toString()
                + " must implement OnCountrySelectedListener");
        }
    }

    public interface OnCountrySelectedListener {
        void onCountrySelected(int position);
    }

    @Override
    public void onItemClick(AdapterView<?> arg0, View arg1, int position,
            long arg3) {
        if (mListener != null) {
            mListener.onCountrySelected(position);
        }
    }
}

Lorsque l’utilisateur clique sur un élément de la liste, il faut tester si le listener a été initialisé pour éviter tout crash de l’application. Il vaut mieux une application qui ne répond pas qu’une application qui quitte de manière imprévue (dans l’éventualité où il n’a pas été initialisé). Cette pratique est une bonne habitude à prendre et évite tous les risques liés à une refactorisation du code ou autres manipulations du même genre.

Dans l’activité hôte, il faut prévoir les différents cas :

  • Soit l’utilisateur est en mode portrait et il faut remplacer les fragments en passant le fragment à afficher ;
  • Soit l’utilisateur est en mode paysage et il faut mettre à jour le drapeau à afficher.

Détecter la configuration de l’appareil est identique à la solution implémentée lors de la création de l’activité. Si le conteneur FrameLayout est différent de null, l’utilisateur est en mode portrait sinon il est en mode paysage. Dans le premier cas, il suffit d’utiliser une transaction pour remplacer le fragment dans le conteneur. Dans le second, il faut déclarer une méthode dans le fragment CountryDetailsFragment pour mettre à jour le drapeau selon la position donnée à partir de la méthode public void onCountrySelected(int position).

Elle s’implémente de la façon suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public void onCountrySelected(int position) {
    if (findViewById(R.id.frameLayoutDynamicUi) == null) {
        // If we are in landscape mode, we show article in the second
        // fragment.
        final CountryDetailsFragment detailsFragment = (CountryDetailsFragment) getSupportFragmentManager().findFragmentById(R.id.fragmentDetails);
        detailsFragment.updateCountry(position);
    } else {
        // Else, we show the other fragment in portrait mode.
        final CountryDetailsFragment detailsFragment = CountryDetailsFragment.newInstance(position);
        final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.replace(R.id.frameLayoutDynamicUi, detailsFragment);
        ft.addToBackStack(null);
        ft.commit();
    }
}

Maintenant, à l’exécution de l’application, la communication est opérationnelle et elle met bien à jour le drapeau à afficher en mode paysage ou elle crée le fragment avec le bon drapeau à afficher dans le mode portrait. Cet exemple est assez simple mais les interfaces peuvent être plus complexes. Il en revient au développeur d’y ajouter l'intelligence nécessaire pour convenir aux besoins de ses applications.

Résultat de l'exécution d'une interface dynamique en mode paysage


En résumé

  • Il faut penser ses interfaces pour les différentes tailles d’écran et dans les différentes configurations ;
  • Une activité reste indépendante des fragments qu’elle contient ;
  • Une transaction permet de gérer des fragments dans une activité hôte ;
  • Un listener permettent de communiquer d’un fragment A à un fragment B en passant par une activité hôte.