Skip to content

2. Squelette d'un client Android communicant avec un service web / jSON

Nous proposons maintenant un squelette d'application Android communicant avec un ou des services web / jSON. C'est le projet [client-android-skel] qu'on trouvera dans le dossier [architecture] des exemples :

  

L'étude de cette application squelette va être l'occasion de revoir certains points que nous avons rencontrés dans les exemples précédents. Cette application servira de squelette à toutes les applications à venir. Elle a été construite après de nombreuses itérations. Elle cherche à factoriser dans des classes abstraites le plus d'éléments possibles des applications que nous allons bientôt construire afin éviter d'avoir à écrire toujours le même type de code se différenciant seulement par des détails. Ses caractéristiques sont les suivantes :

  • la communication asynchrone avec le serveur web / jSON se fait avec la bibliothèque RxJava ;
  • le cycle de vie d'un fragment (update, save, restore) est géré par sa classe parent [AbstractFragment] qui appelle à des moments précis certaines méthodes de ses classes filles. La classe fille n'a ainsi pas à se soucier des étapes du cycle de vie mais seulement d'implémenter certaines méthodes imposées par sa classe parent ;
  • le cycle de vie de l'activité (save / restore) est géré par une classe abstraite [AbstractActivity] qui elle aussi impose à l'activité fille d'implémenter certaines méthodes ;
  • la classe [AbstractActivity] est capable de gérer une application avec ou sans onglets, avec ou sans image d'attente, avec ou sans authentification basique auprès du serveur web / jSON. La présence ou non de ces éléments se fait par configuration ;

Ce squelette a été utilisé pour tous les exemples à venir. A cause de la diversité de ceux-ci, ce qui marchait pour un exemple pouvait ne pas marcher pour l'exemple suivant. Comme le squelette a été utilisé pour sept exemples au total, de nombreuses itérations ont eu lieu. Si on l'utilisait pour un huitième exemple, il est possible qu'on trouverait là encore que la spécificité de ce nouvel exemple génère de nouvelles erreurs. Néanmoins, l'utilisation de ce squelette va considérablement simplifier l'écriture des exemples à venir. En effet, la gestion du cycle de vie d'un fragment (update, save, restore) couplée à la notion d'adjacence des fragments est particulièrement complexe. Ici, elle est totalement cachée dans la classe [AbstractFragment].

2.1. Architecture du client Android

Le client Android proposé repose sur l'architecture suivante :

  • la couche [DAO] implémente une interface [IDao]. C'est elle qui communique avec le serveur web / jSON ;
  • il n'y a qu'une activité qui implémente également l'interface [IDao]. Les vues s'adressent à elle pour atteindre le serveur ;
  • les vues sont implémentées par des fragments ;

Le projet Android reflète cette architecture :

  

Nous allons présenter un à un les différents éléments de ce projet.

2.2. La configuration Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  // options de packaging nécessaires pour être capable de produire l'APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • tous les n°s de version sont sujets à modification. On peut néanmoins partir des n°s actuels si on configure Android Studio pour que ces versions des outils Android (lignes 15-16, 47-48) soient bien présentes (cf paragraphe 6.11) ;

2.3. Le manifeste de l'application

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.android">

  <uses-permission android:name="android.permission.INTERNET"/>

  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".activity.MainActivity_"
      android:label="@string/app_name"
      android:windowSoftInputMode="stateHidden"
      android:theme="@style/AppTheme.NoActionBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
  • ligne 3 : on changera le package de l'application ;
  • lignes 10, 15 : on fixera la valeur de l'item [app_name] dans le fichier [res / values / strings.xml]. Pour l'instant celui-ci est le suivant :

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- nom de l'application -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. L'organisation du code Java

  
  • [architecture] regroupe les éléments principaux d'organisation du code ;
  • [activity] contient l'activité unique de l'application ;
  • [fragments] regroupe les fragments ou vues de l'application ;
  • [dao] regroupe les éléments de communication avec le serveur web / jSON ;

2.5. Eléments de l'activité

 

Image

2.5.1. La vue associée à l'activité

La vue [activity_main.xml] associée à l'activité est la suivante :


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- conteneur de fragments -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • ligne 29 : on utilise un conteneur de fragments spécifique ;

L'activité a également un menu [res / menu / menu_main.xml] pour sa vue :


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
</menu>

Pour l'instant, il est vide. Le développeur le complètera si besoin est.

2.5.2. Le conteneur de fragments [MyPager]

  

package client.android.architecture;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

public class MyPager extends ViewPager {

  // contrôle le swipe
  private boolean isSwipeEnabled;
  // contrôle le scrolling
  private boolean isScrollingEnabled;

  // constructeurs
  public MyPager(Context context) {
    super(context);
  }

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

  // méthodes à redéfinir pour gérer le swipe
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe autorisé ?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe autorisé ?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // contrôle du scrolling
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // setters
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }
}

Cette classe étend la classe standard Android [ViewPager] uniquement pour gérer le swipe (ligne 11) et le scrolling (ligne 13) entre vues.

  • lignes 26-43 : les méthodes qui inhibent le swipe si celui-ci a été désactivé ;
  • lignes 46-49 : redéfinition de la méthode [setCurrentItem] qui sert à changer la vue affichée. Si le scrolling a été inhibé, le changement de vue se fera sans scrolling. A noter que le développeur peut contourner ce mode de fonctionnement en utilisant la méthode [setCurrentItem(int position, boolean smoothScrolling)] qui lui permet de préciser le scrolling qu'il désire ;

2.5.3. La classe [CoreState]

  

La classe [CoreState] est la classe parent des états des différents fragments :


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo : ajouter ici les sous-classes de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visité ou non
  protected boolean hasBeenVisited = false;
  // état de l'éventuel menu du fragment
  protected MenuItemState[] menuOptionsState;

  // getters et setters
...
}
  • ligne 16 : chaque fragment a dans son état un booléen [hasBeenVisited] qui dit s'il a déjà été visité ou non. Ceci est nécessaire car parfois, lors du 1er affichage d'un fragment, il y a des choses particulières à faire ;
  • ligne 18 : le projet [client-android-skel] sauvegarde et restaure automatiquement les menus des fragments s'ils en ont un. Dans le tableau MenuItemState[] menuOptionsState, on stocke l'état visible ou non de toutes les options du menu ;
  • lignes 10-13 : comme il a été fait dans [Exemple-22], l'état de l'activité et de ses fragments sera sauvegardé dans la session qui elle, sera sauvegardée sous la forme d'une chaîne jSON. Nous allons voir que la session mémorise un tableau d'éléments de type [CoreState]. Si on ne fait rien, c'est alors la chaîne jSON d'un type [CoreState] qui sera sauvegardée. Or nous, nous voulons sauvegarder les états des fragments, des états dérivés de [CoreState]. Pour que ce soit la chaîne jSON du type dérivé qui soit produite et non pas celle du type parent, il faut déclarer les types dérivés comme il est indiqué aux lignes 10-13. La classe [CoreState] est l'une des classes de l'architecture que le développeur doit modifier pour chaque nouvelle application (lignes 10-13) ;

2.5.4. L'interface [IMainActivity]

  

L'interface [IMainActivity] fixe ce que peuvent demander les fragments à l'activité dans l'architecture suivante :

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accès à la session
  ISession getSession();

  // changement de vue
  void navigateToView(int position, ISession.Action action);

  // gestion de l'attente
  void beginWaiting();

  void cancelWaiting();

  // constantes de l'application (à modifier) -------------------------------------

  // mode debug
  boolean IS_DEBUG_ENABLED = true;

  // délai maximal d'attente de la réponse du serveur
  int TIMEOUT = 1000;

  // délai d'attente avant exécution de la requête client
  int DELAY = 0;

  // authentification basique
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacence des fragments
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barre d'onglets
  boolean ARE_TABS_NEEDED = false;

  // image d'attente
  boolean IS_WAITING_ICON_NEEDED = false;

  // nombre de fragments de l'application
  int FRAGMENTS_COUNT = 0;

  // todo ajoutez ici vos constantes et autres méthodes
}
  • ligne 6 : l'interface [IMainActivity] étend l'interface [IDao] de la couche [DAO] ;
  • ligne 9 : c'est l'activité qui donne accès à la session sous la forme d'une instance de l'interface [ISession] ;
  • ligne 12 : c'est via l'activité qu'on change de vue. Le second paramètre est l'action qui provoque ce changement de vue, l'une des valeurs SUBMIT, NAVIGATION, RESTORE ;
  • lignes 15-17 : c'est l'activité qui gère l'image d'attente ;
  • ligne 22 : pour le débogage de l'application ;
  • ligne 25 : pour ne pas attendre trop longtemps si le serveur ne répond plus ;
  • ligne 28 : en débogage, on mettra une valeur de quelques secondes pour avoir le temps d'annuler l'opération avec le serveur et voir ce qui se passe ;
  • ligne 31 : à true si le service jSON demande une authentification basique ;
  • ligne 34 : adjacence de fragments ;
  • ligne 37 : à vrai si l'application a des onglets ;
  • ligne 39 : à vrai si l'application communique avec un serveur web / jSON et qu'on veut montrer une image d'attente lors des échanges ;
  • ligne 43 : le nombre de fragments gérés par l'application ;

L'interface [IMainActivity] est le second élément de l'architecture que le développeur doit compléter (ligne 45).

2.5.5. L'interface [IDao]

L'interface [IMainActivity] étend l'interface [IDao] suivante :

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Url du service web
  void setUrlServiceWebJson(String url);

  // utilisateur
  void setUser(String user, String mdp);

  // timeout du client
  void setTimeout(int timeout);

  // authentification basique
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // mode debug
  void setDebugMode(boolean isDebugEnabled);

  // délai d'attente en millisecondes du client avant requête
  void setDelay(int delay);

  // todo : déclarez votre interface ici
}
  • ligne 24 : le développeur complètera l'interface ici ;

2.5.6. La session

  

La classe [Session] encapsule les éléments partagés par l'activité et les fragments. Elle implémente l'interface [ISession] suivante :


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // numéro de la dernière vue affichée
  int getPreviousView();

  void setPreviousView(int numView);

  // dernier état d'une vue
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // action en cours
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // états de toutes les vues -
  // pas utilisé par le code mais est nécessaire pour sérialisation / désérialisation jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // n° du dernier onglet sélectionné
  int getPreviousTab();

  void setPreviousTab(int position);

  // navigation sur sélection onglet
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

Nous introduisons l'interface [ISession] pour imposer la présence de certaines méthodes dans la session :

  • lignes 7-10 : le n° de la dernière vue (fragment) affichée ;
  • lignes 12-15 : l'état d'une vue particulière ;
  • lignes 17-24 : nous introduisons la notion d'action en cours. Il y en a quatre (ligne 17) :
    • RESTORE : une sauvegarde / restauration est en cours. Il n'y a pas de changement de vue ;
    • NAVIGATION : une navigation est en cours. On appellera ici navigation, un changement de vue où la nouvelle vue peut être restaurée à partir de son dernier état stocké en session ;
    • SUBMIT : on donnera le type [SUBMIT] à une action en cours, lorsqu'il y a changement de vue et que la nouvelle vue dépend de l'état de l'activité en général et non de son seul état à elle. Parfois, la distinction entre NAVIGATION et SUBMIT est difficile à faire. Dans ce cas, on prendra le cas le plus général du SUBMIT ;
    • NONE : valeur de l'action lorsque celle-ci n'a pas encore reçu sa première valeur ;
  • lignes 26-30 : les états de l'activité et des fragments seront mémorisés dans un tableau de type CoreState[]. Pour que celui-ci soit géré correctement lors des sérialisations / désérialisations jSON, il faut qu'il ait un getter et un setter ;
  • lignes 32-35 : n° du dernier onglet sélectionné. Est utilisé lors du cycle sauvegarde / restauration pour resélectionner l'onglet qui était sélectionné avant la rotation du périphérique ;
  • lignes 37-40 : gestion d'un booléen qui indique si la sélection d'un onglet doit s'accompagner d'un changement de fragment ;

L'interface [ISession] est implémentée par la classe abstraite [AbstractSession] suivante :


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // n° de la vue précédente
  private int preViousView;

  // état des vues
  private CoreState[] coreStates = new CoreState[0];

  // action en cours
  private Action action = Action.NONE;

  // onglet sélectionné précédemment
  private int previousTab;

  // navigation sur sélection onglet
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // constructeur
  public AbstractSession() {
    // on initialise le tableau des états des fragments
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // interface ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • ligne 9 : le n° de de la vue qui était affichée avant celle actuellement affichée. Cette information est utile lorsqu'on peut arriver sur une vue de plusieurs endroits. C'est typiquement le cas dans une navigation par onglets. La vue affichée peut alors savoir quelle était la vue précédente ;
  • ligne 12 : le tableau des états de tous les fragments affichés par l'activité ;
  • ligne 18 : le n° de l'onglet précédemment sélectionné. Joue un rôle analogue à celui du n° de la vue précédente de la ligne 9. Cette information est utile lorsqu'il y a rotation du périphérique et qu'on doit se repositionner sur l'onglet qui était sélectionné avant la rotation ;
  • ligne 22 : un booléen indiquant si la sélection d'un onglet doit s'accompagner d'un changement du fragment affiché. Il faut savoir que le projet [client-android-skel] fait une gestion séparée des onglets et des fragments afin de pouvoir être utilisé dans des cas où le nombre d'onglets est inférieur au nombre de fragments. Il y a deux sortes de sélection :
    • une sélection par l'utilisateur lorsqu'il clique sur un onglet. Dans ce cas, généralement le fragment affiché doit changer ;
    • une sélection logicielle via la méthode [Tablayout.Tab.select()]. Dans ce cas, le changement du fragment affiché n'est pas toujours souhaitable. Voici deux exemples :
      • lors d'une rotation du périphérique, l'activité est recréée et les onglets également. Or lorsque le 1er onglet est créé, il subit automatiquement une opération logicielle [select]. Il n'est alors pas souhaitable de changer de fragment affiché car on est dans une phase de recréation de l'activité où le fragment finalement affiché ne sera pas forcément celui associé au 1er onglet ;
      • puisque la gestion des onglets est séparée de celle des fragments, on peut vouloir mettre à jour les onglets (suppression, ajout) sans interférer avec leurs fragments associés. Or certaines de ces opérations peuvent là encore déclencher une opération logicielle [select] implicite sur l'un des onglets. Cette sélection ne doit alors pas forcément se traduire par une navigation vers le fragment associé ;
  • ligne 21 : le champ [navigationOnTabSelectionNeeded] n'a pas vocation à être sauvegardé lors des opérations de sauvegarde de l'activité et des ses fragments. L'annotation [@JsonIgnore] fait que le champ est ignoré lors des sérialisations / désérialisations jSON ;
  • lignes 25-31 : le constructeur initialise le tableau des états des [FRAGMENTS_COUNT] fragments de l'application. Les éléments de ce tableau sont initialisés avec le champ [hasBeeenVisited=false]. Cette information est utilisée pour savoir si on a affaire ou non à la 1ère visite du fragment ;

La classe [Session] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // données à partager entre fragments eux-mêmes et entre fragments et activité
  // les éléments qui ne peuvent être sérialisés en jSON doivent avoir l'annotation @JsonIgnore
  // ne pas oublier les getters et setters nécessaires pour la sérialisation / désérialisation jSON
}
  • ligne 5 : la classe [Session] étend la classe [AbstractSession] que nous venons de voir. Le développeur y placera les éléments à partager entre fragments eux-mêmes et entre fragments et activité. On notera que la classe [Session] n'est plus annotée par l'annotation AA [@EBean]. Elle est devenue une classe normale ;

2.5.7. La classe abstraite [AbstractActivity]

  

2.5.7.1. Squelette

La classe [AbstractActivity] est une classe de plus de 300 lignes. Nous allons l'étudier par étapes. Son squelette est le suivant :


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // couche [DAO]
  private IDao dao;
  // la session
  protected Session session;

  // le conteneur des fragments
  protected MyPager mViewPager;
  // la barre d'outils
  private Toolbar toolbar;
  // l'image d'attente
  private ProgressBar loadingPanel;
  // barre d'onglets
  protected TabLayout tabLayout;

  // le gestionnaire de fragments ou sections
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // nom de la classe
  protected String className;
  // mappeur jSON
  private ObjectMapper jsonMapper;

  // constructeur
  public AbstractActivity() {
    // nom de la classe
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // implémentation IMainActivity --------------------------------------------------------------------
  ...

  // cycle de vie - sauvegarde / restauration de l'activité ------------------------------------
  ...

  // gestion de l'image d'attente ---------------------------------
  ...

  // interface IDao -----------------------------------------------------
  ...

  // le gestionnaire de fragments --------------------------------
  ...

  // classes filles
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

La classe [AbstractActivity] :

  • implémente l'interface [IMainActivity] (lignes 21, 55) ;
  • gère la sauvegarde et la restauration de l'activité et de ses fragments lors d'une rotation du périphérique (ligne 58) ;
  • gère l'image d'attente lors d'un échange avec le serveur web / jSON (ligne 61) ;
  • implémente l'interface IDao de la couche [DAO] (ligne 64) ;
  • implémente le gestionnaire de fragments (ligne 67) ;
  • impose à ses classes filles la présence de six méthodes (lignes 71-81) ;

2.5.7.2. Implémenter l'interface [IMainActivity]

L'implémentation de l'interface [IMainActivity] (cf paragraphe 2.5.4) est la suivante :


  // implémentation IMainActivity --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // affichage nouveau fragment
    mViewPager.setCurrentItem(position);
    // on note l'action en cours lors de ce changement de vue
    session.setAction(action);
}

2.5.7.3. Sauvegarde de l'état de l'activité et de ses fragments

L'état de l'activité et de ses fragments est entièrement dans la session. Il s'agit donc de sauvegarder celle-ci. Nous reprenons ici ce qui a été fait dans le projet [Exemple-22] (cf paragraphe 1.23) :


  // gestion sauvegarde / restauration de l'activité ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // sauvegarde session sous la forme d'une chaîne jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

2.5.7.4. Restauration de l'état de l'activité et de ses fragments

Il s'agit de restaurer la session. Nous faisons comme il a été montré dans [Exemple-22] :


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // qq chose à restaurer ?
    if (savedInstanceState != null) {
      // récupération session
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // session
      session = new Session();
    }
...
  • lignes 10-26 : si le paramètre [Bundle savedInstanceState] de la ligne 2 est non null, alors la session est restaurée (lignes 12-17) ;
  • lignes 26-29 : le cas où le paramètre [Bundle savedInstanceState] de la ligne 2 est null correspond au 1er démarrage de l'activité. On crée alors une session vide ;

2.5.7.5. Initialisation de la couche [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // couche [DAO]
    dao = getDao();
    if (dao != null) {
      // configuration de la couche [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // classes filles
  protected abstract IDao getDao();
....
}
  • ligne 11 : une référence sur la couche [DAO] est demandée à l'activité fille (ligne 21) ;
  • lignes 14-17 : si la couche [DAO] existe, on la configure à partir des informations contenues dans l'interface [IMainActivity] ;

2.5.7.6. Initialisation de la vue associée à l'activité

La vue associée à l'activité a été présentée au paragraphe 2.5.1 :


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">

  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">

    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
    </android.support.v7.widget.Toolbar>
  </android.support.design.widget.AppBarLayout>

  <!-- conteneur de fragments -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

Cette vue est initialisée avec le code suivant :


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // vue associée
    setContentView(R.layout.activity_main);
    // composants de la vue ---------------------
    // barre d'outils
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // image d'attente ?
    if (IS_WAITING_ICON_NEEDED) {
      // on ajoute l'image d'attente
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // création ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // ajout du ProgressBar à la barre d'outils
      toolbar.addView(loadingPanel);
    }
...
  • ligne 11 : la vue XML [activity_main] est associée à l'activité ;
  • lignes 14-15 : la barre d'outils est intégrée et supportée ;
  • lignes 17-27 : ajout éventuel d'une image d'attente : si le booléen [IS_WAITING_ICON_NEEDED] est à vrai dans l'interface [IMainActivity] ;
  • ligne 23 : création de l'image d'attente de type [ProgressBar] référencée par le champ [loadingPanel] ;
  • ligne 24 : au départ, cette image est cachée ;
  • ligne 26 : elle est ajoutée à la barre d'outils ;

2.5.7.7. Gestion des onglets

L'interface [IMainActivity] peut demander une barre d'onglets. Celle-ci est ajoutée et gérée de la façon suivante :


// barre d'onglets
  protected TabLayout tabLayout;
...

    // barre d'onglets ?
    if (ARE_TABS_NEEDED) {
      // on ajoute la barre d'onglets
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // pas de navigation sur sélection jusqu'à l'affichage d'un fragment
      session.setNavigationOnTabSelectionNeeded(false);
      // création barre d'onglets
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // ajout de la barre d'onglets à la barre d'application
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // gestionnaire d'évt de la barre d'onglets
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // un onglet a été sélectionné
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // position de l'onglet
            int position = tab.getPosition();
            // mémoire
            session.setPreviousTab(position);
            // affichage fragment associé ?
            navigateOnTabSelected(position);
          }
        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {

        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
      });
    }

...
  // classes filles
  protected abstract void navigateOnTabSelected(int position);
...
  • lignes 12-48 : ajout et gestion d'une barre d'onglets ;
  • ligne 6 : l'ajout de la barre d'onglets se fait si la constante [ARE_TABS_NEEDED] est positionnée à vrai dans l'interface [IMainActivity] ;
  • ligne 12 : lors de la création de la barre d'onglets, il y a des opérations [Tablayout.Tab.select] implicites qui peuvent survenir (ce n'est pas l'utilisateur qui les provoque). On met le booléen [session.navigationOnTabSelectionNeeded] à faux pour éviter toute navigation pendant ces fausses sélections. Ce sera au développeur de sélectionner le fragment à afficher avec la méthode [navigateToView]. Le booléen [session.navigationOnTabSelectionNeeded] sera remis à vrai lorsque ce fragment sera affiché (cf classe AbstractFragment) ;
  • ligne 14 : création d'une barre d'onglets référencée par le champ [tabLayout]. Nous utilisons une barre d'onglets personnalisée [CustomTabLayout] sur laquelle nous allons revenir ;
  • ligne 15 : nous fixons les couleurs des titres des onglets. Celles-ci sont trouvées dans le fichier [res / color / tab_txt.xml] suivant :

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • ligne (c) : la couleur du titre de l'onglet lorsque celui-ci est sélectionné ;
    • ligne (d) : la couleur du titre de l'onglet lorsque celui-ci n'est pas sélectionné ;

Ce fichier est bien sûr modifiable. On trouvera les codes hexadécimaux des couleurs par exemple ici.

  • lignes 17-18 : ajout de cette barre d'onglets à la barre d'application présente dans la vue XML [activity_main] ;
  • lignes 20-47 : gestionnaire des événements de la barre d'onglets ;
  • lignes 22-36 : seul l'événement [onTabSelected] est géré. Il correspond à un clic sur l'onglet [Tab tab] passé en paramètre à la méthode ou bien à une opération logicielle [TabLayout.Tab.select] ;
  • ligne 30 : position de l'onglet sélectionné ;
  • ligne 32 : cette position est mémorisée en session ;
  • ligne 34 : il s'agit maintenant d'afficher le fragment associé à cet onglet. Seule la classe fille (ligne 52) peut faire cette association. On notera qu'on n'associe pas la barre d'onglets avec le conteneur de fragments [mViewPager] comme il a été fait dans certains exemples étudiés. Ici, on dissocie totalement la gestion de la barre d'onglets de celle des fragments. C'est pourquoi on est obligé, lorsqu'un onglet est cliqué, d'indiquer quelle vue on veut voir affichée ;
  • ligne 28 : on distingue la sélection d'onglet avec ou sans navigation. En général, lorsque l'utilisateur clique sur un onglet, on veut une navigation et lors d'une sélection logicielle on n'en veut pas. C'est le développeur qui distingue ces deux cas avec l'élément [session.navigationOnTabSelectionNeeded]. Lorsque la navigation n'est pas faite, le n° du dernier onglet sélectionné n'est pas enregistré en session. Ce sera au développeur de le faire ;

2.5.7.8. Le gestionnaire d'onglets [CustomTabLayout]

  

Nous utilisons un gestionnaire d'onglets personnalisé pour pouvoir afficher le titre des onglets avec différentes polices de caractères. La classe [CustomTabLayout] est la suivante :


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

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

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

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • la personnalisation de la police des titres des onglets se fait aux lignes 30 et 44 ;

Le dossier [fonts] est le suivant :

  

Sources :

2.5.7.9. Dernières initialisations


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instanciation du gestionnaire de fragments
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // le conteneur de fragments est associé au gestionnaire de fragments
    // ç-à-d que le fragment n° i du conteneur de fragments est le fragment n° i délivré par le gestionnaire de fragments
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // on inhibe le swipe entre fragments
    mViewPager.setSwipeEnabled(false);
    // adjacence des fragments
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // on affiche la 1ère vue
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // on passe la main à l'activité fille
    onCreateActivity();
  }
...
  // classes filles
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • lignes 10-19 : on retrouve là du code souvent rencontré dans les exemples étudiés ;
  • lignes 21-23 : affichage de la toute première vue. Il y a sans doute plusieurs façons de discriminer ce cas. Ici, nous avons utilisé le fait que pour la toute première vue, la valeur de l'action qui provoque le changement de vue est NONE ;
  • ligne 22 : on ne fait pas d'hypothèse sur le 1er fragment à afficher. Dans nos exemples, cela a été souvent le fragment n° 0, mais pas toujours (cf Exemple-22). On demandera donc à l'activité fille (ligne 30) de nous dire quelle est cette première vue ;
  • ligne 25 : on a factorisé ici tout ce qu'on pouvait. Maintenant, la classe fille a ses propres initialisations à faire (ligne 29) ;

2.5.7.10. Gestion de l'image d'attente

Dans la classe [AbstractActivity], l'image d'attente est gérée par les deux méthodes suivantes :


  // gestion de l'image d'attente ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. Implémentation de l'interface [IDao]

Dans la classe [AbstractActivity], l'interface [IDao] (cf paragraphe 2.5.5) est implémentée de la façon suivante :


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // couche [DAO]
  private IDao dao;
...
  // interface IDao -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • ligne 3 : on rappelle que la valeur de ce champ a été fournie par l'activité fille dans la méthode [onCreate] ;

2.5.7.12. Implémentation du gestionnaire de fragments

Dans la classe [AbstractActivity], le gestionnaire de fragments est implémenté de la façon suivante :


...
  // le gestionnaire de fragments --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // constructeur
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // fragments de la classe fille
      fragments = getFragments();
    }

    // doit rendre le fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // on rend le fragment
      return fragments[position];
    }

    // rend le nombre de fragments à gérer
    @Override
    public int getCount() {
      return fragments.length;
    }

    // rend le titre du fragment n° position
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // classes filles
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • ligne 5 : le tableau des fragments associés à l'activité. Tous les fragments seront dérivés de la classe [AbstractFragment] ;
  • lignes 8-12 : c'est le constructeur qui initialise le tableau des fragments. Il demande ceux-ci à la classe fille de l'activité (ligne 35) ;
  • lignes 28-31 : les titres de fragments peuvent être utilisés dans une application où il y a autant d'onglets que de fragments. Dans ce cas, on peut donner à l'onglet le titre du fragment. Ici, ces titres sont demandés à la classe fille (ligne 37) ;

2.5.7.13. La méthode [onResume]

La méthode [onResume] est exécutée un peu avant que la vue associée à l'activité ne devienne visible. On l'utilise ici pour sélectionner un onglet après une sauvegarde / restauration :


  @Override
  public void onResume() {
    // parent
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // si restauration, alors il faut restaurer le dernier onglet sélectionné
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • ligne 10 : sélection de l'onglet qui était sélectionné avant le processus de sauvegarde / restauration. Il faut se rappeler ici que dans la méthode [onCreate] qui, dans le cycle de vie de l'activité, est exécutée avant la méthode [onResume], la navigation sur sélection d'un onglet a été inhibée. Donc ici, il y a sélection d'un onglet mais pas de changement de fragment ;

2.5.7.14. Résumé

La classe abstraite [AbstractActivity] sera la classe parent de l'activité unique de l'application.

L'activité fille devra implémenter les six méthodes suivantes :


  // classes filles
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

L'activité fille a par ailleurs accès aux éléments protégés suivants de sa classe parent :


  // la session
  protected ISession session;
  // le conteneur des fragments
  protected MyPager mViewPager;
  // barre d'onglets
  protected CustomTabLayout tabLayout;
  // nom de la classe
protected String className;

2.5.8. L'activité [MainActivity]

  

La classe [MainActivity] peut s'appeler différemment. Sa seule contrainte est d'implémenter l'interface [IMainActivity]. La classe fournie de base est la suivante :


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;

  // méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo : on continue les initialisations commencées par la classe parent
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // todo : définir les fragments ici
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo : définir les titres des fragments ici
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // todo : navigation par onglets - définir la vue à afficher
  }

  @Override
  protected int getFirstView() {
    // todo : navigation par onglets - définir la première vue à afficher
    return 0;
  }
}
  • ligne 14 : pour que la notation AA [@Bean] de la ligne 19 soit comprise, il faut que l'activité ait la notation AA [@EActivity] ;
  • ligne 15 : l'activité est associée au menu XML [menu_main]. Actuellement ce menu est vide. Le développeur aura à le compléter s'il en a besoin ;
  • ligne 16 : la classe étend la classe [AbstractActivity] ;
  • lignes 19-20 : une référence sur la couche [DAO]. Celle-ci sera instanciée par la bibliothèque AA avant que ce champ ne soit initialisé. Cela entraîne que le bean AA [Dao] doit exister. C'est toujours le cas avec l'application squelette que nous livrons. Même dans une application sans couche [DAO], on peut laisser le package [dao] exister. Cela n'entraîne pas de complications ;
  • ligne 22 : la session en tant qu'instance du type [Session]. La session existe dans la classe parent [AbstractActivity] mais en tant qu'instance de l'interface [ISession] (ligne 32) ;
  • lignes 24-63 : les six méthodes imposées par la classe parent [AbstractActivity] ;
  • lignes 36-39 : la méthode [getDao] rend une référence sur la couche [DAO]. Ici, cette référence n'est jamais null. Or dans la classe parent [AbstractActivity], on a prévu le cas où la classe fille rendait une référence null pour indiquer qu'il n'y avait pas de couche [DAO]. Si on souhaite user de cette possibilité (pas très utile à mon avis), c'est ici qu'il faut rendre le pointeur null ;

2.6. La couche [DAO]

Image

  

2.6.1. L'interface IDao

Elle a été présentée au paragraphe 2.5.5 :


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Url du service web
  void setUrlServiceWebJson(String url);

  // utilisateur
  void setUser(String user, String mdp);

  // timeout du client
  void setTimeout(int timeout);

  // authentification basique
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // mode debug
  void setDebugMode(boolean isDebugEnabled);

  // délai d'attente en millisecondes du client avant requête
  void setDelay(int delay);

  // todo : déclarez votre interface ici
}

Le développeur ajoutera les méthodes de sa couche [DAO] à partir de la ligne 24.

2.6.2. L'interface [WebClient]

  

L'interface [WebClient] est la suivante :


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // todo : déclarez ici les URL à atteindre
}

Le développeur ajoutera les méthodes communiquant avec les URL exposées par le serveur jSON à partir de la ligne 17.

2.6.3. L'intercepteur d'authentification[MyAuthInterceptor]

  

La classe [MyAuthInterceptor] est la suivante :


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // utilisateur
  private String user;
  // mot de passe
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // entêtes HTTP de la requête HTTP interceptée
    HttpHeaders headers = request.getHeaders();
    // l'entête HTTP d'authentification basique
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // ajout aux entêtes HTTP
    headers.setAuthorization(auth);
    // on continue le cycle de vie de la requête HTTP
    return execution.execute(request, body);
  }

  // éléments de l'authentification
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Cette classe génère l'entête HTTP d'authentification suivant :

Authorization: Basic code

où [code] est le code Base64 de la chaîne 'user:mp'. Cette classe ne sert que si le serveur jSON attend cette forme d'authentification. Il en existe d'autres.

Note : l'usage de cette classe est illustrée au paragraphe 3.6.3.1.

2.6.4. La classe [AbstractDao]

  

La classe [AbstractDao] est la suivante :


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // mappeur jSON
  private ObjectMapper mapper = new ObjectMapper();
  // mode debug
  protected boolean isDebugEnabled;
  // nom de la classe
  protected String className;
  // délay d'attente avant exécution requête
  private int delay;

  // constructeur
  public AbstractDao() {
    // nom de la classe
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

  // méthodes protégées ----------------------------------------------------------
  // interface générique
  protected interface IRequest<T> {
    T getResponse();
  }

  // requête générique vers un service web / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // exécution service - on attend une unique réponse
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // exécution service
        try {
          // attente ?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // on exécute la requête synchrone
          T response = request.getResponse();
          // log
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // on émet la réponse vers l'observateur
          subscriber.onNext(response);
          // on signale la fin de l'observable
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // on émet une exception
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // mode debug
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • lignes 35-81 : la méthode [getResponse] utilise la bibliothèque RxAndroid pour rendre un type [Observable<T>]. Contrairement à certains exemples vus précédemment, on ne rend pas un type [Response<T>] qui est un type propriétaire mais un type T quelconque ;
  • ligne 35 : la méthode [getResponse] reçoit en paramètre une instance du type [IRequest<T>] des lignes 30-32, dont la méthode [IRequest.getReponse()] obtient le type T par une opération HTTP synchrone ;
  • lignes 48-50 : artificiellement on attend [delay] millisecondes. En production on mettra [delay=0]. En phase de débogage on mettra [delay=qqs secondes] pour donner une chance à l'utilisateur d'annuler l'opération asynchrone et ainsi de voir comment se comporte alors le code ;
  • ligne 52 : la réponse attendue est demandée avec une requête synchrone ;
  • ligne 64 : une fois la réponse reçue, elle est passée à l'observateur ;
  • ligne 66 : on indique qu'il n'y aura plus d'émission. On est ici dans le cas particulier d'une action asynchrone qui ne rend qu'un élément ;
  • lignes 67-78 : en cas d'exception, on émet l'exception vers l'observateur (ligne 77) ;

2.6.5. La classe [Dao]

  

La classe [Dao] est la suivante :


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client du service web
  @RestService
  protected WebClient webClient;
  // sécurité
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // le RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // on construit le restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // on fixe le convertisseur jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // on fixe le restTemplate du client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // on fixe l'URL du service web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // on enregistre l'utilisateur dans l'intercepteur
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercepteur d'authentification ?
    if (isBasicAuthentificationNeeded) {
      // on ajoute l'intercepteur d'authentification
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // todo : implémentation IDao
}
  • lignes 21-22 : injection du bean AA [WebClient] qui va assurer les échanges avec le serveur web / jSON ;
  • lignes 24-25 : injection de l'intercepteur d'autentification ;
  • lignes 31-42 : méthode exécutée après injection des champs des lignes 21-25 ;
  • ligne 37 : l'objet [RestTemplate] qui assure les échanges client / serveur est créé à partir d'une factory. Ce n'est pas indispensable mais c'est via la factory qu'on peut configurer les timeouts des échanges. C'est pourquoi, nous n'utilisons pas le constructeur sans paramètres [RestTemplate()] ;
  • ligne 39 : on ajoute un convertisseur jSON aux convertisseurs du [RestTemplate]. Ce sera le seul convertisseur. Aussi lorsqu'une méthode du client [WebClient] va recevoir une chaîne jSON du serveur, celle-ci sera automatiquement désérialisée en l'objet que la méthode doit rendre ;
  • ligne 41 : l'objet [RestTemplate] ainsi configuré est passé au client web qui va assurer les échanges client / serveur grâce à lui ;
  • lignes 44-48 : on fixe l'URL racine du serveur web / jSON. Toutes les URL déclarées dans la classe [WebClient] sont des URL relatives à cette URL racine ;
  • lignes 50-54 : cette méthode permet de préciser le propriétaire de la connexion lorsque celle-ci est contrôlée par une autorisation de type basique (cf paragraphe 2.6.3) ;
  • lignes 56-64 : fixent les timeouts des échanges client / serveur. Cela se fait via la factory de l'objet [RestTemplate] qui gouverne les échanges ;
  • lignes 66-78 : cette méthode permet d'indiquer que le serveur est un serveur protégé par une authentification de type basique ;
  • lignes 72-77 : si une authentification de type basique est demandée, l'intercepteur d'authentification injecté ligne 25 est ajouté aux intercepteurs de l'objet [RestTemplate]. Cet intercepteur ajoutera automatiquement à toutes les requêtes du client web, la ligne HTTP d'authentification basique attendue par le serveur ;
  • le développeur implémentera l'interface [IDao] à partir de la ligne 87 ;

2.7. Les fragments

  

2.7.1. La classe [MenuItemState]

La classe [MenuItemState] encapsule l'état d'une option de menu :


package client.android.architecture;

public class MenuItemState {

  // identifiant de l'option de menu
  private int menuItemId;
  // visibilité de l'option
  private boolean isVisible;

  // constructeurs
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // getters et setters
...
}

2.7.2. La classe [Utils]

La classe [Utils] rassemble des méthodes statiques utilitaires :


package client.android.architecture;

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

public class Utils {

  // liste de messages d'une exception - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // on crée une liste avec les msg d'erreur de la pile d'exceptions
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // liste de messages d'une exception - version 2
  static public String getMessageForAlert(Throwable th) {
    // on construit le texte à afficher
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // résultat
    return texte.toString();
  }

  // liste de messages d'une exception - version 3
  static public String getMessageForAlert(List<String> messages) {
    // on construit le texte à afficher
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // résultat
    return texte.toString();
  }
}

2.7.3. La classe parent [AbstractFragment]

La classe [AbstractFragment] rassemble ce qui est commun à tous les fragments de l'application. Comme dans la classe [AbstractActivity], son code est complexe. Nous allons là aussi l'analyser par étapes.

2.7.3.1. Le squelette


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

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

public abstract class AbstractFragment extends Fragment {

  // données privées ------------------------------------------------------------
  // les abonnements aux observables
  private List<Subscription> abonnements = new ArrayList<>();
  // menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // état du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // tâches asynchrones
  private boolean runningTasksHaveBeenCanceled;

  // données  accessibles aux classes filles ---------------------------------------
  // mode debug
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nom de la classe
  protected String className;
  // tâches asynchrones
  protected int numberOfRunningTasks;
  // activité
  protected IMainActivity mainActivity;
  protected Activity activity;
  // session
  protected Session session;


  // update Fragment ----------------------------------------------------------------------------------
 ...

  // gestion du menu ------------------------------------------
  ...

  // gestion de l'attente -------------------------------------------------------------
...

  // gestion des opérations asynchrones --------------------------------------------------------------------
...

  // gestion exception -------------------------------------------------------------------
....

  // gestion du cycle de vie du fragment --------------------------------------------------------
...

  // classes filles -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • lignes 28-45 : les données privées de la classe ;
  • lignes 47-58 : les données protégées accessibles par les classes filles ;
  • lignes 61-62 : code qui met à jour le fragment qui va être affiché ;
  • lignes 64-65 : code utilitaire pour gérer l'éventuel menu ;
  • lignes 67-68 : code utilitaire pour gérer l'attente lors d'une opération asynchrone ;
  • lignes 70-71 : code pour faciliter la communication du fragment avec la couche [DAO] ;
  • lignes 73-74 : code utilitaire pour gérer toute exception de façon standard ;
  • lignes 76-77 : code gérant le cycle de vie du fragment ;
  • lignes 80-94 : la classe parent impose 8 méthodes à ses classes filles ;

2.7.3.2. Le constructeur

Le constructeur de la classe est le suivant :


  // nom de la classe
  protected String className;
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
...
  // constructeur ----------------------
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • ligne 9 : on note le nom de la classe fille qui est ici instanciée. Ce nom est utilisé dans tous les logs de la classe parent ;
  • ligne 10 : on note que le fragment subit une construction. Cette information sera utilisée lorsqu'il sera demandé au fragment fille de se mettre à jour ;

2.7.3.3. Gestion du menu

Dans notre architecture, tout fragment doit avoir un menu, même vide. Les logs ont en effet montré que lorsque la méthode [onCreateOptionsMenu] exécutée lorsque le fragment a un menu, s'exécute, le fragment a déjà été associé à son activité, sa vue et son menu et va devenir visible. C'est donc un moment où la mise à jour de l'interface visuelle et du menu peut être faite. C'est dans cette méthode [onCreateOptionsMenu] que nous demandons au fragment fille de se mettre à jour.

La gestion du menu regroupe des méthodes utilitaires qui permettent au fragment fille d'afficher ou non des éléments du menu :


  // menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // gestion du menu ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // on parcourt tous les items du menu
    for (int i = 0; i < menu.size(); i++) {
      // item n° i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // si item n° i est un sous-menu, alors on recommence
      if (menuItem.hasSubMenu()) {
        // récursivité
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // résultat
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // on récupère les identifiants des options du menu
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // on transfère les options de menu dans un tableau
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identifiant option
      int id = menuOptionsIds.get(i);
      // état option
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // résultat
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // états des options de menu
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // état
      MenuItemState state = this.menuOptionsStates[i];
      // id du menu
      int id = state.getMenuItemId();
      // initialisation état
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // résultat
    return menuOptionsStates;
  }

  // affichage options de menu -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // on met à jour toutes les options du menu
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // on met à jour certaines options du menu
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • ligne 6-18 : cette méthode permet d'obtenir les identifiants numériques de toutes les options du menu ;
  • ligne 6 : la méthode [getMenuOptions] reçoit deux paramètres :
    • [Menu menu] : le menu du fragment ;
    • [List<Integer> menuOptionsIds] : la liste des identifiants Android des options de menu. Au départ cette liste est vide. Elle est remplie ensuite par un parcours récursif (ligne 15) de l'arbre du menu ;
  • lignes 20-40 : à partir du menu, construit le tableau des états (identifiant, visibilité) des options du menu. Ce tableau est stocké en ligne 3. La classe [MenuItemState] a été décrite au paragraphe 2.7.1 ;
  • lignes 43-55 : une variante de la méthode précédente. Elle fait la même chose, mais au lieu de recalculer les identifiants de toutes les options du menu, ce qui a déjà été fait, elle utilise les identifiants du tableau des états de la ligne 3 ;
  • lignes 58-63 : la méthode [setAllMenuOptionsStates] permet de cacher ou montrer la totalité des options du menu du fragment ;
  • lignes 65-69 : la méthode [setMenuOptionsStates] permet, de façon sélective, d'afficher ou cacher certaines des options du menu ;
  • les méthodes [getMenuOptions, getMenuOptionsStates] sont déclarées privées car utilisées uniquement dans [AbstractFragment]. Les méthodes [setAllMenuOptionsStates] (ligne 58) et [setMenuOptionsStates] (ligne 65) sont déclarées protégées afin d'être disponibles aux classes filles ;

2.7.3.4. Gestion de l'attente de la fin d'une tâche asynchrone


   // les abonnements aux observables
  private List<Subscription> abonnements = new ArrayList<>();
 // tâches asynchrones
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // gestion de l'attente de la fin d'une opération asynchrone -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // on note le nombre de tâches qui vont s'exécuter
    this.numberOfRunningTasks = numberOfRunningTasks;
    // on met l'image d'attente
    mainActivity.beginWaiting();
    // on vide la liste des abonnements
    abonnements.clear();
    // pas encore d'annulation
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // on cache l'image d'attente
    mainActivity.cancelWaiting();
  }

  • lignes 9-18 : pour démarrer une ou des opérations asynchrones, le fragment fille appellera la méthode parent [beginRunningTasks]. Le paramètre de cette méthode est le nombre de tâches asynchrones que le fragment fille va lancer ;
  • ligne 11 : on mémorise le paramètre de la méthode ;
  • ligne 13 : l'image d'attente est rendue visible ;
  • ligne 15 : on nettoie la liste des abonnements aux opérations asynchrones. Celles-ci n'ont pas encore été créées par le fragment fille ;
  • ligne 17 : on entretient un booléen pour signaler que les tâches asynchrones demandées par le fragment fille ont été annulées. Au départ de booléen a la valeur false ;
  • lignes 20-25 : le fragment fille appelle la méthode parent [cancelWaitingTasks] pour indiquer qu'il veut annuler les tâches qu'il a lancées ;
  • ligne 22 : l'image d'attente est cachée ;

2.7.3.5. Gestion des exceptions


  // gestion exception -------------------------------------------------------------------

  // affichage alerte sur exception
  protected void showAlert(Throwable th) {
    // on affiche les messages de la pile d'exceptions du Throwable th
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // affichage liste de messages
  protected void showAlert(List<String> messages) {
    // on affiche la liste des messages
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • lignes 4-7 : la méthode [showAlert(Throwable)] permet à un fragment fille de faire afficher dans une fenêtre les messages de la pile d'exceptions du Throwable passé en paramètre ;
  • lignes 10-13 : la méthode [showAlert(List<String>] permet à un fragment fille de faire afficher dans une fenêtre la liste de messages passée en paramètre ;
  • la classe [Utils] utilisée aux lignes 6 et 12 a été décrite au paragraphe 2.7.2 ;

2.7.3.6. Gestion des opérations asynchrones


...
  // les abonnements aux observables
  private List<Subscription> abonnements = new ArrayList<>();
  // tâches asynchrones
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // exécution d'une tâche asynchrone avec RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process : l'observable à exécuter / observer
    // consumeResult : la méthode qui exploite la réponse obtenue
    // 
    // on ne crée de nouveaux abonnements que s'il n'y a pas eu annulation
    if (!runningTasksHaveBeenCanceled) {
      // exécution sur thread d'E/S et observation sur thread de l'Ui
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // on exécute l'observable
      try {
        abonnements.add(process.subscribe(
          // consommation résultat
          consumeResult,
          // consommation exception
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // fin de tâche
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // une opération asynchrone a émis une exception
  // ou une exception s'est produite pendant l'exécution d'une opération asynchrone
  private void consumeThrowable(Throwable th) {
...
  }

  • lignes 9-41 : exécutent une tâche asynchrone ;
  • ligne 9 : la méthode [executeInBackground] attend deux paramètres :
    • [Observable<T> process] : le processus asynchrone à exécuter ;
    • [Action1<T> consumeResult] : la méthode du fragment fille à appeler pour lui transmettre les éléments émis par le processus. Dans nos exemples précédents, les processus n'ont toujours émis qu'un élément. Le type T de [Action1<T>] est le type T du résultat rendu par le processus observé ;
  • ligne 14 : on ne lance la tâche asynchrone que si une annulation par l'utilisateur ou par le programme (à cause d'une exception) n'a pas déjà eu lieu ;
  • ligne 16 : le processus est configuré pour s'exécuter sur un thread d'E/S et observé sur le thread de l'Ui ;
  • ligne 16 : l'instruction [process.subscribe] lance l'exécution du processus dans le thread d'E/S. A l'intérieur de ce thread, les choses s'exécutent de façon synchrone parce que nous utilisons une bibliothèque HTTP qui est synchrone ;
  • ligne 19 : la méthode [process.subscribe] a trois paramètres :
    • ligne 21 : [consumeResult] : la méthode du fragment fille qui va consommer les éléments émis par le processus ;
    • lignes 22-28 : la méthode exécutée lorsqu'il y a eu une exception pendant le traitement de la tâche asynchrone. Le traitement est délégué à la méthode [consumeThrowable] de la ligne 49 ;
    • lignes 29-36 : la méthode exécutée lorsque la tâche émet la notification de fin d'émission. Le traitement est délégué à la méthode [endOfTask] de la ligne 43 ;
  • ligne 19 : la tâche asynchrone qui vient d'être lancée est enregistrée dans le champ [abonnements] qui enregistre toutes les tâches asynchrones lancées. Cela va permettre de les annuler si nécessaire ;
  • lignes 37-39 : méthode exécutée lorsqu'il y a eu une exception pendant le traitement de la tâche asynchrone. Le traitement est délégué à la méthode [consumeThrowable] de la ligne 49 ;

La méthode [endOfTask] est la suivante :


  // tâches asynchrones
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // une tâche en moins à attendre
    numberOfRunningTasks--;
    // fini ?
    if (numberOfRunningTasks == 0) {
      // fin attente
      cancelWaitingTasks();
      // on signale la fin des tâches à la classe fille
      notifyEndOfTasks(false);
    }
  }
...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • ligne 6 : une tâche asynchrone vient de s'achever. On décrémente le compteur des tâches actives ;
  • ligne 8 : s'il n'y a plus de tâches actives, alors le fragment fille a obtenu toutes ses réponses ;
  • ligne 10 : on annule l'attente ;
  • ligne 12 : on signale au fragment fille que toutes les tâches qu'elle a lancées sont terminées en appelant sa méthode [notifyEndOfTasks]. Le paramètre de cette méthode indique comment les tâches se sont terminées, normalement ou sur annulation de l'utilisateur ou du code parce qu'une exception s'est produite. Ligne 12, on signale une fin normale. On notera que le fragment fille n'a pas à se soucier de tenir un compte des tâches encore actives. Sa classe parent le fait pour elle ;

La méthode [consumeThrowable] est la suivante :


  // tâches asynchrones
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // une opération asynchrone a émis une exception
  // ou une exception s'est produite pendant l'exécution d'une opération asynchrone
  private void consumeThrowable(Throwable th) {
    // th : l'exception à traiter
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // on annule les tâches déjà lancées
    cancelRunningTasks();
    // on affiche les messages d'erreur
    showAlert(th);
  }

  // annulation des tâches
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // on annule toutes les tâches asynchrones enregistrées
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // on note l'annulation
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // fin de l'attente
    cancelWaitingTasks();
    // on signale l'annulation des tâches au fragment fille
    notifyEndOfTasks(true);
}

...
  // classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • ligne 3 : la méthode [consumeThrowable] reçoit l'exception qui s'est produite ;
  • ligne 15 : toutes les tâches encore actives sont annulées ;
  • ligne 17 : on affiche le texte de l'exception ;
  • lignes 21-37 : annulation de toutes les tâches ;
  • lignes 27-29 : tous les abonnements sont annulés ;
  • ligne 31 : on note qu'il y a eu annulation ;
  • ligne 32 : on remet le compteur de tâches à zéro ;
  • ligne 34 : l'attente est annulée ;
  • ligne 36 : on signale au fragment fille la fin des tâches sur annulation ;

2.7.3.7. Gestion du cycle de vie du fragment


  // cycle de vie --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // parent
    super.onDestroy();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • lignes 2-20 : les méthodes [onDestroyView, onDestroy] ne sont là que pour les logs. Ceux-ci permettent au développeur de mieux appréhender le cycle de vie des fragments ;

La sauvegarde du fragment lors d'une rotation du périphérique est réalisée par les méthodes [setUserVisibleHint, onSaveInstanceState, saveState] suivantes :


  // cycle de vie du fragment
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // sauvegarde ?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // le fragment va être caché - on le sauvegarde
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // mémoire
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // parent
    super.onSaveInstanceState(outState);
    // sauvegarde du fragment seulement s'il est visible
    if (isVisibleToUser) {
      // peut-être que la sauvegarde a déjà été faite
      if (!saveFragmentDone) {
        saveState();
      }
      // restauration à faire dans tous les cas
      session.setAction(ISession.Action.RESTORE);
    }
}
  • lignes 6-19 : la sauvegarde du fragment est faite si celui-ci passe de l'état affiché à l'état caché (ligne 11). C'est la méthode [setUserVisibleHint] qui nous donne cette information ;
  • ligne 14 : la sauvegarde est faite par la méthode privée des lignes 21-23 ;
  • lignes 25-41 : lors d'une rotation du périphérique, la méthode [onSaveInstanceState] va être appelée. Le fragment est sauvegardé à deux conditions :
    • il est visible (ligne 34) ;
    • il n'a pas encore été sauvegardé (ligne 36). Il est possible que les méthodes [setUserVisibleHint, onSaveInstanceState] ne puissent pas s'exécuter toutes deux lorsque le fragment est visible, et que donc la gestion du booléen [saveFragmentDone] soit inutile. Dans le doute, j'ai préféré utiliser celui-ci ;
  • ligne 40 : après la sauvegarde viendra la restauration. On note, pour la prochaine fois que le fragment devra se mettre à jour, qu'il devra le faire sur une opération [RESTORE] ;

On notera les deux moments où une sauvegarde du fragment est demandée :

  1. lorsque celui-ci passe de l'état visible à l'état caché ;
  2. lorsqu'il y a une rotation du périphérique ;

La méthode privée [saveState] est la suivante :


...
  private void saveState() {
    // tâches à annuler ?
    if (numberOfRunningTasks != 0) {
      // on annule les tâches
      cancelRunningTasks();
    }
    // on sauvegarde l'état du fragment
    CoreState currentState = saveFragment();
    // le fragment a été visité
    currentState.setHasBeenVisited(true);
    // sauvegarde état du menu
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // mise en session
    session.setCoreState(getNumView(), currentState);
    // sauvegarde faite
    saveFragmentDone = true;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // classes filles -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • ligne 4-7 : la rotation du périphérique peut avoir lieu alors que des opérations asynchrones sont en cours. On prend ici la décision de les annuler toutes. Ce n'est pas une bonne décision pour l'utilisateur qui va devoir refaire une nouvelle requête potentiellement longue alors qu'il a seulement bougé son téléphone ou sa tablette ou bien reçu un appel téléphonique. Il est possible de conserver les connexions réseau au travers d'un cycle sauvegarde / restauration. Simplement les solutions ne sont pas évidentes et j'ai décidé de ne pas les aborder dans ce cours pour débutants. La voie à suivre est de faire ces connexions réseau via un fragment sans interface visuelle attachée et qui n'est pas détruit lors du cycle sauvegarde / restauration. Il suffit pour cela d'utiliser l'instruction [Fragment.setRetainInstance(true)] ;
  • ligne 9 : on demande au fragment fille de sauvegarder son état dans un type dérivé de [CoreState] (ligne 31) ;
  • ligne 11 : on note que le fragment a été visité. Cette information est utile. Lorsqu'un fragment est visité pour la 1ère fois, sa mise à jour peut être différente des suivantes car alors il n'a pas d'état précédent dans la session ;
  • ligne 13 : on sauvegarde l'état du menu ce qui nous permettra de le restaurer automatiquement ;
  • ligne 15 : cet état courant est sauvegardé en session. Dans celle-ci, les états sont regroupés par vue / fragment, chacune d'elles ayant un état. Le n° de la vue est fourni par le fragment fille (ligne 33) ;
  • ligne 17 : on note que la sauvegarde du fragment a été faite. Ceci parce que deux méthodes sont susceptibles d'appeler la méthode [saveState] et qu'il est inutile de faire deux sauvegardes ;

La régénération de la vue associée au fragment est assurée par la méthode suivante :


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // la vue doit être restaurée
    viewHasToBeInitialized = true;
}

Dans le cycle de vie, la méthode [onActivityCreated] est exécutée juste après la méthode [onCreateView]. L'appel de cette dernière méthode indique que la vue associée au fragment doit être reconstruite. On se contente de le noter ligne 10.

2.7.3.8. Mise à jour du fragment

La mise à jour du fragment est la dernière opération faite sur le fragment avant qu'il ne soit visible et ne se mette en attente des actions de l'utilisateur. Elle est assurée par le code suivant :


  // menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // états du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // update Fragment ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // mémoire
    this.menu = menu;
    // on récupère les # options du menu si cela n'a pas déjà été fait
    if (fragmentHasToBeInitialized) {
      // on récupère les # options du menu
      getMenuOptionsStates(menu);
      // activité
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // on récupère l'état précédent du fragment (la toute 1ère fois, seul le booléen hasBeenVisited représente quelque chose)
    previousState = session.getCoreState(getNumView());
    // mise à jour du fragment fille en plusieurs étapes
    // étape 1 - est-ce la 1ère visite ?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // ce n'est pas la 1ère visite
      // étape 2 : le fragment doit-il être initialisé ?
      ...
      // étape 3 : la vue doit elle être initialisée ?
      ...
    }
    // étape 4 : un submit, une navigation, un restore ?
    ...

    // étape 5 : mises à jour terminales ----------------------
...
  }
...
  // classes filles -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • ligne 19 : c'est la méthode [onCreateOptionsMenu] qui est utilisée pour mettre à jour le fragment. Pour cette raison, le fragment doit avoir un menu, vide si besoin est. Lorsque cette méthode s'exécute, le fragment a été associé à sa vue et son activité et il est de plus visible ;
  • ligne 25 : on mémorise le menu qui a été passé en paramètre (ligne 22) à la méthode ;
  • lignes 27-34 : si le fragment doit être initialisé :
    • ligne 29 : les états des options du menu sont mis dans le tableau [menuOptionsStates] de la ligne 3 ;
    • ligne 31 : l'activité est mémorisée comme instance du type Android [Activity] ;
    • ligne 32 : l'activité est mémorisée comme instance de l'interface [IMainActivity] ;
    • ligne 33 : la session est mémorisée. Le changement de type est nécessaire, car la méthode [mainActivity.getSession()] rend un type [ISession] ;
  • ligne 36 : on récupère dans la session, l'état précédent du fragment. Si c'est la 1ère visite du fragment, seul le booléen [previousState.hasBeenVisited] a une signification ;
  • lignes 39-44 : code exécuté lorsque c'est la 1ère visite faite au fragment. Dans ce cas, son état précédent n'est pas significatif ;
  • lignes 44-50 : code exécuté lorsque ce n'est pas la 1ère visite faite au fragment ;
  • lignes 46-47 : code exécuté si le constructeur du fragment a été appelé (fragmentHasToBeInitialized==true) ;
  • lignes 48-49 : code exécuté si la vue associée au fragment a été reconstruite (viewHasToBeInitialized==true) ;
  • lignes 51-52 : code exécuté selon l'action (SUBMIT, NAVIGATION, RESTORE) en cours ;
  • lignes 54-55 : code toujours exécuté ;

Les cinq étapes de la mise à jour sont les suivantes :

étape 1


  // menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // états du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // on récupère l'état précédent du fragment (la toute 1ère fois, seul le booléen hasBeenVisited représente quelque chose)
    previousState = session.getCoreState(getNumView());
    // mise à jour du fragment fille en plusieurs étapes
    // étape 1 - est-ce la 1ère visite ?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // initialisation fragment et vue
      initFragment(null);
      initView(null);
      // raz previousState pour la suite
      previousState = null;
    } else {
      // ce n'est pas la 1ère visite
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • ligne 19 : l'état précédent du fragment est récupéré dans la session ;
  • lignes 22-31 : code exécuté si le fragment n'a jamais été visité ;
  • ligne 27 : on demande à la classe fille d'initialiser le fragment. Le paramètre de la méthode [initFragment] de la ligne 35 est l'état précédent du fragment. Ici, on passe null pour indiquer au fragment fille que c'est la 1ère visite ;
  • ligne 28 : on demande à la classe fille d'initialiser la vue associée au fragment. Le paramètre de la méthode [initView] de la ligne 37 est l'état précédent du fragment. Ici, on passe null pour indiquer au fragment fille que c'est la 1ère visite ;
  • ligne 30 : on met l'état précédent à null pour les étapes qui vont suivre ;

étapes 2 et 3


// menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // états du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // on récupère l'état précédent du fragment (la toute 1ère fois, seul le booléen hasBeenVisited représente quelque chose)
    previousState = session.getCoreState(getNumView());
    // mise à jour du fragment fille en plusieurs étapes
    // étape 1 - est-ce la 1ère visite ?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // ce n'est pas la 1ère visite
      // étape 2 : le fragment doit-il être initialisé ?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // fragment fille
        initFragment(previousState);
      }
      // étape 3 : la vue doit elle être initialisée ?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // fragment fille
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • lignes 24-42 : exécutées lorsque ce n'est pas la 1ère visite du fragment ;
  • lignes 27-33 : si le fragment vient d'être reconstruit, on le réinitialise en appelant la méthode [initFragment] de la classe fille (lignes 32, 46). On lui passe l'état précédent du fragment ;
  • lignes 35-51 : si la vue associée au fragment doit être initialisée ou réinitialisée, on demande au fragment fille de le faire (lignes 40, 48). Là encore, on lui passe le dernier état connu du fragment ;

étape 4


// menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // états du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // on récupère l'état précédent du fragment (la toute 1ère fois, seul le booléen hasBeenVisited représente quelque chose)
    previousState = session.getCoreState(getNumView());
    // mise à jour du fragment fille en plusieurs étapes
 ...

    // étape 4 : un submit, une navigation, un restore ?
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // action en cours
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // fragment fille
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // restauration menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // fragment fille
          updateOnRestore(previousState);
        } else {
          // il s'agit d'une 1ère visite - rien à faire
        }
        break;
      case RESTORE:
        // restauration
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // restauration menu (previousState ne peut être null)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // fragment fille
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • lignes 34-66 : on traite l'action en cours qui peut être l'une des trois suivantes :
    • RESTORE : on est en train de faire une restauration du fragment après une rotation du périphérique ;
    • NAVIGATION : on revient vers le fragment en voulant le retrouver dans l'état où on l'a laissé la dernière fois qu'on l'a utilisé ;
    • SUBMIT : tous les autres cas ;
  • ligne 34 : on récupère l'action en cours ;
  • lignes 36-42 : pour une action de type SUBMIT, on appelle la méthode [updateOnSubmit] du fragment fille (lignes 41, 68) en lui passant le dernier état connu du fragment ;
  • lignes 43-55 : pour une action de type NAVIGATION ;
  • lignes 47-54 : nous voulons remettre le fragment dans son dernier état connu. L'opération de NAVIGATION peut se conjuguer avec une 1ère visite. Ce serait le cas par exemple dans une application à onglets : si je passe de l'onglet 1 à l'onglet 4 :
    • je dois initialiser le fragment de l'onglet 4 s'il s'agit de la 1ère visite ;
    • rétablir le fragment de l'onglet 4 dans son état précédent s'il ne s'agit pas de la 1ère visite ;
  • lignes 52-54 : on ne fait rien s'il s'agit d'une 1ère visite. Ce sera à la méthode fille [initView(CoreState previousState)] de faire cette initialisation. La 1ère visite est caractérisée par la condition [previousState==null] ;
  • ligne 49 : si ce n'est pas la 1ère visite du fragment, on lui restitue son menu ;
  • ligne 51 : on demande à la classe fille de se mettre à jour en appelant la méthode de la ligne 70. On lui passe l'état précédent du fragment pour qu'elle puisse faire son travail ;
  • lignes 56-66 : dans le cas d'une opération de restauration du fragment, on fait la même chose que dans le cas d'une navigation hors 1ère visite ;

étape 5


// menu du fragment
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // cycle de vie du fragment
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // états du fragment
  private CoreState previousState;
  // mappeur jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // cycle de vie du fragment
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // étape 5 : mises à jour terminales ----------------------
    // on a changé de vue
    session.setPreviousView(getNumView());
    // plus d'action en cours
    session.setAction(ISession.Action.NONE);
    // lorsqu'on quittera ce fragment, il devra être sauvegardé
    saveFragmentDone = false;
    // tant que le fragment n'est pas reconstruit, il n'a pas à être initialisé
    fragmentHasToBeInitialized = false;
    // tant que la vue n'est pas reconstruite, elle n'a pas à être initialisée
    viewHasToBeInitialized = false;
    // on revient à un fonctionnement normal de la sélection d'onglets
    session.setNavigationOnTabSelectionNeeded(true);

    // on signale au fragment que la vue est prête
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • lignes 18-30 : lorsqu'on arrive ici, le fragment a été initialisé et est prêt à être affiché. On remet alors tous les indicateurs utilisés dans la gestion de vie du fragment dans un état initial ;
  • ligne 20 : on a changé de vue : on le note dans la session ;
  • ligne 22 : il n'y a plus d'action en cours ;
  • ligne 24 : lorsqu'on va quitter le fragment maintenant affiché, il faudra le sauvegarder lorsqu'on le quittera ;
  • ligne 26 : le fragment n'a plus besoin d'être reconstruit. Cet indicateur sera remis à vrai lorsque le constructeur du fragment sera de nouveau exécuté ;
  • ligne 28 : la vue associée au fragment n'a plus besoin d'être initialisée. Cet indicateur sera remis à vrai lorsque la méthode [onActivityCreated] sera de nouveau exécutée ;
  • ligne 30 : le fragment est affiché peut-être dans une application à onglets. Dans ce cas, lorsque l'utilisateur va cliquer sur l'un d'eux, un changement de fragment doit s'opérer ;
  • ligne 36 : on indique à la classe fille que le fragment est prêt. Celle-ci peut mettre dans la méthode [notifyEndOfUpdates] des mises à jour qui seraient à faire dans tous les cas, lancer une opération asynchrone pour obtenir de nouvelles données, ...

2.7.4. Un exemple de fragment

  

On a mis dans le projet [client-android-skel] un exemple de fragment pour montrer au lecteur la structure typique d'un fragment d'une application basée sur ce projet.

La classe [DummyFragment] est la suivante :


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // champs hérités de la classe parent -------------------------------------------------------

  // mode debug
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nom de la classe
  //-- protected String className;
  // tâches asynchrones
  //-- protected int numberOfRunningTasks;
  // activité
  //-- protected IMainActivity mainActivity;
  //-- protected Activity activity;
  // session
  //-- protected Session session;

  // méthodes héritées de la classe parent -------------------------------------------------------

  // affichage options de menu
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // gestion de l'attente de la fin d'une série de tâches asynchrones
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // exécution d'une tâche asynchrone avec RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // annulation des tâches
  //-- protected void cancelRunningTasks() {
  // affichage alerte sur exception
  //-- protected void showAlert(Throwable th) {
  // affichage liste de messages
  //-- protected void showAlert(List<String> messages) {

  // méthodes imposées par la classe parent -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // il faut sauvegarder le fragment
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // s'il n'y a rien à sauvegarder faire [return new CoreState();] et supprimer la classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // il faut retourner le n° du fragment dans le tableau des fragments gérés par l'activité (cf MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // le fragment devient visible et a subi une construction dans cette étape ou une étape précédente
    // cela se produit au démarrage de l'application et à chaque rotation du périphérique Android
    // est forcément suivie par l'exécution de [initView]
    // il faut initialiser les champs du fragment qui a été reconstruit
    // previousState est la dernière sauvegarde du fragment - vaut null si c'est la 1ère visite du fragment
  }

  @Override
  protected void initView(CoreState previousState) {
    // le fragment devient visible et la vue associée a été reconstruite dans cette étape ou une étape précédente
    // cela se produit à chaque fois que [initFragment] est exécutée et à chaque fois que le fragment sort de l'adjacence du fragment affiché
    // il faut initialiser les composants de la vue qui a été reconstruite
    // previousState est la dernière sauvegarde du fragment - vaut null si c'est la 1ère visite du fragment

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // est exécutée après [initFragment, initView] si ces méthodes sont exécutées
    // la vue va être affichée après une opération de type SUBMIT
    // il faut en général initialiser le fragment et la vue associée à partir de la session
    // previousState est la dernière sauvegarde du fragment - vaut null si c'est la 1ère visite du fragment
    // il n'y a rien à faire si on ne peut arriver au fragment par une opération SUBMIT
    // si on peut arriver sur le fragment par des opérations SUBMIT à partir de fragments différents, on peut connaître la vue précédente par [session.getPreviousView]
    // si on peut arriver sur le fragment par plusieurs opérations SUBMIT à partir du même fragment, alors il faut mettre en session un indicateur pour différencier les différents types de SUBMIT à partir de ce fragment
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // est exécutée après [initFragment, initView] si ces méthodes sont exécutées
    // la vue va être affichée après une opération de type RESTORE ou NAVIGATION
    // previousState est la dernière sauvegarde du fragment - ne vaut jamais null
    // il faut remettre la vue dans son état précédent

  }

  @Override
  protected void notifyEndOfUpdates() {
    // intervient après les méthodes [updateOnSubmit, updateOnRestore]
    // lorsqu'on est là, la vue a été construite et initialisée
    // il n'y a souvent rien à faire ici mais on peut aussi factoriser ici des actions qu'il faudrait faire quelque soit la façon dont on arrive sur cette vue
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // appelée lorsque les tâches asynchrones lancées par le fragment sont soit terminées soit annulées
    // ces deux cas peuvent être différenciés grâce au paramètre runningTasksHaveBeenCanceled
    // il faut en général remettre la vue dans un état différent de celui qu'elle avait pendant qu'elle attendait les réponses des tâches asynchrones

  }
}

La classe [DummyFragment] peut ne pas avoir d'état. Ici, on en a mis un pour rappeler ce qui est attendu dedans :


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // état du fragment [DummyFragment]
  // ne mettre que des champs sérialisables en jSON
  // mettre l'annotation @JsonIgnore sur les autres mais on voit mal à quoi ils pourraient servir
  // ne pas oublier les getters / setters - ils sont utilisés pour la sérialisation / désérialisation
}

Pour illustrer l'utilisation du projet [client-android-skel], nous allons d'abord utiliser des exemples simples avant de passer à une étude de cas plus complète.

2.8. Exercices d'illustration

Nous allons commencer par refactoriser des exemples déjà écrits.

2.8.1. Exemple-17B

Nous reprenons l'exemple 17 étudié au paragraphe 1.18. C'est une application avec un unique fragment sans tâches asynchrones et sans onglets. Nous l'examinons pour voir comment elle se comporte lors d'une rotation du périphérique. Nous faisons les saisies suivantes :

Image

Puis en [1], nous faisons tourner le périphérique deux fois. La nouvelle vue est alors la suivante :

Image

Si nous comparons les vues, tout a été conservé sauf la liste [2] qui est désormais vide.

Par ailleurs, si on clique sur le bouton [Valider] on a une boîte de dialogue montrant les saisies faites sur le formulaire. Si à ce moment là, on fait tourner le périphérique, on perd la boîte de dialogue.

Il nous faudra donc, lors d'une rotation, régénérer :

  • la liste déroulante et son élément sélectionné ;
  • la boîte de dialogue si elle était affichée lors de la rotation ;

2.8.1.1. Le projet [Exemple-17B]

Nous dupliquons le projet [client-android-skel] dans exemples/Exemple-17B. Puis nous chargeons le nouveau projet [1] :

  • en [2-3], dans le dossier [behavior], nous collons le fragment [Vue1Fragment] du projet [Exemple-17] ;
  • en [4-5], dans le dossier [layout] de [Exemple-17B], on colle la vue [vue1.xml] de [Exemple-17]. C'est la vue associée au fragment ;
  • en [6], le dossier [values] de [Exemple-17B] est remplacé par le dossier [values] de [Exemple-17] ;

On modifiera la marge haute de la vue [vue1.xml] à 80 dp :


    <TextView
      android:id="@+id/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

A ce stade, on peut tenter une première compilation pour voir les erreurs. Les premières erreurs signalées proviennent d'imports de packages qui ont changé de place. On les corrige (Ctrl-Shift-O). D'autres erreurs proviennent du fait que la vue [Vue1Fragment] n'implémente pas toutes les méthodes imposées par sa classe parent [AbstractParent] :

Image

On génère les méthodes manquantes (Alt-Entrée).

Une autre erreur de compilation signalée est la suivante :

Image

On corrige cela dans le fichier [build.gradle] du module (ligne 20 ci-dessous) :

 

A ce stade, on peut recompiler pour voir les erreurs restantes. L'unique erreur signalée est sur la méthode [Vue1Fragment.updateFragment] :

 

Il faut supprimer l'annotation [@Override] de la ligne 135. Il n'y a désormais plus d'erreurs. Nous allons partir de là pour modifier le projet.

2.8.1.2. L'état du fragment [Vue1Fragment]

Le fragment [Vue1Fragment] a besoin de sauver des informations lors de la rotation du périphérique afin qu'il puisse être restauré complètement. Nous créons une classe [Vue1FragmentState] pour cela :

  

Pour l'instant, cette classe est vide :


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Personnalisation du projet

  

Dans le dossier [custom] se trouvent les éléments d'architecture personnalisables par le développeur.

Les constantes de l'interface [IMainActivity] seront les suivantes :


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accès à la session
  ISession getSession();

  // changement de vue
  void navigateToView(int position, ISession.Action action);

  // gestion de l'attente
  void beginWaiting();

  void cancelWaiting();

  // constantes de l'application -------------------------------------

  // mode debug
  boolean IS_DEBUG_ENABLED = true;

  // délai maximal d'attente de la réponse du serveur
  int TIMEOUT = 1000;

  // délai d'attente avant exécution de la requête client
  int DELAY = 0;

  // authentification basique
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacence des fragments
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barre d'onglets
  boolean ARE_TABS_NEEDED = false;

  // image d'attente
  boolean IS_WAITING_ICON_NEEDED = false;

  // nombre de fragments de l'application
  int FRAGMENTS_COUNT = 1;

}
  • lignes 24-31 : l'application n'utilise pas ici sa couche [DAO]. Ces constantes ne seront pas utilisées ;
  • ligne 34 : une adjacence de fragments de 1 qui est la valeur par défaut. Comme l'application n'a qu'un fragment (ligne 43), cette valeur n'a pas d'importance ;
  • lignes 39-40 : comme il n'y a pas d'opérations avec la couche [DAO], il est inutile d'avoir une image d'attente ;
  • ligne 37 : ce n'est pas une application à onglets ;
  • ligne 43 : il n'y a qu'un fragment ;

La classe [Session] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // les éléments qui ne peuvent être sérialisés en jSON doivent avoir l'annotation @JsonIgnore

}

Elle est vide. En effet, comme il n'y a qu'un fragment, il n'y a pas lieu de prévoir une communication inter-fragments avec une session.

Enfin, la classe [CoreState] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragment visité ou non
  protected boolean hasBeenVisited = false;
  // état de l'éventuel menu du fragment
  protected MenuItemState[] menuOptionsState;

  // getters et setters
...
}
  • lignes 11-13 : il nous faut mettre toutes les classes dérivée de [CoreState] qui mémorisent l'état des différents fragments. Ici, il n'y en a qu'une (ligne 12) ;

2.8.1.4. L'activité [MainActivity]

L'activité [MainActivity] est actuellement la suivante :


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;

  // méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo : on continue les initialisations commencées par la classe parent
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // todo : définir les fragments ici
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo : définir les titres des fragments ici
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // todo : navigation par onglets - définir la vue à afficher lorsque l'onglet n° [position] est sélectionné
  }

  @Override
  protected int getFirstView() {
    // todo : définir le n° de la première vue (fragment) à afficher
    return 0;
  }
}

Les commentaires [//todo] indiquent ce que le développeur doit faire. La classe [MainActivity] évolue comme suit :


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;

  // méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

Seule la méthode des lignes 41-44 doit être modifiée. Elle doit rendre le tableau des fragments de l'application. Ligne 43, il ne faut pas oublier de mettre l'underscore derrière le nom du fragment.

2.8.1.5. L'état du fragment [FragmentState]

Suite aux tests de rotation faits sur le projet [Exemple-17], on décide de mémoriser les éléments suivants du fragment :

  • la liste des valeurs de la liste déroulante ;
  • la position de l'élément sélectionné dans cette liste ;
  • le message affiché par la boîte de dialogue si celle-ci est présente au moment de la rotation ;

La classe [Vue1FragmentState] sera la suivante :

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // les valeurs de la liste déroulante
  private List<String> list;
  // l'élément sélectionné dans la liste déroulante
  private int listSelectedPosition;
  // le message affiché dans la boîte de dialogue
  private String message;

  // getters et setters
...
}

2.8.1.6. Le fragment [AbstractFragment]

Actuellement le cycle de vie du fragment est géré par deux méthodes (lignes 6 et 32) :


// liste déroulante
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // on coche le premier bouton
    radioButton1.setChecked(true);
    // le calendrier
    datePicker1.setCalendarViewShown(false);
    // le seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // la liste déroulante
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // initialisation adaptateur de la liste déroulante
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

Le code de ces deux méthodes va migrer dans les méthodes imposées par la classe [AbstractFragment] de la façon suivante :


// gestion du cycle de vie du fragment ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 1ère visite ?
    if (previousState == null) {
      // on crée les valeurs de la liste déroulante
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // on restitue les valeurs de la liste déroulante
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // et le message de la boîte de dialogue
      message = state.getMessage();
    }
    // initialisation adaptateur de la liste déroulante
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
  }

  @Override
  protected void initView(CoreState previousState) {
    // le calendrier
    datePicker1.setCalendarViewShown(false);
    // le seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // initialisation adaptateur de la liste déroulante
    dropDownList.setAdapter(dataAdapter);
    // 1ère visite ?
    if (previousState == null) {
      // on coche le premier bouton
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // valeur seekbar
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // élément sélectionné dans liste déroulante
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // dialogue visible ?
    if (message != null) {
      // on l'affiche
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • lignes 2-9 : la méthode [saveFragment] doit mettre les éléments du fragment à mémoriser dans une classe dérivée de [CoreState] et rendre l'instance de celle-ci ;
  • lignes 11-14 : la méthode [getNumView] doit rendre le n° du fragment. Ici, il n'y a qu'un fragment dont le n° est 0 ;
  • lignes 16-34 : la méthode [initFragment] doit initialiser les champs du fragment. Elle reçoit l'état précédent du fragment. Si [previousState] vaut null, alors il s'agit de la 1ère visite ;
  • lignes 19-25 : lors de la 1ère visite, on crée les valeurs de la liste déroulante ;
  • lignes 26-30 : s'il ne s'agit pas de la 1ère visite, les champs [list, message] du fragment sont restaurés à partir de l'état précédent ;
  • lignes 33-34 : initialisation du champ [dataAdapter] du fragment. C'est la source de données de la liste déroulante ;
  • lignes 37-62 : la méthode [initView] sert à initialiser les composants de l'interface visuelle. Elle reçoit comme paramètre, l'état précédent [previousState]. Si [previousState==null], alors il s'agit de la 1ère visite ;
  • on retrouve ici, ce qu'il y avait auparavant dans la méthode [@AfterViews] ;
  • lignes 57-61 : lors de la 1ère visite, on s'assure que c'est le premier bouton radio qui est coché ;
  • lignes 64-67 : la méthode [updateOnSubmit] est exécutée lorsque l'action en cours est [SUBMIT]. Ici, il n'y a pas de navigation inter-fragments et donc pas d'action en cours ;
  • lignes 69-81 : la méthode [updateOnRestore] est exécutée lorsque l'action en cours est [NAVIGATION] ou [RESTORE]. Ici, il n'y a pas de navigation inter-fragments et donc pas d'action [NAVIGATION] possible ;
  • ligne 72 : on recalcule (pas restaure) la valeur du TextView seekBarValue. En effet, lors de rotations, on perdait parfois sa valeur ;
  • lignes 74-75 : on positionne la liste sur l'élément qui était sélectionné avant la rotation. Sans cela, la liste se positionnait sur son 1er élément ;
  • lignes 76-80 : on réaffiche la boîte de dialogue si le message de l'état précédent est non null. Nous reviendrons sur la méthode [showMessage] (ligne 79) ;
  • lignes 83-86 : la méthode [notifyEndOfUpdates] est la dernière méthode appelée par la classe parent avant de laisser le fragment fille tranquille. Ici il n'y a rien à faire ;
  • lignes 88-91 : la méthode [notifyEndOfTasks] signale la fin des tâches asynchrones lancées par le fragment. Ici, il n'y en a pas ;

La restauration de la boîte de dialogue se fait de la façon suivante :


  // le message de la boîte de dialogue
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // liste des messages à afficher
    List<String> messages = new ArrayList<>();
    ...
    // affichage
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // on construit le texte à affiche
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // on mémorise le message
    message = texte.toString();
    // on l'affiche
    showMessage();
  }

  private void showMessage() {
    // on l'affiche
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // reset du message
        message = null;
      }
    }).show();
}

Lorsque l'utilisateur valide le formulaire, la méthode [doValider] (ligne 5) construit une liste de messages qu'elle fait ensuite afficher (ligne 10) dans la boîte de dialogue.

  • lignes 14-20 : la liste de messages est concaténée en un seul message qui est mémorisé en ligne 2 ;
  • lignes 25-33 : c'est ce message qu'affiche la boîte de dialogue et c'est ce même message que la méthode [updateOnRestore] fait afficher ;
  • ligne 27 : le second paramètre de la méthode [setNeutralButton] est la méthode exécutée lorsque l'utilisateur clique sur le bouton [Fermer] de la boîte de dialogue ;
  • ligne 31 : sur la fermeture de la boîte de dialogue, on remet le message à null pour indiquer que la boîte de dialogue n'est plus présente ;

2.8.1.7. Tests

Le lecteur est invité à tester ce projet et à vérifier que le fragment est bien conservé après une ou plusieurs rotations successives.

2.8.2. Exemple-23 : client météo

Certains sites permettent d'avoir des informations météo sous forme de chaînes jSON. Voici un exemple :

Image

L'URL est de la forme : http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} avec :

  • city : la ville dont on veut la météo, ici Angers ;
  • country : le pays de la ville, ici la France (fr) ;
  • APPID : une clé obtenue en s'inscrivant sur le site [https://home.openweathermap.org/users/sign_up];

2.8.2.1. Le projet

  

Le projet a été construit à partir du projet [client-android-skel]. Il présente les caractéristiques suivantes :

  • il n'a qu'un fragment dont on n'a pas à conserver l'état ;
  • il fait des requêtes asynchrones ;

2.8.2.2. Personnalisation du projet

  

L'interface [IMainActivity] permet de spécifier certaines caractéristiques du projet :


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accès à la session
  ISession getSession();

  // changement de vue
  void navigateToView(int position, ISession.Action action);

  // gestion de l'attente
  void beginWaiting();

  void cancelWaiting();

  // constantes de l'application -------------------------------------

  // mode debug
  boolean IS_DEBUG_ENABLED = true;

  // délai maximal d'attente de la réponse du serveur
  int TIMEOUT = 1000;

  // délai d'attente avant exécution de la requête client
  int DELAY = 5000;

  // authentification basique
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacence des fragments
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barre d'onglets
  boolean ARE_TABS_NEEDED = false;

  // image d'attente
  boolean IS_WAITING_ICON_NEEDED = true;

  // nombre de fragments de l'application
  int FRAGMENTS_COUNT = 1;

}
  • lignes 25, 28, 31, 40 : caractéristiques de la couche [DAO]. Ligne 31, il n'y a pas besoin d'authentification basique ;
  • ligne 34 : adjacence des fragments. Ici cette constante n'a pas d'importance puisqu'il n'y a qu'un fragment ;
  • ligne 37 : ce n'est pas une application à onglets ;
  • ligne 43 : il n'y a qu'un fragment ;

La classe [CoreState] qui mémorise l'état des fragments sera la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo : ajouter ici les sous-classes de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragment visité ou non
  protected boolean hasBeenVisited = false;
  // état de l'éventuel menu du fragment
  protected MenuItemState[] menuOptionsState;

  // getters et setters
...
}
  • lignes 10-13 : il n'y a rien à déclarer puisque dans cette application il n'y a qu'un fragment dont on ne conserve pas l'état ;

La classe [Session] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // les éléments qui ne peuvent être sérialisés en jSON doivent avoir l'annotation @JsonIgnore
}

Elle est vide puisque dans cette application, il n'y a pas de communication inter-fragments.

2.8.2.3. La couche [DAO]

  

Dans la couche [DAO], trois classes doivent être personnalisées :

  • l'interface IDao ;
  • son implémentation Dao ;
  • l'interface WebClient de dialogue avec le serveur web / jSON ;

L'interface [WebClient] sera la suivante :


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // service météo
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • lignes 18-19 : l'URL du service météo. On rappelle que celle-ci est relative à l'URL racine (RestClientRootUrl, ligne 12) du client. Ici cette URL racine sera [http://api.openweathermap.org/];

L'interface [IDao] sera la suivante :


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Url du service web
  void setUrlServiceWebJson(String url);

  // utilisateur
  void setUser(String user, String mdp);

  // timeout du client
  void setTimeout(int timeout);

  // authentification basique
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // mode debug
  void setDebugMode(boolean isDebugEnabled);

  // délai d'attente en millisecondes du client avant requête
  void setDelay(int delay);

  //  service météo
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • on rappelle que les méthodes des lignes 6-22 sont présentes par défaut dans l'interface IDao du projet [client-android-skel] ;
  • ligne 25 : la méthode [getWeatherForecast] permet d'obtenir la chaîne jSON de la météo de la ville [city] du pays [country]. Le 3ième paramètre est la clé obtenue sur le site [https://home.openweathermap.org/users/sign_up];

L'interface [IDao] est implémentée par la classe [Dao] suivante :


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client du service web
  @RestService
  protected WebClient webClient;
  // sécurité
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // le RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;

  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // on construit le restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // on fixe le convertisseur jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // on fixe le restTemplate du client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // on fixe l'URL du service web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // on enregistre l'utilisateur dans l'intercepteur
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // mémoire
    this.timeout = timeout;
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercepteur d'authentification ?
    if (isBasicAuthentificationNeeded) {
      // on ajoute l'intercepteur d'authentification
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // service météo ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // résultat
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • on rappelle que les lignes 17-90 sont présentes par défaut dans la classe [Dao] du projet [client-android-skel]. Il faut juste ajouter les méthodes d'implémentation de l'interface [IDao], spécifiques à l'application (ligne 92) ;
  • lignes 93-105 : implémentation de la méthode [getWeatherForecast]. Celle-ci est très simple et réalisée en 6 lignes, lignes 100-105 ;
  • ligne 100 : la méthode [getResponse] est une méthode de la classe parent [AbstractDao]. Elle attend un paramètre de type [IRequest<T>] où T est le type de la réponse attendue du serveur, ici un String puisqu'on attend une chaîne jSON. Le type T de [IRequest<T>] doit être le type T de la méthode [Observable<T> getWeatherForecast] ;
  • l'interface [IRequest<T>] n'a qu'une méthode : getResponse. Celle-ci a pour rôle de fournir la réponse de type T que doit rendre la méthode [Observable<T> getWeatherForecast] ;
  • ligne 103 : c'est l'interface [WebClient] qui fournit cette réponse. On lui passe les trois paramètres reçus ligne 94. Pour cette raison, ceux-ci doivent avoir l'attribut final ;

2.8.2.4. L'activité [MainActivity]

  

L'activité [MainActivity] est la suivante :


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • on rappelle que les lignes 15-55 sont présentes par défaut dans le projet [client-android-skel]. Il faut juste les personnaliser ;
  • lignes 37-40 : le tableau des fragments. Il n'y en a qu'un ici ;
  • lignes 43-46 : pas de titres de fragments nécessaires ;
  • lignes 48-50 : pas d'onglets ici ;
  • lignes 52-55 : la 1ère vue à afficher est la vue n° 0, celle de [MeteoFragment] ;
  • lignes 58-61 : implémentation de l'interface [IDao]. Ici, il n'y a rien à faire d'autre qu'à déléguer le travail à la couche [DAO] de la ligne 21 ;

2.8.2.5. Le fragment[MeteoFragment]

  

Le fragment [MeteoFragment] interroge le service web / jSON de météo. Son squelette est le suivant :


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • ligne 14 : la vue [res / layout / meteo_fragment.xml] est la suivante :

<?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">

  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

La vue n'affiche que le texte de la ligne 10 ;

  • ligne 15 : le menu [res / menu / menu_meteo.xml] est le suivant :

<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • lignes 10-12 : cette option de menu sert à demander la météo d'une ville ;
  • lignes 14-15 : cette option de menu sert à annuler cette demande si elle est en cours ;
  • lignes 16-18 : cette option de menu termine l'application ;

Le code complet du fragment est le suivant :


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // données locales
  private int nbReponsesRecues;

  // gestion des événements ---------------------------------------------------------------------------------------
  // villes dont on veut la météo
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // son pays
    String country = "fr";
    // obtenez un identifiant API en créant un compte [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL du service web / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // début de l'attente des [paysDeLoire.length] tâches asynchrones
    beginWaiting(paysDeLoire.length);
    // nbre de réponses reçues
    nbReponsesRecues = 0;
    // on fait les appels asynchrones en parallèle
    for (String city : paysDeLoire) {
      // météo
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // exploitation de la réponse
          consumeResponse(response);
          // une réponse de +
          nbReponsesRecues++;
        }
      });
    }
  }

  // exploitation réponse du serveur
  private void consumeResponse(String response) {
    // log
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // début de l'attente
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // parent
    beginRunningTasks(numberOfRunningTasks);
    // on  affiche l'option [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // affichage résultats
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // méthodes privées -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // gestion du cycle de vie ---------------------------------------------------------------------------------------
...
}
  • lignes 25-50 : gestion du clic sur l'option de menu [Météo] ;
  • ligne 32 : construction de l'URL du service web / jSON du service météo. Celle-ci est ensuite passée à la couche [DAO] via l'activité ;
  • ligne 34 : on commence l'attente. On passe le nombre de tâches qui vont être lancées, ceci afin que la classe parent puisse nous signaler la fin de celles-ci. Ici, il y a cinq tâches car on va demander la météo des cinq villes de la ligne 23 ;
  • ligne 16 : on va compter le nombre de réponses reçues afin de pouvoir l'afficher ;
  • lignes 38-50 : on boucle sur les villes dont on veut la météo ;
  • ligne 40 : on va faire 5 requêtes HTTP en parallèle ;
  • ligne 40 : on demande à la classe parent [AbstractParent] d'interroger le service web / jSON ;
  • lignes 40-48 : la méthode [executeInBackground] attend deux paramètres :
    • ligne 40 : le processus à observer et exécuter est fourni par la méthode [mainActivity.getWeatherForecast] ;
    • lignes 40-48 : l'instance [Action1] à exécuter lorsqu'on reçoit la réponse du service asynchrone. Le type T de [Action1<T>] doit être le type T du résultat de la méthode [getWeatherForecast] ;
  • ligne 44 : une réponse a été reçue. On la passe à la méthode [consumeResponse] de la ligne 53 ;
  • ligne 46 : on incrémente le compteur des réponses reçues ;
  • lignes 53-56 : consommation d'une réponse jSON du service météo ;
  • ligne 55 : on se contente de loguer la chaîne jSON ;
  • lignes 59-72 : code exécuté avant le lancement des tâches asynchrones ;
  • ligne 65 : on passe le nombre de tâches à exécuter à la classe parent [AbstractParent]. C'est ce qui permet à celle-ci de nous avertir lorsqu'elles seront toutes terminées ;
  • lignes 67-70 : préparation du menu pour une attente. On ne garde que l'option [Actions/Annuler] qui va permettre à l'utilisateur d'annuler les tâches lancées ;
  • lignes 74-92 : code exécuté lorsque la classe parent nous avertit que toutes les tâches lancées sont terminées ;
  • ligne 77 : on remet le menu dans son état initial. La méthode [initMenu] (lignes 95-102) affiche le menu avec toutes ses options sauf l'option [Actions/Annuler] qui est cachée ;
  • lignes 80-91 : on affiche le nombre de réponses reçues ;

Le clic sur l'option de menu [Annuler] est géré par le code suivant :


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // on annule les tâches asynchrones
    cancelRunningTasks();
}
  • ligne 7 : on demande à la classe parent d'annuler les tâches encore actives ;

Le clic sur l'option de menu [Terminer] est géré par le code suivant :


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // on arrête tout
    System.exit(0);
}

La gestion du cycle de vie du fragment est assuré par les méthodes suivantes :


  // gestion du cycle de vie ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // 1ère visite ?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • lignes 3-6 : servent à mémoriser l'état du fragment dans une classe dérivée de [CoreState]. Si le fragment n'a pas d'état à mémoriser comme ici, on se contente de rendre une instance de [CoreState]. Il ne faut pas rendre null car cela amènerait ultérieurement à un plantage ;
  • lignes 8-11 : doivent rendre le n° de la vue. Ici le fragment [MeteoFragment] a le n° 0 ;
  • ligne 13-16 : servent à initialiser le fragment une fois qu'il a été construit (previousState==null) ou reconstruit (previousState!=null). Ici, il n'y a rien à faire. Le seul champ susceptible d'initialisation est le suivant :

  // villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

mais il s'initialise tout seul ;

  • lignes 18-24 : servent à initialiser la vue associée au fragment une fois qu'elle a été construite (previousState==null) ou reconstruite (previousState!=null) ;
  • lignes 21-23 : si c'est la 1ère visite faite au fragment, on initialise son menu pour cacher l'option [Annuler] ;
  • lignes 27-30 : appelées si pour arriver au fragment, il y a eu navigation avec une action de type [SUBMIT]. Ici, il n'y a pas de navigation inter-fragments puisqu'il n'y a qu'un fragment ;
  • lignes 32-35 : appelées lors d'un cycle sauvegarde / restauration due à une rotation du périphérique ou une autre raison. Ici, comme on n'a pas sauvegardé d'état, il n'y a rien à faire ;
  • lignes 37-40 : appelées lorsque toutes les mises à jour précédentes ont été effectuées. Ici, il n'y a rien à faire ;

2.8.2.6. Tests

Nous exécutons maintenant l'exemple :

Image

Image

Les logs sont alors les suivants :


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
  • lignes 32-36 : les réponses jSON sont obtenues sur des threads d'E/S
  • lignes 37-41 : le fragment récupère les 5 réponses sur le thread de l'Ui ;

Maintenant, on fait la requête avec un identifant API incorrect :


    String APIID = "";

Image

Les logs sont alors les suivants :


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • lignes 3-6, 10 : les 5 appels HTTP ont généré 5 exceptions ;
  • ligne 7 : le fragment [MeteoFragment] reçoit la 1ère exception. Il va alors annuler toutes les tâches ;

Maintenant mettons un temps d'attente de 5 secondes [IMainActivity.DELAY] et annulons l'opération. Les logs sont alors les suivants :


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • ligne 3 : demande d'annulation ;
  • ligne 4 : l'attente est annulée parce qu'une annulation a eu lieu ;
  • lignes 6-10 : l'annulation des tâches provoque une exception sur chacun des threads des cinq tâches. Le type d'exception dépend des applications. L'exception est ici [java.lang.InterruptedException] parce que les tâches ont été interrompues pendant qu'elles exécutaient l'instruction [Thread.sleep(delay)] qui les fait attendre artificiellement [delay] millisecondes ;

2.8.3. Exemple-16B

Nous refactorisons ici l'exemple 16 du paragraphe 1.17. Il présente un fragment qui fait des appels asynchrones à un serveur de nombres aléatoires. Voyons comment il se comporte lors d'une rotation du périphérique :

Image

  • en [1], on fait tourner le périphérique deux fois ;

Image

On voit qu'on a perdu tous les messages d'erreur. Nous allons essayer d'améliorer cela.

2.8.3.1. Le projet Exemple-16B

Nous copions le projet [client-android-skel] dans le projet [exemples/Exemple-16B] puis nous chargeons le nouveau projet :

  

Du projet initial [Exemple-16], nous copions dans [Exemple-16B] les éléments suivants :

  • le fichier [res/layout/vue1.xml], le dossier [res/values] :
  

On modifiera la marge haute de la vue [vue1.xml] à 80 dp :


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • le fragment [Vue1Fragment] :
  
  • la classe [dao / service / Response] :
  

A ce stade, on peut tenter une 1ère compilation :

  • un premier type d'erreurs est celui des imports. Des classes ont changé de package dans la migration vers [Exemple-16B]. On commence par corriger ce type d'erreurs ;
  • un second type d'erreur est signalée sur la classe [Vue1Fragment] parce qu'elle n'implémente pas les méthodes imposées par la classe parent [AbstractParent]. On fait une génération automatique de celles-ci ;

On tente une seconde compilation :

  • toutes les erreurs restantes sont désormais concentrées sur la classe [Vue1Fragment], la classe qui va subir le plus de modifications ;

2.8.3.2. Création d'un état pour le fragment [Vue1Fragment]

Nous avons vu que certaines informations du fragment allaient devoir être sauvegardées lors d'une rotation afin de restaurer le fragment tel qu'il était avant la rotation. Nous créons donc un état [Vue1FragmentState] vide pour le moment :

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Personnalisation du projet

  

L'interface [IMainActivity] permet de spécifier certaines caractéristiques du projet :


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accès à la session
  ISession getSession();

  // changement de vue
  void navigateToView(int position, ISession.Action action);

  // gestion de l'attente
  void beginWaiting();

  void cancelWaiting();

  // constantes de l'application -------------------------------------

  // mode debug
  boolean IS_DEBUG_ENABLED = true;

  // délai maximal d'attente de la réponse du serveur
  int TIMEOUT = 1000;

  // délai d'attente avant exécution de la requête client
  int DELAY = 5000;

  // authentification basique
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacence des fragments
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barre d'onglets
  boolean ARE_TABS_NEEDED = false;

  // image d'attente
  boolean IS_WAITING_ICON_NEEDED = true;

  // nombre de fragments de l'application
  int FRAGMENTS_COUNT = 1;

}
  • lignes 25, 28, 31, 40 : caractéristiques de la couche [DAO]. Il n'y a pas besoin d'authentification basique ;
  • ligne 34 : adjacence des fragments. Ici cette constante n'a pas d'importance puisqu'il n'y a qu'un fragment ;
  • ligne 37 : ce n'est pas une application à onglets ;
  • ligne 43 : il n'y a qu'un fragment ;

La classe [CoreState] qui mémorise l'état des fragments sera la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // fragment visité ou non
  protected boolean hasBeenVisited = false;
  // état de l'éventuel menu du fragment
  protected MenuItemState[] menuOptionsState;

  // getters et setters
...
}
  • ligne 12 : nous déclarons la classe de l'état du fragment [Vue1Fragment] ;

La classe [Session] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // les éléments qui ne peuvent être sérialisés en jSON doivent avoir l'annotation @JsonIgnore
}

Elle est vide puisque dans cette application, il n'y a pas de communication inter-fragments.

2.8.3.4. La couche [DAO]

  

Dans la couche [DAO], trois classes doivent être personnalisées :

  • l'interface IDao ;
  • son implémentation Dao ;
  • l'interface WebClient de dialogue avec le serveur web / jSON ;

La classe [Response] vient du projet [Exemple-16] qui l'utilise :


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- propriétés
    // statut de l'opération
    private int status;
    // les éventuels messages d'erreur
    private List<String> messages;
    // le corps de la réponse
    private T body;

    // constructeurs
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters et setters
...
}

L'interface [WebClient] sera la suivante :


package client.android.dao.service;

import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 1 nombre aléatoire dans l'intervalle [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • lignes 18-19 : l'URL du service des nombres aléatoires. On rappelle que celle-ci est relative à l'URL racine (RestClientRootUrl, ligne 12) du client. Ici cette URL racine sera [http://localhost:8080];

L'interface [IDao] sera la suivante :


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // Url du service web
  void setUrlServiceWebJson(String url);

  // utilisateur
  void setUser(String user, String mdp);

  // timeout du client
  void setTimeout(int timeout);

  // authentification basique
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // mode debug
  void setDebugMode(boolean isDebugEnabled);

  // délai d'attente en millisecondes du client avant requête
  void setDelay(int delay);

  // service de nombres aléatoires
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • on rappelle que les méthodes des lignes 6-22 sont présentes par défaut dans l'interface IDao du projet [client-android-skel] ;
  • ligne 25 : la méthode [getAlea] permet d'obtenir un nombre aléatoire dans l'intervalle [a,b]. Ce nombre est obtenu dans une réponse de type [Response<Integer>] où le nombre aléatoire est dans le champ [body] de ce type ;

L'interface [IDao] est implémentée par la classe [Dao] suivante :


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client du service web
  @RestService
  protected WebClient webClient;
  // sécurité
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // le RestTemplate
  private RestTemplate restTemplate;
  // factory du RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // on construit le restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // on fixe le convertisseur jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // on fixe le restTemplate du client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // on fixe l'URL du service web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // on enregistre l'utilisateur dans l'intercepteur
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercepteur d'authentification ?
    if (isBasicAuthentificationNeeded) {
      // on ajoute l'intercepteur d'authentification
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // méthodes privées -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // service de nombres aléatoires
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // exécution client web
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • on rappelle que les lignes 17-85 sont présentes par défaut dans la classe [Dao] du projet [client-android-skel]. Il faut juste ajouter les méthodes d'implémentation de l'interface [IDao] ;
  • lignes 88-97 : implémentation de la méthode [getAlea]. Celle-ci est très simple et réalisée en 6 lignes, lignes 91-96 ;
  • ligne 91 : la méthode [getResponse] est une méthode de la classe parent [AbstractDao]. Elle attend un paramètre de type [IRequest<T>] où T est le type de la réponse attendue, ici un type Response<Integer>. Le type T de [IRequest<T>] (ligne 91) doit être le type T de la méthode [Observable<T> getAlea] (ligne 89) ;
  • l'interface [IRequest<T>] n'a qu'une méthode : getResponse. Celle-ci a pour rôle de fournir la réponse de type T que doit rendre la méthode [Observable<T> getAlea] ;
  • ligne 94 : c'est l'interface [WebClient] qui fournit cette réponse. On lui passe les deux paramètres reçus ligne 89. Pour cette raison, ceux-ci doivent avoir l'attribut final ;

2.8.3.5. L'activité [MainActivity]

  

L'activité [MainActivity] est la suivante :


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // on continue les initialisations commencées par la classe parent
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // définir les fragments ici
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // définir les titres des fragments ici
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // navigation par onglets - définir la vue à afficher
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interface IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • on rappelle que les lignes 15-61 sont présentes par défaut dans le projet [client-android-skel]. Il faut juste les personnaliser ;
  • lignes 40-44 : le tableau des fragments. Il n'y en a qu'un ici ;
  • lignes 47-51 : pas de titres de fragments nécessaires ;
  • lignes 53-56 : pas d'onglets ici ;
  • lignes 58-61 : la 1ère vue à afficher est la vue n° 0, celle de [Vue1Fragment] ;
  • lignes 64-67 : implémentation de l'interface [IDao]. Ici, il n'y a rien à faire d'autre qu'à déléguer le travail à la couche [DAO] de la ligne 23 ;

2.8.3.6. L'état du fragment[Vue1Fragment]

  

La classe [Vue1FragmentState] sera la suivante :


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

public class Vue1FragmentState extends CoreState {

  // état fragment ------------------------
  // liste des réponses
  private List<String> reponses = new ArrayList<>();
  // état vue ------------------------
  // msg d'erreur sur le nombre de nombres aléatoires demandés
  private boolean txtErrorAleasVisible = false;
  // msg d'erreur sur l'intervalle [a,b] de génération
  private boolean txtErrorIntervalleVisible = false;
  // msg d'erreur sur l'URL du service web
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // msg d'erreur sur la durée d'attente
  private boolean textViewErreurDelayVisible = false;
  // état visible ou non du bouton Exécuter
  private boolean btnExecuterVisible = true;

  // getters et setters
...
}

Pour obtenir ce qu'il fallait mémoriser dans le fragment, on a fait faire des rotations au périphérique dans diverses situations et on a regardé ce qui avait disparu à la restauration. On est arrivé à la conclusion qu'il fallait mémoriser les informations des lignes 10-23.

2.8.3.7. Le fragment[Vue1Fragment]

  

Actuellement la vue [Vue1Fragment] présente diverses erreurs dûes au fait que la classe parent [AbstractFragment] dont elle dérive a changé. Plutôt que de décrire un à un les changements à faire, nous allons commenter directement la version finale.

Le squelette du fragment est le suivant :


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {

...
}
  • ligne 26 on rappelle que tout fragment doit avoir un menu, même vide. C'est le cas ici.

2.8.3.7.1. Gestion du clic sur le bouton [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // on vérifie les données saisies
    if (!isPageValid()) {
      return;
    }
    // on efface les reponses précédentes
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // on remet à 0 le compteur de réponses
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // initialisation activité
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // on prépare la tâche aléatoire
    beginWaiting(1);
    // on demande les nombres aléatoires
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // on crée le processus à observer
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // on demande les nombres aléatoires
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // on consomme la réponse
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // une réponse de +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // on analyse la réponse
    // erreur ?
    if (response.getStatus() != 0) {
      // affichage
      showAlert(response.getMessages());
      // annulation
      doAnnuler();
      // retour à l'Ui
      return;
    }
    // on ajoute l'information à la liste des reponses
    reponses.add(0, String.valueOf(response.getBody()));
    // on rafraîchit les reponses
    dataAdapterReponses.notifyDataSetChanged();
  }

  // annulation ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // on annule les tâches asynchrones
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // on met le sablier
    beginRunningTasks(nbRunningTasks);
    // le bouton [Annuler] remplace le bouton [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • lignes 4-6 : on vérifie d'abord que les saisies sont valides. Des messages d'erreur peuvent alors apparaître ;
  • lignes 8-9 : la liste des réponses est vidée. On répercute ce changement sur le ListView qui les affiche ;
  • lignes 11-12 : le nombre de réponses reçues est mis à zéro ;
  • ligne 14 : on fixe l'URL du service des nombres aléatoires. Cette information va être transmise à la couche [DAO] ;
  • ligne 15 : on fixe le délai d'attente avant de faire la requête au service des nombres aléatoires. Cette information va être transmise à la couche [DAO] ;
  • ligne 17 : on se prépare à lancer 1 tâche asynchrone (et non pas N, on verra pourquoi) ;
  • lignes 24-27 : des N tâches asynchrones, on en fait une par une suite d'opérations [merge] ;
  • lignes 29-36 : on demande à la classe parent [AbstractParent] d'interroger le service web / jSON de nombres aléatoires ;
  • lignes 29-36 : la méthode [executeInBackground] attend deux paramètres :
    • ligne 29 : le processus à observer et exécuter est celui qui a été calculé dans les lignes qui précèdent ;
    • lignes 29-36 : l'instance [Action1] à exécuter lorsqu'on reçoit la réponse du service asynchrone. Le type T de [Action1<T>] doit être le type T du résultat de la méthode [getAlea], ç-à-d un type [Response<Integer>] ;
  • ligne 34 : lorsqu'une réponse arrive (un nombre aléatoire), on la consomme dans la méthode de la ligne 39 ;
  • lignes 49-50 : on note et on signale qu'on a reçu une nouvelle réponse ;
  • lignes 53-60 : le type [Response<T>] a un champ [status] qui est un code d'erreur. Si ce code est différent de zéro, alors le serveur a rencontré un problème ;
  • ligne 55 : un message d'erreur est affiché. La méthode [showAlert] appartient à la classe parent ;
  • ligne 57 : la méthode des lignes 68-75 est appelée. Elle va annuler les tâches encore actives (ligne 74) ;
  • ligne 62 : la réponse est ajoutée à la liste des réponses qui est la source de données du ListView ;
  • ligne 64 : le ListView est rafraîchi ;
  • lignes 77-83 : la méthode [beginWaiting(int nbRunningTasks)] prépare la vue pour l'attente (lignes 81-82) et transmet à la classe parent que [nbRunningTasks] tâches vont bientôt s'exécuter (ligne 79) ;

2.8.3.7.2. Le cycle de vie du fragment

Le cycle de vie du fragment est assuré par les méthodes suivantes :


  // données locales
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // gestion du cycle de vie ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // état actuel de la vue
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // 1ère visite ?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // source de données du listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // nre de réponses
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // lien listview / adaptateur
    listReponses.setAdapter(dataAdapterReponses);
    // 1ère visite ?
    if (previousState == null) {
      // on cache les msg d'erreur
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // les boutons
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // état précédent de la vue
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // on montre / cache les msg d'erreur
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // boutons
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // nb de réponses
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // le bouton [Exécuter] remplace le bouton [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • lignes 7-18 : assurent la sauvegarde du fragment lorsque la classe parent demande de le faire ;
  • ligne 11 : visibilité du message d'erreur sur le délai d'attente ;
  • ligne 12 : visibilité du message d'erreur sur le nombre de nombres aléatoires demandés ;
  • ligne 13 : visibilité du message d'erreur sur l'URL du service web / jSON ;
  • ligne 14 : visibilité du message d'erreur sur l'intervalle [a,b] de génération des nombres aléatoires ;
  • ligne 15 : visibilité du bouton [Exécuter] ;
  • ligne 16 : la liste des réponses reçues ;
  • lignes 20-23 : doivent rendre le n° de la vue. Le n° du fragment est ici 0 puisqu'il n'y en a qu'un ;
  • lignes 25-38 : initialisation des champs du fragment, soit sur une 1ère visite (previousState==null), soit sur une visite ultérieure ;
    • lignes 29-30 : si ce n'est pas la 1ère visite, le champ [reponses] est restauré à partir de l'état précédent du fragment ;
    • lignes 31-33 : si c'est la 1ère visite, alors le champ [reponses] est initialisé avec une liste vide ;
    • lignes 34-37 : à partir du champ [reponses] on peut construire la source de données du ListView du fragment (ligne 35) ainsi que le nombre de réponses (ligne 37) ;
  • lignes 40-55 : exécutées pour initialiser la vue associée au fragment, soit sur une 1ère visite (previousState==null), soit sur une visite ultérieure ;
    • ligne 43 : on associe le ListView du fragment à la source de données qui vient d'être construite dans la méthode [initFragment] ;
    • lignes 45-54 : si c'est la 1ère visite, on prépare la vue pour son 1er affichage ;
  • lignes 57-60 : exécutées lors d'une navigation inter-fragments associée à une action de type [SUBMIT]. Ici, il n'y a qu'un fragment et donc pas de navigation inter-fragments ;
  • lignes 63-76 : exécutées lors d'une navigation inter-fragments associée à une action de type [NAVIGATION] ou bien lors d'un cycle sauvegarde / restauration dûe à une rotation du périphérique ou une autre raison. Ici, seul ce dernier cas peut se produire. Il faut se rappeler qu'ici, dans tous les cas, [previousState] est toujours non null ;
  • ligne 65 : on caste l'état précédent dans le type de l'état du fragment ;
  • lignes 66-75 : on utilise le contenu de l'état précédent pour restaurer la vue ;
  • lignes 78-81 : appelées lorsque toutes les mises à jour précédentes ont été effectuées. Ici, il n'y a rien à faire ;
  • lignes 83-89 : exécutées lorsque toutes les tâches asynchrones sont terminées. Ici on cache le bouton [Annuler] pour le remplacer par le bouton [Exécuter] ;

2.8.3.8. Les tests

Le lecteur est invité à faire les tests suivants :

  • créer des erreurs et faire tourner le périphérique : les messages d'erreur doivent être conservés ;
  • obtenir des nombres aléatoires et faire tourner le périphérique : les nombres aléatoires obtenus doivent rester affichés ;
  • mettre une attente de plusieurs secondes et faire tourner le périphérique pendant l'attente : les tâches doivent avoir été annulées (cela se voit dans les logs) ;

2.8.4. Exemple-22B

Nous reprenons ici l'exemple 22 pour le refactoriser selon le modèle du projet [client-android-skel]. On rappelle que le projet [Exemple-22] gère correctement le cycle sauvegarde / restauration des fragments lors d'une rotation et que c'est lui qui a servi de base au projet [client-android-skel].

Nous dupliquons le projet [client-android-skel] dans [exemples/Exemple-22B] et nous chargeons ce dernier projet :

  

Puis nous copions divers éléments du projet [Exemple-22] dans le projet [Exemple-22B].

Tout d'abord, nous copions des éléments du dossier [res] :

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, le dossier [values] ;
  

On modifiera la marge haute des deux vues à 120 dp :

[vue1.xml] :


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main] :


  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>

Ensuite nous copions les éléments [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState] :

 

A ce stade, nous pouvons tenter une 1ère compilation. Un premier type d'erreurs apparaît : celui des imports incorrects parce que des classes ont changé de package. On corrige ces imports. Un second type d'erreurs est dû au fait que les fragments n'implémentent pas toutes les méthodes de leur classe parent [AbstractFragment]. On corrige par (Alt+Entrée).

Les erreurs restantes proviennent des différences existantes entre l'ancienne et la nouvelle classe [AbstractFragment]. Pour l'instant, on les ignore.

2.8.4.1. Personnalisation du projet

  

Dans le dossier [custom] se trouvent les éléments d'architecture personnalisables par le développeur.

L'interface [IMainActivity] permet de spécifier certaines caractéristiques du projet :


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accès à la session
  ISession getSession();

  // changement de vue
  void navigateToView(int position, ISession.Action action);

  // gestion de l'attente
  void beginWaiting();

  void cancelWaiting();

  // mode debug
  boolean IS_DEBUG_ENABLED = true;

  // délai maximal d'attente de la réponse du serveur
  int TIMEOUT = 1000;

  // délai d'attente avant exécution de la requête client
  int DELAY = 0;

  // authentification basique
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacence des fragments
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barre d'onglets
  boolean ARE_TABS_NEEDED = true;

  // image d'attente
  boolean IS_WAITING_ICON_NEEDED = false;

  // nombre de fragments
  int FRAGMENTS_COUNT = 5;

}
  • lignes 23, 26, 29, 38 : caractéristiques de la couche [DAO]. Il n'y en a pas ici ;
  • ligne 41 : il y a ici cinq fragments ;
  • ligne 32 : adjacence des fragments. Cette constante peut avoir ici une valeur dans [1,4]. Le lecteur est encouragé à faire varier cette valeur pour voir si l'application continue à fonctionner ;
  • ligne 35 : c'est une application à onglets ;

La classe [CoreState] qui mémorise l'état des fragments sera la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // fragment visité ou non
  protected boolean hasBeenVisited = false;
  // état de l'éventuel menu du fragment
  protected MenuItemState[] menuOptionsState;

  // getters et setters
...
}
  • ligne 12 : nous déclarons la classe de l'état du fragment [PlaceHolderFragment]. Le fragment [Vue1Fragment] n'a lui pas d'état ;

La classe [Session] est la suivante :


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // données à partager entre fragments eux-mêmes et entre fragments et activité
  // les éléments qui ne peuvent être sérialisés en jSON doivent avoir l'annotation @JsonIgnore
  // ne pas oublier les getters et setters nécessaires pour la sérialisation / désérialisation jSON

  // nombre de fragments visités
  private int numVisit;
  // n° fragment de type [PlaceholderFragment] affiché dans second onglet
  private int numFragment = -1;

  // getters et setters
...
}

C'est la session du projet [Exemple-22].

2.8.4.2. L'activité [MainActivity]

  

L'activité [MainActivity] est la suivante :


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // couche [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;

  // gestion du menu-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // implémentation méthodes de la classe parent ---------------------------------------------------
  ...
}

Ici, la classe [MainActivity] est plus conséquente que celle des exemples précédents pour deux raisons :

  • il y a des onglets à gérer ;
  • il y a un menu à gérer ;

2.8.4.2.1. Implémentation des méthodes de la classe parent

// méthodes classe parent -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // on continue les initialisations commencées par la classe parent
    // session
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // n° de fragment
    final String ARG_SECTION_NUMBER = "section_number";
    // initialisation du tableau des fragments
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // on crée un fragment
      fragments[i] = new PlaceholderFragment_();
      // on peut passer des arguments au fragment
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // un fragment de +
    fragments[i] = new Vue1Fragment_();
    // résultat
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // pas de titres ici
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • lignes 2-12 : la méthode [onCreateActivity] est appelée par la classe parent [AbstractActivity] lorsque l'activité est créée la 1ère fois ou recréée lors d'un cycle sauvegarde / restauration. Lorsque cette méthode est appelée, la classe parent a déjà restauré la session ;
  • ligne 10 : on récupère une référence locale de la session. Le changement de type est dû au fait que la session de la classe parent est de type [AbstractSession] ;
  • lignes 19-38 : la méthode [getFragments] doit rendre à la classe parent le tableau des fragments gérés par l'application. Ici il y en a [FRAGMENTS_COUNT], nombre défini dans [IMainActivity]. Les [FRAGMENTS_COUNT-1] premiers fragments sont de type [PlaceHolderFragment] et le dernier de type [Vue1Fragment] ;
  • lignes 41-45 : la méthode [getFragmentTitle] doit rendre les titres des fragments lorsque cette information peut être utile. Ce n'est pas le cas ici ;
  • lignes 47-50 : cette méthode est appelée par la classe parent lorsque l'utilisateur clique sur un onglet. Nous allons y revenir dans le paragraphe suivant ;
  • lignes 52-55 : rend le n° de la 1ère vue à afficher lorsque l'application démarre. Ici c'est le fragment [Vue1Fragment] qui doit être affiché en premier. La méthode [getFirstView] pourrait être avantageusement remplacée par une constante dans [IMainActivity] ;

2.8.4.2.2. Gestion des onglets

Les onglets sont gérés par les méthodes suivantes :


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // on continue les initialisations commencées par la classe parent
    // session
    this.session = (Session) super.session;
    // 1er onglet
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2ième onglet ?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // n° du fragment à afficher
    int numFragment;
    switch (position) {
      case 0:
        // n° fragment [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // n° fragment [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // affichage fragment
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • lignes 1-20 : la méthode [onCreateActivity] est appelée par la classe parent [AbstractActivity] lorsque l'activité est créée la 1ère fois ou recréée lors d'un cycle sauvegarde / restauration. Lorsque cette méthode est appelée, la classe parent a déjà restauré la session ;
  • ligne 9 : on récupère une référence locale de la session. Le changement de type est dû au fait que la session de la classe parent est de type [AbstractSession] ;
  • lignes 11-13 : on crée le 1er onglet ;
  • lignes 15-20 : on crée le second onglet si un n° de fragment est enregistré dans la session (ligne 15). Ce n° vaut initialement -1 lors de la 1ère construction de l'activité ;
  • lignes 23-39 : cette méthode est appelée par la classe parent lorsque l'utilisateur clique sur un onglet ;
  • lignes 28-31 : si c'est l'onglet 0 qui est cliqué, alors on doit faire afficher [Vue1Fragment]. On sait que c'est la 1ère vue qui a été affichée au démarrage de l'application ;
  • lignes 32-35 : si c'est l'onglet 1 qui est cliqué, alors on doit faire afficher le fragment dont le n° est enregistré dans la session ;
  • lignes 37-39 : on navigue vers le fragment choisi. L'action associée est [SUBMIT]. Aurait-ce pu être [NAVIGATION] ? Dans ce document, on utilise [NAVIGATION] uniquement lorsque l'affichage du nouveau fragment ne nécessite de connaître que son état précédent. Ici, ce n'est pas le cas puisque l'affichage du fragment affiché doit changer par rapport à son état précédent pour afficher une visite de plus ;

2.8.4.2.3. Gestion du menu

L'activité est associée au menu [menu_main.xml] suivant :


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

qui affiche la chose suivante :

  

La gestion du menu est assuré par les méthodes suivantes :


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // traitement des options de menu
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // item traité
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // pas de navigation sur sélection logicielle d'un onglet
      session.setNavigationOnTabSelectionNeeded(false);
      // on recrée les deux onglets pour une histoire de police de caractères des titres
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // le n° du fragment à afficher est mis en session
      session.setNumFragment(i);
      // on sélectionne l'onglet n° 2 avec navigation
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • lignes 16-31 : gestion du clic sur une option de menu de type [Fragmenti] ;
  • lignes 37-50 : affichent le fragment n° i (il s'agit des fragments de type PlaceHolderFragment) dans l'onglet n° 1 (2ième onglet) ;
  • lignes 42-44 : on décide de supprimer les onglets existants pour en recréer deux nouveaux. Cette décision a été prise pour contourner le problème suivant : lorsqu'on se contente d'afficher le fragment dans l'onglet 1 existant (sans donc le supprimer), curieusement son titre a une allure (police de caractères, taille) différente de celle du titre de l'onglet 0 ;
  • lignes 43-44 : les deux onglets sont créés mais pas sélectionnés (dernier paramètre à false) ;
  • ligne 40 : les opérations des lignes 42-44 sont susceptibles de faire des opérations [select] sur les onglets ce qui va appeler le gestionnaire [onTabSelected]. Si on ne fait rien, il y aura alors navigation vers un fragment. On évite cela en mettant le booléen [navigationOnTabSelectionNeeded] à faux dans la session. Ce booléen est automatiquement remis à vrai par la classe [AbstractFragment] lorsqu'un fragment devient visible ;
  • ligne 46 : on note le n° du fragment à afficher dans la session ;
  • lignes 48-50 : on sélectionne l'onglet n° 2 avec navigation (ligne 48). Cela va déclencher la procédure [onTabSelected] qui va :
    • faire afficher le fragment dont le n° a été mis en session ;
    • mémoriser en session le n° de l'onglet sélectionné ;

2.8.4.3. Le fragment [Vue1Fragment]

Nous donnons ici la version finale du fragment :


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {

  // les éléments de l'interface visuelle
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // gestionnaire d'évt
  @Click(R.id.buttonValider)
  protected void doValider() {
    // on affiche le nom saisi
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // cycle de vie du fragment -----------------------------------------------
  private void initFragment() {
    // rien à faire
  }

  // sauvegarde état fragment
  @Override
  public CoreState saveFragment() {
    // état de la vue - rien à sauvegarder
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // rien à faire
  }

  @Override
  protected void initView(CoreState previousState) {
    // 1ère visite ?
    if (previousState == null) {
      // on affiche le n° de la visite
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // on affiche le n° de la visite
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // méthodes privées -------------------------------------
  // affichage n° de visite
  private void showNumVisit() {
    // incrément n° de visite
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // on affiche le n° de la visite
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

La classe est quasi vide.

  • lignes 35-39 : appelées par la classe parent lorsque le fragment doit sauver son état. Le fragment [Vue1Fragment] n'a pas d'état à sauver. On rend simplement une instance de la classe de base [CoreState] (rappel : on ne doit pas rendre null) ;
  • lignes 41-44 : doivent rendre le n° du fragment. Le fragment [Vue1Fragment] a par construction le n° [FRAGMENTS_COUNT-1] ;
  • lignes 51-59 : appelées par la classe parent lorsque le fragment est construit pour la 1ère fois (previousState==null) ou les fois suivantes (previousState!=null) ;
    • lignes 54-57 : si c'est la 1ère visite, on incrémente le n° de visite et on l'affiche (lignes 85-92) ;
  • lignes 61-65 : appelées lorsque le fragment va être affiché associé à une action [SUBMIT]. On incrémente le n° de visite et on l'affiche. Ici, il n'est pas possible que le n° de visite soit incrémenté deux fois dans le cycle de vie. En effet, la 1ère visite au fragment [Vue1Fragment] est faite au démarrage de l'application lorsque l'action vaut [NONE] par construction dans la session. Ceci assure que la méthode [updateOnSubmit] ne va pas être appelée. Ensuite, ce ne sera plus jamais la 1ère visite et la méthode [initView] ne fera rien ;
  • lignes 68-71 : appelées dans un cycle sauvegarde / restauration. Le fragment n'ayant pas d'état, il n'y a ici rien à restaurer ;
  • lignes 73-76 : appelées lorsque toutes les mises à jour précédentes ont été effectuées. Ici, il n'y a rien de plus à faire ;
  • lignes 78-81 : appelées lorsque les tâches asynchrones lancées sont toutes terminées. Ici, il n'y a pas de tâches asynchrones ;

2.8.4.4. L'état [PlaceHolderFragmentState]

L'état du fragment [PlaceHolderFragment] sera le suivant :


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // texte
  private String text;

  // constructeurs
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // getters et setters
 ...
}
  • lorsqu'il faudra sauver l'état du fragment, on sauvera le texte qu'il affichait (ligne 7) ;

2.8.4.5. Le fragment [PlaceHolderFragment]

Le fragment [PlaceHolderFragment] sera le suivant :


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

  // composants de l'interface visuelle
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // data
  private String text;

  // n° de fragment
  private static final String ARG_SECTION_NUMBER = "section_number";

  // implémentation méthodes de la classe parent ----------------------------
  @Override
  public CoreState saveFragment() {
    // on sauvegarde l'état du fragment
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // texte original
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // on met à jour le texte affiché
    // incrément n° de visite
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texte modifié
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // on restaure le texte affiché
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • lignes 30-36 : lorsque la classe parent demande au fragment de sauver son état, on sauve le texte affiché par le fragment (ligne 34) ;
  • lignes 38-41 : rendent le n° du fragment. Celui-ci dépend du n° de section qu'on lui a passé en argument lors de sa création ;
  • lignes 43-47 : appelées lors de la 1ère construction du fragment (previousState==null) ou lors des suivantes (previousState !=null) ;
    • ligne 46 : ici, on n'exploite pas l'état précédent. Le texte initial [text] (ligne 24) affiché lors de la 1ère visite est recalculé à chaque fois. C'est discutable. On aurait pu choisir de mettre également cette information dans l'état du fragment ;
  • lignes 49-51 : appelées lors de la 1ère construction de la vue associée au fragment (previousState==null) ou lors des suivantes (previousState!=null). Il n'y a rien à faire ;
  • lignes 53-56 : appelées lorsque le fragment va être affiché associé à une action [SUBMIT]. C'est toujours le cas sauf pour le cycle sauvegarde / restauration où l'action est [RESTORE]. On incrémente donc le n° de la visite et on l'affiche ;
  • lignes 68-74 : appelées dans un cycle sauvegarde / restauration. On restaure le texte qui avait été sauvegardé dans l'état du fragment ;
  • lignes 76-79 : appelées lorsque toutes les mises à jour précédentes ont été effectuées. Ici, il n'y a rien de plus à faire ;
  • lignes 82-83 : appelées lorsque les tâches asynchrones lancées sont toutes terminées. Ici, il n'y a pas de tâches asynchrones ;

2.8.4.6. Tests

Le lecteur est invité à tester l'application en faisant tourner le périphérique pour vérifier que le fragment affiché ne perd pas son état. On regardera également les logs.

2.9. Conclusion

A l'issue de ce chapitre, nous disposons d'un projet modèle [client-android-skel] de client Android communiquant avec un service web / jSON avec les caractéristiques suivantes :

  • la communication asynchrone avec le serveur web / jSON se fait avec la bibliothèque RxJava ;
  • le cycle de vie d'un fragment (update, save, restore) est géré par sa classe parent [AbstractFragment] qui appelle à des moments précis certaines méthodes de ses classes filles. Le fragment fille n'a ainsi pas à se soucier des étapes du cycle de vie mais seulement d'implémenter certaines méthodes imposées par sa classe parent ;
  • le cycle de vie de l'activité (save / restore) est géré par une classe abstraite [AbstractActivity] qui elle aussi impose à l'activité fille d'implémenter certaines méthodes ;
  • la classe [AbstractActivity] est capable de gérer une application avec ou sans onglets, avec ou sans image d'attente, avec ou sans authentification basique auprès du serveur web / jSON. La présence ou non de ces éléments se fait par configuration ;

Nous allons maintenant présenter une étude de cas plus complexe que les exemples qui ont précédé. La nouvelle application s'appuiera sur le projet modèle [client-android-skel].