Skip to content

2. Estructura básica de un cliente Android que se comunica con un servicio web / jSON

Ahora ofrecemos una estructura básica de una aplicación para Android que se comunica con uno o varios servicios web / jSON. Se trata del proyecto [client-android-skel], que se encuentra en la carpeta [architecture] de los ejemplos:

  

El análisis de esta aplicación base nos dará la oportunidad de repasar algunos puntos que hemos visto en los ejemplos anteriores. Esta aplicación servirá de estructura base para todas las aplicaciones futuras. Se ha desarrollado tras numerosas iteraciones. Su objetivo es factorizar en clases abstractas la mayor cantidad posible de elementos de las aplicaciones que pronto construiremos, para evitar tener que escribir siempre el mismo tipo de código que solo se diferencia en detalles. Sus características son las siguientes:

  • la comunicación asíncrona con el servidor web / jSON se realiza mediante la biblioteca RxJava;
  • el ciclo de vida de un fragmento (actualizar, guardar, restaurar) es administrado por su clase padre [AbstractFragment], la cual invoca, en momentos específicos, ciertos métodos de sus clases hijas. De esta manera, la clase hija no tiene que preocuparse por las etapas del ciclo de vida, sino solo por implementar ciertos métodos impuestos por su clase padre;
  • el ciclo de vida de la actividad (guardar/restaurar) es administrado por una clase abstracta, [AbstractActivity], que a su vez exige que la actividad hija implemente ciertos métodos;
  • la clase [AbstractActivity] es capaz de gestionar una aplicación con o sin pestañas, con o sin imagen de espera, con o sin autenticación básica en el servidor web / jSON. La presencia o ausencia de estos elementos se configura;

Este esqueleto se utilizó para todos los ejemplos siguientes. Debido a la diversidad de estos, lo que funcionaba en un ejemplo podía no funcionar en el siguiente. Como el esqueleto se utilizó para un total de siete ejemplos, se realizaron numerosas iteraciones. Si se utilizara para un octavo ejemplo, es posible que, una vez más, la especificidad de este nuevo ejemplo generara nuevos errores. No obstante, el uso de esta plantilla simplificará considerablemente la redacción de los ejemplos que vendrán a continuación. De hecho, la gestión del ciclo de vida de un fragmento (actualizar, guardar, restaurar), junto con el concepto de adyacencia entre fragmentos, es particularmente compleja. Aquí, está totalmente oculta en la clase [AbstractFragment].

2.1. Arquitectura del cliente de Android

El cliente de Android propuesto se basa en la siguiente arquitectura:

  • la capa [DAO] implementa una interfaz [IDao]. Es esta la que se comunica con el servidor web / jSON;
  • solo hay una actividad que también implementa la interfaz [IDao]. Las vistas se dirigen a ella para acceder al servidor;
  • las vistas se implementan mediante fragmentos;

El proyecto de Android refleja esta arquitectura:

  

Vamos a presentar uno por uno los diferentes elementos de este proyecto.

2.2. La configuración de Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Desde la versión 0.11 del complemento Gradle de Android, debes usar 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'
    }
  }

  // Opciones de empaquetado necesarias para poder generar el 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'
  }
}
  • Todos los números de versión están sujetos a cambios. No obstante, podemos partir de los números actuales si configuramos Android Studio para que estas versiones de las herramientas de Android (líneas 15-16, 47-48) estén bien presentes (véase el párrafo 6.11);

2.3. El manifiesto de la aplicación

 

<?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>
  • línea 3: cambiaremos el paquete de la aplicación;
  • líneas 10, 15: se establecerá el valor del elemento [app_name] en el archivo [res / values / strings.xml]. Por el momento, este es el siguiente:

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

  <!-- nombre de la aplicación -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. La organización del código Java

  
  • [architecture] agrupa los elementos principales de organización del código;
  • [activity] contiene la única actividad de la aplicación;
  • [fragments] agrupa los fragmentos o vistas de la aplicación;
  • [dao] agrupa los elementos de comunicación con el servidor web / jSON;

2.5. Elementos de la actividad

 

Image

2.5.1. La vista asociada a la actividad

La vista [activity_main.xml] asociada a la actividad es la siguiente:


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

  <!-- contenedor de fragmentos -->
  <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>
  • línea 29: se utiliza un contenedor de fragmentos específico;

La actividad también cuenta con un menú [res / menu / menu_main.xml] para su vista:


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

Por el momento, está vacío. El desarrollador lo completará si es necesario.

2.5.2. El contenedor de fragmentos [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 {

  // controla el deslizamiento
  private boolean isSwipeEnabled;
  // controla el desplazamiento
  private boolean isScrollingEnabled;

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

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

  // métodos que se deben redefinir para gestionar el deslizamiento
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // ¿Deslizamiento permitido?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // ¿Se permite el deslizamiento?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // control del desplazamiento
  @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;
  }
}

Esta clase extiende la clase estándar de Android [ViewPager] únicamente para manejar el deslizamiento (línea 11) y el desplazamiento (línea 13) entre vistas.

  • líneas 26-43: los métodos que desactivan el deslizamiento si este ha sido deshabilitado;
  • líneas 46-49: redefinición del método [setCurrentItem] que sirve para cambiar la vista que se muestra. Si se ha desactivado el desplazamiento, el cambio de vista se realizará sin desplazamiento. Cabe señalar que el desarrollador puede anular este modo de funcionamiento utilizando el método [setCurrentItem(int position, boolean smoothScrolling)], que le permite especificar el desplazamiento que desee;

2.5.3. La clase [CoreState]

  

La clase [CoreState] es la clase padre de los estados de los distintos fragmentos:


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)
// Tareas pendientes: agregar aquí las subclases de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragmento visitado o no
  protected boolean hasBeenVisited = false;
  // estado del posible menú del fragmento
  protected MenuItemState[] menuOptionsState;

  // getters y setters
...
}
  • línea 16: cada fragmento tiene en su estado un valor booleano [hasBeenVisited] que indica si ya ha sido visitado o no. Esto es necesario porque, a veces, al mostrar un fragmento por primera vez, hay acciones específicas que deben realizarse;
  • línea 18: el proyecto [client-android-skel] guarda y restaura automáticamente los menús de los fragmentos, si es que tienen alguno. En la tabla MenuItemState[] menuOptionsState, se almacena el estado de visibilidad de todas las opciones del menú;
  • líneas 10-13: tal como se hizo en [Exemple-22], el estado de la actividad y de sus fragmentos se guardará en la sesión, la cual, a su vez, se guardará en forma de una cadena jSON. Veremos que la sesión almacena un array de elementos de tipo [CoreState]. Si no hacemos nada, se guardará la cadena jSON de tipo [CoreState]. Sin embargo, queremos guardar los estados de los fragmentos, es decir, los estados derivados de [CoreState]. Para que se genere la cadena jSON del tipo derivado y no la del tipo padre, es necesario declarar los tipos derivados tal como se indica en las líneas 10 a 13. La clase [CoreState] es una de las clases de la arquitectura que el desarrollador debe modificar para cada nueva aplicación (líneas 10 a 13);

2.5.4. La interfaz [IMainActivity]

  

La interfaz [IMainActivity] define lo que los fragmentos pueden solicitar a la actividad en la siguiente arquitectura:

Image


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // acceso a la sesión
  ISession getSession();

  // cambio de vista
  void navigateToView(int position, ISession.Action action);

  // gestión de la espera
  void beginWaiting();

  void cancelWaiting();

  // constantes de la aplicación (a modificar) -------------------------------------

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;

  // tiempo máximo de espera para la respuesta del servidor
  int TIMEOUT = 1000;

  // tiempo de espera antes de ejecutar la solicitud del cliente
  int DELAY = 0;

  // autenticación básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de pestañas
  boolean ARE_TABS_NEEDED = false;

  // imagen de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos de la aplicación
  int FRAGMENTS_COUNT = 0;

  // tareas pendientes: agregue aquí sus constantes y otros métodos
}
  • línea 6: la interfaz [IMainActivity] extiende la interfaz [IDao] de la capa [DAO];
  • línea 9: esta es la actividad que da acceso a la sesión en forma de una instancia de la interfaz [ISession];
  • línea 12: es a través de esta actividad que se cambia de vista. El segundo parámetro es la acción que provoca este cambio de vista, uno de los valores SUBMIT, NAVIGATION o RESTORE;
  • líneas 15-17: esta es la actividad que maneja la imagen de espera;
  • línea 22: para la depuración de la aplicación;
  • línea 25: para no esperar demasiado tiempo si el servidor deja de responder;
  • línea 28: durante la depuración, se establecerá un valor de unos segundos para tener tiempo de cancelar la operación con el servidor y ver qué sucede;
  • línea 31: a true si el servicio jSON solicita una autenticación básica;
  • línea 34: adyacencia de fragmentos;
  • línea 37: a vrai si la aplicación tiene pestañas;
  • línea 39: a vrai si la aplicación se comunica con un servidor web / jSON y se desea mostrar una imagen de espera durante los intercambios;
  • línea 43: el número de fragmentos que maneja la aplicación;

La interfaz [IMainActivity] es el segundo elemento de la arquitectura que el desarrollador debe completar (línea 45).

2.5.5. La interfaz [IDao]

La interfaz [IMainActivity] extiende la siguiente interfaz [IDao]:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servicio web
  void setUrlServiceWebJson(String url);

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

  // tiempo de espera del cliente
  void setTimeout(int timeout);

  // autenticación básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);

  // tiempo de espera del cliente en milisegundos antes de la solicitud
  void setDelay(int delay);

  // tarea pendiente: declare su interfaz aquí
}
  • línea 24: el desarrollador completará la interfaz aquí;

2.5.6. La sesión

  

La clase [Session] encapsula los elementos compartidos por la actividad y los fragmentos. Implementa la siguiente interfaz [ISession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // número de la última vista mostrada
  int getPreviousView();

  void setPreviousView(int numView);

  // último estado de una vista
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // acción en curso
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // estados de todas las vistas -
  // no se utiliza en el código, pero es necesario para la serialización/deserialización jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // N.º de la última pestaña seleccionada
  int getPreviousTab();

  void setPreviousTab(int position);

  // navegación al seleccionar una pestaña
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

Introducimos la interfaz [ISession] para exigir la presencia de ciertos métodos en la sesión:

  • líneas 7-10: el número de la última vista (fragmento) mostrada;
  • líneas 12-15: el estado de una vista en particular;
  • líneas 17-24: introducimos el concepto de acción en curso. Hay cuatro (línea 17):
    • RESTORE: se está realizando una copia de seguridad o restauración. No hay cambio de vista;
    • NAVIGATION: se está realizando una navegación. Denominaremos aquí «navegación» a un cambio de vista en el que la nueva vista se puede restaurar a partir de su último estado guardado en la sesión;
    • SUBMIT: se asignará el tipo [SUBMIT] a una acción en curso cuando haya un cambio de vista y la nueva vista dependa del estado de la actividad en general y no únicamente de su propio estado. A veces, es difícil distinguir entre NAVIGATION y SUBMIT. En ese caso, se tomará el caso más general, el de SUBMIT;
    • NONE: valor de la acción cuando esta aún no ha recibido su primer valor;
  • líneas 26-30: los estados de la actividad y de los fragmentos se almacenarán en un array de tipo CoreState[]. Para que este se maneje correctamente durante las serializaciones y deserializaciones de jSON, debe contar con un getter y un setter;
  • líneas 32-35: número de la última pestaña seleccionada. Se utiliza durante el ciclo de guardado/restauración para volver a seleccionar la pestaña que estaba seleccionada antes de girar el dispositivo;
  • líneas 37-40: gestión de un valor booleano que indica si la selección de una pestaña debe ir acompañada de un cambio de fragmento;

La interfaz [ISession] está implementada por la siguiente clase abstracta [AbstractSession]:


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 vista anterior
  private int preViousView;

  // estado de las vistas
  private CoreState[] coreStates = new CoreState[0];

  // acción en curso
  private Action action = Action.NONE;

  // pestaña seleccionada anteriormente
  private int previousTab;

  // navegación al seleccionar una pestaña
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // constructor
  public AbstractSession() {
    // se inicializa la tabla de estados de los fragmentos
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // interfaz 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;
  }
}
  • línea 9: el número de la vista que se mostraba antes de la que se muestra actualmente. Esta información es útil cuando se puede acceder a una vista desde varios lugares. Este suele ser el caso en la navegación por pestañas. De esta manera, la vista mostrada puede saber cuál era la vista anterior;
  • línea 12: la tabla de estados de todos los fragmentos mostrados por la actividad;
  • línea 18: el número de la pestaña seleccionada anteriormente. Desempeña un papel similar al del número de la vista anterior de la línea 9. Esta información es útil cuando se produce una rotación del dispositivo y es necesario volver a la pestaña que estaba seleccionada antes de la rotación;
  • línea 22: un valor booleano que indica si la selección de una pestaña debe ir acompañada de un cambio en el fragmento mostrado. Es importante saber que el proyecto [client-android-skel] gestiona por separado las pestañas y los fragmentos para poder utilizarse en casos en los que el número de pestañas es menor que el número de fragmentos. Existen dos tipos de selección:
    • una selección por parte del usuario al hacer clic en una pestaña. En este caso, por lo general, el fragmento que se muestra debe cambiar;
    • una selección por parte del software mediante el método [Tablayout.Tab.select()]. En este caso, no siempre es deseable que cambie el fragmento que se muestra. A continuación, dos ejemplos:
      • al girar el dispositivo, se vuelve a crear la actividad y también las pestañas. Sin embargo, cuando se crea la primera pestaña, esta se somete automáticamente a una operación de software [select]. Por lo tanto, no es conveniente cambiar el fragmento que se muestra, ya que nos encontramos en una fase de recreación de la actividad en la que el fragmento que finalmente se mostrará no será necesariamente el asociado a la primera pestaña;
      • dado que la gestión de las pestañas está separada de la de los fragmentos, es posible que se desee actualizar las pestañas (eliminar, agregar) sin interferir con sus fragmentos asociados. Sin embargo, algunas de estas operaciones pueden, una vez más, desencadenar una operación de software implícita [select] en una de las pestañas. Esta selección no debe traducirse necesariamente en una navegación hacia el fragmento asociado;
  • línea 21: el campo [navigationOnTabSelectionNeeded] no está destinado a guardarse durante las operaciones de guardado de la actividad y sus fragmentos. La anotación [@JsonIgnore] hace que el campo se ignore durante las serializaciones y deserializaciones jSON;
  • líneas 25-31: el constructor inicializa la matriz de estados de los fragmentos [FRAGMENTS_COUNT] de la aplicación. Los elementos de esta matriz se inicializan con el campo [hasBeeenVisited=false]. Esta información se utiliza para determinar si se trata o no de la primera visita al fragmento;

La clase [Session] es la siguiente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // datos que se deben compartir entre los propios fragmentos y entre los fragmentos y la actividad
  // los elementos que no se pueden serializar en jSON deben tener la anotación @JsonIgnore
  // no olvide los getters y setters necesarios para la serialización/deserialización en jSON
}
  • línea 5: la clase [Session] extiende la clase [AbstractSession] que acabamos de ver. El desarrollador colocará allí los elementos que se compartirán entre los propios fragmentos y entre los fragmentos y la actividad. Cabe señalar que la clase [Session] ya no está anotada con la anotación AA [@EBean]. Se ha convertido en una clase normal;

2.5.7. La clase abstracta [AbstractActivity]

  

2.5.7.1. Squelette

La clase [AbstractActivity] tiene más de 300 líneas. La analizaremos por etapas. Su estructura básica es la siguiente:


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 {
  // capa [DAO]
  private IDao dao;
  // la sesión
  protected Session session;

  // el contenedor de fragmentos
  protected MyPager mViewPager;
  // la barra de herramientas
  private Toolbar toolbar;
  // la imagen de espera
  private ProgressBar loadingPanel;
  // barra de pestañas
  protected TabLayout tabLayout;

  // el administrador de fragmentos o secciones
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // nombre de la clase
  protected String className;
  // mapeador jSON
  private ObjectMapper jsonMapper;

  // constructor
  public AbstractActivity() {
    // nombre de la clase
    className = getClass().getSimpleName();
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // implementación IMainActivity --------------------------------------------------------------------
  ...

  // ciclo de vida - copia de seguridad / restauración de la actividad ------------------------------------
  ...

  // gestión de la imagen de espera ---------------------------------
  ...

  // interfaz IDao -----------------------------------------------------
  ...

  // el administrador de fragmentos --------------------------------
  ...

  // clases hijas
  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 clase [AbstractActivity]:

  • implementa la interfaz [IMainActivity] (líneas 21, 55);
  • gestiona el guardado y la restauración de la actividad y sus fragmentos al girar el dispositivo (línea 58);
  • gestiona la imagen de espera durante un intercambio con el servidor web / jSON (línea 61);
  • implementa la interfaz IDao de la capa [DAO] (línea 64);
  • implementa el administrador de fragmentos (línea 67);
  • exige que sus clases hijas cuenten con seis métodos (líneas 71-81);

2.5.7.2. Implementar la interfaz [IMainActivity]

La implementación de la interfaz [IMainActivity] (véase el párrafo 2.5.4) es la siguiente:


  // Implementación 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));
    }
    // visualización de un nuevo fragmento
    mViewPager.setCurrentItem(position);
    // se registra la acción en curso durante este cambio de vista
    session.setAction(action);
}

2.5.7.3. Guardar el estado de la actividad y sus fragmentos

El estado de la actividad y de sus fragmentos se encuentra íntegramente en la sesión. Por lo tanto, se trata de guardar dicha sesión. Aquí retomamos lo que se hizo en el proyecto [Exemple-22] (véase el párrafo 1.23):


  // gestión de guardado/restauración de la actividad ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // padre
    super.onSaveInstanceState(outState);
    // guardar sesión en forma de cadena jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // registro
    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. Restauración del estado de la actividad y sus fragmentos

Se trata de restaurar la sesión. Procedemos tal como se mostró en [Exemple-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // padre
    super.onCreate(savedInstanceState);
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // ¿algo que restaurar?
    if (savedInstanceState != null) {
      // recuperación de sesión
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // registro
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // sesión
      session = new Session();
    }
...
  • líneas 10-26: si el parámetro [Bundle savedInstanceState] de la línea 2 no es null, entonces se restaura la sesión (líneas 12-17);
  • líneas 26-29: el caso en que el parámetro [Bundle savedInstanceState] de la línea 2 sea null corresponde al primer inicio de la actividad. En ese caso, se crea una sesión vacía;

2.5.7.5. Inicialización de la capa [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // padre
    super.onCreate(savedInstanceState);
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // capa [DAO]
    dao = getDao();
    if (dao != null) {
      // configuración de la capa [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // clases hijas
  protected abstract IDao getDao();
....
}
  • línea 11: se solicita una referencia a la capa [DAO] a la actividad hija (línea 21);
  • líneas 14-17: si existe la capa [DAO], se configura a partir de la información contenida en la interfaz [IMainActivity];

2.5.7.6. Inicialización de la vista asociada a la actividad

La vista asociada a la actividad se presentó en el párrafo 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>

  <!-- contenedor de fragmentos -->
  <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>

Esta vista se inicializa con el siguiente código:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // padre
    super.onCreate(savedInstanceState);
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // vista asociada
    setContentView(R.layout.activity_main);
    // componentes de la vista ---------------------
    // barra de herramientas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // ¿Imagen de espera?
    if (IS_WAITING_ICON_NEEDED) {
      // se agrega la imagen de espera
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // creación de ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // se agrega ProgressBar a la barra de herramientas
      toolbar.addView(loadingPanel);
    }
...
  • línea 11: la vista XML [activity_main] está asociada a la actividad;
  • líneas 14-15: la barra de herramientas está integrada y es compatible;
  • líneas 17-27: posible adición de una imagen de espera: si el valor booleano [IS_WAITING_ICON_NEEDED] es verdadero en la interfaz [IMainActivity];
  • línea 23: creación de la imagen de espera de tipo [ProgressBar], a la que hace referencia el campo [loadingPanel];
  • línea 24: inicialmente, esta imagen está oculta;
  • línea 26: se agrega a la barra de herramientas;

2.5.7.7. Administración de pestañas

La interfaz [IMainActivity] puede requerir una barra de pestañas. Esta se agrega y se administra de la siguiente manera:


// barra de pestañas
  protected TabLayout tabLayout;
...

    // ¿Barra de pestañas?
    if (ARE_TABS_NEEDED) {
      // se agrega la barra de pestañas
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // sin navegación por selección hasta que se muestre un fragmento
      session.setNavigationOnTabSelectionNeeded(false);
      // creación de la barra de pestañas
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // Agregar la barra de pestañas a la barra de aplicaciones
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // gestor de eventos de la barra de pestañas
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // se ha seleccionado una pestaña
          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()) {
            // posición de la pestaña
            int position = tab.getPosition();
            // memoria
            session.setPreviousTab(position);
            // ¿Mostrar fragmento asociado?
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  // clases hijas
  protected abstract void navigateOnTabSelected(int position);
...
  • líneas 12-48: adición y gestión de una barra de pestañas;
  • línea 6: la barra de pestañas se agrega si la constante [ARE_TABS_NEEDED] está establecida en vrai en la interfaz [IMainActivity];
  • línea 12: al crear la barra de pestañas, pueden ocurrir operaciones [Tablayout.Tab.select] implícitas (no son provocadas por el usuario). Se establece el valor booleano [session.navigationOnTabSelectionNeeded] en faux para evitar cualquier navegación durante estas selecciones falsas. Será el desarrollador quien seleccione el fragmento que se mostrará con el método [navigateToView]. La variable booleana [session.navigationOnTabSelectionNeeded] se restablecerá a vrai cuando se muestre este fragmento (véase la clase AbstractFragment);
  • línea 14: creación de una barra de pestañas referenciada por el campo [tabLayout]. Utilizamos una barra de pestañas personalizada [CustomTabLayout], a la que volveremos más adelante;
  • línea 15: establecemos los colores de los títulos de las pestañas. Estos se encuentran en el siguiente archivo [res / color / tab_txt.xml]:

<?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>
    • línea (c): el color del título de la pestaña cuando está seleccionada;
    • línea (d): el color del título de la pestaña cuando no está seleccionada;

Por supuesto, este archivo se puede modificar. Los códigos hexadecimales de los colores se pueden encontrar, por ejemplo, aquí.

  • líneas 17-18: agregación de esta barra de pestañas a la barra de aplicaciones presente en la vista XML [activity_main];
  • líneas 20-47: gestor de eventos de la barra de pestañas;
  • líneas 22-36: solo se maneja el evento [onTabSelected]. Corresponde a un clic en la pestaña [Tab tab] pasada como parámetro al método o bien a una operación de software [TabLayout.Tab.select];
  • línea 30: posición de la pestaña seleccionada;
  • línea 32: esta posición se guarda en la sesión;
  • línea 34: ahora se trata de mostrar el fragmento asociado a esta pestaña. Solo la clase hija (línea 52) puede realizar esta asociación. Cabe señalar que no se asocia la barra de pestañas con el contenedor de fragmentos [mViewPager], como se hizo en algunos ejemplos analizados. Aquí se separa por completo la gestión de la barra de pestañas de la de los fragmentos. Por eso, cuando se hace clic en una pestaña, es necesario indicar qué vista se desea que se muestre;
  • línea 28: se distingue entre la selección de una pestaña con o sin navegación. En general, cuando el usuario hace clic en una pestaña, se desea que haya navegación, mientras que en una selección por parte del software no se desea. Es el desarrollador quien distingue estos dos casos con el elemento [session.navigationOnTabSelectionNeeded]. Cuando no se realiza la navegación, el número de la última pestaña seleccionada no se guarda en la sesión. Será el desarrollador quien deba hacerlo;

2.5.7.8. El administrador de pestañas [CustomTabLayout]

  

Utilizamos un administrador de pestañas personalizado para poder mostrar el título de las pestañas con diferentes tipos de letra. La clase [CustomTabLayout] es la siguiente:


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 personalización de la fuente de los títulos de las pestañas se realiza en las líneas 30 y 44;

El archivo [fonts] es el siguiente:

  

Fuentes:

  • el código de la clase [CustomTabLayout] se encontró en URL y [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
  • las fuentes se encontraron en URL y [https://www.fontsquirrel.com/fonts/roboto];

2.5.7.9. Últimas inicializaciones


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // clase padre
    super.onCreate(savedInstanceState);
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instanciación del administrador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // se deshabilita el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);
    // adyacencia de los fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // se muestra la primera vista
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // se pasa el control a la actividad hija
    onCreateActivity();
  }
...
  // clases hijas
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • líneas 10-19: aquí encontramos código que aparece con frecuencia en los ejemplos estudiados;
  • líneas 21-23: visualización de la primera vista. Sin duda, hay varias formas de distinguir este caso. Aquí hemos aprovechado el hecho de que, para la primera vista, el valor de la acción que provoca el cambio de vista es NONE;
  • línea 22: no hacemos ninguna suposición sobre el primer fragmento que se debe mostrar. En nuestros ejemplos, a menudo ha sido el fragmento n.º 0, pero no siempre (véase el Ejemplo 22). Por lo tanto, le pediremos a la actividad hija (línea 30) que nos indique cuál es esa primera vista;
  • línea 25: aquí hemos factorizado todo lo que se podía. Ahora, la clase hija tiene que realizar sus propias inicializaciones (línea 29);

2.5.7.10. Manejo de la imagen de espera

En la clase [AbstractActivity], la imagen de espera se gestiona mediante los dos métodos siguientes:


  // gestión de la imagen de espera ---------------------------------
  public void cancelWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.INVISIBLE);
    }
  }

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

2.5.7.11. Implementación de la interfaz [IDao]

En la clase [AbstractActivity], la interfaz [IDao] (véase el párrafo 2.5.5) se implementa de la siguiente manera:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // capa [DAO]
  private IDao dao;
...
  // interfaz 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);
}
  • línea 3: cabe recordar que el valor de este campo fue proporcionado por la actividad hija en el método [onCreate];

2.5.7.12. Implementación del administrador de fragmentos

En la clase [AbstractActivity], el administrador de fragmentos se implementa de la siguiente manera:


...
  // el administrador de fragmentos --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // fragmentos de la clase hija
      fragments = getFragments();
    }

    // debe generar el fragmento n.º posición
    @Override
    public AbstractFragment getItem(int position) {
      // se muestra el fragmento
      return fragments[position];
    }

    // muestra el número de fragmentos que se deben gestionar
    @Override
    public int getCount() {
      return fragments.length;
    }

    // muestra el título del fragmento en la posición n.º
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // clases hijas
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • línea 5: la matriz de fragmentos asociados a la actividad. Todos los fragmentos se derivarán de la clase [AbstractFragment];
  • líneas 8-12: este es el constructor que inicializa la matriz de fragmentos. Solicita estos fragmentos a la clase hija de la actividad (línea 35);
  • líneas 28-31: los títulos de los fragmentos se pueden utilizar en una aplicación donde haya tantas pestañas como fragmentos. En este caso, se le puede asignar a la pestaña el título del fragmento. Aquí, estos títulos se solicitan a la clase hija (línea 37);

2.5.7.13. El método [onResume]

El método [onResume] se ejecuta un poco antes de que la vista asociada a la actividad se vuelva visible. Aquí se utiliza para seleccionar una pestaña después de guardar o restaurar:


  @Override
  public void onResume() {
    // clase principal
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // si se restaura, entonces hay que restaurar la última pestaña seleccionada
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • línea 10: selección de la pestaña que estaba seleccionada antes del proceso de guardado/restauración. Hay que recordar aquí que en el método [onCreate], el cual, en el ciclo de vida de la actividad, se ejecuta antes del método [onResume], se ha desactivado la navegación al seleccionar una pestaña. Por lo tanto, aquí se selecciona una pestaña, pero no hay cambio de fragmento;

2.5.7.14. Résumé

La clase abstracta [AbstractActivity] será la clase padre de la única actividad de la aplicación.

La actividad hija deberá implementar los siguientes seis métodos:


  // clases hijas
  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();

Además, la actividad hija tiene acceso a los siguientes elementos protegidos de su clase padre:


  // la sesión
  protected ISession session;
  // el contenedor de fragmentos
  protected MyPager mViewPager;
  // barra de pestañas
  protected CustomTabLayout tabLayout;
  // nombre de la clase
protected String className;

2.5.8. La actividad [MainActivity]

  

La clase [MainActivity] puede tener un nombre diferente. Su única restricción es implementar la interfaz [IMainActivity]. La clase básica proporcionada es la siguiente:


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 {

  // capa [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sesión
  private Session session;

  // métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sesión
    this.session = (Session) super.session;
    // tareas pendientes: continuamos con las inicializaciones iniciadas por la clase padre
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // por hacer: definir los fragmentos aquí
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // por hacer: definir aquí los títulos de los fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // tareas pendientes: navegación por pestañas — definir la vista que se mostrará
  }

  @Override
  protected int getFirstView() {
    // tarea pendiente: navegación por pestañas — definir la primera vista que se mostrará
    return 0;
  }
}
  • línea 14: para que se entienda la notación AA [@Bean] de la línea 19, la actividad debe tener la notación AA [@EActivity];
  • línea 15: la actividad está asociada al menú XML [menu_main]. Actualmente, este menú está vacío. El desarrollador deberá completarlo si lo necesita;
  • línea 16: la clase extiende la clase [AbstractActivity];
  • líneas 19-20: una referencia a la capa [DAO]. Esta será instanciada por la biblioteca AA antes de que se inicialice este campo. Esto implica que el bean AA [Dao] debe existir. Esto siempre es así con la aplicación de estructura básica que entregamos. Incluso en una aplicación sin la capa [DAO], se puede dejar que el paquete [dao] exista. Esto no genera complicaciones;
  • línea 22: la sesión como instancia del tipo [Session]. La sesión existe en la clase padre [AbstractActivity], pero como instancia de la interfaz [ISession] (línea 32);
  • líneas 24-63: los seis métodos impuestos por la clase padre [AbstractActivity];
  • líneas 36-39: el método [getDao] devuelve una referencia a la capa [DAO]. Aquí, esta referencia nunca es null. Sin embargo, en la clase padre [AbstractActivity], se ha previsto el caso en que la clase hija devuelva una referencia null para indicar que no existe la capa [DAO]. Si se desea aprovechar esta posibilidad (que, en mi opinión, no es muy útil), es aquí donde se debe devolver el puntero null;

2.6. La capa [DAO]

Image

  

2.6.1. La interfaz IDao

Se presentó en el párrafo 2.5.5:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servicio web
  void setUrlServiceWebJson(String url);

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

  // tiempo de espera del cliente
  void setTimeout(int timeout);

  // autenticación básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);

  // tiempo de espera del cliente en milisegundos antes de la solicitud
  void setDelay(int delay);

  // Tarea pendiente: declare su interfaz aquí
}

El desarrollador agregará los métodos de su capa [DAO] a partir de la línea 24.

2.6.2. La interfaz [WebClient]

  

La interfaz [WebClient] es la siguiente:


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);

  // Tarea pendiente: declare aquí los URL que se deben alcanzar
}

El desarrollador agregará los métodos que se comunican con los URL expuestos por el servidor jSON a partir de la línea 17.

2.6.3. El interceptor de autenticación [MyAuthInterceptor]

  

La clase [MyAuthInterceptor] es la siguiente:


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 {

  // usuario
  private String user;
  // contraseña
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // encabezados HTTP de la solicitud HTTP interceptada
    HttpHeaders headers = request.getHeaders();
    // el encabezado HTTP de autenticación básica
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // adición a los encabezados HTTP
    headers.setAuthorization(auth);
    // se continúa con el ciclo de vida de la solicitud HTTP
    return execution.execute(request, body);
  }

  // elementos de la autenticación
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Esta clase genera el siguiente encabezado de autenticación HTTP:

Authorization: Basic code

donde [code] es el código Base64 de la cadena «user:mp». Esta clase solo se utiliza si el servidor jSON espera este tipo de autenticación. Existen otras.

Nota: el uso de esta clase se ilustra en el párrafo 3.6.3.1.

2.6.4. La clase [AbstractDao]

  

La clase [AbstractDao] es la siguiente:


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 {

  // mapeador jSON
  private ObjectMapper mapper = new ObjectMapper();
  // modo de depuración
  protected boolean isDebugEnabled;
  // nombre de la clase
  protected String className;
  // tiempo de espera antes de ejecutar la solicitud
  private int delay;

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

  // métodos protegidos ----------------------------------------------------------
  // interfaz genérica
  protected interface IRequest<T> {
    T getResponse();
  }

  // solicitud genérica a un servicio web / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // ejecución del servicio: se espera una única respuesta
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // ejecución del servicio
        try {
          // ¿En espera?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // se está ejecutando la consulta sincrónica
          T response = request.getResponse();
          // registro
          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()));
          }
          // se envía la respuesta al observador
          subscriber.onNext(response);
          // se notifica el fin del observable
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // registro
          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"));
            }
          }
          // se lanza una excepción
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // modo de depuración
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • líneas 35-81: el método [getResponse] utiliza la biblioteca RxAndroid para representar un tipo [Observable<T>]. A diferencia de algunos ejemplos vistos anteriormente, no se genera un tipo [Response<T>], que es un tipo propio, sino cualquier tipo T;
  • línea 35: el método [getResponse] recibe como parámetro una instancia del tipo [IRequest<T>] de las líneas 30-32, cuyo método [IRequest.getReponse()] obtiene el tipo T mediante una operación HTTP síncrona;
  • líneas 48-50: de manera artificial se espera [delay] milisegundos. En producción se establecerá [delay=0]. En la fase de depuración se establecerá [delay=qqs secondes] para darle al usuario la oportunidad de cancelar la operación asíncrona y así ver cómo se comporta el código en ese caso;
  • línea 52: la respuesta esperada se solicita mediante una consulta sincrónica;
  • línea 64: una vez recibida la respuesta, se pasa al observador;
  • línea 66: se indica que ya no habrá más emisiones. Aquí nos encontramos en el caso particular de una acción asíncrona que solo devuelve un elemento;
  • líneas 67-78: en caso de excepción, se lanza la excepción al observador (línea 77);

2.6.5. La clase [Dao]

  

La clase [Dao] es la siguiente:


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 {

  // cliente del servicio web
  @RestService
  protected WebClient webClient;
  // seguridad
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // el RestTemplate
  private RestTemplate restTemplate;
  // fábrica de RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // se construye el restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // se instala el convertidor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // se configura el restTemplate del cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // se establece el URL del servicio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // se registra al usuario en el interceptor
    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));
    }
    // configuración de fábrica
    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));
    }
    // ¿Interceptor de autenticación?
    if (isBasicAuthentificationNeeded) {
      // se agrega el interceptor de autenticación
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

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

  // tareas pendientes: implementación IDao
}
  • líneas 21-22: inyección del bean AA [WebClient], que se encargará de las comunicaciones con el servidor web / jSON;
  • líneas 24-25: inyección del interceptor de autenticación;
  • líneas 31-42: método que se ejecuta después de la inserción de los campos de las líneas 21-25;
  • línea 37: el objeto [RestTemplate], que se encarga de las comunicaciones entre el cliente y el servidor, se crea a partir de un factory. No es imprescindible, pero es a través del factory que se pueden configurar los tiempos de espera de las comunicaciones. Por eso no utilizamos el constructor sin parámetros [RestTemplate()];
  • línea 39: se agrega un convertidor jSON a los convertidores del [RestTemplate]. Este será el único convertidor. Además, cuando un método del cliente [WebClient] reciba una cadena jSON del servidor, esta se deserializará automáticamente en el objeto que el método debe devolver;
  • línea 41: el objeto [RestTemplate], así configurado, se pasa al cliente web, quien se encargará de los intercambios entre el cliente y el servidor gracias a él;
  • líneas 44-48: se establece el URL raíz del servidor web / jSON. Todos los URL declarados en la clase [WebClient] son URL relacionados con este URL raíz;
  • líneas 50-54: este método permite especificar el propietario de la conexión cuando esta está controlada por una autorización de tipo básico (véase el párrafo 2.6.3);
  • líneas 56-64: establecen los timeouts de los intercambios entre el cliente y el servidor. Esto se realiza a través del factory del objeto [RestTemplate] que rige los intercambios;
  • líneas 66-78: este método permite indicar que el servidor está protegido por una autenticación de tipo básico;
  • líneas 72-77: si se solicita una autenticación de tipo básico, el interceptor de autenticación inyectado en la línea 25 se agrega a los interceptores del objeto [RestTemplate]. Este interceptor agregará automáticamente a todas las solicitudes del cliente web la línea HTTP de autenticación básica que espera el servidor;
  • el desarrollador implementará la interfaz [IDao] a partir de la línea 87;

2.7. Los fragmentos

  

2.7.1. La clase [MenuItemState]

La clase [MenuItemState] encapsula el estado de una opción de menú:


package client.android.architecture;

public class MenuItemState {

  // identificador de la opción de menú
  private int menuItemId;
  // visibilidad de la opción
  private boolean isVisible;

  // constructores
  public MenuItemState() {

  }

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

  // getters y setters
...
}

2.7.2. La clase [Utils]

La clase [Utils] reúne métodos estáticos de utilidad:


package client.android.architecture;

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

public class Utils {

  // lista de mensajes de una excepción - versión 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // se crea una lista con los mensajes de error de la pila de excepciones
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // lista de mensajes de una excepción - versión 2
  static public String getMessageForAlert(Throwable th) {
    // Se construye el texto que se mostrará
    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--;
    }
    // resultado
    return texte.toString();
  }

  // lista de mensajes de una excepción - versión 3
  static public String getMessageForAlert(List<String> messages) {
    // Se genera el texto que se mostrará
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // resultado
    return texte.toString();
  }
}

2.7.3. La clase padre [AbstractFragment]

La clase [AbstractFragment] reúne lo que es común a todos los fragmentos de la aplicación. Al igual que en la clase [AbstractActivity], su código es complejo. También en este caso lo analizaremos por etapas.

2.7.3.1. El esqueleto


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 {

  // datos privados ------------------------------------------------------------
  // suscripciones a observables
  private List<Subscription> abonnements = new ArrayList<>();
  // menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estado del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // tareas asincrónicas
  private boolean runningTasksHaveBeenCanceled;

  // datos  accesibles a las clases hijas ---------------------------------------
  // modo de depuración
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nombre de la clase
  protected String className;
  // tareas asincrónicas
  protected int numberOfRunningTasks;
  // actividad
  protected IMainActivity mainActivity;
  protected Activity activity;
  // sesión
  protected Session session;


  // Actualización de fragmento ----------------------------------------------------------------------------------
 ...

  // gestión del menú ------------------------------------------
  ...

  // gestión de la espera -------------------------------------------------------------
...

  // gestión de operaciones asincrónicas --------------------------------------------------------------------
...

  // gestión de excepciones -------------------------------------------------------------------
....

  // gestión del ciclo de vida del fragmento --------------------------------------------------------
...

  // clases hijas -----------------------------------------------------
  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);

}
  • líneas 28-45: los datos privados de la clase;
  • líneas 47-58: los datos protegidos a los que pueden acceder las clases hijas;
  • líneas 61-62: código que actualiza el fragmento que se va a mostrar;
  • líneas 64-65: código de utilidad para manejar el posible menú;
  • líneas 67-68: código de utilidad para gestionar la espera durante una operación asíncrona;
  • líneas 70-71: código para facilitar la comunicación del fragmento con la capa [DAO];
  • líneas 73-74: código de utilidad para manejar cualquier excepción de manera estándar;
  • líneas 76-77: código que administra el ciclo de vida del fragmento;
  • líneas 80-94: la clase padre impone 8 métodos a sus clases hijas;

2.7.3.2. El constructor

El constructor de la clase es el siguiente:


  // nombre de la clase
  protected String className;
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
...
  // constructor ----------------------
  public AbstractFragment() {
    // inicialización
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // registro
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • línea 9: se indica el nombre de la clase hija que se instancia aquí. Este nombre se utiliza en todos los registros de la clase padre;
  • línea 10: se indica que el fragmento está en proceso de construcción. Esta información se utilizará cuando se le pida al fragmento hijo que se actualice;

2.7.3.3. Gestión del menú

En nuestra arquitectura, todo fragmento debe tener un menú, aunque esté vacío. De hecho, los registros han demostrado que cuando se ejecuta el método [onCreateOptionsMenu] —que se ejecuta cuando el fragmento tiene un menú—, el fragmento ya ha sido asociado a su actividad, su vista y su menú, y se volverá visible. Por lo tanto, este es el momento en el que se puede realizar la actualización de la interfaz visual y del menú. Es en este método [onCreateOptionsMenu] donde le pedimos al fragmento hijo que se actualice.

La gestión del menú incluye métodos de utilidad que permiten al fragmento hijo mostrar o no los elementos del menú:


  // menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // gestión del menú ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // recorre todos los elementos del menú
    for (int i = 0; i < menu.size(); i++) {
      // elemento n.º i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // si el elemento n.º i es un submenú, entonces se vuelve a empezar
      if (menuItem.hasSubMenu()) {
        // recursividad
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // resultado
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // se recuperan los identificadores de las opciones del menú
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // se transfieren las opciones del menú a una tabla
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identificador de opción
      int id = menuOptionsIds.get(i);
      // estado de la opción
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultado
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // estados de las opciones del menú
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // estado
      MenuItemState state = this.menuOptionsStates[i];
      // ID del menú
      int id = state.getMenuItemId();
      // inicialización del estado
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // resultado
    return menuOptionsStates;
  }

  // visualización de opciones de menú -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // se actualizan todas las opciones del menú
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // se actualizan algunas opciones del menú
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • líneas 6-18: este método permite obtener los identificadores numéricos de todas las opciones del menú;
  • línea 6: el método [getMenuOptions] recibe dos parámetros:
    • [Menu menu]: el menú del fragmento;
    • [List<Integer> menuOptionsIds]: la lista de identificadores de Android de las opciones del menú. Inicialmente, esta lista está vacía. Luego se llena mediante un recorrido recursivo (línea 15) del árbol del menú;
  • líneas 20-40: a partir del menú, construye la matriz de estados (identificador, visibilidad) de las opciones del menú. Esta matriz se almacena en la línea 3. La clase [MenuItemState] se describió en el párrafo 2.7.1;
  • líneas 43-55: una variante del método anterior. Hace lo mismo, pero en lugar de volver a calcular los identificadores de todas las opciones del menú —lo cual ya se ha hecho—, utiliza los identificadores de la tabla de estados de la línea 3;
  • líneas 58-63: el método [setAllMenuOptionsStates] permite ocultar o mostrar todas las opciones del menú del fragmento;
  • líneas 65-69: el método [setMenuOptionsStates] permite, de manera selectiva, mostrar u ocultar algunas de las opciones del menú;
  • los métodos [getMenuOptions, getMenuOptionsStates] se declaran privados, ya que se utilizan únicamente en [AbstractFragment]. Los métodos [setAllMenuOptionsStates] (línea 58) y [setMenuOptionsStates] (línea 65) se declaran como protegidos para que estén disponibles para las clases hijas;

2.7.3.4. Manejo de la espera hasta que finalice una tarea asíncrona


   // las suscripciones a los observables
  private List<Subscription> abonnements = new ArrayList<>();
// tareas asincrónicas
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // gestión de la espera hasta que finalice una operación asíncrona -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // se registra el número de tareas que se ejecutarán
    this.numberOfRunningTasks = numberOfRunningTasks;
    // se muestra la imagen de espera
    mainActivity.beginWaiting();
    // se vacía la lista de suscripciones
    abonnements.clear();
    // Aún no hay cancelación
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // se oculta la imagen de espera
    mainActivity.cancelWaiting();
  }

  • líneas 9-18: para iniciar una o varias operaciones asincrónicas, el fragmento hijo llamará al método padre [beginRunningTasks]. El parámetro de este método es el número de tareas asincrónicas que el fragmento hijo va a iniciar;
  • línea 11: se almacena el parámetro del método;
  • línea 13: se muestra la imagen de espera;
  • línea 15: se limpia la lista de suscripciones a las operaciones asincrónicas. Estas aún no han sido creadas por el fragmento hijo;
  • línea 17: se mantiene un valor booleano para indicar que las tareas asincrónicas solicitadas por el fragmento hijo han sido canceladas. Inicialmente, este valor booleano tiene el valor false;
  • líneas 20-25: el fragmento hijo llama al método padre [cancelWaitingTasks] para indicar que desea cancelar las tareas que ha iniciado;
  • línea 22: se oculta la imagen de espera;

2.7.3.5. Manejo de excepciones


  // gestión de excepciones -------------------------------------------------------------------

  // se muestra una alerta sobre la excepción
  protected void showAlert(Throwable th) {
    // se muestran los mensajes de la pila de excepciones del Throwable th
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // Mostrar lista de mensajes
  protected void showAlert(List<String> messages) {
    // se muestra la lista de mensajes
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • líneas 4-7: el método [showAlert(Throwable)] permite que un fragmento hijo muestre en una ventana los mensajes de la pila de excepciones del Throwable pasado como parámetro;
  • líneas 10-13: el método [showAlert(List<String>] permite que un fragmento hijo muestre en una ventana la lista de mensajes pasada como parámetro;
  • la clase [Utils] utilizada en las líneas 6 y 12 se describió en el párrafo 2.7.2;

2.7.3.6. Gestión de operaciones asincrónicas


...
  // suscripciones a observables
  private List<Subscription> abonnements = new ArrayList<>();
  // tareas asincrónicas
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // ejecución de una tarea asíncrona con RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // proceso: el observable que se va a ejecutar u observar
    // consumeResult: el método que procesa la respuesta obtenida
    // 
    // solo se crean nuevas suscripciones si no ha habido cancelación
    if (!runningTasksHaveBeenCanceled) {
      // ejecución en el hilo de E/S y observación en el hilo de la interfaz de usuario
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      //: se ejecuta el observable
      try {
        abonnements.add(process.subscribe(
          // Consumo del resultado
          consumeResult,
          // consumo de excepción
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // fin de la tarea
          new Action0() {

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

  private void endOfTask() {
...
  }

  // una operación asíncrona generó una excepción
  // o se produjo una excepción durante la ejecución de una operación asíncrona
  private void consumeThrowable(Throwable th) {
...
  }

  • líneas 9-41: ejecutan una tarea asíncrona;
  • línea 9: el método [executeInBackground] espera dos parámetros:
    • [Observable<T> process]: el proceso asíncrono que se debe ejecutar;
    • [Action1<T> consumeResult]: el método del fragmento hijo al que se debe llamar para transmitirle los elementos emitidos por el proceso. En nuestros ejemplos anteriores, los procesos siempre han emitido solo un elemento. El tipo T de [Action1<T>] es el tipo T del resultado devuelto por el proceso observado;
  • línea 14: la tarea asíncrona solo se inicia si aún no se ha producido una cancelación por parte del usuario o del programa (debido a una excepción);
  • línea 16: el proceso está configurado para ejecutarse en un hilo de E/S y se observa en el hilo de la interfaz de usuario;
  • línea 16: la instrucción [process.subscribe] inicia la ejecución del proceso en el hilo de E/S. Dentro de este hilo, las operaciones se ejecutan de manera síncrona porque utilizamos una biblioteca HTTP que es síncrona;
  • línea 19: el método [process.subscribe] tiene tres parámetros:
    • línea 21: [consumeResult]: el método del fragmento secundario que consumirá los elementos emitidos por el proceso;
    • líneas 22-28: el método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. El procesamiento se delega al método [consumeThrowable] de la línea 49;
    • líneas 29-36: el método que se ejecuta cuando la tarea emite la notificación de fin de emisión. El procesamiento se delega al método [endOfTask] de la línea 43;
  • línea 19: la tarea asíncrona que acaba de iniciarse se registra en el campo [abonnements], que almacena todas las tareas asíncronas iniciadas. Esto permitirá cancelarlas si es necesario;
  • líneas 37-39: método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. El procesamiento se delega al método [consumeThrowable] de la línea 49;

El método [endOfTask] es el siguiente:


  // tareas asincrónicas
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // una tarea menos que esperar
    numberOfRunningTasks--;
    // ¿Ya terminó?
    if (numberOfRunningTasks == 0) {
      // fin de la espera
      cancelWaitingTasks();
      // se notifica el fin de las tareas a la clase hija
      notifyEndOfTasks(false);
    }
  }
...
  // clases hijas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • línea 6: acaba de finalizar una tarea asíncrona. Se decrementa el contador de tareas activas;
  • línea 8: si ya no hay tareas activas, entonces el fragmento hijo ha obtenido todas sus respuestas;
  • línea 10: se cancela la espera;
  • línea 12: se le indica al fragmento hijo que todas las tareas que lanzó han finalizado llamando a su método [notifyEndOfTasks]. El parámetro de este método indica cómo terminaron las tareas: de manera normal, o por cancelación del usuario o del código debido a que se produjo una excepción. En la línea 12, se indica un final normal. Cabe señalar que el fragmento hijo no tiene que preocuparse por llevar la cuenta de las tareas aún activas. Su clase padre lo hace por él;

El método [consumeThrowable] es el siguiente:


  // tareas asincrónicas
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // una operación asincrónica generó una excepción
  // o se produjo una excepción durante la ejecución de una operación asincrónica
  private void consumeThrowable(Throwable th) {
    // th: la excepción que se debe tratar
    // 
    // registro
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // se cancelan las tareas ya iniciadas
    cancelRunningTasks();
    // se muestran los mensajes de error
    showAlert(th);
  }

  // cancelación de tareas
  protected void cancelRunningTasks() {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // se cancelan todas las tareas asincrónicas registradas
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // se registra la cancelación
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // fin de la espera
    cancelWaitingTasks();
    // se notifica la cancelación de las tareas al fragmento hijo
    notifyEndOfTasks(true);
}

...
  // clases hijas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • línea 3: el método [consumeThrowable] recibe la excepción que se produjo;
  • línea 15: se cancelan todas las tareas que aún están activas;
  • línea 17: se muestra el texto de la excepción;
  • líneas 21-37: se cancelan todas las tareas;
  • líneas 27-29: se cancelan todas las suscripciones;
  • línea 31: se registra que se ha producido una cancelación;
  • línea 32: se pone a cero el contador de tareas;
  • línea 34: se cancela la espera;
  • línea 36: se notifica al fragmento hijo el fin de las tareas tras la cancelación;

2.7.3.7. Gestión del ciclo de vida del fragmento


  // ciclo de vida --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // padre
    super.onDestroyView();
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // clase principal
    super.onDestroy();
    // registro
    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) {
...
}
  • líneas 2-20: los métodos [onDestroyView, onDestroy] están ahí únicamente para los registros. Estos permiten al desarrollador comprender mejor el ciclo de vida de los fragmentos;

El guardado del fragmento al girar el dispositivo se realiza mediante los siguientes métodos [setUserVisibleHint, onSaveInstanceState, saveState]:


  // ciclo de vida del fragmento
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    super.setUserVisibleHint(isVisibleToUser);
    // ¿copia de seguridad?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // el fragmento se va a ocultar; lo guardamos
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memoria
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // padre
    super.onSaveInstanceState(outState);
    // guardar el fragmento solo si está visible
    if (isVisibleToUser) {
      // quizás ya se haya guardado
      if (!saveFragmentDone) {
        saveState();
      }
      // se debe realizar la restauración en cualquier caso
      session.setAction(ISession.Action.RESTORE);
    }
}
  • líneas 6-19: el fragmento se guarda si pasa del estado «visible» al estado «oculto» (línea 11). Es el método [setUserVisibleHint] el que nos brinda esta información;
  • línea 14: el guardado se realiza mediante el método privado de las líneas 21-23;
  • líneas 25-41: al girar el dispositivo, se llamará al método [onSaveInstanceState]. El fragmento se guarda bajo dos condiciones:
    • que sea visible (línea 34);
    • aún no se haya guardado (línea 36). Es posible que los métodos [setUserVisibleHint, onSaveInstanceState] no puedan ejecutarse ambos cuando el fragmento está visible y que, por lo tanto, la gestión del valor booleano [saveFragmentDone] sea innecesaria. En caso de duda, preferí utilizar este;
  • línea 40: después de guardar vendrá la restauración. Cabe señalar que, la próxima vez que el fragmento deba actualizarse, deberá hacerlo mediante una operación [RESTORE];

Cabe señalar los dos momentos en los que se solicita una copia de seguridad del fragmento:

  1. cuando este pasa del estado visible al estado oculto;
  2. cuando se produce una rotación del dispositivo;

El método privado [saveState] es el siguiente:


...
  private void saveState() {
    // ¿Hay tareas que se deban cancelar?
    if (numberOfRunningTasks != 0) {
      // se cancelan las tareas
      cancelRunningTasks();
    }
    // se guarda el estado del fragmento
    CoreState currentState = saveFragment();
    // el fragmento ha sido visitado
    currentState.setHasBeenVisited(true);
    // se guarda el estado del menú
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // inicio de sesión
    session.setCoreState(getNumView(), currentState);
    // guardado realizado
    saveFragmentDone = true;
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // clases hijas -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • líneas 4-7: la rotación del dispositivo puede ocurrir mientras se están realizando operaciones asincrónicas. Aquí se toma la decisión de cancelarlas todas. Esta no es una buena decisión para el usuario, quien tendrá que volver a realizar una nueva consulta que podría tardar bastante tiempo, cuando lo único que hizo fue mover su teléfono o tableta, o bien recibió una llamada telefónica. Es posible conservar las conexiones de red a lo largo de un ciclo de respaldo y restauración. Sin embargo, las soluciones no son obvias y he decidido no abordarlas en este curso para principiantes. La solución es establecer estas conexiones de red a través de un fragmento sin interfaz visual asociada y que no se elimine durante el ciclo de copia de seguridad y restauración. Para ello, basta con utilizar la instrucción [Fragment.setRetainInstance(true)];
  • línea 9: se le pide al fragmento hijo que guarde su estado en un tipo derivado de [CoreState] (línea 31);
  • línea 11: se registra que el fragmento ha sido visitado. Esta información es útil. Cuando un fragmento se visita por primera vez, su actualización puede ser diferente a las siguientes, ya que en ese momento no tiene un estado previo en la sesión;
  • línea 13: se guarda el estado del menú, lo que nos permitirá restaurarlo automáticamente;
  • línea 15: este estado actual se guarda en la sesión. En esta, los estados se agrupan por vista/fragmento, y cada uno de ellos tiene un estado. El número de la vista lo proporciona el fragmento hijo (línea 33);
  • línea 17: se registra que se ha guardado el fragmento. Esto se debe a que dos métodos pueden llamar al método [saveState] y no tiene sentido realizar dos guardados;

La regeneración de la vista asociada al fragmento se lleva a cabo mediante el siguiente método:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // clase principal
    super.onActivityCreated(savedInstanceState);
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // se debe restaurar la vista
    viewHasToBeInitialized = true;
}

En el ciclo de vida, el método [onActivityCreated] se ejecuta inmediatamente después del método [onCreateView]. La llamada a este último método indica que la vista asociada al fragmento debe reconstruirse. Simplemente lo anotamos en la línea 10.

2.7.3.8. Actualización del fragmento

La actualización del fragmento es la última operación que se realiza sobre el fragmento antes de que sea visible y quede a la espera de las acciones del usuario. Se lleva a cabo mediante el siguiente código:


  // menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // Actualización del fragmento ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memoria
    this.menu = menu;
    // se recuperan las # opciones del menú si aún no se ha hecho
    if (fragmentHasToBeInitialized) {
      // se recuperan las # opciones del menú
      getMenuOptionsStates(menu);
      // actividad
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
    previousState = session.getCoreState(getNumView());
    // Actualización del fragmento secundario en varios pasos
    // etapa 1: ¿es la primera visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // No es la primera visita
      // etapa 2: ¿se debe inicializar el fragmento?
      ...
      // paso 3: ¿se debe inicializar la vista?
      ...
    }
    // paso 4: ¿un envío, una navegación, una restauración?
    ...

    // paso 5: actualizaciones de los terminales ----------------------
...
  }
...
  // clases hijas -----------------------------------------------------
  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();
  • línea 19: se utiliza el método [onCreateOptionsMenu] para actualizar el fragmento. Por esta razón, el fragmento debe tener un menú, vacío si es necesario. Cuando se ejecuta este método, el fragmento ya se ha asociado a su vista y a su actividad, y además está visible;
  • línea 25: se almacena el menú que se pasó como parámetro (línea 22) al método;
  • líneas 27-34: si el fragmento debe inicializarse:
    • línea 29: los estados de las opciones del menú se almacenan en la matriz [menuOptionsStates] de la línea 3;
    • línea 31: la actividad se almacena como una instancia del tipo Android [Activity];
    • línea 32: la actividad se almacena como una instancia de la interfaz [IMainActivity];
    • línea 33: se almacena la sesión. El cambio de tipo es necesario, ya que el método [mainActivity.getSession()] devuelve un tipo [ISession];
  • línea 36: se recupera de la sesión el estado anterior del fragmento. Si es la primera visita al fragmento, solo el valor booleano [previousState.hasBeenVisited] tiene significado;
  • líneas 39-44: código que se ejecuta cuando es la primera visita al fragmento. En este caso, su estado anterior no es relevante;
  • líneas 44-50: código que se ejecuta cuando no es la primera visita al fragmento;
  • líneas 46-47: código que se ejecuta si se ha llamado al constructor del fragmento (fragmentHasToBeInitialized==true);
  • líneas 48-49: código que se ejecuta si se ha reconstruido la vista asociada al fragmento (viewHasToBeInitialized==true);
  • líneas 51-52: código que se ejecuta según la acción (SUBMIT, NAVIGATION, RESTORE) en curso;
  • líneas 54-55: código que siempre se ejecuta;

Los cinco pasos de la actualización son los siguientes:

etapa 1


  // menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Estados del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
    previousState = session.getCoreState(getNumView());
    // Actualización del fragmento secundario en varias etapas
    // etapa 1: ¿es la primera visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // inicialización del fragmento y la vista
      initFragment(null);
      initView(null);
      // borrar previousState para continuar
      previousState = null;
    } else {
      // No es la primera visita
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • línea 19: se recupera el estado anterior del fragmento en la sesión;
  • líneas 22-31: código ejecutado si el fragmento nunca ha sido visitado;
  • línea 27: se le pide a la clase hija que inicialice el fragmento. El parámetro del método [initFragment] de la línea 35 es el estado anterior del fragmento. Aquí se pasa null para indicarle al fragmento hijo que es la primera visita;
  • línea 28: se le pide a la clase hija que inicialice la vista asociada al fragmento. El parámetro del método [initView] de la línea 37 es el estado anterior del fragmento. Aquí se pasa null para indicarle al fragmento hija que es la primera visita;
  • línea 30: se establece el estado anterior en null para los pasos siguientes;

pasos 2 y 3


// menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Estados del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
    previousState = session.getCoreState(getNumView());
    // Actualización del fragmento secundario en varias etapas
    // etapa 1: ¿es la primera visita?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // No es la primera visita
      // etapa 2: ¿se debe inicializar el fragmento?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // fragmento secundario
        initFragment(previousState);
      }
      // paso 3: ¿se debe inicializar la vista?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // fragmento secundario
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • líneas 24-42: se ejecutan cuando no es la primera visita al fragmento;
  • líneas 27-33: si el fragmento acaba de ser reconstruido, se reinicia llamando al método [initFragment] de la clase hija (líneas 32, 46). Se le pasa el estado anterior del fragmento;
  • líneas 35-51: si la vista asociada al fragmento debe inicializarse o reinicializarse, se le pide al fragmento hijo que lo haga (líneas 40, 48). Una vez más, se le pasa el último estado conocido del fragmento;

paso 4


// menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Estados del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
    previousState = session.getCoreState(getNumView());
    // Actualización del fragmento secundario en varias etapas
 ...

    // etapa 4: ¿un envío, una navegación, una restauración?
    // registro
    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();
      }
    }
    // acción en curso
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // fragmento secundario
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // restauración del menú
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // fragmento secundario
          updateOnRestore(previousState);
        } else {
          // se trata de una primera visita - no hay nada que hacer
        }
        break;
      case RESTORE:
        // restauración
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // restauración del menú (previousState no puede ser nulo)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // fragmento secundario
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • líneas 34-66: se procesa la acción en curso, que puede ser una de las tres siguientes:
    • RESTORE: se está restaurando el fragmento después de una rotación del dispositivo;
    • NAVIGATION: se regresa al fragmento con la intención de encontrarlo en el estado en que se dejó la última vez que se utilizó;
    • SUBMIT: todos los demás casos;
  • línea 34: se recupera la acción en curso;
  • líneas 36-42: para una acción de tipo SUBMIT, se llama al método [updateOnSubmit] del fragmento hijo (líneas 41, 68) pasándole el último estado conocido del fragmento;
  • líneas 43-55: para una acción de tipo NAVIGATION;
  • líneas 47-54: queremos restablecer el fragmento a su último estado conocido. La operación de NAVIGATION puede coincidir con una primera visita. Este sería el caso, por ejemplo, en una aplicación con pestañas: si paso de la pestaña 1 a la pestaña 4:
    • debo inicializar el fragmento de la pestaña 4 si se trata de la primera visita;
    • restablecer el fragmento de la pestaña 4 a su estado anterior si no es la primera visita;
  • líneas 52-54: no se hace nada si se trata de la primera visita. Será el método secundario [initView(CoreState previousState)] el que realice esta inicialización. La primera visita se caracteriza por la condición [previousState==null];
  • línea 49: si no es la primera visita al fragmento, se le devuelve su menú;
  • línea 51: se le pide a la clase hija que se actualice llamando al método de la línea 70. Se le pasa el estado anterior del fragmento para que pueda realizar su trabajo;
  • líneas 56-66: en el caso de una operación de restauración del fragmento, se hace lo mismo que en el caso de una navegación fuera de la primera visita;

paso 5


// menú del fragmento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo de vida del fragmento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // estados del fragmento
  private CoreState previousState;
  // mapeador jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo de vida del fragmento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // etapa 5: actualizaciones finales ----------------------
    // Se ha cambiado de vista
    session.setPreviousView(getNumView());
    // ya no hay ninguna acción en curso
    session.setAction(ISession.Action.NONE);
    // al salir de este fragmento, deberá guardarse
    saveFragmentDone = false;
    // Mientras el fragmento no se haya reconstruido, no es necesario inicializarlo
    fragmentHasToBeInitialized = false;
    // Mientras la vista no se haya reconstruido, no es necesario inicializarla
    viewHasToBeInitialized = false;
    // se vuelve al funcionamiento normal de la selección de pestañas
    session.setNavigationOnTabSelectionNeeded(true);

    // se le indica al fragmento que la vista está lista
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • líneas 18-30: al llegar aquí, el fragmento ya se ha inicializado y está listo para mostrarse. Entonces, se restablecen a su estado inicial todos los indicadores utilizados en la gestión del ciclo de vida del fragmento;
  • línea 20: hemos cambiado de vista; lo anotamos en la sesión;
  • línea 22: ya no hay ninguna acción en curso;
  • línea 24: cuando se salga del fragmento que se muestra actualmente, habrá que guardarlo al salir;
  • línea 26: ya no es necesario reconstruir el fragmento. Este indicador se restablecerá a vrai cuando se vuelva a ejecutar el constructor del fragmento;
  • línea 28: ya no es necesario inicializar la vista asociada al fragmento. Este indicador se restablecerá a vrai cuando se vuelva a ejecutar el método [onActivityCreated];
  • línea 30: el fragmento se muestra, tal vez, en una aplicación con pestañas. En este caso, cuando el usuario haga clic en una de ellas, debe producirse un cambio de fragmento;
  • línea 36: se le indica a la clase hija que el fragmento está listo. Esta puede incluir en el método [notifyEndOfUpdates] las actualizaciones que se deban realizar en cualquier caso, iniciar una operación asíncrona para obtener nuevos datos, etc.

2.7.4. Un ejemplo de fragmento

  

Se ha incluido en el proyecto [client-android-skel] un ejemplo de fragmento para mostrarle al lector la estructura típica de un fragmento de una aplicación basada en este proyecto.

La clase [DummyFragment] es la siguiente:


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 {

  // campos heredados de la clase padre -------------------------------------------------------

  // modo de depuración
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nombre de la clase
  //-- String protegido className;
  // tareas asincrónicas
  //-- int protegido numberOfRunningTasks;
  // actividad
  //-- protegido IMainActivity mainActivity;
  //-- actividad protegida;
  // sesión
  //-- protegido Sesión sesión;

  // métodos heredados de la clase padre -------------------------------------------------------

  // mostrar opciones de menú
  //-- protegido void setAllMenuOptionsStates(booleano isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // gestión de la espera hasta que finalice una serie de tareas asincrónicas
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // ejecución de una tarea asíncrona con RxAndroid
  //-- protegido <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // cancelación de tareas
  //-- protegido void cancelRunningTasks() {
  // visualización de alerta por excepción
  //-- protected void showAlert(Throwable th) {
  // visualización de la lista de mensajes
  //-- protected void showAlert(List<String> mensajes) {

  // métodos impuestos por la clase padre -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // se debe guardar el fragmento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // sino hay nada que guardar, hacer [return new CoreState();] y eliminar la clase [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // se debe devolver el número del fragmento en la tabla de fragmentos administrados por la actividad (véase MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // el fragmento se vuelve visible y se ha creado en este paso o en uno anterior
    // Esto ocurre al iniciar la aplicación y cada vez que se gira el dispositivo Android
    // necesariamente va seguida de la ejecución de [initView]
    // es necesario inicializar los campos del fragmento que se ha reconstruido
    // previousState es la última copia de seguridad del fragmento; toma el valor null si es la primera visita al fragmento
  }

  @Override
  protected void initView(CoreState previousState) {
    // el fragmento se vuelve visible y la vista asociada se ha reconstruido en este paso o en uno anterior
    // esto ocurre cada vez que se ejecuta [initFragment] y cada vez que el fragmento sale de la adyacencia del fragmento que se muestra
    // Es necesario inicializar los componentes de la vista que se ha reconstruido
    // previousState es la última copia de seguridad del fragmento; toma el valor null si es la primera visita al fragmento

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // se ejecuta después de [initFragment, initView] si se ejecutan estos métodos
    //: la vista se mostrará después de una operación del tipo SUBMIT
    //: por lo general, es necesario inicializar el fragmento y la vista asociada a partir de la sesión
    // previousState es la última guardada del fragmento; toma el valor null si es la primera visita al fragmento
    // no hay nada que hacer si no se puede llegar al fragmento mediante una operación SUBMIT
    // si se puede llegar al fragmento mediante operaciones SUBMIT desde diferentes fragmentos, se puede conocer la vista anterior mediante [session.getPreviousView]
    // si se puede llegar al fragmento mediante varias operaciones SUBMIT a partir del mismo fragmento, entonces hay que establecer un indicador en la sesión para diferenciar los distintos tipos de SUBMIT a partir de ese fragmento
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // se ejecuta después de [initFragment, initView] si se ejecutan estos métodos
    //: la vista se mostrará después de una operación del tipo RESTORE o NAVIGATION
    // previousState es la última copia de seguridad del fragmento; nunca es nulo
    //: se debe restablecer la vista a su estado anterior

  }

  @Override
  protected void notifyEndOfUpdates() {
    // se ejecuta después de los métodos [updateOnSubmit, updateOnRestore]
    // al llegar aquí, la vista ya se ha construido e inicializado
    // a menudo no hay nada que hacer aquí, pero también se pueden agrupar aquí las acciones que se deberían realizar independientemente de cómo se llegue a esta vista
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // se llama cuando las tareas asincrónicas iniciadas por el fragmento se han completado o se han cancelado
    // Estos dos casos se pueden diferenciar gracias al parámetro runningTasksHaveBeenCanceled
    // por lo general, es necesario restablecer la vista a un estado diferente al que tenía mientras esperaba las respuestas de las tareas asincrónicas

  }
}

La clase [DummyFragment] puede no tener un estado. Aquí se ha incluido uno para recordar lo que se espera de ella:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // estado del fragmento [DummyFragment]
  // incluir solo campos serializables en jSON
  // Agregar la anotación @JsonIgnore a los demás, aunque no queda claro para qué podrían servir
  // No olvides los getters y setters; se utilizan para la serialización y deserialización
}

Para ilustrar el uso del proyecto [client-android-skel], primero utilizaremos ejemplos sencillos antes de pasar a un caso de estudio más completo.

2.8. Ejercicios ilustrativos

Comenzaremos por refactorizar ejemplos ya escritos.

2.8.1. Ejemplo-17B

Retomamos el ejemplo 17 estudiado en el párrafo 1.18. Se trata de una aplicación con un único fragmento, sin tareas asincrónicas ni pestañas. La examinamos para ver cómo se comporta al girar el dispositivo. Realizamos las siguientes entradas:

Image

Luego, en [1], giramos el dispositivo dos veces. La nueva vista es entonces la siguiente:

Image

Si comparamos las vistas, todo se ha conservado, excepto la lista [2], que ahora está vacía.

Por otra parte, si hacemos clic en el botón [Valider], aparece un cuadro de diálogo que muestra los datos ingresados en el formulario. Si en ese momento giramos el dispositivo, se pierde el cuadro de diálogo.

Por lo tanto, al girar el dispositivo, tendremos que regenerar:

  • la lista desplegable y su elemento seleccionado;
  • el cuadro de diálogo, si estaba visible en el momento de la rotación;

2.8.1.1. El proyecto [Exemple-17B]

Duplicamos el proyecto [client-android-skel] en ejemplos/Ejemplo-17B. Luego cargamos el nuevo proyecto [1]:

  • en [2-3], dentro de la carpeta [behavior], pegamos el fragmento [Vue1Fragment] del proyecto [Exemple-17];
  • en [4-5], dentro de la carpeta [layout] de [Exemple-17B], pegamos la vista [vue1.xml] de [Exemple-17]. Esta es la vista asociada al fragmento;
  • en [6], la carpeta [values] de [Exemple-17B] se reemplaza por la carpeta [values] de [Exemple-17];

Se modificará el margen superior de la vista [vue1.xml] a 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"/>

En este punto, podemos intentar una primera compilación para ver los errores. Los primeros errores señalados provienen de imports, debido a paquetes que han cambiado de lugar. Los corregimos (Ctrl-Shift-O). Otros errores se deben a que la vista [Vue1Fragment] no implementa todos los métodos exigidos por su clase padre [AbstractParent]:

Image

Generamos los métodos que faltan (Alt-Intro).

Otro error de compilación reportado es el siguiente:

Image

Esto se corrige en el archivo [build.gradle] del módulo (línea 20 a continuación):

 

En este punto, podemos volver a compilar para ver los errores restantes. El único error reportado se encuentra en el método [Vue1Fragment.updateFragment]:

 

Hay que eliminar la anotación [@Override] de la línea 135. Ya no hay errores. Partiremos de ahí para modificar el proyecto.

2.8.1.2. El estado del fragmento [Vue1Fragment]

El fragmento [Vue1Fragment] necesita guardar información al girar el dispositivo para que pueda restaurarse por completo. Para ello, creamos una clase [Vue1FragmentState]:

  

Por el momento, esta clase está vacía:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Personalización del proyecto

  

En el archivo [custom] se encuentran los elementos de arquitectura que el desarrollador puede personalizar.

Las constantes de la interfaz [IMainActivity] serán las siguientes:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // acceso a la sesión
  ISession getSession();

  // cambio de vista
  void navigateToView(int position, ISession.Action action);

  // gestión de la espera
  void beginWaiting();

  void cancelWaiting();

  // constantes de la aplicación -------------------------------------

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;

  // tiempo máximo de espera para la respuesta del servidor
  int TIMEOUT = 1000;

  // tiempo de espera antes de ejecutar la solicitud del cliente
  int DELAY = 0;

  // autenticación básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de pestañas
  boolean ARE_TABS_NEEDED = false;

  // imagen de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos de la aplicación
  int FRAGMENTS_COUNT = 1;

}
  • líneas 24-31: la aplicación no utiliza aquí su capa [DAO]. Estas constantes no se utilizarán;
  • línea 34: una adyacencia de fragmentos de 1, que es el valor por defecto. Como la aplicación solo tiene un fragmento (línea 43), este valor no tiene importancia;
  • líneas 39-40: como no hay operaciones con la capa [DAO], no es necesario tener una imagen de espera;
  • línea 37: no es una aplicación con pestañas;
  • línea 43: solo hay un fragmento;

La clase [Session] es la siguiente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // los elementos que no se pueden serializar en jSON deben tener la anotación @JsonIgnore

}

Está vacía. De hecho, como solo hay un fragmento, no es necesario prever una comunicación entre fragmentos con una sesión.

Por último, la clase [CoreState] es la siguiente:


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 {
  // fragmento visitado o no
  protected boolean hasBeenVisited = false;
  // estado del posible menú del fragmento
  protected MenuItemState[] menuOptionsState;

  // getters y setters
...
}
  • líneas 11-13: debemos incluir todas las clases derivadas de [CoreState] que almacenan el estado de los distintos fragmentos. En este caso, solo hay una (línea 12);

2.8.1.4. La actividad [MainActivity]

La actividad [MainActivity] actualmente es la siguiente:


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 {

  // capa [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sesión
  private Session session;

  // métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sesión
    this.session = (Session) super.session;
    // tareas pendientes: continuamos con las inicializaciones iniciadas por la clase padre
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // por hacer: definir los fragmentos aquí
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // por hacer: definir aquí los títulos de los fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // tareas pendientes: navegación por pestañas — definir la vista que se mostrará cuando se seleccione la pestaña n.º [position]
  }

  @Override
  protected int getFirstView() {
    // tarea pendiente: definir el n.º de la primera vista (fragmento) que se mostrará
    return 0;
  }
}

Los comentarios de [//todo] indican lo que debe hacer el desarrollador. La clase [MainActivity] evoluciona de la siguiente manera:


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 {

  // capa [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sesión
  private Session session;

  // métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sesión
    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;
  }
}

Solo se debe modificar el método de las líneas 41 a 44. Este debe devolver la matriz de fragmentos de la aplicación. En la línea 43, no hay que olvidar poner el guión bajo después del nombre del fragmento.

2.8.1.5. El estado del fragmento [FragmentState]

Tras las pruebas de rotación realizadas en el proyecto [Exemple-17], se decide almacenar los siguientes elementos del fragmento:

  • la lista de valores del menú desplegable;
  • la posición del elemento seleccionado en dicha lista;
  • el mensaje que muestra el cuadro de diálogo, si este está presente en el momento de la rotación;

La clase [Vue1FragmentState] será la siguiente:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // los valores de la lista desplegable
  private List<String> list;
  // el elemento seleccionado en la lista desplegable
  private int listSelectedPosition;
  // el mensaje que se muestra en el cuadro de diálogo
  private String message;

  // métodos getter y setter
...
}

2.8.1.6. El fragmento [AbstractFragment]

Actualmente, el ciclo de vida del fragmento se gestiona mediante dos métodos (líneas 6 y 32):


// lista desplegable
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // se marca el primer botón
    radioButton1.setChecked(true);
    // el calendario
    datePicker1.setCalendarViewShown(false);
    // el 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 lista desplegable
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // inicialización del adaptador de la lista desplegable
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }

El código de estos dos métodos se migrará a los métodos definidos por la clase [AbstractFragment] de la siguiente manera:


// gestión del ciclo de vida del fragmento ---------------------------------------------------------------------
  @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) {
    // ¿Primera visita?
    if (previousState == null) {
      // se crean los valores de la lista desplegable
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // se recuperan los valores de la lista desplegable
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // y el mensaje del cuadro de diálogo
      message = state.getMessage();
    }
    // inicialización del adaptador de la lista desplegable
    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) {
    // el calendario
    datePicker1.setCalendarViewShown(false);
    // el 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));
      }
    });
    // inicialización del adaptador de la lista desplegable
    dropDownList.setAdapter(dataAdapter);
    // ¿Es tu primera visita?
    if (previousState == null) {
      // se marca el primer botón
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // valor de la barra de desplazamiento
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // elemento seleccionado en la lista desplegable
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // ¿Diálogo visible?
    if (message != null) {
      // se muestra
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • líneas 2-9: el método [saveFragment] debe colocar los elementos del fragmento que se van a almacenar en una clase derivada de [CoreState] y devolver la instancia de esta;
  • líneas 11-14: el método [getNumView] debe devolver el número del fragmento. En este caso, solo hay un fragmento cuyo número es 0;
  • líneas 16-34: el método [initFragment] debe inicializar los campos del fragmento. Recibe el estado anterior del fragmento. Si [previousState] es igual a null, entonces se trata de la primera visita;
  • líneas 19-25: durante la primera visita, se crean los valores de la lista desplegable;
  • líneas 26-30: si no se trata de la primera visita, los campos [list, message] del fragmento se restauran a partir del estado anterior;
  • líneas 33-34: inicialización del campo [dataAdapter] del fragmento. Esta es la fuente de datos de la lista desplegable;
  • líneas 37-62: el método [initView] sirve para inicializar los componentes de la interfaz visual. Recibe como parámetro el estado anterior [previousState]. Si es [previousState==null], entonces se trata de la primera visita;
  • aquí se encuentra lo que antes estaba en el método [@AfterViews];
  • líneas 57-61: durante la primera visita, se verifica que el primer botón de opción esté marcado;
  • líneas 64-67: el método [updateOnSubmit] se ejecuta cuando la acción en curso es [SUBMIT]. Aquí no hay navegación entre fragmentos y, por lo tanto, no hay ninguna acción en curso;
  • líneas 69-81: el método [updateOnRestore] se ejecuta cuando la acción en curso es [NAVIGATION] o [RESTORE]. Aquí no hay navegación entre fragmentos y, por lo tanto, no es posible la acción [NAVIGATION];
  • línea 72: se vuelve a calcular (no se restaura) el valor de TextView seekBarValue. De hecho, durante las rotaciones, a veces se perdía su valor;
  • líneas 74-75: se posiciona la lista en el elemento que estaba seleccionado antes de la rotación. De lo contrario, la lista se posicionaba en su primer elemento;
  • líneas 76-80: se vuelve a mostrar el cuadro de diálogo si el mensaje del estado anterior no es null. Volveremos sobre el método [showMessage] (línea 79);
  • líneas 83-86: el método [notifyEndOfUpdates] es el último método llamado por la clase padre antes de dejar en paz al fragmento hijo. Aquí no hay nada que hacer;
  • líneas 88-91: el método [notifyEndOfTasks] indica el final de las tareas asincrónicas iniciadas por el fragmento. Aquí no hay ninguna;

La restauración del cuadro de diálogo se realiza de la siguiente manera:


  // el mensaje del cuadro de diálogo
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lista de mensajes a mostrar
    List<String> messages = new ArrayList<>();
    ...
    // visualización
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // se genera el texto que se mostrará
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // se almacena el mensaje
    message = texte.toString();
    // se muestra
    showMessage();
  }

  private void showMessage() {
    // se muestra
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // se reinicia el mensaje
        message = null;
      }
    }).show();
}

Cuando el usuario valida el formulario, el método [doValider] (línea 5) crea una lista de mensajes que luego muestra (línea 10) en el cuadro de diálogo.

  • líneas 14-20: la lista de mensajes se concatena en un solo mensaje que se almacena en la línea 2;
  • líneas 25-33: este es el mensaje que muestra el cuadro de diálogo y es este mismo mensaje el que el método [updateOnRestore] hace que se muestre;
  • línea 27: el segundo parámetro del método [setNeutralButton] es el método que se ejecuta cuando el usuario hace clic en el botón [Fermer] del cuadro de diálogo;
  • línea 31: al cerrar el cuadro de diálogo, se devuelve el mensaje a null para indicar que el cuadro de diálogo ya no está presente;

2.8.1.7. Tests

Se invita al lector a probar este proyecto y a verificar que el fragmento se conserve correctamente después de una o varias rotaciones sucesivas.

2.8.2. Ejemplo 23: cliente de información meteorológica

Algunos sitios web ofrecen información meteorológica en formato de cadenas como jSON. A continuación, un ejemplo:

Image

El URL tiene el siguiente formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} donde:

  • city: la ciudad de la que queremos conocer el clima, en este caso Angers;
  • country: el país de la ciudad, en este caso Francia (fr);
  • APPID: una clave que se obtiene al registrarse en el sitio web [https://home.openweathermap.org/users/sign_up];

2.8.2.1. El proyecto

  

El proyecto se desarrolló a partir del proyecto [client-android-skel]. Presenta las siguientes características:

  • solo tiene un fragmento cuyo estado no es necesario conservar;
  • realiza consultas asincrónicas;

2.8.2.2. Personalización del proyecto

  

La interfaz [IMainActivity] permite especificar ciertas características del proyecto:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // acceso a la sesión
  ISession getSession();

  // cambio de vista
  void navigateToView(int position, ISession.Action action);

  // gestión de la espera
  void beginWaiting();

  void cancelWaiting();

  // constantes de la aplicación -------------------------------------

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;

  // tiempo máximo de espera para la respuesta del servidor
  int TIMEOUT = 1000;

  // tiempo de espera antes de ejecutar la solicitud del cliente
  int DELAY = 5000;

  // autenticación básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de pestañas
  boolean ARE_TABS_NEEDED = false;

  // imagen de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos de la aplicación
  int FRAGMENTS_COUNT = 1;

}
  • líneas 25, 28, 31, 40: características de la capa [DAO]. Línea 31: no se requiere autenticación básica;
  • línea 34: adyacencia de los fragmentos. En este caso, esta constante no tiene importancia, ya que solo hay un fragmento;
  • línea 37: no es una aplicación con pestañas;
  • línea 43: solo hay un fragmento;

La clase [CoreState], que almacena el estado de los fragmentos, será la siguiente:


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)
// tarea pendiente: agregar aquí las subclases de [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // fragmento visitado o no
  protected boolean hasBeenVisited = false;
  // estado del posible menú del fragmento
  protected MenuItemState[] menuOptionsState;

  // getters y setters
...
}
  • líneas 10-13: no hay nada que declarar, ya que en esta aplicación solo hay un fragmento cuyo estado no se conserva;

La clase [Session] es la siguiente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // los elementos que no se pueden serializar en jSON deben tener la anotación @JsonIgnore
}

Está vacía, ya que en esta aplicación no hay comunicación entre fragmentos.

2.8.2.3. La capa [DAO]

  

En la capa [DAO], se deben personalizar tres clases:

  • la interfaz IDao;
  • su implementación Dao;
  • la interfaz WebClient para la comunicación con el servidor web / jSON;

La interfaz [WebClient] será la siguiente:


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);

  // servicio meteorológico
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • líneas 18-19: el URL del servicio meteorológico. Cabe recordar que esta se refiere al URL raíz (RestClientRootUrl, línea 12) del cliente. Aquí, este URL raíz será [http://api.openweathermap.org/];

La interfaz [IDao] será la siguiente:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servicio web
  void setUrlServiceWebJson(String url);

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

  // tiempo de espera del cliente
  void setTimeout(int timeout);

  // autenticación básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);

  // tiempo de espera del cliente en milisegundos antes de la solicitud
  void setDelay(int delay);

  // servicio meteorológico
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Cabe recordar que los métodos de las líneas 6 a 22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
  • línea 25: el método [getWeatherForecast] permite obtener la cadena jSON con el pronóstico del tiempo de la ciudad [city] del país [country]. El tercer parámetro es la clave obtenida en el sitio [https://home.openweathermap.org/users/sign_up];

La interfaz [IDao] está implementada por la siguiente clase [Dao]:


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 {

  // cliente del servicio web
  @RestService
  protected WebClient webClient;
  // seguridad
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // el RestTemplate
  private RestTemplate restTemplate;
  // fábrica de RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // tiempo de espera
  private int timeout;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // se construye el restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // se configura el convertidor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // se configura el restTemplate del cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // se establece el URL del servicio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // se registra al usuario en el interceptor
    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));
    }
    // memoria
    this.timeout = timeout;
    // configuración de fábrica
    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));
    }
    // ¿Interceptor de autenticación?
    if (isBasicAuthentificationNeeded) {
      // se agrega el interceptor de autenticación
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


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

  // servicio meteorológico ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // registro
    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));
    }
    // resultado
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Cabe recordar que las líneas 17 a 90 están presentes por defecto en la clase [Dao] del proyecto [client-android-skel]. Solo hay que agregar los métodos de implementación de la interfaz [IDao], específicos de la aplicación (línea 92);
  • líneas 93-105: implementación del método [getWeatherForecast]. Esta es muy sencilla y se realiza en 6 líneas, líneas 100-105;
  • línea 100: el método [getResponse] es un método de la clase padre [AbstractDao]. Espera un parámetro de tipo [IRequest<T>], donde T es el tipo de la respuesta esperada del servidor; en este caso, un String, ya que se espera una cadena jSON. El tipo T de [IRequest<T>] debe ser el tipo T del método [Observable<T> getWeatherForecast];
  • la interfaz [IRequest<T>] solo tiene un método: getResponse. Este tiene la función de proporcionar la respuesta de tipo T que debe devolver el método [Observable<T> getWeatherForecast];
  • línea 103: es la interfaz [WebClient] la que proporciona esta respuesta. Se le pasan los tres parámetros recibidos en la línea 94. Por esta razón, estos deben tener el atributo final;

2.8.2.4. La actividad [MainActivity]

  

La actividad [MainActivity] es la siguiente:


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 {

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

  // métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    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;
  }

  // interfaz IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • Cabe recordar que las líneas 15 a 55 están presentes por defecto en el proyecto [client-android-skel]. Solo hay que personalizarlas;
  • líneas 37 a 40: la tabla de fragmentos. Aquí solo hay uno;
  • líneas 43-46: no se necesitan títulos de fragmentos;
  • líneas 48-50: aquí no hay pestañas;
  • líneas 52-55: la primera vista que se debe mostrar es la vista n.º 0, la de [MeteoFragment];
  • líneas 58-61: implementación de la interfaz [IDao]. Aquí no hay nada más que hacer que delegar el trabajo a la capa [DAO] de la línea 21;

2.8.2.5. El fragmento [MeteoFragment]

  

El fragmento [MeteoFragment] consulta el servicio web / jSON de meteorología. Su estructura es la siguiente:


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 {
...
}
  • línea 14: la vista [res / layout / meteo_fragment.xml] es la siguiente:

<?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 vista solo muestra el texto de la línea 10;

  • línea 15: el menú [res / menu / menu_meteo.xml] es el siguiente:

<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>
  • líneas 10-12: esta opción del menú sirve para consultar el clima de una ciudad;
  • líneas 14-15: esta opción del menú sirve para cancelar esta consulta si está en curso;
  • líneas 16-18: esta opción del menú cierra la aplicación;

El código completo del fragmento es el siguiente:


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 {

  // datos locales
  private int nbReponsesRecues;

  // gestión de eventos ---------------------------------------------------------------------------------------
  // ciudades de las que se desea conocer el clima
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // su país
    String country = "fr";
    // obtén un identificador API al crear una cuenta [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL del servicio web / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // inicio de la espera de las tareas asincrónicas [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // número de respuestas recibidas
    nbReponsesRecues = 0;
    // las llamadas asincrónicas se realizan en paralelo
    for (String city : paysDeLoire) {
      // clima
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // procesamiento de la respuesta
          consumeResponse(response);
          // una respuesta de +
          nbReponsesRecues++;
        }
      });
    }
  }

  // procesamiento de la respuesta del servidor
  private void consumeResponse(String response) {
    // registro
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // inicio de la espera
  protected void beginWaiting(int numberOfRunningTasks) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // padre
    beginRunningTasks(numberOfRunningTasks);
    // se muestra la opción [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menú
    initMenu();
    // visualización de resultados
    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étodos privados -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menú
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // gestión del ciclo de vida ---------------------------------------------------------------------------------------
...
}
  • líneas 25-50: gestión del clic en la opción de menú [Météo];
  • línea 32: construcción de la URL del servicio web / jSON del servicio meteorológico. A continuación, esta se pasa a la capa [DAO] a través de la actividad;
  • línea 34: comenzamos la espera. Pasamos el número de tareas que se van a ejecutar, para que la clase padre pueda notificarnos cuando estas hayan finalizado. Aquí hay cinco tareas, ya que vamos a solicitar el pronóstico del tiempo de las cinco ciudades de la línea 23;
  • línea 16: vamos a contar el número de respuestas recibidas para poder mostrarlo;
  • líneas 38-50: se recorre un bucle por las ciudades de las que queremos el pronóstico del tiempo;
  • línea 40: vamos a realizar 5 solicitudes HTTP en paralelo;
  • línea 40: le pedimos a la clase padre [AbstractParent] que consulte el servicio web /jSON;
  • líneas 40-48: el método [executeInBackground] espera dos parámetros:
    • línea 40: el proceso que se debe observar y ejecutar lo proporciona el método [mainActivity.getWeatherForecast];
    • líneas 40-48: la instancia [Action1] que se ejecutará al recibir la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getWeatherForecast];
  • línea 44: se ha recibido una respuesta. Se pasa al método [consumeResponse] de la línea 53;
  • línea 46: se incrementa el contador de respuestas recibidas;
  • líneas 53-56: se procesa una respuesta jSON del servicio meteorológico;
  • línea 55: simplemente se registra la cadena jSON;
  • líneas 59-72: código ejecutado antes de iniciar las tareas asincrónicas;
  • línea 65: se pasa el número de tareas a ejecutar a la clase padre [AbstractParent]. Esto es lo que le permite a esta clase avisarnos cuando todas hayan finalizado;
  • líneas 67-70: preparación del menú para un estado de espera. Solo se mantiene la opción [Actions/Annuler], que permitirá al usuario cancelar las tareas iniciadas;
  • líneas 74-92: código que se ejecuta cuando la clase padre nos avisa de que todas las tareas iniciadas han finalizado;
  • línea 77: se restablece el menú a su estado inicial. El método [initMenu] (líneas 95-102) muestra el menú con todas sus opciones, excepto la opción [Actions/Annuler], que está oculta;
  • líneas 80-91: se muestra el número de respuestas recibidas;

Al hacer clic en la opción de menú [Annuler], se ejecuta el siguiente código:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // se cancelan las tareas asincrónicas
    cancelRunningTasks();
}
  • línea 7: se le pide a la clase padre que cancele las tareas que aún están activas;

Al hacer clic en la opción de menú [Terminer], se ejecuta el siguiente código:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // se detiene todo
    System.exit(0);
}

La gestión del ciclo de vida del fragmento se lleva a cabo mediante los siguientes métodos:


  // gestión del ciclo de vida ---------------------------------------------------------------------------------------

  @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) {
    // ¿Primera visita?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • líneas 3-6: sirven para almacenar el estado del fragmento en una clase derivada de [CoreState]. Si el fragmento no tiene un estado que almacenar, como en este caso, simplemente se devuelve una instancia de [CoreState]. No se debe devolver null, ya que esto provocaría un error más adelante;
  • líneas 8-11: deben devolver el número de la vista. Aquí, el fragmento [MeteoFragment] tiene el número 0;
  • líneas 13-16: sirven para inicializar el fragmento una vez que se ha construido (previousState==null) o reconstruido (previousState!=null). Aquí no hay nada que hacer. El único campo susceptible de ser inicializado es el siguiente:

  // ciudades de las que queremos el clima
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

pero se inicializa por sí solo;

  • líneas 18-24: sirven para inicializar la vista asociada al fragmento una vez que se ha construido (previousState==null) o reconstruido (previousState!=null);
  • líneas 21-23: si es la primera visita al fragmento, se inicializa su menú para ocultar la opción [Annuler];
  • líneas 27-30: se ejecutan si, para llegar al fragmento, se ha realizado una navegación con una acción del tipo [SUBMIT]. En este caso, no hay navegación entre fragmentos, ya que solo hay un fragmento;
  • líneas 32-35: se ejecutan durante un ciclo de respaldo/restauración debido a una rotación del dispositivo u otra razón. Aquí, como no se ha respaldado ningún estado, no hay nada que hacer;
  • líneas 37-40: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada que hacer;

2.8.2.6. Tests

Ahora ejecutamos el ejemplo:

Image

Image

Los registros son los siguientes:


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
  • líneas 32-36: las respuestas jSON se obtienen en subprocesos de E/S
  • líneas 37-41: el fragmento recupera las 5 respuestas en el hilo de la interfaz de usuario;

Ahora, realizamos la consulta con un identificador API incorrecto:


    String APIID = "";

Image

Los registros son entonces los siguientes:


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"]]
  • líneas 3-6, 10: las 5 llamadas a HTTP generaron 5 excepciones;
  • línea 7: el fragmento [MeteoFragment] recibe la primera excepción. A continuación, cancelará todas las tareas;

Ahora establezcamos un tiempo de espera de 5 segundos en [IMainActivity.DELAY] y cancelemos la operación. Los registros son entonces los siguientes:


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]]
  • línea 3: solicitud de cancelación;
  • línea 4: la espera se cancela porque se ha producido una cancelación;
  • líneas 6-10: la cancelación de las tareas provoca una excepción en cada uno de los subprocesos de las cinco tareas. El tipo de excepción depende de las aplicaciones. La excepción aquí es [java.lang.InterruptedException] porque las tareas se interrumpieron mientras ejecutaban la instrucción [Thread.sleep(delay)], que las hace esperar artificialmente [delay] milisegundos;

2.8.3. Ejemplo 16B

Aquí refactorizamos el ejemplo 16 del párrafo 1.17. Presenta un fragmento que realiza llamadas asincrónicas a un servidor de números aleatorios. Veamos cómo se comporta durante una rotación del dispositivo:

Image

  • en [1], se gira el dispositivo dos veces;

Image

Se observa que se han perdido todos los mensajes de error. Vamos a intentar mejorar esto.

2.8.3.1. El proyecto Ejemplo-16B

Copiamos el proyecto [client-android-skel] al proyecto [exemples/Exemple-16B] y luego cargamos el nuevo proyecto:

  

Del proyecto inicial [Exemple-16], copiamos en [Exemple-16B] los siguientes elementos:

  • el archivo [res/layout/vue1.xml], la carpeta [res/values]:
  

Modificaremos el margen superior de la vista [vue1.xml] a 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" />
  • el fragmento [Vue1Fragment]:
  
  • la clase [dao / service / Response]:
  

En esta etapa, podemos intentar una primera compilación:

  • un primer tipo de errores es el de imports. Algunas clases cambiaron de paquete durante la migración a [Exemple-16B]. Comenzamos por corregir este tipo de errores;
  • se reporta un segundo tipo de error en la clase [Vue1Fragment] porque no implementa los métodos exigidos por la clase padre [AbstractParent]. Se genera automáticamente dicha implementación;

Intentamos una segunda compilación:

  • todos los errores restantes se concentran ahora en la clase [Vue1Fragment], la clase que sufrirá más modificaciones;

2.8.3.2. Creación de un estado para el fragmento [Vue1Fragment]

Hemos visto que cierta información del fragmento deberá guardarse durante una rotación para poder restaurar el fragmento tal como estaba antes de la rotación. Por lo tanto, creamos un estado [Vue1FragmentState] que, por el momento, está vacío:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Personalización del proyecto

  

La interfaz [IMainActivity] permite especificar ciertas características del proyecto:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // acceso a la sesión
  ISession getSession();

  // cambio de vista
  void navigateToView(int position, ISession.Action action);

  // gestión de la espera
  void beginWaiting();

  void cancelWaiting();

  // constantes de la aplicación -------------------------------------

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;

  // tiempo máximo de espera para la respuesta del servidor
  int TIMEOUT = 1000;

  // tiempo de espera antes de ejecutar la solicitud del cliente
  int DELAY = 5000;

  // autenticación básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de pestañas
  boolean ARE_TABS_NEEDED = false;

  // imagen de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos de la aplicación
  int FRAGMENTS_COUNT = 1;

}
  • líneas 25, 28, 31, 40: características de la capa [DAO]. No se requiere autenticación básica;
  • línea 34: adyacencia de los fragmentos. En este caso, esta constante no tiene importancia, ya que solo hay un fragmento;
  • línea 37: no es una aplicación con pestañas;
  • línea 43: solo hay un fragmento;

La clase [CoreState], que almacena el estado de los fragmentos, será la siguiente:


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 {
  // fragmento visitado o no
  protected boolean hasBeenVisited = false;
  // estado del posible menú del fragmento
  protected MenuItemState[] menuOptionsState;

  // getters y setters
...
}
  • línea 12: declaramos la clase del estado del fragmento [Vue1Fragment];

La clase [Session] es la siguiente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // los elementos que no se pueden serializar en jSON deben tener la anotación @JsonIgnore
}

Está vacía, ya que en esta aplicación no hay comunicación entre fragmentos.

2.8.3.4. La capa [DAO]

  

En la capa [DAO], se deben personalizar tres clases:

  • la interfaz IDao;
  • su implementación Dao;
  • la interfaz WebClient para la comunicación con el servidor web / jSON;

La clase [Response] proviene del proyecto [Exemple-16], que la utiliza:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- propiedades
    // estado de la operación
    private int status;
    // posibles mensajes de error
    private List<String> messages;
    // el cuerpo de la respuesta
    private T body;

    // constructores
    public Response() {

    }

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

    // getters y setters
...
}

La interfaz [WebClient] será la siguiente:


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 número aleatorio en el intervalo [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • líneas 18-19: el URL del servicio de números aleatorios. Cabe recordar que esta se refiere al URL raíz (RestClientRootUrl, línea 12) del cliente. Aquí, este URL raíz será [http://localhost:8080];

La interfaz [IDao] será la siguiente:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servicio web
  void setUrlServiceWebJson(String url);

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

  // tiempo de espera del cliente
  void setTimeout(int timeout);

  // autenticación básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);

  // tiempo de espera del cliente en milisegundos antes de la solicitud
  void setDelay(int delay);

  // servicio de números aleatorios
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • Cabe recordar que los métodos de las líneas 6 a 22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
  • línea 25: el método [getAlea] permite obtener un número aleatorio en el intervalo [a,b]. Este número se obtiene en una respuesta de tipo [Response<Integer>], donde el número aleatorio se encuentra en el campo [body] de dicho tipo;

La interfaz [IDao] está implementada por la siguiente clase [Dao]:


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 {

  // cliente del servicio web
  @RestService
  protected WebClient webClient;
  // seguridad
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // el RestTemplate
  private RestTemplate restTemplate;
  // fábrica de RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // se construye el restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // se instala el convertidor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // se configura el restTemplate del cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // se establece el URL del servicio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // se registra al usuario en el interceptor
    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));
    }
    // configuración de fábrica
    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));
    }
    // ¿Interceptor de autenticación?
    if (isBasicAuthentificationNeeded) {
      // se agrega el interceptor de autenticación
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

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

  // servicio de números aleatorios
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // ejecución del cliente web
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • Cabe recordar que las líneas 17 a 85 están presentes por defecto en la clase [Dao] del proyecto [client-android-skel]. Solo hay que agregar los métodos de implementación de la interfaz [IDao];
  • líneas 88 a 97: implementación del método [getAlea]. Esta es muy sencilla y se realiza en 6 líneas, de la 91 a la 96;
  • línea 91: el método [getResponse] es un método de la clase padre [AbstractDao]. Espera un parámetro de tipo [IRequest<T>], donde T es el tipo de la respuesta esperada, en este caso un tipo Response<Integer>. El tipo T de [IRequest<T>] (línea 91) debe ser el tipo T del método [Observable<T> getAlea] (línea 89);
  • La interfaz [IRequest<T>] solo tiene un método: getResponse. Este método tiene la función de proporcionar la respuesta de tipo T que debe devolver el método [Observable<T> getAlea];
  • línea 94: es la interfaz [WebClient] la que proporciona esta respuesta. Se le pasan los dos parámetros recibidos en la línea 89. Por esta razón, estos deben tener el atributo final;

2.8.3.5. La actividad [MainActivity]

  

La actividad [MainActivity] es la siguiente:


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 {

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

  // métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos con las inicializaciones iniciadas por la clase padre
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // definir los fragmentos aquí
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // definir aquí los títulos de los fragmentos
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // navegación por pestañas: definir la vista que se mostrará
  }

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

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

}
  • Cabe recordar que las líneas 15 a 61 están presentes por defecto en el proyecto [client-android-skel]. Solo hay que personalizarlas;
  • líneas 40 a 44: la tabla de fragmentos. Aquí solo hay uno;
  • líneas 47-51: no se necesitan títulos de fragmentos;
  • líneas 53-56: aquí no hay pestañas;
  • líneas 58-61: la primera vista que se debe mostrar es la vista n.º 0, la de [Vue1Fragment];
  • líneas 64-67: implementación de la interfaz [IDao]. Aquí no hay nada más que hacer que delegar el trabajo a la capa [DAO] de la línea 23;

2.8.3.6. El estado del fragmento [Vue1Fragment]

  

La clase [Vue1FragmentState] será la siguiente:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

public class Vue1FragmentState extends CoreState {

  // estado del fragmento ------------------------
  // lista de respuestas
  private List<String> reponses = new ArrayList<>();
  // estado de la vista ------------------------
  // mensaje de error sobre el número de nombres aleatorios solicitados
  private boolean txtErrorAleasVisible = false;
  // mensaje de error sobre el intervalo de generación [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // mensaje de error sobre el URL del servicio web
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // mensaje de error sobre el tiempo de espera
  private boolean textViewErreurDelayVisible = false;
  // Estado visible o no del botón «Ejecutar»
  private boolean btnExecuterVisible = true;

  // getter y setter
...
}

Para determinar qué información debía almacenarse en el fragmento, se hicieron girar el dispositivo en diversas situaciones y se observó qué había desaparecido al restaurarlo. Se llegó a la conclusión de que era necesario almacenar la información de las líneas 10 a 23.

2.8.3.7. El fragmento [Vue1Fragment]

  

Actualmente, la vista [Vue1Fragment] presenta varios errores debido a que la clase padre [AbstractFragment] de la que deriva ha cambiado. En lugar de describir uno por uno los cambios que hay que hacer, comentaremos directamente la versión final.

La estructura básica del fragmento es la siguiente:


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 {

...
}
  • En la línea 26, se recuerda que todo fragmento debe tener un menú, aunque esté vacío. Este es el caso aquí.

2.8.3.7.1. Manejo del clic en el botón [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // se verifican los datos ingresados
    if (!isPageValid()) {
      return;
    }
    // se borran las respuestas anteriores
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // se pone a 0 el contador de respuestas
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // inicialización de la actividad
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // se prepara la tarea aleatoria
    beginWaiting(1);
    // se solicitan los números aleatorios
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // se crea el proceso a observar
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // se solicitan los números aleatorios
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // se procesa la respuesta
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // una respuesta de +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // se analiza la respuesta
    // ¿Error?
    if (response.getStatus() != 0) {
      // visualización
      showAlert(response.getMessages());
      // cancelación
      doAnnuler();
      // regreso a la interfaz de usuario
      return;
    }
    // se agrega la información a la lista de respuestas
    reponses.add(0, String.valueOf(response.getBody()));
    // se actualizan las respuestas
    dataAdapterReponses.notifyDataSetChanged();
  }

  // cancelación ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // se cancelan las tareas asincrónicas
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // se muestra el reloj de arena
    beginRunningTasks(nbRunningTasks);
    // el botón [Annuler] reemplaza al botón [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • líneas 4-6: primero se verifica que las entradas sean válidas. En ese caso, pueden aparecer mensajes de error;
  • líneas 8-9: se vacía la lista de respuestas. Este cambio se refleja en el ListView, que las muestra;
  • líneas 11-12: el número de respuestas recibidas se pone a cero;
  • línea 14: se establece el URL del servicio de números aleatorios. Esta información se transmitirá a la capa [DAO];
  • línea 15: se establece el tiempo de espera antes de realizar la solicitud al servicio de números aleatorios. Esta información se transmitirá a la capa [DAO];
  • línea 17: nos preparamos para iniciar 1 tarea asíncrona (y no N, ya veremos por qué);
  • líneas 24-27: de las N tareas asincrónicas, se convierte cada una en una secuencia de operaciones [merge];
  • líneas 29-36: se le pide a la clase padre [AbstractParent] que consulte el servicio web / jSON para obtener números aleatorios;
  • líneas 29-36: el método [executeInBackground] espera dos parámetros:
    • línea 29: el proceso que se debe observar y ejecutar es el que se calculó en las líneas anteriores;
    • líneas 29-36: la instancia [Action1] que se ejecutará al recibir la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getAlea], es decir, un tipo [Response<Integer>];
  • línea 34: cuando llega una respuesta (un número aleatorio), se procesa en el método de la línea 39;
  • líneas 49-50: se registra y se indica que se ha recibido una nueva respuesta;
  • líneas 53-60: el tipo [Response<T>] tiene un campo [status] que es un código de error. Si este código es distinto de cero, entonces el servidor ha tenido un problema;
  • línea 55: se muestra un mensaje de error. El método [showAlert] pertenece a la clase padre;
  • línea 57: se invoca el método de las líneas 68 a 75. Este cancelará las tareas que aún estén activas (línea 74);
  • línea 62: la respuesta se agrega a la lista de respuestas, que es la fuente de datos de ListView;
  • línea 64: se actualiza el ListView;
  • líneas 77-83: el método [beginWaiting(int nbRunningTasks)] prepara la vista para la espera (líneas 81-82) y le comunica a la clase padre que las tareas de [nbRunningTasks] se ejecutarán pronto (línea 79);

2.8.3.7.2. El ciclo de vida del fragmento

El ciclo de vida del fragmento se gestiona mediante los siguientes métodos:


  // datos locales
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // gestión del ciclo de vida ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // estado actual de la vista
    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) {
    // ¿Primera visita?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // fuente de datos de listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // Número de respuestas
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // enlace entre la vista de lista y el adaptador
    listReponses.setAdapter(dataAdapterReponses);
    // ¿Es tu primera visita?
    if (previousState == null) {
      // se ocultan los mensajes de error
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // los botones
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Estado anterior de la vista
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // Mostrar/ocultar mensajes de error
    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);
    // botones
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // número de respuestas
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // el botón [Exécuter] reemplaza al botón [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • líneas 7-18: se encargan de guardar el fragmento cuando la clase padre lo solicita;
  • línea 11: muestra el mensaje de error sobre el tiempo de espera;
  • línea 12: muestra el mensaje de error sobre el número de números aleatorios solicitados;
  • línea 13: muestra el mensaje de error sobre el URL del servicio web / jSON;
  • línea 14: visibilidad del mensaje de error sobre el intervalo [a,b] de generación de números aleatorios;
  • línea 15: visibilidad del botón [Exécuter];
  • línea 16: la lista de respuestas recibidas;
  • líneas 20-23: deben devolver el número de la vista. El número del fragmento aquí es 0, ya que solo hay uno;
  • líneas 25-38: inicialización de los campos del fragmento, ya sea en una primera visita (previousState==null) o en una visita posterior;
    • líneas 29-30: si no es la primera visita, el campo [reponses] se restaura a partir del estado anterior del fragmento;
    • líneas 31-33: si es la primera visita, entonces el campo [reponses] se inicializa con una lista vacía;
    • líneas 34-37: a partir del campo [reponses] se puede construir la fuente de datos del ListView del fragmento (línea 35), así como el número de respuestas (línea 37);
  • líneas 40-55: se ejecutan para inicializar la vista asociada al fragmento, ya sea en una primera visita (previousState==null) o en una visita posterior;
    • línea 43: se asocia el ListView del fragmento a la fuente de datos que acaba de crearse en el método [initFragment];
    • líneas 45-54: si se trata de la primera visita, se prepara la vista para su primera visualización;
  • líneas 57-60: se ejecutan durante una navegación entre fragmentos asociada a una acción de tipo [SUBMIT]. En este caso, solo hay un fragmento y, por lo tanto, no hay navegación entre fragmentos;
  • líneas 63-76: se ejecutan durante una navegación entre fragmentos asociada a una acción de tipo [NAVIGATION] o bien durante un ciclo de guardado/restauración debido a una rotación del dispositivo u otra razón. Aquí, solo puede darse este último caso. Hay que recordar que, en todos los casos, [previousState] nunca es null;
  • línea 65: se convierte el estado anterior al tipo del estado del fragmento;
  • líneas 66-75: se utiliza el contenido del estado anterior para restaurar la vista;
  • líneas 78-81: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada que hacer;
  • líneas 83-89: se ejecutan cuando todas las tareas asincrónicas han finalizado. Aquí se oculta el botón [Annuler] para reemplazarlo por el botón [Exécuter];

2.8.3.8. Las pruebas

Se invita al lector a realizar las siguientes pruebas:

  • generar errores y hacer funcionar el dispositivo: los mensajes de error deben permanecer en pantalla;
  • obtener números aleatorios y ejecutar el dispositivo: los números aleatorios obtenidos deben permanecer en pantalla;
  • poner una espera de varios segundos y ejecutar el dispositivo durante la espera: las tareas deben haberse cancelado (esto se puede ver en los registros);

2.8.4. Ejemplo-22B

Retomamos aquí el ejemplo 22 para refactorizarlo según el modelo del proyecto [client-android-skel]. Recordemos que el proyecto [Exemple-22] gestiona correctamente el ciclo de guardado y restauración de los fragmentos durante una rotación y que fue este el que sirvió de base para el proyecto [client-android-skel].

Duplicamos el proyecto [client-android-skel] en [exemples/Exemple-22B] y cargamos este último proyecto:

  

Luego copiamos varios elementos del proyecto [Exemple-22] al proyecto [Exemple-22B].

En primer lugar, copiamos elementos de la carpeta [res]:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, la carpeta [values];
  

Modificaremos el margen superior de las dos vistas a 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"/>

Luego copiamos los elementos [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

En esta etapa, podemos intentar una primera compilación. Aparece un primer tipo de errores: los de los imports incorrectos porque algunas clases han cambiado de paquete. Corregimos estos imports. Un segundo tipo de errores se debe a que los fragmentos no implementan todos los métodos de su clase padre [AbstractFragment]. Se corrigen presionando (Alt+Intro).

Los errores restantes se deben a las diferencias existentes entre la clase antigua y la nueva [AbstractFragment]. Por el momento, se ignoran.

2.8.4.1. Personalización del proyecto

  

En la carpeta [custom] se encuentran los elementos de arquitectura que el desarrollador puede personalizar.

La interfaz [IMainActivity] permite especificar ciertas características del proyecto:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // acceso a la sesión
  ISession getSession();

  // cambio de vista
  void navigateToView(int position, ISession.Action action);

  // gestión de la espera
  void beginWaiting();

  void cancelWaiting();

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;

  // tiempo máximo de espera para la respuesta del servidor
  int TIMEOUT = 1000;

  // tiempo de espera antes de ejecutar la solicitud del cliente
  int DELAY = 0;

  // autenticación básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de pestañas
  boolean ARE_TABS_NEEDED = true;

  // imagen de espera
  boolean IS_WAITING_ICON_NEEDED = false;

  // número de fragmentos
  int FRAGMENTS_COUNT = 5;

}
  • líneas 23, 26, 29, 38: características de la capa [DAO]. Aquí no hay ninguna;
  • línea 41: aquí hay cinco fragmentos;
  • línea 32: adyacencia de los fragmentos. Esta constante puede tener aquí un valor en [1,4]. Se recomienda al lector que varíe este valor para ver si la aplicación sigue funcionando;
  • línea 35: se trata de una aplicación con pestañas;

La clase [CoreState] que almacena el estado de los fragmentos será la siguiente:


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 {
  // fragmento visitado o no
  protected boolean hasBeenVisited = false;
  // estado del posible menú del fragmento
  protected MenuItemState[] menuOptionsState;

  // getters y setters
...
}
  • línea 12: declaramos la clase del estado del fragmento [PlaceHolderFragment]. El fragmento [Vue1Fragment], por su parte, no tiene estado;

La clase [Session] es la siguiente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // datos que se deben compartir entre los propios fragmentos y entre los fragmentos y la actividad
  // los elementos que no se pueden serializar en jSON deben tener la anotación @JsonIgnore
  // no olvide los getters y setters necesarios para la serialización/deserialización en jSON

  // número de fragmentos visitados
  private int numVisit;
  // N.º del fragmento de tipo [PlaceholderFragment] que se muestra en la segunda pestaña
  private int numFragment = -1;

  // getters y setters
...
}

Esta es la sesión del proyecto [Exemple-22].

2.8.4.2. La actividad [MainActivity]

  

La actividad [MainActivity] es la siguiente:


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 {

  // capa [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sesión
  private Session session;

  // gestión del menú-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

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

  // Implementación de métodos de la clase padre ---------------------------------------------------
  ...
}

En este caso, la clase [MainActivity] es más extensa que la de los ejemplos anteriores por dos razones:

  • hay pestañas que administrar;
  • hay un menú que se debe manejar;

2.8.4.2.1. Implementación de los métodos de la clase padre

// métodos de la clase padre -----------------------
  @Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos con las inicializaciones iniciadas por la clase padre
    // sesión
    this.session = (Session) super.session;
    ...
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // N.º de fragmento
    final String ARG_SECTION_NUMBER = "section_number";
    // inicialización de la tabla de fragmentos
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // se crea un fragmento
      fragments[i] = new PlaceholderFragment_();
      // se pueden pasar argumentos al fragmento
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // un fragmento de +
    fragments[i] = new Vue1Fragment_();
    // resultado
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // aquí no hay títulos
    return null;
  }

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

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • líneas 2-12: el método [onCreateActivity] es llamado por la clase padre [AbstractActivity] cuando la actividad se crea por primera vez o se vuelve a crear durante un ciclo de guardado/restauración. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
  • línea 10: se recupera una referencia local de la sesión. El cambio de tipo se debe a que la sesión de la clase padre es de tipo [AbstractSession];
  • líneas 19-38: el método [getFragments] debe devolver a la clase padre la matriz de fragmentos gestionados por la aplicación. Aquí hay [FRAGMENTS_COUNT], número definido en [IMainActivity]. Los primeros fragmentos [FRAGMENTS_COUNT-1] son de tipo [PlaceHolderFragment] y el último es de tipo [Vue1Fragment];
  • líneas 41-45: el método [getFragmentTitle] debe mostrar los títulos de los fragmentos cuando esa información pueda ser útil. Este no es el caso aquí;
  • líneas 47-50: este método es invocado por la clase padre cuando el usuario hace clic en una pestaña. Volveremos a esto en el siguiente párrafo;
  • líneas 52-55: devuelve el número de la primera vista que se debe mostrar al iniciar la aplicación. En este caso, el fragmento [Vue1Fragment] es el que debe mostrarse primero. El método [getFirstView] podría sustituirse ventajosamente por una constante en [IMainActivity];

2.8.4.2.2. Administración de pestañas

Las pestañas se administran mediante los siguientes métodos:


@Override
  protected void onCreateActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // continuamos con las inicializaciones iniciadas por la clase padre
    // sesión
    this.session = (Session) super.session;
    // primera pestaña
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // ¿Segunda pestaña?
    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.º del fragmento a mostrar
    int numFragment;
    switch (position) {
      case 0:
        // N.º del fragmento [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // N.º de fragmento [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // visualización del fragmento
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • líneas 1-20: el método [onCreateActivity] es llamado por la clase padre [AbstractActivity] cuando la actividad se crea por primera vez o se vuelve a crear durante un ciclo de guardado/restauración. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
  • línea 9: se obtiene una referencia local de la sesión. El cambio de tipo se debe a que la sesión de la clase padre es de tipo [AbstractSession];
  • líneas 11-13: se crea la primera pestaña;
  • líneas 15-20: se crea la segunda pestaña si hay un número de fragmento registrado en la sesión (línea 15). Este número tiene inicialmente el valor -1 al crearse la actividad por primera vez;
  • líneas 23-39: la clase padre llama a este método cuando el usuario hace clic en una pestaña;
  • líneas 28-31: si se hace clic en la pestaña 0, entonces se debe mostrar [Vue1Fragment]. Sabemos que esta es la primera vista que se mostró al iniciar la aplicación;
  • líneas 32-35: si se hace clic en la pestaña 1, entonces se debe mostrar el fragmento cuyo número está registrado en la sesión;
  • líneas 37-39: se navega hacia el fragmento seleccionado. La acción asociada es [SUBMIT]. ¿Podría haber sido [NAVIGATION]? En este documento, se utiliza [NAVIGATION] únicamente cuando la visualización del nuevo fragmento solo requiere conocer su estado anterior. Aquí no es el caso, ya que la visualización del fragmento debe cambiar con respecto a su estado anterior para mostrar una visita más;

2.8.4.2.3. Administración del menú

La actividad está asociada al siguiente menú [menu_main.xml]:


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

que muestra lo siguiente:

  

La gestión del menú se lleva a cabo mediante los siguientes métodos:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // procesamiento de las opciones del menú
    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;
      }
    }
    // elemento procesado
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // sin navegación al seleccionar una pestaña mediante el software
      session.setNavigationOnTabSelectionNeeded(false);
      // Se vuelven a crear las dos pestañas por un problema con la fuente de los títulos
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // se establece en la sesión el número del fragmento que se mostrará
      session.setNumFragment(i);
      // se selecciona la pestaña n.º 2 con navegación
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • líneas 16-31: gestión del clic en una opción de menú de tipo [Fragmenti];
  • líneas 37-50: muestran el fragmento n.º i (se trata de fragmentos del tipo PlaceHolderFragment) en la pestaña n.º 1 (segunda pestaña);
  • líneas 42-44: se decide eliminar las pestañas existentes para volver a crear dos nuevas. Esta decisión se tomó para solucionar el siguiente problema: cuando solo se muestra el fragmento en la pestaña 1 existente (sin eliminarla), curiosamente su título tiene un aspecto (tipo de letra, tamaño) diferente al del título de la pestaña 0;
  • líneas 43-44: se crean las dos pestañas, pero no se seleccionan (último parámetro en false);
  • línea 40: las operaciones de las líneas 42-44 pueden realizar operaciones [select] en las pestañas, lo que llamará al administrador [onTabSelected]. Si no se hace nada, se producirá la navegación hacia un fragmento. Esto se evita estableciendo el valor booleano [navigationOnTabSelectionNeeded] en faux en la sesión. La clase [AbstractFragment] restablece automáticamente este valor booleano a vrai cuando un fragmento se vuelve visible;
  • línea 46: se registra el número del fragmento que se va a mostrar en la sesión;
  • líneas 48-50: se selecciona la pestaña n.º 2 con navegación (línea 48). Esto activará el procedimiento [onTabSelected], el cual:
    • mostrará el fragmento cuyo número se ha registrado en la sesión;
    • almacenará en la sesión el número de la pestaña seleccionada;

2.8.4.3. El fragmento [Vue1Fragment]

A continuación presentamos la versión final del fragmento:


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 {

  // los elementos de la interfaz visual
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // gestor de eventos
  @Click(R.id.buttonValider)
  protected void doValider() {
    // se muestra el nombre ingresado
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // Ciclo de vida del fragmento -----------------------------------------------
  private void initFragment() {
    // No hay nada que hacer
  }

  // guardar estado del fragmento
  @Override
  public CoreState saveFragment() {
    // Estado de la vista: no hay nada que guardar
    return new CoreState();
  }

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

  @Override
  protected void initFragment(CoreState previousState) {
    // no hay nada que hacer
  }

  @Override
  protected void initView(CoreState previousState) {
    // ¿Primera visita?
    if (previousState == null) {
      // se muestra el número de visita
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // se muestra el número de la visita
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // métodos privados -------------------------------------
  // Mostrar número de visita
  private void showNumVisit() {
    // incrementa el número de visita
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // se muestra el número de visita
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

La clase está prácticamente vacía.

  • líneas 35-39: llamadas por la clase padre cuando el fragmento debe guardar su estado. El fragmento [Vue1Fragment] no tiene ningún estado que guardar. Simplemente se devuelve una instancia de la clase base [CoreState] (recuerde: no se debe devolver null);
  • líneas 41-44: deben devolver el número del fragmento. El fragmento [Vue1Fragment] tiene, por definición, el número [FRAGMENTS_COUNT-1];
  • líneas 51-59: son llamadas por la clase padre cuando el fragmento se construye por primera vez (previousState==null) o en las siguientes ocasiones (previousState!=null);
    • líneas 54-57: si es la primera visita, se incrementa el número de visita y se muestra (líneas 85-92);
  • líneas 61-65: se ejecutan cuando el fragmento se va a mostrar asociado a una acción [SUBMIT]. Se incrementa el número de visita y se muestra. Aquí no es posible que el número de visita se incremente dos veces durante el ciclo de vida. De hecho, la primera visita al fragmento [Vue1Fragment] se realiza al iniciar la aplicación, cuando la acción es [NONE] por diseño en la sesión. Esto garantiza que no se llame al método [updateOnSubmit]. Posteriormente, ya no será nunca más la primera visita y el método [initView] no hará nada;
  • líneas 68-71: se invocan en un ciclo de guardado/restauración. Como el fragmento no tiene estado, aquí no hay nada que restaurar;
  • líneas 73-76: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada más que hacer;
  • líneas 78-81: se invocan cuando todas las tareas asincrónicas iniciadas han finalizado. Aquí no hay tareas asincrónicas;

2.8.4.4. El estado [PlaceHolderFragmentState]

El estado del fragmento [PlaceHolderFragment] será el siguiente:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

  // constructores
  public PlaceHolderFragmentState() {

  }

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

  // getters y setters
 ...
}
  • cuando sea necesario guardar el estado del fragmento, se guardará el texto que mostraba (línea 7);

2.8.4.5. El fragmento [PlaceHolderFragment]

El fragmento [PlaceHolderFragment] será el siguiente:


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 {

  // componentes de la interfaz visual
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // datos
  private String text;

  // N.º de fragmento
  private static final String ARG_SECTION_NUMBER = "section_number";

  // Implementación de métodos de la clase padre ----------------------------
  @Override
  public CoreState saveFragment() {
    // se guarda el estado del fragmento
    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) {
    // texto original
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // se actualiza el texto que se muestra
    // incremento del número de visitas
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texto modificado
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // registro
    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) {
    // se restaura el texto que se muestra
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • líneas 30-36: cuando la clase padre le pide al fragmento que guarde su estado, se guarda el texto que muestra el fragmento (línea 34);
  • líneas 38-41: devuelven el número del fragmento. Este depende del número de sección que se le haya pasado como argumento al crearlo;
  • líneas 43-47: se invocan durante la primera construcción del fragmento (previousState==null) o durante las siguientes (previousState !=null);
    • línea 46: aquí no se utiliza el estado anterior. El texto inicial [text] (línea 24), que se muestra en la primera visita, se vuelve a calcular cada vez. Esto es discutible. Se podría haber optado por incluir también esta información en el estado del fragmento;
  • líneas 49-51: se invocan durante la primera construcción de la vista asociada al fragmento (previousState==null) o durante las siguientes (previousState!=null). No hay nada que hacer;
  • líneas 53-56: se invocan cuando el fragmento se va a mostrar asociado a una acción [SUBMIT]. Esto siempre es así, excepto en el ciclo de guardado/restauración, donde la acción es [RESTORE]. Por lo tanto, se incrementa el número de visita y se muestra;
  • líneas 68-74: se ejecutan en un ciclo de guardado/restauración. Se restaura el texto que se había guardado en el estado del fragmento;
  • líneas 76-79: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada más que hacer;
  • líneas 82-83: se invocan cuando todas las tareas asincrónicas iniciadas han finalizado. Aquí no hay tareas asincrónicas;

2.8.4.6. Tests

Se invita al lector a probar la aplicación girando el dispositivo para verificar que el fragmento mostrado no pierda su estado. También revisaremos los registros.

2.9. Conclusion

Al finalizar este capítulo, contamos con un proyecto modelo [client-android-skel] de un cliente Android que se comunica con un servicio web / jSON con las siguientes características:

  • la comunicación asíncrona con el servidor web / jSON se realiza mediante la biblioteca RxJava;
  • el ciclo de vida de un fragmento (actualizar, guardar, restaurar) es administrado por su clase padre [AbstractFragment], la cual invoca, en momentos específicos, ciertos métodos de sus clases hijas. De esta manera, el fragmento hijo no tiene que preocuparse por las etapas del ciclo de vida, sino solo por implementar ciertos métodos exigidos por su clase padre;
  • el ciclo de vida de la actividad (guardar / restaurar) es administrado por una clase abstracta, [AbstractActivity], que a su vez exige que la actividad hija implemente ciertos métodos;
  • la clase [AbstractActivity] es capaz de gestionar una aplicación con o sin pestañas, con o sin imagen de espera, con o sin autenticación básica en el servidor web / jSON. La presencia o ausencia de estos elementos se configura;

Ahora presentaremos un caso de estudio más complejo que los ejemplos anteriores. La nueva aplicación se basará en el proyecto modelo [client-android-skel].