Skip to content

2. Esqueleto de un cliente Android comunicándose con un servicio web / JSON

Ahora proporcionamos un esqueleto para una aplicación Android que se comunica con uno o más servicios web / JSON. Se trata del proyecto [client-android-skel], que se encuentra en la carpeta [architecture] de los ejemplos:

  

El estudio de este esqueleto de aplicación nos dará la oportunidad de repasar ciertos puntos que hemos encontrado en los ejemplos anteriores. Esta aplicación servirá de esqueleto para todas las aplicaciones futuras. Se ha creado tras numerosas iteraciones. Su objetivo es integrar en clases abstractas el mayor número posible de elementos de las aplicaciones que construiremos próximamente, para evitar tener que escribir una y otra vez el mismo tipo de código, diferenciándose únicamente en los detalles. Sus características son las siguientes:

  • la comunicación asíncrona con el servidor web/JSON se gestiona mediante la biblioteca RxJava;
  • el ciclo de vida de un fragmento (actualizar, guardar, restaurar) es gestionado por su clase padre [AbstractFragment], que llama a determinados métodos de sus clases hijo en momentos específicos. De este modo, la clase hija no tiene que preocuparse de las etapas del ciclo de vida, sino que sólo necesita implementar ciertos métodos requeridos por su clase padre;
  • el ciclo de vida de la actividad (guardar / restaurar) es gestionado por una clase abstracta [AbstractActivity], que también requiere 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 carga, y con o sin autenticación básica contra el servidor web/JSON. La presencia o ausencia de estos elementos viene determinada por la configuración;

Este esqueleto se utilizó para todos los ejemplos posteriores. Debido a su diversidad, lo que funcionaba para un ejemplo podía no funcionar para el siguiente. Como el esqueleto se utilizó para un total de siete ejemplos, se produjeron numerosas iteraciones. Si tuviéramos que utilizarlo para un octavo ejemplo, es posible que volviéramos a encontrarnos con que la naturaleza específica de este nuevo ejemplo genera nuevos errores. No obstante, la utilización de este esqueleto simplificará considerablemente la escritura de futuros ejemplos. En efecto, la gestión del ciclo de vida de un fragmento (actualizar, guardar, restaurar) combinada con el concepto de adyacencia de fragmentos es especialmente compleja. Aquí, está completamente oculto dentro de la clase [AbstractFragment].

2.1. Arquitectura cliente Android

El cliente Android propuesto se basa en la siguiente arquitectura:

  • la capa [DAO] implementa una interfaz [IDao]. Es responsable de la comunicación con el servidor web/JSON;
  • sólo hay una actividad que también implementa la interfaz [IDao]. Las vistas recurren a ella para acceder al servidor;
  • las vistas se implementan por fragmentos;

El proyecto Android refleja esta arquitectura:

  

Presentaremos uno a uno los distintos elementos de este proyecto.

2.2. La configuración de Gradle

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you must use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  // packaging options required to generate the 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, puede empezar con los números actuales si configura Android Studio para asegurarse de que estas versiones de las herramientas de Android (líneas 15-16, 47-48) están presentes (consulte la sección 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: Cambiar el paquete de aplicación;
  • Líneas 10, 15: Estableceremos el valor del elemento [app_name] en el archivo [res/values/strings.xml]. Por ahora, es el siguiente:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[Name your app]</string>
</resources>

2.4. La organización del código Java

  
  • la [arquitectura] agrupa los principales elementos de organización del código;
  • [activity] contiene la actividad única de la aplicación;
  • [fragmentos] agrupa los fragmentos o vistas de la aplicación;
  • [dao] agrupa los elementos para la 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>
 
  <!-- fragment container -->
  <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 tiene 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>

For now, it is empty. The developer will fill it in as needed.

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 {
 
  // controls swiping
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
  // constructors
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // methods to override to handle swiping
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // Is swiping allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Is swiping allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // scroll control
  @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: métodos que desactivan el barrido si se ha desactivado;
  • líneas 46-49: redefinición del método [setCurrentItem], que se utiliza para cambiar la vista mostrada. Si se ha desactivado el desplazamiento, la vista cambiará sin desplazamiento. Tenga en cuenta que el desarrollador puede anular este comportamiento utilizando el método [setCurrentItem(int position, boolean smoothScrolling)], que le permite especificar el comportamiento de desplazamiento deseado;

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)
// todo: add the subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // whether the fragment has been visited
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • línea 16: Cada fragmento tiene un booleano [hasBeenVisited] en su estado que indica si ya ha sido visitado o no. Esto es necesario porque a veces, cuando un fragmento se muestra 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 fragmentos si tienen uno. En el MenuItemState[] menuOptionsState almacenamos el estado visible u oculto de todas las opciones del menú;
  • Líneas 10-13: Como se hizo en [Ejemplo-22], el estado de la actividad y sus fragmentos se guardarán en la sesión, que a su vez se guardará como 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, que se derivan de [CoreState]. Para asegurar que se genera la cadena JSON del tipo derivado en lugar de la del tipo padre, los tipos derivados deben declararse como se muestra en las líneas 10-13. La clase [CoreState] es una de las clases de arquitectura que el desarrollador debe modificar para cada nueva aplicación (líneas 10-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 {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants (to be modified) -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum wait time for server response
  int TIMEOUT = 1000;
 
  // wait time before executing the client request
  int DELAY = 0;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // loading icon
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 0;
 
  // todo: add your constants and other methods here
}
  • línea 6: la interfaz [IMainActivity] extiende la interfaz [IDao] de la capa [DAO];
  • Línea 9: Esta es la actividad que proporciona acceso a la sesión en forma de una instancia de la interfaz [ISession];
  • línea 12: es la actividad utilizada para cambiar de vista. El segundo parámetro es la acción que desencadena este cambio de vista, uno de los valores SUBMIT, NAVIGATION, o RESTORE;
  • líneas 15-17: es la actividad que gestiona la pantalla de carga;
  • línea 22: para depurar la aplicación;
  • línea 25: para evitar esperar demasiado si el servidor deja de responder;
  • línea 28: durante la depuración, ajuste esto a unos pocos segundos para dar tiempo a cancelar la operación con el servidor y ver qué pasa;
  • línea 31: ajustado a verdadero si el servicio JSON requiere autenticación básica;
  • línea 34: adyacencia de fragmentos;
  • línea 37: establecer en verdadero si la aplicación tiene pestañas;
  • línea 39: ajustado a verdadero si la aplicación se comunica con un servidor web/JSON y desea mostrar una imagen de carga durante los intercambios;
  • línea 43: número de fragmentos gestionados por la aplicación;

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

2.5.5. La interfaz [IDao]

La interfaz [IMainActivity] amplía la siguiente interfaz [IDao]:

  

package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // User
  void setUser(String user, String password);
 
  // Client timeout
  void setTimeout(int timeout);
 
  // Basic authentication
  void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // todo: declare your interface here
}
  • línea 24: el desarrollador completará la interfaz aquí;

2.5.6. La Sesión

  

La clase [Session] encapsula 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 {
 
  // number of the last view displayed
  int getPreviousView();
 
  void setPreviousView(int numView);
 
  // Last state of a view
  CoreState getCoreState(int numView);
 
  void setCoreState(int numView, CoreState coreState);
 
  // current action
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }
 
  Action getAction();
 
  void setAction(Action action);
 
  // States of all views -
  // Not used by the code but required for JSON serialization/deserialization
  CoreState[] getCoreStates();
 
  void setCoreStates(CoreState[] coreStates);
 
  // number of the last selected tab
  int getPreviousTab();
 
  void setPreviousTab(int position);
 
  // navigation on tab selection
  boolean isNavigationOnTabSelectionNeeded();
 
  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

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

  • líneas 7-10: el número de la última vista (fragmento) visualizada;
  • líneas 12-15: el estado de una vista concreta;
  • líneas 17-24: introducimos el concepto de acción en curso. Hay cuatro (línea 17):
    • RESTORE: una operación de guardar/restaurar está en curso. No hay ningún cambio de vista;
    • NAVIGATION: la navegación está en curso. Aquí, definimos la navegación como un cambio de vista en el que la nueva vista se puede restaurar desde su último estado guardado durante la sesión;
    • SUBMIT: asignamos el tipo [SUBMIT] a una acción pendiente cuando hay un cambio de vista y la nueva vista depende del estado global de la actividad, no sólo de su propio estado. A veces, es difícil distinguir entre NAVIGATION y SUBMIT. En tales casos, utilizaremos el caso más general de SUBMIT;
    • NONE: el valor de la acción cuando aún no ha recibido su primer valor;
  • líneas 26-30: los estados de la actividad y los fragmentos se almacenarán en un CoreState[] array. Para asegurar que se maneja correctamente durante la serialización/deserialización JSON, debe tener un getter y un setter;
  • líneas 32-35: número de la última pestaña seleccionada. Se utiliza durante el ciclo de guardar/restaurar para volver a seleccionar la pestaña que estaba seleccionada antes de girar el dispositivo;
  • líneas 37-40: gestiona un 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 {
  // Previous view ID
  private int previousView;
 
  // View state
  private CoreState[] coreStates = new CoreState[0];
 
  // current action
  private Action action = Action.NONE;
 
  // previously selected tab
  private int previousTab;
 
  // navigate to selected tab
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;
 
  // constructor
  public AbstractSession() {
    // initialize the fragment state array
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }
 
 
  // ISession interface ---------------------------------------------------------
  @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 ID de la vista que se mostró antes de la que se muestra actualmente. Esta información es útil cuando se puede acceder a una vista desde varias ubicaciones. Este es el caso típico de la navegación basada en pestañas. La vista mostrada puede entonces determinar qué vista se mostró anteriormente;
  • línea 12: la matriz de estados para todos los fragmentos mostrados por la actividad;
  • línea 18: el ID de la pestaña previamente seleccionada. Desempeña un papel similar al de la vista anterior ID en la línea 9. Esta información es útil cuando se rota el dispositivo y se necesita volver a la pestaña que estaba seleccionada antes de la rotación;
  • línea 22: un booleano que indica si la selección de una pestaña debe provocar un cambio en el fragmento mostrado. Tenga en cuenta que el proyecto [client-android-skel] gestiona las pestañas y los fragmentos por separado para que pueda utilizarse en los casos en que el número de pestañas sea inferior al número de fragmentos. Existen dos tipos de selección:
    • una selección realizada por el usuario al hacer clic en una pestaña. En este caso, suele ser necesario cambiar el fragmento mostrado;
    • una selección por software mediante el método [Tablayout.Tab.select()]. En este caso, no siempre es deseable cambiar el fragmento visualizado. He aquí dos ejemplos:
      • cuando se gira el dispositivo, la actividad se vuelve a crear, al igual que las pestañas. Sin embargo, cuando se crea la primera pestaña, ésta se somete automáticamente a una operación [select] de software. Por lo tanto, no es conveniente cambiar el fragmento visualizado, ya que nos encontramos en una fase de recreación de la actividad en la que el fragmento visualizado finalmente no será necesariamente el asociado a la primera pestaña;
      • dado que la gestión de pestañas es independiente de la gestión de fragmentos, es posible que desee actualizar las pestañas (eliminar, añadir) sin interferir con sus fragmentos asociados. Sin embargo, algunas de estas operaciones pueden desencadenar de nuevo una operación de software [seleccionar] implícita en una de las pestañas. Esta selección no implica necesariamente la navegación hacia el fragmento asociado;
  • línea 21: el campo [navigationOnTabSelectionNeeded] no debe guardarse durante las operaciones de guardado de la actividad y sus fragmentos. La anotación [@JsonIgnore] hace que el campo sea ignorado durante la serialización/deserialización JSON;
  • líneas 25-31: El constructor inicializa el array de estados para los fragmentos [FRAGMENTS_COUNT] de la aplicación. Los elementos de este array se inicializan con el campo [hasBeenVisited=false]. Esta información se utiliza para determinar si se trata o no de la primera visita al fragmento;

La clase [Sesión] es la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and the activity
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
  // Don't forget the getters and setters required for JSON serialization/deserialization
}
  • línea 5: la clase [Session] extiende la clase [AbstractSession] que acabamos de ver. El desarrollador colocará aquí los elementos a compartir entre los propios fragmentos y entre los fragmentos y la actividad. Observa que la clase [Session] ya no está anotada con la anotación [@EBean]. Se ha convertido en una clase normal;

2.5.7. La clase abstracta [AbstractActivity]

  

2.5.7.1. Esqueleto

La clase [AbstractActivity] es una clase con más de 300 líneas. La examinaremos paso a paso. Su esqueleto es el 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 {
  // [DAO] layer
  private IDao dao;
  // the session
  protected Session session;
 
  // fragment container
  protected MyPager mViewPager;
  // the toolbar
  private Toolbar toolbar;
  // the loading image
  private ProgressBar loadingPanel;
  // tab bar
  protected TabLayout tabLayout;
 
  // the fragment or section manager
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // class name
  protected String className;
  // JSON mapper
  private ObjectMapper jsonMapper;
 
  // constructor
  public AbstractActivity() {
    // class name
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructor");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }
 
  // IMainActivity implementation --------------------------------------------------------------------
  ...
 
  // lifecycle - saving/restoring the activity ------------------------------------
  ...
 
  // handling the splash screen ---------------------------------
  ...
 
  // IDao interface -----------------------------------------------------
  ...
 
  // the fragment manager --------------------------------
  ...
 
  // child classes
  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);
  • se encarga de guardar y restaurar la actividad y sus fragmentos cuando el dispositivo gira (línea 58);
  • maneja la pantalla de carga durante la comunicación con el servidor web / JSON (línea 61);
  • implementa la interfaz IDao de la capa [DAO] (línea 64);
  • implementa el gestor de fragmentos (línea 67);
  • requiere que sus clases hijas tengan seis métodos (líneas 71-81);

2.5.7.2. Implementación de la interfaz [IMainActivity]

La implementación de la interfaz [IMainActivity] (véase la sección 2.5.4) es la siguiente:


  // IMainActivity implementation --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigating to view %s on action %s", position, action));
    }
    // display new fragment
    mViewPager.setCurrentItem(position);
    // Record the current action during this view change
    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 está totalmente contenido en la sesión. Por lo tanto, necesitamos guardar la sesión. Aquí, reutilizamos lo que se hizo en el proyecto [Ejemplo-22] (ver sección 1.23):


  // Activity save/restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // Save session as a JSON string
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}

2.5.7.4. Restauración del estado de la actividad y sus fragmentos

Se trata de restaurar la sesión. Se procede como se muestra en [Ejemplo-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // Anything to restore?
    if (savedInstanceState != null) {
      // restore session
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // session
      session = new Session();
    }
...
  • líneas 10-26: si el parámetro [Bundle savedInstanceState] de la línea 2 no es null, se restablece la sesión (líneas 12-17);
  • líneas 26-29: si el parámetro [Bundle savedInstanceState] de la línea 2 es null, esto corresponde a la primera vez que se lanza la actividad. A continuación, se crea una sesión vacía;

2.5.7.5. Inicialización de la capa [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // [DAO] layer
    dao = getDao();
    if (dao != null) {
      // [DAO] layer configuration
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentication(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // child classes
  protected abstract IDao getDao();
....
}
  • línea 11: se solicita a la actividad hija una referencia a la capa [DAO] (línea 21);
  • líneas 14-17: si la capa [DAO] existe, se configura utilizando 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 la sección 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>
 
  <!-- fragment container -->
  <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) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // associated view
    setContentView(R.layout.activity_main);
    // view components ---------------------
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // loading icon?
    if (IS_WAITING_ICON_NEEDED) {
      // add the loading image
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // Create ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // Add the ProgressBar to the toolbar
      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: adición opcional de un icono de carga: si el booleano [IS_WAITING_ICON_NEEDED] es verdadero en la interfaz [IMainActivity];
  • línea 23: creación de la imagen de carga de tipo [ProgressBar]a la que hace referencia el campo [loadingPanel];
  • línea 24: inicialmente, esta imagen está oculta;
  • línea 26: se añade a la barra de herramientas;

2.5.7.7. Gestión de pestañas

La interfaz [IMainActivity] puede solicitar una barra de pestañas. Esta se añade y gestiona de la siguiente manera:


// tab bar
  protected TabLayout tabLayout;
...
 
    // tab bar?
    if (ARE_TABS_NEEDED) {
      // add the tab bar
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // no navigation on selection until a fragment is displayed
      session.setNavigationOnTabSelectionNeeded(false);
      // create tab bar
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // Add the tab bar to the app bar
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // Event handler for the tab bar
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // A tab has been selected
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected #%s, action=%s, tabCount=%s, isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // tab position
            int position = tab.getPosition();
            // memory
            session.setPreviousTab(position);
            // display associated fragment?
            navigateOnTabSelected(position);
          }
        }
 
        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
 
        }
 
        @Override
        public void onTabReselected(TabLayout.Tab tab) {
 
        }
      });
    }
 
...
  // child classes
  protected abstract void navigateOnTabSelected(int position);
...
  • líneas 12-48: añadir y gestionar una barra de pestañas;
  • línea 6: la barra de tabulación se añade si la constante [ARE_TABS_NEEDED] se establece en verdadero en la interfaz [IMainActivity];
  • línea 12: al crear la barra de pestañas, pueden producirse operaciones implícitas [Tablayout.Tab.select] (no son activadas por el usuario). Establecemos el booleano [session.navigationOnTabSelectionNeeded] a falso para evitar cualquier navegación durante estas falsas selecciones. Dependerá del desarrollador seleccionar el fragmento a mostrar utilizando el método [navigateToView]. El booleano [session.navigationOnTabSelectionNeeded] será devuelto a verdadero cuando se muestra este fragmento (véase AbstractFragment clase);
  • línea 14: creación de una barra de pestañas referenciada por el campo [tabLayout]. Utilizamos una barra de pestañas personalizada [CustomTabLayout], de la que hablaremos 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 ésta está seleccionada;
    • línea (d): el color del título de la pestaña cuando no está seleccionada;

Este archivo es, por supuesto, editable. Puede encontrar los códigos de color hexadecimales aquí, por ejemplo.

  • líneas 17-18: añadir esta barra de pestañas a la barra de aplicaciones en la vista [activity_main] XML;
  • líneas 20-47: manejador de eventos para la barra de pestañas;
  • líneas 22-36: sólo se gestiona el evento [onTabSelected]. Corresponde a un clic en la [pestaña tab] pasada como parámetro al método o a una operación software [TabLayout.Tab.select];
  • línea 30: posición de la pestaña seleccionada;
  • línea 32: esta posición se almacena en la sesión;
  • línea 34: ahora debe mostrarse el fragmento asociado a esta pestaña. Sólo la clase hija (línea 52) puede hacer esta asociación. Nótese que no asociamos la barra de pestañas al contenedor de fragmentos [mViewPager] como se hacía en algunos de los ejemplos estudiados. Aquí, separamos completamente la gestión de la barra de pestañas de la de los fragmentos. Por eso, cuando se hace clic en una pestaña, debemos especificar qué vista queremos que se muestre;
  • línea 28: distinguimos entre selección de pestaña con o sin navegación. Generalmente, cuando el usuario hace clic en una pestaña, se espera que haya navegación, mientras que durante una selección programática, no. El desarrollador distingue entre estos dos casos utilizando 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. Corresponde al promotor hacerlo;

2.5.7.8. El gestor de pestañas [CustomTabLayout]

  

Utilizamos un gestor de pestañas personalizado para mostrar los títulos de las pestañas en diferentes fuentes. 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 fuente para los títulos de las pestañas se personaliza en las líneas 30 y 44;

La carpeta [fonts] es la siguiente:

  

Fuentes:

2.5.7.9. Últimas inicializaciones


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // instantiate the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // the fragment container is associated with the fragment manager
    // i.e., fragment #i in the fragment container is fragment #i returned by the fragment manager
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // Disable swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // fragment adjacency
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // display the first view
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // Handle over to the child activity
    onCreateActivity();
  }
...
  // child classes
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • líneas 10-19: Este código se encuentra habitualmente en los ejemplos que hemos estudiado;
  • líneas 21-23: Visualización de la primera vista. Sin duda hay varias formas de distinguir este caso. Aquí, hemos utilizado el hecho de que para la primera vista, el valor de la acción que desencadena el cambio de vista es NONE;
  • línea 22: no hacemos ninguna suposición sobre el primer fragmento a mostrar. En nuestros ejemplos, a menudo ha sido el fragmento #0, pero no siempre (ver Ejemplo-22). Por lo tanto, pediremos a la actividad hija (línea 30) que nos diga de qué vista se trata;
  • línea 25: hemos factorizado todo lo que podíamos aquí. Ahora, la clase hija tiene que realizar sus propias inicializaciones (línea 29);

2.5.7.10. Tratamiento de la imagen de carga

En la clase [AbstractActivity], la imagen del marcador de posición se gestiona mediante los dos métodos siguientes:


  // managing the waiting image ---------------------------------
  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 la sección 2.5.5) se aplica del siguiente modo:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // [DAO] layer
  private IDao dao;
...
  // IDao interface -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }
 
  @Override
  public void setUser(String user, String password) {
    dao.setUser(user, password);
  }
 
  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
    dao.setBasicAuthentication(isBasicAuthenticationNeeded);
  }
 
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }
 
  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • Línea 3: Recordemos que el valor de este campo fue proporcionado por la actividad hija en el método [onCreate];

2.5.7.12. Implementación del gestor de fragmentos

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


...
  // the fragment manager --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    private AbstractFragment[] fragments;
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // fragments of the child class
      fragments = getFragments();
    }
 
    // must return the fragment at position
    @Override
    public AbstractFragment getItem(int position) {
      // return the fragment
      return fragments[position];
    }
 
    // Returns the number of fragments to manage
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // returns the title of the fragment at position
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }
 
  // child classes
  protected abstract AbstractFragment[] getFragments();
 
  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • línea 5: el array de fragmentos asociados a la actividad. Todos los fragmentos derivarán de la clase [AbstractFragment];
  • líneas 8-12: es el constructor que inicializa el array de fragmentos. Los solicita a la clase hija de la actividad (línea 35);
  • líneas 28-31: los títulos de los fragmentos pueden utilizarse en una aplicación en la que haya tantas pestañas como fragmentos. En este caso, se puede dar 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 poco antes de que la vista asociada a la actividad se haga visible. Se utiliza aquí para seleccionar una pestaña después de una operación de guardar/restaurar:


  @Override
  public void onResume() {
    // parent
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      log.d(className, "onResume");
    }
    // if restoring, then restore the last selected tab
    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 guardar/restaurar. Es importante señalar aquí que en el método [onCreate] -que, en el ciclo de vida de la actividad, se ejecuta antes que el método [onResume]- se ha deshabilitado 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. Resumen

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

La actividad infantil debe aplicar los seis métodos siguientes:


  // child classes
  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 actividad hija también tiene acceso a los siguientes miembros protegidos de su clase padre:


  // the session
  protected ISession session;
  // the fragment container
  protected MyPager mViewPager;
  // tab bar
  protected CustomTabLayout tabLayout;
  // class name
protected String className;

2.5.8. La actividad [MainActivity]

  

La clase [MainActivity] puede tener un nombre diferente. Su único requisito es implementar la interfaz [IMainActivity]. La clase proporcionada por defecto 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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define the fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // todo: tab navigation - define the view to display
  }
 
  @Override
  protected int getFirstView() {
    // todo: tab navigation - set the first view to display
    return 0;
  }
}
  • línea 14: para que la anotación AA [@Bean] de la línea 19 sea válida, la actividad debe tener la anotación AA [@EActivity];
  • línea 15: la actividad está asociada al menú XML [menu_main]. Actualmente, este menú está vacío. El desarrollador tendrá que rellenarlo si es necesario;
  • línea 16: la clase extiende la clase [AbstractActivity];
  • Líneas 19-20: una referencia a la capa [DAO]. Esta será instanciada por la librería AA antes de que este campo sea inicializado. Esto significa que el bean AA [Dao] debe existir. Este es siempre el caso con la aplicación esqueleto que proporcionamos. Incluso en una aplicación sin capa [DAO], puedes dejar el paquete [dao] en su lugar. Esto no causa ninguna complicación;
  • línea 22: la sesión como una instancia del tipo [Session]. La sesión existe en la clase padre [AbstractActivity] pero como una instancia de la interfaz [ISession] (línea 32);
  • líneas 24-63: los seis métodos requeridos 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], hemos previsto el caso en que la clase hija devuelva un null para indicar que no hay capa [DAO]. Si desea utilizar esta opción (no muy útil en mi opinión), aquí es donde debe establecer el puntero a nulo;

2.6. La capa [DAO]

Image

  

2.6.1. La interfaz IDao

Se introdujo en la sección 2.5.5:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // User
  void setUser(String user, String password);
 
  // Client timeout
  void setTimeout(int timeout);
 
  // Basic authentication
  void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before sending a request
  void setDelay(int delay);
 
  // todo: declare your interface here
}

El desarrollador añadirá los métodos para 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);
 
  // todo: declare the URLs to be accessed here
}

El desarrollador añadirá los métodos que se comunican con el URLs expuesto 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 {
 
  // user
  private String user;
  // password
  private String password;
 
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // HTTP headers of the intercepted HTTP request
    HttpHeaders headers = request.getHeaders();
    // Basic HTTP authentication header
    HttpAuthentication auth = new HttpBasicAuthentication(user, password);
    // Add to HTTP headers
    headers.setAuthorization(auth);
    // continue the HTTP request lifecycle
    return execution.execute(request, body);
  }
 
  // authentication elements
  public void setUser(String user, String password) {
    this.user = user;
    this.password = password;
  }
}

Esta clase genera la siguiente cabecera de autenticación HTTP:

Authorization: Basic code

donde [code] es la cadena 'user:mp' codificada en Base64. Esta clase sólo se utiliza si el servidor JSON espera esta forma de autenticación. Existen otras formas.

Nota: El uso de esta clase se ilustra en la sección 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 {
 
  // JSON mapper
  private ObjectMapper mapper = new ObjectMapper();
  // debug mode
  protected boolean isDebugEnabled;
  // class name
  protected String className;
  // delay before executing the request
  private int delay;
 
  // constructor
  public AbstractDao() {
    // class name
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructor, thread=%s", Thread.currentThread().getName()));
  }
 
  // protected methods ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    T getResponse();
  }
 
  // Generic request to a web service / JSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // service execution - waiting for a single response
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // wait?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // execute the synchronous request
          T response = request.getResponse();
          // log
          if (isDebugEnabled) {
            String log;
            if (response is of type String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s on thread [%s]", log, Thread.currentThread().getName()));
          }
          // send the response to the observer
          subscriber.onNext(response);
          // signal the end of the observable
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Server communication exception: %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Unexpected JSON error"));
            }
          }
          // throw an exception
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }
 
  // debug mode
  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 librería RxAndroid para devolver un tipo [Observable<T>]. A diferencia de algunos ejemplos vistos anteriormente, no devuelve un tipo [Response<T>] -que es un tipo propietario- sino cualquier tipo T;
  • línea 35: el método [getResponse] toma como parámetro una instancia del tipo [IRequest<T>] de las líneas 30-32, cuyo método [IRequest.getResponse()] obtiene el tipo T mediante una operación síncrona HTTP;
  • líneas 48-50: artificialmente, esperamos [delay] milisegundos. En producción, estableceremos [delay=0]. Durante la depuración, estableceremos [delay=unos segundos] para dar al usuario la oportunidad de cancelar la operación asíncrona y ver así cómo se comporta el código en ese caso;
  • línea 52: se solicita la respuesta esperada con una petición síncrona;
  • línea 64: una vez recibida la respuesta, se pasa al observador;
  • línea 66: indicamos que no habrá más emisiones. Este es el caso específico de una acción asíncrona que devuelve un solo elemento;
  • líneas 67-78: en caso de excepción, ésta se propaga 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 {
 
  // Web service client
  @RestService
  protected WebClient webClient;
  // security
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // the RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate factory
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // create the RestTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Set the JSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Set the RestTemplate for the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // Set the web service URL
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String password) {
    // Register the user in the interceptor
    authInterceptor.setUser(user, password);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthenticationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // private methods -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // todo: implement IDao
}
  • líneas 21-22: inyección del bean AA [WebClient], que se encargará de la comunicación con el servidor web / JSON;
  • líneas 24-25: inyección del interceptor de autenticación;
  • líneas 31-42: método ejecutado tras la inyección de los campos de las líneas 21-25;
  • línea 37: el objeto [RestTemplate], que gestiona la comunicación cliente/servidor, se crea a partir de un objeto fábrica. Esto no es estrictamente necesario, pero el fábrica nos permite configurar los tiempos de espera de la comunicación. Por eso no usamos el constructor sin parámetros [RestTemplate()];
  • línea 39: añadimos un conversor JSON a los conversores de [RestTemplate]. Éste será el único convertidor. Así, cuando un método del [WebClient] reciba una cadena JSON del servidor, será automáticamente deserializada en el objeto que el método debe devolver;
  • línea 41: el objeto [RestTemplate] configurado de esta forma se pasa al cliente web, que gestionará la comunicación cliente/servidor utilizándolo;
  • Líneas 44-48: Establecemos la raíz URL del servidor web/JSON. Todos los URLs declarados en la clase [WebClient] son relativos a este URL raíz;
  • líneas 50-54: este método permite especificar el propietario de la conexión cuando ésta se controla mediante autenticación básica (véase la sección 2.6.3);
  • líneas 56-64: establecer el tiempos de espera para los intercambios cliente/servidor. Esto se hace a través del objeto [RestTemplate] fábrica, que rige los intercambios;
  • líneas 66-78: este método especifica que el servidor está protegido por autenticación básica;
  • líneas 72-77: si se requiere autenticación básica, el interceptor de autenticación inyectado en la línea 25 se añade a los interceptores del objeto [RestTemplate]. Este interceptor añadirá automáticamente la cabecera de autenticación básica HTTP esperada por el servidor a todas las peticiones del cliente web;
  • El desarrollador implementará la interfaz [IDao] a partir de la línea 87;

2.7. 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 {
 
  // menu item ID
  private int menuItemId;
  // visibility of the option
  private boolean isVisible;
 
  // constructors
  public MenuItemState() {
 
  }
 
  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }
 
  // getters and setters
...
}

2.7.2. La clase [Utils]

La clase [Utils] contiene métodos de utilidad estáticos:


package client.android.architecture;
 
import java.util.ArrayList;
import java.util.List;
 
public class Utils {
 
  // list of messages from an exception - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // create a list containing the error messages from the exception stack
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }
 
  // List of messages for an exception - version 2
  static public String getMessageForAlert(Throwable th) {
    // build the text to display
    StringBuilder text = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      text.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return text.toString();
  }
 
  // list of messages for an exception - version 3
  static public String getMessageForAlert(List<String> messages) {
    // build the text to display
    StringBuilder text = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      text.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // result
    return text.toString();
  }
}

2.7.3. La clase padre [AbstractFragment]

La clase [AbstractFragment] contiene los elementos comunes a todos los fragmentos de la aplicación. Al igual que la clase [AbstractActivity], su código es complejo. También lo analizaremos paso a paso.

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 {
 
  // private data ------------------------------------------------------------
  // subscriptions to observables
  private List<Subscription> subscriptions = new ArrayList<>();
  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment state
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // Asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
 
  // data accessible to child classes ---------------------------------------
  // debug mode
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  protected String className;
  // asynchronous tasks
  protected int numberOfRunningTasks;
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
  // session
  protected Session session;
 
 
  // update Fragment ----------------------------------------------------------------------------------
 ...
 
  // Menu management ------------------------------------------
  ...
 
  // Queue management -------------------------------------------------------------
...
 
  // asynchronous operation management --------------------------------------------------------------------
...
 
  // exception handling -------------------------------------------------------------------
....
 
  // fragment lifecycle management --------------------------------------------------------
...
 
  // child classes -----------------------------------------------------
  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: datos protegidos accesibles por las clases hijas;
  • líneas 61-62: código que actualiza el fragmento a mostrar;
  • líneas 64-65: código de utilidad para manejar el menú, si está presente;
  • 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 entre el fragmento y la capa [DAO];
  • líneas 73-74: código de utilidad para manejar cualquier excepción de forma estándar;
  • líneas 76-77: código que gestiona 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:


  // class name
  protected String className;
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
...
  // constructor ----------------------
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructor");
    }
}
  • Línea 9: Aquí se anota el nombre de la clase hija que se está instanciando. Este nombre se utiliza en todos los registros de la clase padre;
  • línea 10: observamos que el fragmento se está construyendo. Esta información se utilizará cuando se pida al fragmento hijo que se actualice;

2.7.3.3. Gestión de menús

En nuestra arquitectura, cada fragmento debe tener un menú, aunque esté vacío. Efectivamente, los registros han mostrado que cuando se ejecuta el método [onCreateOptionsMenu] -que se ejecuta cuando el fragmento tiene un menú-, el fragmento ya se ha asociado con su actividad, vista y menú y está a punto de hacerse visible. Este es, por tanto, el momento en el que la interfaz visual y el menú pueden actualizarse. Es dentro de este método [onCreateOptionsMenu] donde ordenamos al fragmento hijo que se actualice.

La gestión de menús incluye métodos de utilidad que permiten al fragmento hijo mostrar u ocultar elementos del menú:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // menu management ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // iterate through all menu items
    for (int i = 0; i < menu.size(); i++) {
      // item #i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // if item #i is a submenu, then we start over
      if (menuItem.hasSubMenu()) {
        // recursion
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }
 
  private void getMenuOptionsStates(Menu menu) {
    // result
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // retrieve the IDs of the menu options
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // transfer the menu options to an array
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // option ID
      int id = menuOptionsIds.get(i);
      // option state
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    if (isDebugEnabled) {
      Log.d(className, String.format("Number of menu options=%s", menuOptionsStates.length));
    }
  }
 
  // menu option states
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // state
      MenuItemState state = this.menuOptionsStates[i];
      // menu ID
      int id = state.getMenuItemId();
      // initialize state
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // result
    return menuOptionsStates;
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // update all menu options
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }
 
  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • líneas 6-18: este método recupera los identificadores numéricos de todas las opciones del menú;
  • línea 6: el método [getMenuOptions] toma dos parámetros:
    • [Menú]: el menú del fragmento;
    • [Lista<Integer> menuOptionsIds]: la lista de Android IDs para las opciones del menú. Inicialmente, esta lista está vacía. Se rellena mediante un recorrido recursivo (línea 15) del árbol de menús;
  • líneas 20-40: basándose en el menú, construye el array de estados (ID, visibilidad) para las opciones del menú. Este array se almacena en la línea 3. La clase [MenuItemState] se describió en la sección 2.7.1;
  • líneas 43-55: una variante del método anterior. Hace lo mismo, pero en lugar de recalcular los identificadores de todas las opciones del menú -lo que ya se ha hecho- utiliza los identificadores de la matriz 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 mostrar u ocultar selectivamente ciertas opciones del menú;
  • Los métodos [getMenuOptions, getMenuOptionsStates] se declaran privados porque sólo se usan dentro de [AbstractFragment]. Los métodos [setAllMenuOptionsStates] (línea 58) y [setMenuOptionsStates] (línea 65) se declaran protected para que estén disponibles para las clases hijas;

2.7.3.4. Gestión de la espera de finalización de una tarea asíncrona


   // subscriptions to observables
  private List<Subscription> subscriptions = new ArrayList<>();
 // asynchronous tasks
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...
 
  // Handling the wait for the completion of an asynchronous operation -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // note the number of tasks that will be executed
    this.numberOfRunningTasks = numberOfRunningTasks;
    // display the loading image
    mainActivity.beginWaiting();
    // clear the list of subscriptions
    subscriptions.clear();
    // No cancellations yet
    runningTasksHaveBeenCanceled = false;
  }
 
  protected void cancelWaitingTasks() {
    // hide the loading image
    mainActivity.cancelWaiting();
  }
 
  • líneas 9-18: Para iniciar una o más operaciones asíncronas, el fragmento hijo llamará al método padre [beginRunningTasks]. El parámetro de este método es el número de tareas asíncronas que lanzará el fragmento hijo;
  • línea 11: almacenamos el parámetro del método;
  • línea 13: se hace visible la pantalla de carga;
  • línea 15: se borra la lista de suscripciones a operaciones asíncronas. Estas aún no han sido creadas por el fragmento hijo;
  • línea 17: se mantiene un booleano para indicar que se han cancelado las tareas asíncronas solicitadas por el fragmento hijo. Inicialmente, el booleano tiene el valor falso;
  • líneas 20-25: el fragmento hijo llama al método padre [cancelWaitingTasks] para indicar que quiere cancelar las tareas que ha lanzado;
  • línea 22: se oculta la imagen en espera;

2.7.3.5. Gestión de excepciones


  // exception handling -------------------------------------------------------------------
 
  // display exception alert
  protected void showAlert(Throwable th) {
    // display messages from the exception stack of Throwable th
    new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Close", null).show();
  }
 
  // display list of messages
  protected void showAlert(List<String> messages) {
    // Display the list of messages
    new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Close", null).show();
}
  • líneas 4-7: el método [showAlert(Throwable)] permite que un fragmento hijo muestre los mensajes de la pila de excepciones del método Throwable pasado como parámetro en una ventana;
  • líneas 10-13: el método [showAlert(List<String>)] permite a un fragmento hijo mostrar 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 la sección 2.7.2;

2.7.3.6. Gestión de operaciones asíncronas


...
  // subscriptions to observables
  private List<Subscription> subscriptions = new ArrayList<>();
  // asynchronous tasks
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // executing an asynchronous task with RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process: the observable to execute/observe
    // consumeResult: the method that processes the response
    // 
    // new subscriptions are created only if there has been no cancellation
    if (!runningTasksHaveBeenCanceled) {
      // Execute on the I/O thread and observe on the UI thread
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // execute the observable
      try {
        subscriptions.add(process.subscribe(
          // consume result
          consumeResult,
          // consume exception
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // end of task
          new Action0() {
 
            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }
 
  private void endOfTask() {
...
  }
 
  // an asynchronous operation threw an exception
  // or an exception occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
...
  }
 
  • líneas 9-41: ejecutar una tarea asíncrona;
  • línea 9: el método [executeInBackground] espera dos parámetros:
    • [Observable<T> process]: el proceso asíncrono a ejecutar;
    • [Acción1<T> consumeResult]: el método del fragmento hijo a llamar para pasarle los elementos emitidos por el proceso. En nuestros ejemplos anteriores, los procesos siempre han emitido un solo 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 sólo se lanza si aún no ha sido cancelada por el usuario o por el programa (debido a una excepción);
  • línea 16: el proceso está configurado para ejecutarse en un hilo de E/S y observado en el hilo UI;
  • línea 16: la sentencia [process.subscribe] lanza el proceso en el hilo de E/S. Dentro de este hilo, las operaciones se ejecutan de forma síncrona porque estamos utilizando una librería HTTP síncrona;
  • línea 19: el método [process.subscribe] tiene tres parámetros:
    • línea 21: [consumeResult]: el método del fragmento hijo 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. La gestión se delega en el 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. La gestión se delega en el método [endOfTask] de la línea 43;
  • línea 19: la tarea asíncrona que se acaba de lanzar se registra en el campo [subscriptions], que realiza un seguimiento de todas las tareas asíncronas lanzadas. Esto permitirá cancelarlas en caso necesario;
  • líneas 37-39: método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. La gestión se delega en el método [consumeThrowable] de la línea 49;

El método [endOfTask] es el siguiente:


  // asynchronous tasks
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // one less task to wait for
    numberOfRunningTasks--;
    // Done?
    if (numberOfRunningTasks == 0) {
      // end wait
      cancelWaitingTasks();
      // signal the end of tasks to the child class
      notifyEndOfTasks(false);
    }
  }
...
  // child classes -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • línea 6: una tarea asíncrona acaba de finalizar. Se decrementa el contador de tareas activas;
  • línea 8: si no hay más tareas activas, el subproceso hijo ha recibido todas sus respuestas;
  • línea 10: se cancela la espera;
  • línea 12: notificamos 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-normalmente, o debido a la cancelación por parte del usuario o del código porque se produjo una excepción. En la línea 12, indicamos un final normal. Tenga en cuenta que el fragmento hijo no necesita realizar un seguimiento de las tareas que siguen activas. Su clase padre lo hace por él;

El método [consumeThrowable] es el siguiente:


  // asynchronous tasks
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // an asynchronous operation threw an exception
  // or an exception occurred during the execution of an asynchronous operation
  private void consumeThrowable(Throwable th) {
    // th: the exception to be handled
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception received");
    }
    // cancel tasks that have already been started
    cancelRunningTasks();
    // display error messages
    showAlert(th);
  }
 
  // cancel tasks
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Canceling running tasks");
    }
    // cancel all registered asynchronous tasks
    for (Subscription subscription : subscriptions) {
      subscription.unsubscribe();
    }
    // note the cancellation
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // end of wait
    cancelWaitingTasks();
    // Notify the child fragment of the task cancellation
    notifyEndOfTasks(true);
}
 
...
  // child classes -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • Línea 3: El método [consumeThrowable] captura la excepción ocurrida;
  • línea 15: se cancelan todas las tareas que siguen activas;
  • línea 17: se muestra el texto de excepción;
  • líneas 21-37: se cancelan todas las tareas;
  • líneas 27-29: se cancelan todas las suscripciones;
  • línea 31: se anota que se ha producido una cancelación;
  • línea 32: el contador de tareas se pone a cero;
  • línea 34: se cancela la espera;
  • línea 36: se notifica al fragmento hijo que las tareas han finalizado al cancelarse;

2.7.3.7. Gestión del ciclo de vida de los fragmentos


  // Lifecycle --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }
 
  @Override
  public void onDestroy() {
    // parent
    super.onDestroy();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  private void saveState() {
...
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • Líneas 2-20: Los métodos [onDestroyView, onDestroy] se incluyen únicamente con fines de registro. Permiten al desarrollador comprender mejor el ciclo de vida del fragmento;

Guardar el fragmento cuando el dispositivo gira se gestiona mediante los siguientes métodos: [setUserVisibleHint, onSaveInstanceState, saveState]:


  // fragment lifecycle
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...
 
@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // save?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // the fragment will be hidden - save it
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  private void saveState() {
...
  }
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // parent
    super.onSaveInstanceState(outState);
    // save the fragment only if it is visible
    if (isVisibleToUser) {
      // maybe the save has already been done
      if (!saveFragmentDone) {
        saveState();
      }
      // restore in any case
      session.setAction(ISession.Action.RESTORE);
    }
}
  • líneas 6-19: el fragmento se guarda si pasa del estado visible al estado oculto (línea 11). El método [setUserVisibleHint] proporciona 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: Cuando el dispositivo gira, se llama al método [onSaveInstanceState]. El fragmento se guarda bajo dos condiciones:
    • es visible (línea 34);
    • aún no se ha guardado (línea 36). Es posible que los métodos [setUserVisibleHint] y [onSaveInstanceState] no se ejecuten ambos cuando el fragmento es visible, y que por tanto gestionar el booleano [saveFragmentDone] sea innecesario. En caso de duda, he optado por utilizarlo;
  • línea 40: después de guardar viene restaurar. Ten en cuenta que la próxima vez que el fragmento necesite actualizarse, lo hará mediante una operación [RESTORE];

Observe los dos momentos en los que se solicita un fragmento guardado:

  1. cuando pasa del estado visible al estado oculto;
  2. cuando el aparato gira;

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


...
  private void saveState() {
    // tasks to cancel?
    if (numberOfRunningTasks != 0) {
      // cancel the tasks
      cancelRunningTasks();
    }
    // Save the fragment's state
    CoreState currentState = saveFragment();
    // the fragment has been visited
    currentState.setHasBeenVisited(true);
    // save menu state
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // set session
    session.setCoreState(getNumView(), currentState);
    // Save complete
    saveFragmentDone = true;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }
 
 
...
  // child classes -----------------------------------------------------
public abstract CoreState saveFragment();
 
protected abstract int getNumView();
  • líneas 4-7: La rotación del dispositivo puede producirse mientras se realizan operaciones asíncronas. Aquí se toma la decisión de cancelarlas todas. No es una buena decisión para el usuario, que tendrá que hacer una nueva solicitud, que puede llevar mucho tiempo, simplemente porque ha movido su teléfono o tableta o ha recibido una llamada. Es posible mantener las conexiones de red mediante un ciclo de guardar/restaurar. Sin embargo, las soluciones no son sencillas, y he decidido no tratarlas en este curso para principiantes. El camino a seguir es establecer estas conexiones de red a través de un fragmento que no tiene ningún UI adjunto y no se destruye durante el ciclo de guardar/restaurar. Para ello, basta con utilizar la instrucción [Fragment.setRetainInstance(true)];
  • línea 9: pedimos al fragmento hijo que guarde su estado en un tipo derivado de [CoreState] (línea 31);
  • línea 11: observamos que el fragmento ha sido visitado. Esta información es útil. Cuando se visita un fragmento por primera vez, su actualización puede diferir de las posteriores porque no tiene ningún estado previo en la sesión;
  • línea 13: guardamos el estado del menú, lo que nos permitirá restaurarlo automáticamente;
  • línea 15: este estado actual se guarda en la sesión. En la sesión, los estados se agrupan por vista/fragmento, cada uno de los cuales tiene un estado. El número de vista lo proporciona el fragmento hijo (línea 33);
  • línea 17: observamos que el fragmento se ha guardado. Esto se debe a que dos métodos pueden llamar al método [saveState], y es innecesario realizar dos guardados;

La vista asociada al fragmento se regenera mediante el siguiente método:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the view must be restored
    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 ser reconstruida. Simplemente anotamos esto 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 en el fragmento antes de que se haga visible y espere la entrada del usuario. Se realiza mediante el siguiente código:


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
  // Update Fragment ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memory
    this.menu = menu;
    // retrieve the menu options if this hasn't already been done
    if (fragmentHasToBeInitialized) {
      // retrieve the # menu options
      getMenuOptionsStates(menu);
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
    previousState = session.getCoreState(getNumView());
    // Update the child fragment in several steps
    // step 1 - is this the first visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // this is not the first visit
      // step 2: should the fragment be initialized?
      ...
      // Step 3: Should the view be initialized?
      ...
    }
    // Step 4: A submit, a navigation, a restore?
    ...
 
    // Step 5: Terminal updates ----------------------
...
  }
...
  // child classes -----------------------------------------------------
  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ú, aunque esté vacío. Cuando se ejecuta este método, el fragmento se ha asociado a su vista y actividad y también es visible;
  • línea 25: se almacena el menú pasado como parámetro (línea 22) al método;
  • líneas 27-34: si es necesario inicializar el fragmento:
    • línea 29: los estados de las opciones del menú se almacenan en el array [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 type cast es necesario porque el método [mainActivity.getSession()] devuelve un tipo [ISession];
  • línea 36: se recupera de la sesión el estado anterior del fragmento. Si se trata de la primera visita al fragmento, sólo es relevante el booleano [previousState.hasBeenVisited];
  • líneas 39-44: código ejecutado cuando se trata de la primera visita al fragmento. En este caso, su estado anterior no es relevante;
  • líneas 44-50: código ejecutado cuando no es la primera visita al fragmento;
  • líneas 46-47: código ejecutado si se ha llamado al constructor del fragmento (fragmentHasToBeInitialized == true);
  • líneas 48-49: código ejecutado si la vista asociada al fragmento ha sido reconstruida (viewHasToBeInitialized==true);
  • líneas 51-52: código ejecutado en función de la acción actual (SUBMIT, NAVIGATION, RESTORE);
  • líneas 54-55: código siempre ejecutado;

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

paso 1


  // fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
    previousState = session.getCoreState(getNumView());
    // Update the child fragment in several steps
    // step 1 - is this the first visit?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // Initialize fragment and view
      initFragment(null);
      initView(null);
      // reset previousState for later
      previousState = null;
    } else {
      // this is not the first visit
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • línea 19: se recupera de la sesión el estado anterior del fragmento;
  • líneas 22-31: código ejecutado si el fragmento nunca ha sido visitado;
  • línea 27: se 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í, null para indicar al fragmento hijo que se trata de la primera visita;
  • línea 28: se 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í, null para indicar al fragmento hijo que se trata de la primera visita;
  • línea 30: establecemos el estado anterior en null para los pasos siguientes;

Pasos 2 y 3


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
    previousState = session.getCoreState(getNumView());
    // Update the child fragment in several steps
    // step 1 - is this the first visit?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // This is not the first visit
      // Step 2: Does the fragment need to be initialized?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initializing fragment");
        }
        // child fragment
        initFragment(previousState);
      }
      // Step 3: Does the view need to be initialized?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "view initialization");
        }
        // child fragment
        initView(previousState);
      }
    }
 
...
 
  protected abstract void initFragment(CoreState previousState);
 
protected abstract void initView(CoreState previousState);
  • líneas 24-42: se ejecuta cuando no es la primera visita al fragmento;
  • líneas 27-33: si el fragmento se acaba de reconstruir, se reinicializa 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 es necesario inicializar o restablecer la vista asociada al fragmento, se pide al fragmento hijo que lo haga (líneas 40, 48). De nuevo, se le pasa el último estado conocido del fragmento;

Paso 4


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
    previousState = session.getCoreState(getNumView());
    // Update the child fragment in several steps
 ...
 
    // step 4: a submit, a navigation, a restore?
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("previous state=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // current action
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // child fragment
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // restore menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // child fragment
          updateOnRestore(previousState);
        } else {
          // this is a first visit - nothing to do
        }
        break;
      case RESTORE:
        // restore
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // restore menu (previousState cannot be null)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // child fragment
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);
 
protected abstract void updateOnRestore(CoreState previousState);
  • líneas 34-66: procesamos la acción actual, que puede ser una de las tres siguientes:
    • RESTORE: Estamos restaurando el fragmento después de una rotación del dispositivo;
    • NAVIGATION: Volvemos al fragmento, con la intención de encontrarlo en el estado en que lo dejamos la última vez que lo usamos;
    • SUBMIT: todos los demás casos;
  • línea 34: recuperar la acción actual;
  • líneas 36-42: para una acción de tipo SUBMIT, llamamos 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 NAVEGACIÓN;
  • líneas 47-54: queremos restaurar el fragmento a su último estado conocido. La operación 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 para la pestaña 4 si es la primera visita;
    • restaurar el fragmento de la pestaña 4 a su estado anterior si no es la primera visita;
  • líneas 52-54: no hace nada si es la primera visita. El método hijo [initView(CoreState previousState)] se encargará de esta inicialización. La primera visita se identifica por la condición [previousState == null];
  • línea 49: si no es la primera visita al fragmento, restaura su menú;
  • línea 51: pedimos a la clase hija que se actualice llamando al método de la línea 70. Le pasamos el estado anterior del fragmento para que haga su trabajo. Le pasamos el estado previo del fragmento para que pueda hacer su trabajo;
  • líneas 56-66: en el caso de una operación de restauración de fragmentos, hacemos lo mismo que en el caso de la navegación fuera de la primera visita;

Paso 5


// fragment menu
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // fragment lifecycle
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // fragment states
  private CoreState previousState;
  // JSON mapper
  private ObjectMapper jsonMapper = new ObjectMapper();
  // fragment lifecycle
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...
 
 
    // Step 5: Terminal updates ----------------------
    // we have changed views
    session.setPreviousView(getNumView());
    // no more actions in progress
    session.setAction(ISession.Action.NONE);
    // When we leave this fragment, it must be saved
    saveFragmentDone = false;
    // as long as the fragment hasn't been rebuilt, it doesn't need to be initialized
    fragmentHasToBeInitialized = false;
    // as long as the view is not rebuilt, it does not need to be initialized
    viewHasToBeInitialized = false;
    // We return to normal tab selection behavior
    session.setNavigationOnTabSelectionNeeded(true);
 
    // Notify the fragment that the view is ready
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • líneas 18-30: cuando llegamos a este punto, el fragmento se ha inicializado y está listo para mostrarse. A continuación, restablecemos todos los indicadores utilizados en la gestión del ciclo de vida del fragmento a su estado inicial;
  • línea 20: la vista ha cambiado; se hace constar en la sesión;
  • línea 22: no hay más acciones en curso;
  • línea 24: cuando salgamos del fragmento visualizado actualmente, tendremos que guardarlo al salir;
  • línea 26: ya no es necesario reconstruir el fragmento. Esta bandera se pondrá a verdadero cuando se vuelva a ejecutar el constructor del fragmento;
  • línea 28: ya no es necesario inicializar la vista asociada al fragmento. Esta bandera se pondrá a verdadero de nuevo cuando se vuelva a ejecutar el método [onActivityCreated];
  • línea 30: el fragmento puede mostrarse en una aplicación con pestañas. En este caso, cuando el usuario hace clic en una de las pestañas, debe producirse un cambio de fragmento;
  • línea 36: se notifica a la clase hija que el fragmento está listo. Puede utilizar el método [notifyEndOfUpdates] para realizar actualizaciones que habría que hacer en cualquier caso, lanzar una operación asíncrona para obtener nuevos datos, etc.

2.7.4. Un ejemplo de fragmento

  

Hemos incluido un fragmento de ejemplo en el proyecto [client-android-skel] para mostrar al lector la estructura típica de un fragmento en 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 {
 
  // fields inherited from the parent class -------------------------------------------------------
 
  // debug mode
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // class name
  //-- protected String className;
  // asynchronous tasks
  //-- protected int numberOfRunningTasks;
  // activity
  //-- protected IMainActivity mainActivity;
  //-- protected Activity activity;
  // session
  //-- protected Session session;
 
  // methods inherited from the parent class -------------------------------------------------------
 
  // display menu options
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // managing the wait for the completion of a series of asynchronous tasks
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // executing an asynchronous task with RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // Cancel tasks
  //-- protected void cancelRunningTasks() {
  // display an alert on exception
  //-- protected void showAlert(Throwable th) {
  // display list of messages
  //-- protected void showAlert(List<String> messages) {
 
  // methods required by the parent class -------------------------------------------------------
 
  @Override
  public CoreState saveFragment() {
    // the fragment must be saved
    DummyFragmentState state = new DummyFragmentState();
    // ...
    return state;
    // if there is nothing to save, use [return new CoreState();] and remove the [DummyFragmentState] class
  }
 
  @Override
  protected int getNumView() {
    // Return the fragment number in the array of fragments managed by the activity (see MainActivity)
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // The fragment becomes visible and has been constructed in this step or a previous step
    // This occurs when the application starts and every time the Android device rotates
    // is necessarily followed by the execution of [initView]
    // the fields of the fragment that has been reconstructed must be initialized
    // previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // The fragment becomes visible and the associated view has been reconstructed in this step or a previous step
    // this occurs every time [initFragment] is executed and every time the fragment leaves the vicinity of the displayed fragment
    // The components of the view that has been reconstructed must be initialized
    // previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after a SUBMIT operation
    // You generally need to initialize the fragment and the associated view from the session
    // previousState is the last saved state of the fragment—is null if this is the first visit to the fragment
    // there is nothing to do if the fragment cannot be reached via a SUBMIT operation
    // if the fragment can be reached via SUBMIT operations from different fragments, the previous view can be obtained via [session.getPreviousView]
    // if the fragment can be reached via multiple SUBMIT operations from the same fragment, then a flag must be set in the session to distinguish between the different types of SUBMIT operations originating from that fragment
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // is executed after [initFragment, initView] if these methods are executed
    // the view will be displayed after a RESTORE or NAVIGATION operation
    // previousState is the last saved state of the fragment—never null
    // the view must be restored to its previous state
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
    // occurs after the [updateOnSubmit, updateOnRestore] methods
    // At this point, the view has been constructed and initialized
    // There is often nothing to do here, but you can also factor in actions that need to be performed regardless of how you arrive at this view
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // called when the asynchronous tasks launched by the fragment have either completed or been canceled
    // These two cases can be distinguished using the runningTasksHaveBeenCanceled parameter
    // generally, the view must be reset to a state different from the one it was in while waiting for responses from the asynchronous tasks
 
  }
}

La clase [DummyFragment] puede no tener un estado. Aquí, hemos incluido uno para recordarnos lo que se espera dentro de ella:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class DummyFragmentState extends CoreState {
  // state of the [DummyFragment] fragment
  // include only fields serializable as JSON
  // Add the @JsonIgnore annotation to the others, though it's unclear what purpose they might serve
  // Don't forget the getters/setters—they are used for serialization/deserialization
}

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

2.8. Ejercicios ilustrativos

Empezaremos por refactorizar ejemplos ya escritos.

2.8.1. Ejemplo 17B

Volveremos al ejemplo 17 de la sección 1.18. Se trata de una aplicación con un único fragmento, sin tareas asíncronas ni pestañas. La examinaremos para ver cómo se comporta cuando se gira el dispositivo. Introduciremos lo siguiente:

Image

A continuación, en [1], giramos el dispositivo dos veces. La nueva vista es la siguiente:

Image

If we compare the views, everything has been preserved except for list [2], which is now empty.

Además, si hace clic en el botón [Enviar], aparece un cuadro de diálogo que muestra las entradas realizadas en el formulario. Si gira el dispositivo en ese momento, el cuadro de diálogo desaparece.

Por lo tanto, durante una rotación, necesitaremos regenerarnos:

  • la lista desplegable y su elemento seleccionado;
  • el cuadro de diálogo si se mostró durante la rotación;

2.8.1.1. El proyecto [Ejemplo-17B

Duplicamos el proyecto [client-android-skel] en examples/Example-17B. A continuación, cargamos el nuevo proyecto [1]:

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

Cambiaremos el margen superior de la vista [vue1.xml] a 80 dp:


    <TextView
      android:id="@+id/textViewFormTitle"
      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/view1_title"
android:textSize="30sp"/>

En este punto, podemos intentar una compilación inicial para comprobar si hay errores. Los primeros errores notificados proceden del paquete importaciones que se han movido. Los corregimos (Ctrl-Shift-O). Otros errores, como , surgen porque la vista [Vue1Fragment] no implementa todos los métodos requeridos por su clase padre [AbstractParent]:

Image

Generar los métodos que faltan (Alt-Enter).

Otro error de compilación notificado es el siguiente

Image

Arreglamos esto en el archivo [build.gradle] del módulo (línea 20 más abajo):

 

En este punto, podemos recompilar para ver los errores restantes. El único error reportado es en el método [Vue1Fragment.updateFragment]:

 

Debe eliminar la anotación [@Override] de la línea 135. Ahora ya no hay errores. Usaremos esto como punto de partida para modificar el proyecto.

2.8.1.2. El estado del fragmento [Vue1Fragment]

El fragmento [Vue1Fragment] necesita guardar información cuando el dispositivo gira para que pueda ser restaurado completamente. Para ello creamos una clase [Vue1FragmentState]:

  

For now, this class is empty:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

2.8.1.3. Personalización de proyectos

  

La carpeta [custom] contiene elementos de arquitectura que pueden ser personalizados por el desarrollador.

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 {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum wait time for the server response
  int TIMEOUT = 1000;
 
  // timeout before executing the client request
  int DELAY = 0;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // loading icon
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • líneas 24-31: La aplicación no utiliza su capa [DAO] aquí. Estas constantes no se utilizarán;
  • línea 34: una adyacencia de fragmento de 1, que es el valor por defecto. Dado que la aplicación sólo tiene un fragmento (línea 43), este valor es irrelevante;
  • líneas 39-40: dado que no hay operaciones con la capa [DAO], no hay necesidad de una imagen de marcador de posición;
  • línea 37: esto no es una aplicación con pestañas;
  • línea 43: sólo hay un fragmento;

La clase [Sesión] es la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
 
}

Está vacía. De hecho, como sólo hay un fragmento, no es necesario prever la comunicación entre fragmentos mediante 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 {
  // whether the fragment has been visited
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Líneas 11-13: Necesitamos listar todas las clases derivadas de [CoreState] que almacenan el estado de los distintos fragmentos. Aquí sólo hay una (línea 12);

2.8.1.4. El [MainActivity]

La actividad [MainActivity] tiene actualmente este aspecto:


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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
    // todo: continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // todo: define the fragments here
    return new AbstractFragment[0];
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // todo: define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // To-do: tab navigation - define the view to display when tab #[position] is selected
  }
 
  @Override
  protected int getFirstView() {
    // todo: define the number of the first view (fragment) to display
    return 0;
  }
}

Los comentarios [//todo] indican lo que debe hacer el desarrollador. La clase [MainActivity] evoluciona como sigue:


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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // session
    this.session = (Session) super.session;
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
 
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
}

Sólo el método de las líneas 41-44 necesita ser modificado. Debe devolver el array de los fragmentos de la aplicación. En la línea 43, no olvides añadir 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 [Ejemplo-17], decidimos almacenar los siguientes elementos del fragmento:

  • la lista de valores de la lista desplegable;
  • la posición del elemento seleccionado en esta lista;
  • el mensaje mostrado por el cuadro de diálogo si 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 {
 
  // the values of the dropdown list
  private List<String> list;
  // the selected item in the dropdown list
  private int listSelectedPosition;
  // the message displayed in the dialog box
  private String message;
 
  // getters and setters
...
}

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


// dropdown list
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;
 
  @AfterViews
  void afterViews() {
    // Check the first button
    radioButton1.setChecked(true);
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // the 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));
      }
    });
    // the dropdown list
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // initialize the dropdown list adapter
    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 trasladará a los métodos definidos por la clase [AbstractFragment] de la siguiente manera:


// Fragment lifecycle management ---------------------------------------------------------------------
  @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) {
    // First visit?
    if (previousState == null) {
      // Create the values for the dropdown list
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // restore the values from the dropdown list
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // and the dialog message
      message = state.getMessage();
    }
    // initialize the dropdown list adapter
    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) {
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // the 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));
      }
    });
    // Initialize the drop-down list adapter
    dropDownList.setAdapter(dataAdapter);
    // First visit?
    if (previousState == null) {
      // check the first button
      radioButton1.setChecked(true);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // seekbar value
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // selected item in dropdown list
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // Is the dialog visible?
    if (message != null) {
      // display it
      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 a guardar en una clase derivada de [CoreState] y devolver una instancia de esa clase;
  • líneas 11-14: el método [getNumView] debe devolver el número de fragmento. Aquí sólo 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 previo del fragmento. Si [previousState] es null, entonces esta es la primera visita;
  • líneas 19-25: En la primera visita, se crean los valores para la lista desplegable;
  • líneas 26-30: si no es la primera visita, los campos [lista, mensaje] del fragmento se restauran desde el estado anterior;
  • líneas 33-34: inicialización del campo [dataAdapter] del fragmento. Esta es la fuente de datos para la lista desplegable;
  • líneas 37-62: el método [initView] se utiliza para inicializar los componentes de la interfaz visual. Recibe el estado anterior [previousState] como parámetro. Si [previousState == null], entonces es la primera visita;
  • Aquí vemos lo que había antes en el método [@AfterViews];
  • líneas 57-61: en la primera visita, nos aseguramos de que se selecciona el primer botón de opción;
  • líneas 64-67: el método [updateOnSubmit] se ejecuta cuando la acción actual es [SUBMIT]. Aquí no hay navegación entre fragmentos y, por tanto, no hay acción actual;
  • líneas 69-81: el método [updateOnRestore] se ejecuta cuando la acción actual es [NAVIGATION] o [RESTORE]. Aquí no hay navegación entre fragmentos y, por tanto, no hay acción [NAVIGATION] posible;
  • línea 72: recalculamos (no restauramos) el valor del TextView seekBarValue. Esto se debe a que, durante las rotaciones, a veces se perdía su valor;
  • líneas 74-75: La lista se posiciona en el elemento que estaba seleccionado antes de la rotación. Sin esto, la lista se situaría por defecto en su primer elemento;
  • líneas 76-80: el cuadro de diálogo se muestra de nuevo si el mensaje del estado anterior no es null. Volveremos al 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 solo al fragmento hijo. Aquí no hay nada que hacer;
  • líneas 88-91: el método [notifyEndOfTasks] señala el fin de las tareas asíncronas lanzadas por el fragmento. Aquí no hay ninguna;

El cuadro de diálogo se restablece de la siguiente manera:


  // the dialog message
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValidate() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    ...
    // display
    doDisplay(messages);
  }
 
  private void display(final List<String> messages) {
    // construct the text to be displayed
    StringBuilder text = new StringBuilder();
    for (String message : messages) {
      text.append(String.format("%s\n", message));
    }
    // store the message
    message = text.toString();
    // display it
    showMessage();
  }
 
  private void showMessage() {
    // display it
    new AlertDialog.Builder(activity).setTitle("Entered values").setMessage(message).setNeutralButton("Close", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // reset the message
        message = null;
      }
    }).show();
}

Cuando el usuario envía el formulario, el método [doValider] (línea 5) construye 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 único 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 el mismo mensaje que muestra el método [updateOnRestore];
  • 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 [Cerrar] del cuadro de diálogo;
  • línea 31: cuando se cierra el cuadro de diálogo, el mensaje se establece en null para indicar que el cuadro de diálogo ya no está presente;

2.8.1.7. Pruebas

Invitamos a los lectores a probar este proyecto y comprobar que el fragmento se conserva tras una o varias rotaciones sucesivas.

2.8.2. Ejemplo-23: Cliente meteorológico

Algunos sitios web ofrecen información meteorológica en forma de cadenas JSON. He aquí un ejemplo:

Image

El URL tiene la forma: http://api.openweathermap.org/data/2.5/weather?q={ciudad},{país}&APPID={APPID} donde:

  • ciudad: la ciudad para la que desea el tiempo, aquí Angers;
  • país: el país de la ciudad, en este caso Francia (fr);
  • APPID: una clave obtenida al registrarse en el sitio [https://home.openweathermap.org/users/sign_up];

2.8.2.1. El proyecto

  

El proyecto se construyó basándose en el proyecto [client-android-skel]. Tiene las siguientes características:

  • sólo tiene un fragmento cuyo estado no es necesario mantener;
  • realiza peticiones asíncronas;

2.8.2.2. Personalización de proyectos

  

La interfaz [IMainActivity] permite especificar determinadas 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 {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum wait time for server response
  int TIMEOUT = 1000;
 
  // wait time before executing the client request
  int DELAY = 5000;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // tab bar
  boolean ARE_TABS_NEEDED = false;
 
  // loading icon
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • Líneas 25, 28, 31, 40: características de la capa [DAO]. Línea 31: La autenticación básica no es necesaria;
  • línea 34: adyacencia del fragmento. Aquí, esta constante es irrelevante ya que sólo hay un fragmento;
  • línea 37: esto no es una aplicación con pestañas;
  • línea 43: sólo 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)
// todo: add the subclasses of [CoreState] here
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // whether the fragment has been visited
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • líneas 10-13: no hay nada que declarar ya que esta aplicación sólo tiene un fragmento cuyo estado no se guarda;

La clase [Sesión] es la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
}

Está vacío porque no hay comunicación entre fragmentos en esta aplicación.

2.8.2.3. La capa [DAO]

  

En la capa [DAO], deben personalizarse tres clases:

  • el IDao interfaz;
  • su Dao aplicación;
  • el WebClient interfaz 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);
 
  // weather service
  @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. Nótese que esto es relativo al URL raíz del cliente (RestClientRootUrl, línea 12). 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 {
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // User
  void setUser(String user, String password);
 
  // Client timeout
  void setTimeout(int timeout);
 
  // Basic authentication
  void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // weather service
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Ten en cuenta que los métodos de las líneas 6-22 están incluidos por defecto en la interfaz IDao del proyecto [client-android-skel];
  • Línea 25: El método [getWeatherForecast] recupera la cadena JSON para el tiempo en la ciudad [city] del país [country]. El tercer parámetro es la clave obtenida del sitio web [https://home.openweathermap.org/users/sign_up];

La interfaz [IDao] es 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 {
 
  // Web service client
  @RestService
  protected WebClient webClient;
  // security
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // the RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate factory
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // create the RestTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Set the JSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Set the RestTemplate for the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // Set the web service URL
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String password) {
    // Register the user in the interceptor
    authInterceptor.setUser(user, password);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // memory
    this.timeout = timeout;
    // factory configuration
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthenticationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
 
  // private methods -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // weather service ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // result
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Ten en cuenta que las líneas 17-90 están incluidas por defecto en la clase [Dao] del proyecto [client-android-skel]. Sólo tienes que añadir los métodos de implementación de la interfaz [IDao], específicos para la aplicación (línea 92);
  • líneas 93-105: implementación del método [getWeatherForecast]. Esto es muy sencillo y ocupa 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; aquí, es un Cadena ya que esperamos una cadena JSON. El tipo T de [IRequest<T>] debe ser el tipo T del método [Observable<T> getWeatherForecast];
  • la interfaz [IRequest<T>] sólo tiene un método: getResponse. Su función es 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. Le pasamos los tres parámetros recibidos en la línea 94. Por esta razón, éstos deben tener el final atributo;

2.8.2.4. El [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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
 
  // Parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
  }
 
  @Override
  protected int getFirstView() {
    return 0;
  }
 
  // IDao interface ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • Ten en cuenta que las líneas 15-55 están incluidas por defecto en el proyecto [client-android-skel]. Sólo tienes que personalizarlas;
  • Líneas 37-40: la matriz de fragmentos. Aquí sólo hay uno;
  • Líneas 43-46: No se requieren títulos de fragmentos;
  • líneas 48-50: aquí no hay pestañas;
  • Líneas 52-55: La primera vista a mostrar es la vista #0, la de [MeteoFragment];
  • líneas 58-61: implementación de la interfaz [IDao]. Aquí, no hay nada que hacer aparte de delegar el trabajo a la capa [DAO] en la línea 21;

2.8.2.5. El fragmento [MeteoFragment]

  

El [MeteoFragment] consulta el servicio web meteorológico / JSON. Su esqueleto es el siguiente:


package client.android.fragments;
 
import android.util.Log;
import android.util.Log;
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="Build your visual interface"
    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 sólo 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/actionWeather"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionCancel"
        android:title="@string/actionCancel"/>
      <item
        android:id="@+id/actionFinish"
        android:title="@string/actionFinish"/>
    </menu>
  </item>
</menu>
  • líneas 10-12: esta opción de menú se utiliza para solicitar el tiempo de una ciudad;
  • líneas 14-15: esta opción de menú se utiliza para cancelar la solicitud si está en curso;
  • líneas 16-18: esta opción de 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 {
 
  // local data
  private int countOfReceivedResponses;
 
  // event handling ---------------------------------------------------------------------------------------
  // cities for which we want the weather
  final String[] paysDeLoire = new String[]{"Angers", "Le Mans", "Nantes", "Laval", "La Roche-sur-Yon"};
 
  @OptionsItem(R.id.actionMeteo)
  protected void getWeather() {
    // its country
    String country = "fr";
    // Get an API key by creating an account [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // Web service URL / JSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // Start waiting for [paysDeLoire.length] asynchronous tasks
    beginWaiting(paysDeLoire.length);
    // Number of responses received
    nbResponsesReceived = 0;
    // make asynchronous calls in parallel
    for (String city : paysDeLoire) {
      // weather
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // process the response
          consumeResponse(response);
          // a response received
          nbReceivedResponses++;
        }
      });
    }
  }
 
  // process server response
  private void consumeResponse(String response) {
    // log
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }
 
  // start waiting
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // parent
    beginRunningTasks(numberOfRunningTasks);
    // display the [Cancel] option
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionCancel, true)});
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // display results
    String message;
    switch (numberOfResponsesReceived) {
      case 0:
        message = "No responses were received";
        break;
      case 1:
        message = "One response was received. Check your logs...";
        break;
      default:
        message = String.format("%s responses were received. Check your logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }
 
  // private methods -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionCancel, false)});
  }
 
  // lifecycle management ---------------------------------------------------------------------------------------
...
}
  • líneas 25-50: manejo del clic en la opción de menú [Tiempo];
  • línea 32: construcción del servicio web URL / JSON para el servicio meteorológico. A continuación, se pasa a la capa [DAO] a través de la actividad;
  • línea 34: iniciamos la espera. Pasamos el número de tareas a lanzar para que la clase padre nos avise cuando estén completas. Aquí, hay cinco tareas porque estamos solicitando el tiempo para las cinco ciudades listadas en la línea 23;
  • línea 16: contamos el número de respuestas recibidas para poder mostrarlo;
  • líneas 38-50: recorremos en bucle las ciudades de las que queremos conocer el tiempo;
  • línea 40: haremos 5 peticiones HTTP en paralelo;
  • línea 40: pedimos a la clase padre [AbstractParent] que consulte al servicio web / JSON;
  • líneas 40-48: el método [executeInBackground] espera dos parámetros:
    • línea 40: el proceso a observar y ejecutar es proporcionado por el método [mainActivity.getWeatherForecast];
    • líneas 40-48: la instancia [Action1] que se ejecutará cuando se reciba 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] en la línea 53;
  • línea 46: se incrementa el contador de respuestas recibidas;
  • líneas 53-56: consumir una respuesta JSON del servicio meteorológico;
  • línea 55: simplemente registramos la cadena JSON;
  • líneas 59-72: código ejecutado antes de lanzar las tareas asíncronas;
  • línea 65: pasamos el número de tareas a ejecutar a la clase padre [AbstractParent]. Esto permite que nos notifique cuando hayan terminado todas;
  • líneas 67-70: preparación del menú para una espera. Mantenemos únicamente la opción [Acciones/Cancelar], que permitirá al usuario cancelar las tareas lanzadas;
  • líneas 74-92: código que se ejecuta cuando la clase padre nos notifica que todas las tareas lanzadas han finalizado;
  • línea 77: reseteamos 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 [Acciones/Cancelar], que está oculta;
  • líneas 80-91: se muestra el número de respuestas recibidas;

Al hacer clic en la opción [Cancelar] del menú, se utiliza el siguiente código:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnul() {
    if (isDebugEnabled) {
      Log.d(className, "Cancelation requested");
    }
    // cancel asynchronous tasks
    cancelRunningTasks();
}
  • línea 7: pedimos a la clase padre que cancele las tareas que aún están activas;

Al hacer clic en la opción [Finalizar] del menú, se utiliza el siguiente código:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminate() {
    // shut everything down
    System.exit(0);
}

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


  // lifecycle management ---------------------------------------------------------------------------------------
 
  @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) {
    // First visit?
    if (previousState == null) {
      initMenu();
    }
  }
 
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
}
  • líneas 3-6: se utiliza para almacenar el estado del fragmento en una clase derivada de [CoreState]. Si el fragmento no tiene estado que almacenar, como en este caso, simplemente devolvemos una instancia de [CoreState]. No devuelva nulo, ya que esto acabaría provocando un accidente;
  • líneas 8-11: debe devolver la vista ID. Aquí, el [MeteoFragment] tiene ID 0;
  • líneas 13-16: se utiliza para inicializar el fragmento una vez que ha sido construido (previousState == null) o reconstruido (previousState != null). Aquí no hay nada que hacer. El único campo que se puede inicializar es el siguiente:

  // cities for which we want the weather
final String[] paysDeLoire = new String[]{"Angers", "Le Mans", "Nantes", "Laval", "La Roche-sur-Yon"};

pero se inicializa sola;

  • líneas 18-24: se utilizan para inicializar la vista asociada al fragmento una vez que ha sido construido (previousState == null) o reconstruido (previousState != null);
  • líneas 21-23: si es la primera vez que se visita el fragmento, se inicializa su menú para ocultar la opción [Cancelar];
  • líneas 27-30: llamada si la navegación al fragmento implicó una acción [SUBMIT]. Aquí no hay navegación entre fragmentos, ya que solo hay un fragmento;
  • líneas 32-35: llamada durante un ciclo de guardar/restaurar debido a la rotación del dispositivo u otra razón. Aquí, como no se ha guardado ningún estado, no hay nada que hacer;
  • líneas 37-40: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada que hacer;

2.8.2.6. Pruebas

Ahora ejecutamos el ejemplo:

Image

Image

Los registros son los siguientes:


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructor
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructor, 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_: setBasicAuthentication thread=main, isBasicAuthenticationNeeded=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_: constructor
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigating to view 0 on 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_: Number of menu options=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_: previousState=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} on thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} on thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} on 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: JSON las respuestas se obtienen en hilos de E/S
  • líneas 37-41: el fragmento recupera las 5 respuestas en el hilo UI;

Ahora, hacemos la petición con un API ID incorrecto:


    String APIID = "";

Image

Los registros son 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], Server communication exception: [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], Server communication exception: [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], Server communication exception: [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], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception received
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Canceling launched tasks
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], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • líneas 3-6, 10: las 5 llamadas 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 [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_: Cancellation requested
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Canceling launched tasks
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], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Server communication exception: [java.lang.InterruptedException,[null]]
  • línea 3: solicitud de anulación;
  • línea 4: la espera se anula 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 cinco hilos de tareas. El tipo de excepción depende de las aplicaciones. La excepción aquí es [java.lang.InterruptedException] porque las tareas fueron interrumpidas mientras se ejecutaba la instrucción [Thread.sleep(delay)], lo que hace que esperen artificialmente durante [delay] milisegundos;

2.8.3. Ejemplo-16B

Aquí refactorizamos el Ejemplo 16 de la Sección 1.17. Presenta un fragmento que realiza llamadas asíncronas a un servidor de números aleatorios. Veamos cómo se comporta durante una rotación del dispositivo:

Image

  • En [1], el dispositivo gira dos veces;

Image

Vemos que hemos perdido todos los mensajes de error. Intentaremos mejorar esto.

2.8.3.1. El Proyecto Ejemplo-16B

Copiamos el proyecto [client-android-skel] en el proyecto [examples/Example-16B] y, a continuación, cargamos el nuevo proyecto:

  

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

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

Cambiaremos 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 [View1Fragment]:
  
  • la clase [DAO / servicio / Respuesta]:
  

En esta fase, podemos intentar una compilación inicial:

  • El primer tipo de error consiste en importaciones. Algunas clases se han movido a paquetes diferentes durante la migración a [Ejemplo-16B]. Comenzamos corrigiendo estos errores;
  • Un segundo tipo de error se reporta en la clase [Vue1Fragment] porque no implementa los métodos requeridos por la clase padre [AbstractParent]. Generamos automáticamente estos métodos;

Intentamos una segunda recopilación:

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

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

Hemos visto que cierta información del fragmento tendrá que ser guardada durante una rotación con el fin de restaurar el fragmento a su estado anterior a la rotación. Por lo tanto, creamos un estado [Vue1FragmentState], que por ahora 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 de proyectos

  

La interfaz [IMainActivity] permite especificar determinadas 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 {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // application constants -------------------------------------
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // Maximum wait time for the server response
  int TIMEOUT = 1000;
 
  // wait time before executing the client request
  int DELAY = 5000;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = false;

  // loading icon
  boolean IS_WAITING_ICON_NEEDED = true;
 
  // number of application fragments
  int FRAGMENTS_COUNT = 1;
 
}
  • líneas 25, 28, 31, 40: características de la capa [DAO]. La autenticación básica no es necesaria;
  • línea 34: adyacencia del fragmento. Aquí, esta constante es irrelevante ya que sólo hay un fragmento;
  • línea 37: esto no es una aplicación con pestañas;
  • línea 43: sólo 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 {
  // whether the fragment has been visited
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Línea 12: Declaramos la clase de estado del fragmento [Vue1Fragment];

La clase [Sesión] es la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
}

Está vacío porque no hay comunicación entre fragmentos en esta aplicación.

2.8.3.4. La capa [DAO]

  

En la capa [DAO], deben personalizarse tres clases:

  • el IDao interfaz;
  • su Dao aplicación;
  • el WebClient interfaz para comunicarse con el servidor web / JSON;

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


package client.android.dao.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // response body
    private T body;
 
    // constructors
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and 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 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getRandom(@Path("a") int a, @Path("b") int b);
 
}
  • Líneas 18-19: El URL para el servicio de números aleatorios. Observa que este URL es relativo al URL raíz del cliente (RestClientRootUrl, línea 12). Aquí, el URL raíz es [http://localhost:8080];

La interfaz [IDao] será la siguiente:


package client.android.dao.service;
 
import rx.Observable;
 
public interface IDao {
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // User
  void setUser(String user, String password);
 
  // Client timeout
  void setTimeout(int timeout);
 
  // Basic authentication
  void setBasicAuthentication(boolean isBasicAuthenticationNeeded);

  // debug mode
  void setDebugMode(boolean isDebugEnabled);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // random number service
  Observable<Response<Integer>> getRandom(int a, int b);
 
}
  • Ten en cuenta que los métodos de las líneas 6-22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
  • Línea 25: El método [getAlea] devuelve un número aleatorio en el rango [a,b]. Este número se devuelve en una respuesta [Response<Integer>], donde el número aleatorio está contenido en el campo [body] de ese tipo;

La interfaz [IDao] es 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 {
 
  // Web service client
  @RestService
  protected WebClient webClient;
  // security
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // the RestTemplate
  private RestTemplate restTemplate;
  // RestTemplate factory
  private SimpleClientHttpRequestFactory factory;
 
  @AfterInject
  public void afterInject() {
    // log
    Log.d(className, "afterInject");
    // create the RestTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Set the JSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Set the RestTemplate for the web client
    webClient.setRestTemplate(restTemplate);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    // Set the web service URL
    webClient.setRootUrl(url);
  }
 
  @Override
  public void setUser(String user, String password) {
    // Register the user in the interceptor
    authInterceptor.setUser(user, password);
  }
 
  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuration factory
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }
 
  @Override
  public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
    }
    // authentication interceptor?
    if (isBasicAuthenticationNeeded) {
      // add the authentication interceptor
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }
 
  // private methods -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }
 
  // random number service
  @Override
  public Observable<Response<Integer>> getRandom(final int a, final int b) {
    // web client execution
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getRandom(a, b);
      }
    });
  }
 
}
  • Ten en cuenta que las líneas 17-85 están incluidas por defecto en la clase [Dao] del proyecto [client-android-skel]. Sólo tienes que añadir los métodos para implementar la interfaz [IDao];
  • líneas 88-97: implementación del método [getAlea]. Esto es muy sencillo y ocupa 6 líneas, líneas 91-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 Respuesta<Integer> tipo. 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>] sólo tiene un método: getResponse. Su función es 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, éstos deben tener el valor final atributo;

2.8.3.5. El [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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
 
  // Parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Continue the initializations started by the parent class
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // define fragments here
    return new AbstractFragment[]{new Vue1Fragment_()};
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // define fragment titles here
    return null;
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // tab navigation - set the view to display
  }

  @Override
  protected int getFirstView() {
    return 0;
  }
 
  // IDao interface ------------------------------------------
  @Override
  public Observable<Response<Integer>> getRandom(int a, int b) {
    return dao.getRandom(a, b);
  }
 
}
  • Ten en cuenta que las líneas 15-61 están incluidas por defecto en el proyecto [client-android-skel]. Sólo tienes que personalizarlas;
  • líneas 40-44: la matriz de fragmentos. Aquí sólo 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 a mostrar es la vista #0, la de [Vue1Fragment];
  • líneas 64-67: implementación de la interfaz [IDao]. Aquí, no hay nada que hacer aparte de delegar el trabajo a la capa [DAO] en 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 {

  // fragment state ------------------------
  // list of responses
  private List<String> answers = new ArrayList<>();
  // view status ------------------------
  // error message regarding the number of random numbers requested
  private boolean txtErrorRandomVisible = false;
  // error message regarding the [a,b] generation interval
  private boolean txtErrorIntervalVisible = false;
  // error message regarding the web service URL
  private boolean txtWebServiceURLErrorVisible = false;
  // error message regarding the wait time
  private boolean textViewErrorDelayVisible = false;
  // Visibility of the Run button
  private boolean btnExecuteVisible = true;
 
  // getters and setters
...
}

Para determinar qué había que guardar en el fragmento, giramos el dispositivo en varias situaciones y observamos qué se perdía al restaurarlo. Llegamos a la conclusión de que había que guardar la información de las líneas 10-23.

2.8.3.7. El fragmento [View1Fragment]

  

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

El esqueleto del fragmento es el 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_empty)
public class Vue1Fragment extends AbstractFragment {
 
...
}
  • Línea 26: Tenga en cuenta que cada fragmento debe tener un menú, incluso si está vacío. Este es el caso aquí.

2.8.3.7.1. Manejo del clic en el botón [Ejecutar

@Click(R.id.btn_Executer)
  protected void doExecute() {
    // Check the entered data
    if (!isPageValid()) {
      return;
    }
    // clear previous answers
    answers.clear();
    dataAdapterAnswers.notifyDataSetChanged();
    // reset the response counter to 0
    nbAnswers = 0;
    infoAnswers.setText("List of answers (0)");
    // initialize activity
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // prepare the random task
    beginWaiting(1);
    // request random numbers
    getRandomNumbersInBackground(nbRandomNumbers, a, b);
  }
 
  void getRandomNumbersInBackground(int nbRandomNumbers, int a, int b) {
    // create the observable process
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getRandom(a, b));
    }
    // request the random numbers
    executeInBackground(process, new Action1<Response<Integer>>() {
 
      @Override
      public void call(Response<Integer> response) {
        // process the response
        consumeRandomResponse(response);
      }
    });
  }
 
  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // a response of +
    nbReponses++;
    infoReponses.setText(String.format("List of answers (%s)", nbReponses));
    // analyze the response
    // error?
    if (response.getStatus() != 0) {
      // display
      showAlert(response.getMessages());
      // Cancel
      doCancel();
      // return to UI
      return;
    }
    // add the information to the list of responses
    responses.add(0, String.valueOf(response.getBody()));
    // refresh the responses
    dataAdapterResponses.notifyDataSetChanged();
  }
 
  // Cancel ----------
  @Click(R.id.btn_Cancel)
  protected void cancel() {
    if (isDebugEnabled) {
      Log.d(className, "Cancelation requested");
    }
    // cancel asynchronous tasks
    cancelRunningTasks();
}
 
  private void beginWaiting(int nbRunningTasks) {
    // start the timer
    beginRunningTasks(nbRunningTasks);
    // The [Cancel] button replaces the [Run] button
    btnExecute.setVisibility(View.INVISIBLE);
    btnCancel.setVisibility(View.VISIBLE);
  }
  • líneas 4-6: En primer lugar, se comprueba que las entradas son válidas. A continuación pueden aparecer mensajes de error;
  • líneas 8-9: se borra la lista de respuestas. Este cambio se refleja en ListView que los muestra;
  • líneas 11-12: El número de respuestas recibidas se pone a cero;
  • línea 14: Establecemos el URL para el servicio de números aleatorios. Esta información se pasará a la capa [DAO];
  • línea 15: se establece el tiempo de espera antes de enviar la petición al servicio de números aleatorios. Esta información se pasará a la capa [DAO];
  • línea 17: nos preparamos para lanzar 1 tarea asíncrona (no N; ya veremos por qué);
  • líneas 24-27: Combinamos las N tareas asíncronas en una única secuencia de operaciones [fusión];
  • líneas 29-36: pedimos a la clase padre [AbstractParent] que consulte el servicio web de números aleatorios / JSON;
  • líneas 29-36: el método [executeInBackground] espera dos parámetros:
    • línea 29: el proceso a observar y ejecutar es el calculado en las líneas anteriores;
    • líneas 29-36: la instancia [Action1] que se ejecutará cuando se reciba la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getAlea], i.e., un tipo [Response<Integer>];
  • línea 34: cuando llega una respuesta (un número aleatorio), se consume en el método de la línea 39;
  • líneas 49-50: registramos y señalamos 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, el servidor ha encontrado un problema;
  • línea 55: se muestra un mensaje de error. El método [showAlert] pertenece a la clase padre;
  • línea 57: se llama al método de las líneas 68-75. Cancelará las tareas que aún estén activas (línea 74);
  • línea 62: la respuesta se añade a la lista de respuestas, que es la fuente de datos para el ListView;
  • línea 64: el ListView se actualiza;
  • líneas 77-83: el método [beginWaiting(int nbRunningTasks)] prepara la vista para la espera (líneas 81-82) y notifica a la clase padre que las tareas [nbRunningTasks] se ejecutarán en breve (línea 79);

2.8.3.7.2. Ciclo de vida del fragmento

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


  // local data
  private List<String> responses;
  private ArrayAdapter<String> dataAdapterAnswers;
  private int numberOfAnswers = 0;
...
  // lifecycle management ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // current state of the view
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErrorDelayVisible(textViewErrorDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorRandomVisible(txtErrorRandom.getVisibility() == View.VISIBLE);
    state.setTxtMsgWebServiceErrorUrlVisible(txtMsgWebServiceErrorUrl.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalVisible(txtErrorInterval.getVisibility() == View.VISIBLE);
    state.setExecuteButtonVisible(executeButton.getVisibility() == View.VISIBLE);
    state.setResponses(responses);
    return state;
  }
 
  @Override
  protected int getNumView() {
    return 0;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // First visit?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      answers = state.getAnswers();
    } else {
      answers = new ArrayList<>();
    }
    // listView data source
    dataAdapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
    // number of answers
    nbAnswers = answers.size();
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // Link listview / adapter
    listAnswers.setAdapter(dataAdapterAnswers);
    // First visit?
    if (previousState == null) {
      // hide error messages
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorInterval.setVisibility(View.INVISIBLE);
      txtWebServiceErrorMessage.setVisibility(View.INVISIBLE);
      textViewErrorDelay.setVisibility(View.INVISIBLE);
      // buttons
      btnCancel.setVisibility(View.INVISIBLE);
      btnExecute.setVisibility(View.VISIBLE);
    }
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // previous state of the view
    View1FragmentState state = (View1FragmentState) previousState;
    // show/hide error messages
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorInterval.setVisibility(state.isTxtErrorIntervalVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErrorDelay.setVisibility(state.isTextViewErrorDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // buttons
    btnCancel.setVisibility(state.isBtnExecuteVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecute.setVisibility(state.isBtnExecuteVisible() ? View.VISIBLE : View.INVISIBLE);
    // number of answers
    infoAnswers.setText(String.format("List of answers (%s)", numberOfAnswers));
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // The [Run] button replaces the [Cancel] button
    btnCancel.setVisibility(View.INVISIBLE);
    btnExecute.setVisibility(View.VISIBLE);
 
}
  • líneas 7-18: asegurar que el fragmento se guarda cuando la clase padre lo solicita;
  • línea 11: muestra el mensaje de error relativo al tiempo de espera;
  • línea 12: visibilidad del mensaje de error relativo al número de números aleatorios solicitados;
  • línea 13: visibilidad del mensaje de error relativo al servicio web URL / JSON;
  • línea 14: muestra el mensaje de error relativo al rango [a,b] para la generación de números aleatorios;
  • línea 15: visibilidad del botón [Ejecutar];
  • línea 16: la lista de respuestas recibidas;
  • líneas 20-23: debe devolver la vista ID. El fragmento ID aquí es 0 ya que sólo 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 desde el estado anterior del fragmento;
    • líneas 31-33: si se trata de la primera visita, el campo [reponses] se inicializa con una lista vacía;
    • líneas 34-37: utilizando el campo [reponses], podemos construir la fuente de datos del fragmento ListView (línea 35), así como el número de respuestas (línea 37);
  • líneas 40-55: se ejecuta para inicializar la vista asociada al fragmento, ya sea en una primera visita (previousState == null) o en una visita posterior;
    • línea 43: el fragmento ListView se vincula a la fuente de datos que se acaba de construir en el método [initFragment];
    • líneas 45-54: si se trata de la primera visita, la vista se prepara para su primera visualización;
  • líneas 57-60: se ejecuta durante la navegación entre fragmentos asociada a una acción [SUBMIT]. Aquí solo hay un fragmento y, por lo tanto, no hay navegación entre fragmentos;
  • líneas 63-76: se ejecuta durante la navegación entre fragmentos asociada a una acción [NAVIGATION] o durante un ciclo de guardar/restaurar debido a la rotación del dispositivo u otro motivo. Aquí sólo puede darse este último caso. Recuerde que aquí, en todos los casos, [previousState] es siempre no nulo;
  • línea 65: el estado anterior se convierte al tipo de estado del fragmento;
  • líneas 66-75: el contenido del estado anterior se utiliza para restaurar la vista;
  • líneas 78-81: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada que hacer;
  • líneas 83-89: se ejecuta cuando se completan todas las tareas asíncronas. Aquí, el botón [Cancelar] se oculta y se sustituye por el botón [Ejecutar];

2.8.3.8. Pruebas

Se invita al lector a realizar las siguientes pruebas:

  • crear errores y ejecutar el dispositivo: los mensajes de error deben permanecer en pantalla;
  • Genere números aleatorios y ejecute el dispositivo: los números aleatorios generados deben permanecer en pantalla;
  • set a wait of several seconds and run the device during the wait: the tasks must have been canceled (this can be seen in the logs);

2.8.4. Ejemplo-22B

Aquí revisitamos el Ejemplo 22 para refactorizarlo según el modelo de proyecto [client-android-skel]. Recordemos que el proyecto [Ejemplo-22] gestiona correctamente el ciclo de guardar/restaurar fragmentos durante la rotación y que sirvió de base para el proyecto [client-android-skel].

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

  

A continuación, copiamos varios elementos del proyecto [Ejemplo-22] en el proyecto [Ejemplo-22B].

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

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

Cambiaremos el margen superior de ambas vistas a 120 dp:

[view1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/title_view1"
    android:id="@+id/textViewTitleView1"
    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"/>

Next, we copy the elements [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

En este punto, podemos intentar una primera compilación. Aparece un primer tipo de error: incorrecto importaciones porque las clases han cambiado de paquete. Corregimos estos importaciones. Un segundo tipo de error se debe a que los fragmentos no implementan todos los métodos de su clase padre [AbstractFragment]. Corregimos esto pulsando (Alt+Enter).

Los errores restantes se deben a diferencias entre las clases antiguas y las nuevas [AbstractFragment]. Por ahora, los ignoramos.

2.8.4.1. Personalización de proyectos

  

La carpeta [custom] contiene elementos de arquitectura que pueden ser personalizados por el desarrollador.

La interfaz [IMainActivity] permite especificar determinadas 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 {
 
  // access to the session
  ISession getSession();
 
  // change view
  void navigateToView(int position, ISession.Action action);
 
  // wait management
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
 
  // maximum wait time for the server response
  int TIMEOUT = 1000;
 
  // wait time before executing the client request
  int DELAY = 0;
 
  // Basic authentication
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
 
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
  // tab bar
  boolean ARE_TABS_NEEDED = true;
 
  // loading icon
  boolean IS_WAITING_ICON_NEEDED = false;
 
  // number of fragments
  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 del fragmento. Esta constante puede tomar aquí un valor comprendido entre [1,4]. Se anima al lector a variar 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 {
  // whether the fragment has been visited
  protected boolean hasBeenVisited = false;
  // state of the fragment's menu (if any)
  protected MenuItemState[] menuOptionsState;
 
  // getters and setters
...
}
  • Línea 12: Declaramos la clase de estado del fragmento [PlaceHolderFragment]. El fragmento [Vue1Fragment] en sí no tiene estado;

La clase [Sesión] es la siguiente:


package client.android.architecture.custom;
 
import client.android.architecture.core.AbstractSession;
 
public class Session extends AbstractSession {
  // data to be shared between fragments themselves and between fragments and activities
  // Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
  // Don't forget the getters and setters required for JSON serialization/deserialization
 
  // number of fragments visited
  private int numVisit;
  // Fragment ID of type [PlaceholderFragment] displayed in the second tab
  private int numFragment = -1;
 
  // getters and setters
...
}

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

2.8.4.2. El [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 {
 
  // [DAO] layer
  @Bean(Dao.class)
  protected IDao dao;
  // session
  private Session session;
 
  // menu management-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }
 
  private void showFragment(int i) {
...
  }

  // Implementation of methods from the parent class ---------------------------------------------------
  ...
}

En este caso, la clase [MainActivity] es mayor que en los ejemplos anteriores por dos motivos:

  • hay pestañas que gestionar;
  • hay un menú que gestionar;

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

// parent class methods -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Continue the initializations started by the parent class
    // session
    this.session = (Session) super.session;
    ...
  }
 
  @Override
  protected IDao getDao() {
    return dao;
  }
 
  @Override
  protected AbstractFragment[] getFragments() {
    // fragment number
    final String ARG_SECTION_NUMBER = "section_number";
    // initialize the fragment array
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // create a fragment
      fragments[i] = new PlaceholderFragment_();
      // Pass arguments to the fragment
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // a fragment of +
    fragments[i] = new Vue1Fragment_();
    // result
    return fragments;
  }
 
 
  @Override
  protected CharSequence getFragmentTitle(int position) {
    // no titles here
    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 es creada por primera vez o recreada durante un ciclo de guardar/restaurar. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
  • línea 10: se recupera una referencia local a la sesión. La conversión de tipo es necesaria porque 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 el array de fragmentos gestionados por la aplicación. Aquí hay [FRAGMENTS_COUNT] fragmentos, un número definido en [IMainActivity]. Los primeros [FRAGMENTS_COUNT-1] fragmentos son de tipo [PlaceHolderFragment] y el último es de tipo [Vue1Fragment];
  • líneas 41-45: el método [getFragmentTitle] debe devolver los títulos de los fragmentos cuando esta información sea útil. Este no es el caso aquí;
  • líneas 47-50: este método es llamado por la clase padre cuando el usuario hace clic en una pestaña. Volveremos sobre ello en la siguiente sección;
  • líneas 52-55: devuelve el número de la primera vista a mostrar cuando se inicia la aplicación. Aquí, el fragmento [Vue1Fragment] debe mostrarse en primer lugar. El método [getFirstView] podría sustituirse ventajosamente por una constante en [IMainActivity];

2.8.4.2.2. Gestión de pestañas

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


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Continue the initializations started by the parent class
    // session
    this.session = (Session) super.session;
    // 1st tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("View 1");
    tabLayout.addTab(tab);
    // 2nd tab?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment #%s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
  }
 
  @Override
  protected void navigateOnTabSelected(int position) {
    // Fragment ID to display
    int fragmentNumber;
    switch (position) {
      case 0:
        // fragment number [View1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // fragment number [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // display fragment
    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 es creada por primera vez o recreada durante un ciclo de guardar/restaurar. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
  • línea 9: se recupera una referencia local a la sesión. La conversión de tipo es necesaria porque 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: la segunda pestaña se crea si se almacena un fragmento ID en la sesión (línea 15). Este ID se establece inicialmente en -1 cuando la actividad se construye por primera vez;
  • líneas 23-39: este método es llamado por la clase padre cuando el usuario hace clic en una pestaña;
  • líneas 28-31: si se hace clic en la pestaña 0, debe mostrarse [Vue1Fragment]. Sabemos que esta es la primera vista que se mostró cuando se inició la aplicación;
  • líneas 32-35: si se hace clic en la pestaña 1, debe mostrarse el fragmento cuyo número está almacenado en la sesión;
  • líneas 37-39: Navegamos hasta el fragmento seleccionado. La acción asociada es [SUBMIT]. ¿Podría haber sido [NAVIGATION]? En este documento, utilizamos [NAVIGATION] sólo cuando la visualización del nuevo fragmento requiere conocer únicamente su estado anterior. Aquí no es el caso, ya que la visualización del fragmento debe cambiar de su estado anterior para mostrar una visita más;

2.8.4.2.3. Gestión de menús

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="examples.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:

  

El menú se gestiona mediante los siguientes métodos:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // no navigation on software tab selection
      session.setNavigationOnTabSelectionNeeded(false);
      // recreate both tabs due to font issues with the titles
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("View1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment #%s", (i + 1))), false);
      // The fragment number to display is set in the session
      session.setNumFragment(i);
      // Select tab #2 with navigation
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • líneas 16-31: manejar un clic en un [Fragmenti];
  • líneas 37-50: mostrar el fragmento #i (son fragmentos de tipo PlaceHolderFragment) en la pestaña #1 (la segunda pestaña);
  • líneas 42-44: decidimos eliminar las pestañas existentes para crear dos nuevas. Esta decisión se tomó para solucionar el siguiente problema: cuando simplemente mostramos el fragmento en la pestaña 1 existente (sin eliminarla), curiosamente su título (fuente, tamaño) diferente al del título de la pestaña 0;
  • líneas 43-44: las dos pestañas se crean pero no se seleccionan (último parámetro establecido en falso);
  • Línea 40: Las operaciones de las líneas 42-44 pueden desencadenar operaciones [select] en las pestañas, que llamarán al manejador [onTabSelected]. Si no se toma ninguna acción, esto resultará en la navegación a un fragmento. Esto se evita estableciendo el booleano [navigationOnTabSelectionNeeded] a falso en la sesión. Este booleano se restablece automáticamente a verdadero por la clase [AbstractFragment] cuando un fragmento se hace visible;
  • línea 46: almacenamos el número del fragmento que se mostrará en la sesión;
  • líneas 48-50: Seleccione la pestaña #2 con la navegación (línea 48). Esto activará el procedimiento [onTabSelected], que:
    • mostrar el fragmento cuyo número se almacenó en la sesión;
    • almacena el número de la pestaña seleccionada en la sesión;

2.8.4.3. El fragmento [Vue1Fragment]

Aquí está 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 {
 
  // UI elements
  @ViewById(R.id.editTextName)
  protected EditText editTextName;
 
  // event handler
  @Click(R.id.buttonValider)
  protected void validate() {
    // display the entered name
    Toast.makeText(activity, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // fragment lifecycle -----------------------------------------------
  private void initFragment() {
    // nothing to do
  }
 
  // save fragment state
  @Override
  public CoreState saveFragment() {
    // view state - nothing to save
    return new CoreState();
  }
 
  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
 
  @Override
  protected void initFragment(CoreState previousState) {
    // nothing to do
  }
 
  @Override
  protected void initView(CoreState previousState) {
    // First visit?
    if (previousState == null) {
      // display the visit number
      showVisitNumber();
    }
 
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // display the visit number
    showNumVisit();
 
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
 
  }
 
  @Override
  protected void notifyEndOfUpdates() {
 
  }
 
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
 
  }
 
  // private methods -------------------------------------
  // display number of visits
  private void showVisitCount() {
    // increment visit count
    int visitCount = session.getVisitCount();
    numVisit++;
    session.setNumVisit(numVisit);
    // display the visit count
    Toast.makeText(activity, String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

La clase está casi vacía.

  • líneas 35-39: llamada por la clase padre cuando el fragmento necesita guardar su estado. El fragmento [Vue1Fragment] no tiene ningún estado que guardar. Simplemente devolvemos una instancia de la clase base [CoreState] (recordatorio: no debemos volver null);
  • líneas 41-44: debe devolver el fragmento ID. Por diseño, el fragmento [Vue1Fragment] tiene el ID [FRAGMENTS_COUNT-1];
  • líneas 51-59: llamada por la clase padre cuando el fragmento se construye por primera vez (previousState == null) o en visitas posteriores (previousState != null);
    • líneas 54-57: si se trata de la primera visita, incrementa el recuento de visitas y lo muestra (líneas 85-92);
  • líneas 61-65: se llama cuando el fragmento está a punto de mostrarse en asociación con una acción [SUBMIT]. El recuento de visitas se incrementa y se muestra. En este caso, no es posible que el recuento de visitas se incremente dos veces durante el ciclo de vida. De hecho, la primera visita al fragmento [Vue1Fragment] se produce al inicio de la aplicación cuando la acción se establece en [NONE] por diseño en la sesión. Esto asegura que el método [updateOnSubmit] no será llamado. Después de eso, nunca será la primera visita de nuevo, y el método [initView] no hará nada;
  • líneas 68-71: llamada durante un ciclo de guardar/restaurar. Como el fragmento no tiene estado, no hay nada que restaurar aquí;
  • líneas 73-76: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí ya no queda nada por hacer;
  • líneas 78-81: llamada cuando todas las tareas asíncronas lanzadas han finalizado. Aquí no hay tareas asíncronas;

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 {
  // text
  private String text;
 
  // constructors
  public PlaceHolderFragmentState() {
 
  }
 
  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }
 
  // getters and setters
 ...
}
  • Cuando necesitemos guardar el estado del fragmento, guardaremos el texto que estaba mostrando (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 {
 
  // UI components
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;
 
  // data
  private String text;
 
  // fragment ID
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // implementation of parent class methods ----------------------------
  @Override
  public CoreState saveFragment() {
    // save the fragment state
    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) {
    // original text
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }
 
  @Override
  protected void initView(CoreState previousState) {
  }
 
  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // update the displayed text
    // increment visit count
    int visitCount = session.getVisitCount();
    numVisit++;
    session.setNumVisit(numVisit);
    // updated text
    textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, displayed text=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }
 
  @Override
  protected void updateOnRestore(CoreState previousState) {
    // restore the displayed text
    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 pide al fragmento que guarde su estado, se guarda el texto mostrado por el fragmento (línea 34);
  • líneas 38-41: devuelve el ID del fragmento. Esto depende de la sección ID pasada como argumento cuando se creó el fragmento;
  • líneas 43-47: llamada durante la primera construcción del fragmento (previousState == null) o durante construcciones posteriores (previousState != null);
    • línea 46: aquí no se utiliza el estado anterior. El texto inicial [texto] (línea 24) mostrado en la primera visita se recalcula cada vez. Esto es discutible. Podríamos haber optado por incluir también esta información en el estado del fragmento;
  • líneas 49-51: llamada durante la primera renderización de la vista asociada al fragmento (previousState == null) o durante renderizaciones posteriores (previousState != null). No hay nada que hacer;
  • líneas 53-56: llamada cuando el fragmento está a punto de mostrarse en asociación con una acción [SUBMIT]. Este es siempre el caso excepto durante el ciclo guardar/restaurar, donde la acción es [RESTORE]. Por lo tanto, incrementamos el número de visita y lo mostramos;
  • líneas 68-74: llamada durante un ciclo de guardar/restaurar. Restauramos el texto que se guardó en el estado del fragmento;
  • líneas 76-79: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada más que hacer;
  • líneas 82-83: llamada cuando todas las tareas asíncronas lanzadas han finalizado. Aquí no hay tareas asíncronas;

2.8.4.6. Pruebas

Invitamos al lector a probar la aplicación girando el dispositivo para comprobar que el fragmento visualizado no pierde su estado. También examinaremos los registros.

2.9. Conclusión

Al final de este capítulo, tenemos un proyecto de ejemplo [client-android-skel] para 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 gestiona mediante la biblioteca RxJava;
  • el ciclo de vida del fragmento (actualizar, guardar, restaurar) es gestionado por su clase madre [AbstractFragment], que llama a métodos específicos de sus clases hijas en momentos precisos. De este modo, el fragmento hijo no necesita preocuparse de las etapas del ciclo de vida, sino únicamente de implementar determinados métodos requeridos por su clase padre;
  • el ciclo de vida de la actividad (guardar / restaurar) es gestionado por una clase abstracta [AbstractActivity], que también requiere que la actividad hija implemente ciertos métodos;
  • La clase [AbstractActivity] puede manejar una aplicación con o sin pestañas, con o sin imagen de carga, y con o sin autenticación básica contra el servidor web / JSON. La presencia o ausencia de estos elementos viene determinada por la configuración;

A continuación presentaremos un caso práctico más complejo que los ejemplos anteriores. La nueva aplicación se basará en el proyecto de plantilla [client-android-skel].