2. Estructura básica de una aplicación para Android que se comunica con un servicio web / jSON
Ahora ofrecemos una estructura básica de una aplicación para Android que se comunica con uno o varios servicios web / jSON. Se trata del proyecto [client-android-skel], que se encuentra en la carpeta [architecture] de los ejemplos:
![]() |
El análisis de esta aplicación base nos dará la oportunidad de repasar algunos puntos que hemos visto en los ejemplos anteriores. Esta aplicación servirá de estructura básica para todas las aplicaciones futuras. Se ha desarrollado tras numerosas iteraciones. Su objetivo es factorizar en clases abstractas el mayor número posible de elementos de las aplicaciones que vamos a crear próximamente, con el fin de evitar tener que escribir siempre el mismo tipo de código, que solo se diferencia en detalles. Sus características son las siguientes:
- la comunicación asíncrona con el servidor web / jSON se realiza mediante la biblioteca RxJava;
- el ciclo de vida de un fragmento (actualización, guardado, restauración) lo gestiona su clase padre [AbstractFragment], que invoca en momentos concretos determinados métodos de sus clases hijas. De este modo, la clase hija no tiene que preocuparse por las etapas del ciclo de vida, sino únicamente de implementar ciertos métodos impuestos por su clase padre;
- el ciclo de vida de la actividad (guardar/restaurar) lo gestiona una clase abstracta, [AbstractActivity], que a su vez exige a la actividad hija que implemente determinados métodos;
- la clase [AbstractActivity] es capaz de gestionar una aplicación con o sin pestañas, con o sin imagen de espera, con o sin autenticación básica en el servidor web / jSON. La presencia o ausencia de estos elementos se configura mediante la configuración;
Este esqueleto se ha utilizado para todos los ejemplos posteriores. Debido a la diversidad de estos, lo que funcionaba en un ejemplo podía no funcionar en el siguiente. Dado que el esqueleto se ha utilizado para un total de siete ejemplos, se han producido numerosas iteraciones. Si se utilizara para un octavo ejemplo, es posible que, una vez más, la especificidad de este nuevo ejemplo generara nuevos errores. No obstante, el uso de esta plantilla simplificará considerablemente la redacción de los ejemplos que vendrán a continuación. De hecho, la gestión del ciclo de vida de un fragmento (actualización, guardado, restauración), junto con el concepto de adyacencia de los fragmentos, resulta especialmente compleja. En este caso, queda totalmente oculta en la clase [AbstractFragment].
2.1. Arquitectura del cliente Android
El cliente de Android propuesto se basa en la siguiente arquitectura:
![]() |
- la capa [DAO] implementa una interfaz [IDao]. Es esta la que se comunica con el servidor web / jSON;
- solo hay una actividad que también implementa la interfaz [IDao]. Las vistas se dirigen a ella para acceder al servidor;
- las vistas se implementan mediante fragmentos;
El proyecto de Android refleja esta arquitectura:
![]() |
Vamos a presentar uno a uno los diferentes elementos de este proyecto.
2.2. La configuración de Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Desde la versión 0.11 del complemento Gradle para Android, hay que utilizar android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// Opciones de empaquetado necesarias para poder generar el APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- Todos los números de versión están sujetos a cambios. No obstante, podemos partir de los números actuales si configuramos Android Studio para que estas versiones de las herramientas de Android (líneas 15-16, 47-48) estén bien presentes (véase el apartado 6.11);
2.3. El manifiesto de la aplicación
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="client.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- línea 3: cambiaremos el paquete de la aplicación;
- líneas 10 y 15: se fijará el valor del elemento [app_name] en el archivo [res / values / strings.xml]. Por el momento, este es el siguiente:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- nombre de la aplicación -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. La organización del código Java
![]() |
- [architecture] agrupa los principales elementos de organización del código;
- [activity] contiene la única actividad de la aplicación;
- [fragments] agrupa los fragmentos o vistas de la aplicación;
- [dao] agrupa los elementos de comunicación con el servidor web / jSON;
2.5. Elementos de la actividad
![]() | ![]() |

2.5.1. La vista asociada a la actividad
La vista [activity_main.xml] asociada a la actividad es la siguiente:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- contenedor de fragmentos -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
- línea 29: se utiliza un contenedor de fragmentos específico;
La actividad también 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>
Por el momento, está vacío. El desarrollador lo completará si es necesario.
2.5.2. El contenedor de fragmentos [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// controla el deslizamiento
private boolean isSwipeEnabled;
// controla el desplazamiento
private boolean isScrollingEnabled;
// constructores
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// métodos que hay que redefinir para gestionar el deslizamiento
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// ¿Deslizamiento permitido?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// ¿Deslizamiento permitido?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// control del desplazamiento
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
Esta clase amplía la clase estándar de Android [ViewPager] únicamente para gestionar el deslizamiento (línea 11) y el desplazamiento (línea 13) entre vistas.
- líneas 26-43: los métodos que desactivan el deslizamiento si este se ha desactivado;
- líneas 46-49: redefinición del método [setCurrentItem], que sirve para cambiar la vista mostrada. Si se ha desactivado el desplazamiento, el cambio de vista se realizará sin desplazamiento. Cabe señalar que el desarrollador puede eludir este modo de funcionamiento utilizando el método [setCurrentItem(int position, boolean smoothScrolling)], que le permite especificar el desplazamiento que desee;
2.5.3. La clase [CoreState]
![]() |
La clase [CoreState] es la clase principal de los estados de los distintos fragmentos:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// Tareas pendientes: añadir aquí las subclases de [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragmento visitado o no
protected boolean hasBeenVisited = false;
// estado del posible menú del fragmento
protected MenuItemState[] menuOptionsState;
// getters y setters
...
}
- línea 16: cada fragmento tiene en su estado un valor booleano [hasBeenVisited] que indica si ya se ha visitado o no. Esto es necesario porque, a veces, la primera vez que se muestra un fragmento, hay que realizar algunas acciones específicas;
- línea 18: el proyecto [client-android-skel] guarda y restaura automáticamente los menús de los fragmentos, si es que tienen alguno. En la tabla MenuItemState[] menuOptionsState se almacena el estado de visibilidad de todas las opciones del menú;
- líneas 10-13: al igual que se ha hecho en [Exemple-22], el estado de la actividad y de sus fragmentos se guardará en la sesión, que a su vez se guardará en forma de cadena jSON. Veremos que la sesión almacena una matriz de elementos de tipo [CoreState]. Si no hacemos nada, se guardará la cadena jSON de tipo [CoreState]. Sin embargo, nosotros queremos guardar los estados de los fragmentos, es decir, los estados derivados de [CoreState]. Para que se genere la cadena jSON del tipo derivado y no la del tipo padre, hay que declarar los tipos derivados tal y como se indica en las líneas 10-13. La clase [CoreState] es una de las clases de la arquitectura que el desarrollador debe modificar para cada nueva aplicación (líneas 10-13);
2.5.4. La interfaz [IMainActivity]
![]() |
La interfaz [IMainActivity] establece lo que los fragmentos pueden solicitar a la actividad en la siguiente arquitectura:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// acceso a la sesión
ISession getSession();
// cambio de vista
void navigateToView(int position, ISession.Action action);
// gestión de la espera
void beginWaiting();
void cancelWaiting();
// constantes de la aplicación (a modificar) -------------------------------------
// modo de depuración
boolean IS_DEBUG_ENABLED = true;
// tiempo máximo de espera de la respuesta del servidor
int TIMEOUT = 1000;
// tiempo de espera antes de ejecutar la solicitud del cliente
int DELAY = 0;
// autenticación básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adyacencia de fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de pestañas
boolean ARE_TABS_NEEDED = false;
// imagen de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos de la aplicación
int FRAGMENTS_COUNT = 0;
// Tareas pendientes: añade aquí tus constantes y otros métodos
}
- línea 6: la interfaz [IMainActivity] amplía la interfaz [IDao] de la capa [DAO];
- línea 9: es la actividad la que da acceso a la sesión en forma de una instancia de la interfaz [ISession];
- línea 12: a través de esta actividad se cambia de vista. El segundo parámetro es la acción que provoca este cambio de vista, uno de los valores SUBMIT, NAVIGATION o RESTORE;
- líneas 15-17: es la actividad que gestiona la imagen de espera;
- línea 22: para la depuración de la aplicación;
- línea 25: para no esperar demasiado tiempo si el servidor deja de responder;
- línea 28: en modo de depuración, se establecerá un valor de unos segundos para tener tiempo de cancelar la operación con el servidor y ver qué ocurre;
- línea 31: a true si el servicio jSON solicita una autenticación básica;
- línea 34: adyacencia de fragmentos;
- línea 37: a vrai si la aplicación tiene pestañas;
- línea 39: a vrai si la aplicación se comunica con un servidor web / jSON y se quiere mostrar una imagen de espera durante los intercambios;
- línea 43: el número de fragmentos gestionados por la aplicación;
La interfaz [IMainActivity] es el segundo elemento de la arquitectura que el desarrollador debe completar (línea 45).
2.5.5. La interfaz [IDao]
La interfaz [IMainActivity] amplía la siguiente interfaz [IDao]:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL del servicio web
void setUrlServiceWebJson(String url);
// usuario
void setUser(String user, String mdp);
// tiempo de espera del cliente
void setTimeout(int timeout);
// autenticación básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuración
void setDebugMode(boolean isDebugEnabled);
// tiempo de espera del cliente en milisegundos antes de la solicitud
void setDelay(int delay);
// Tarea pendiente: declara tu interfaz aquí
}
- línea 24: el desarrollador completará la interfaz aquí;
2.5.6. La sesión
![]() |
La clase [Session] encapsula los elementos compartidos por la actividad y los fragmentos. Implementa la siguiente interfaz [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// número de la última vista mostrada
int getPreviousView();
void setPreviousView(int numView);
// último estado de una vista
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// acción en curso
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// Estados de todas las vistas -
// no se utiliza en el código, pero es necesario para la serialización/deserialización jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// N.º de la última pestaña seleccionada
int getPreviousTab();
void setPreviousTab(int position);
// navegación al seleccionar una pestaña
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introducimos la interfaz [ISession] para exigir la presencia de ciertos métodos en la sesión:
- líneas 7-10: el número de la última vista (fragmento) mostrada;
- líneas 12-15: el estado de una vista concreta;
- líneas 17-24: introducimos el concepto de acción en curso. Hay cuatro (línea 17):
- RESTORE: se está realizando una copia de seguridad o una restauración. No hay cambio de vista;
- NAVIGATION: se está realizando una navegación. Denominaremos aquí «navegación» a un cambio de vista en el que la nueva vista puede restaurarse a partir de su último estado guardado en la sesión;
- SUBMIT: se asignará el tipo [SUBMIT] a una acción en curso cuando se produzca un cambio de vista y la nueva vista dependa del estado de la actividad en general y no únicamente de su propio estado. A veces, resulta difícil distinguir entre NAVIGATION y SUBMIT. En ese caso, se tomará el caso más general, el de SUBMIT;
- NONE: valor de la acción cuando esta aún no ha recibido su primer valor;
- líneas 26-30: los estados de la actividad y de los fragmentos se almacenarán en una matriz de tipo CoreState[]. Para que esta se gestione correctamente durante las serializaciones/deserializaciones 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 guardado/restauración para volver a seleccionar la pestaña que estaba seleccionada antes de girar el dispositivo;
- líneas 37-40: gestión de un valor booleano que indica si la selección de una pestaña debe ir acompañada de un cambio de fragmento;
La interfaz [ISession] está implementada por la siguiente clase abstracta [AbstractSession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class AbstractSession implements ISession {
// N.º de la vista anterior
private int preViousView;
// estado de las vistas
private CoreState[] coreStates = new CoreState[0];
// acción en curso
private Action action = Action.NONE;
// pestaña seleccionada anteriormente
private int previousTab;
// navegación al seleccionar una pestaña
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// constructor
public AbstractSession() {
// se inicializa la tabla de estados de los fragmentos
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// interfaz ISession ---------------------------------------------------------
@Override
public int getPreviousView() {
return preViousView;
}
@Override
public void setPreviousView(int numView) {
this.preViousView = numView;
}
@Override
public CoreState getCoreState(int numView) {
return coreStates[numView];
}
@Override
public void setCoreState(int numView, CoreState coreState) {
coreStates[numView] = coreState;
}
@Override
public Action getAction() {
return action;
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public CoreState[] getCoreStates() {
return coreStates;
}
@Override
public void setCoreStates(CoreState[] coreStates) {
this.coreStates = coreStates;
}
@Override
public int getPreviousTab() {
return previousTab;
}
@Override
public void setPreviousTab(int position) {
this.previousTab = position;
}
@Override
public boolean isNavigationOnTabSelectionNeeded() {
return navigationOnTabSelectionNeeded;
}
@Override
public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
}
}
- línea 9: el número de la vista que se mostraba antes de la que se muestra actualmente. Esta información resulta útil cuando se puede acceder a una vista desde varios lugares. Este suele ser el caso en la navegación por pestañas. De este modo, la vista mostrada puede saber cuál era la vista anterior;
- línea 12: la tabla de estados de todos los fragmentos mostrados por la actividad;
- línea 18: el número de la pestaña seleccionada anteriormente. Desempeña una función similar a la del número de la vista anterior de la línea 9. Esta información resulta útil cuando se produce una rotación del dispositivo y es necesario volver a situarse en la pestaña que estaba seleccionada antes de la rotación;
- línea 22: un valor booleano que indica si la selección de una pestaña debe ir acompañada de un cambio en el fragmento mostrado. Hay que tener en cuenta que el proyecto [client-android-skel] gestiona por separado las pestañas y los fragmentos para poder utilizarse en casos en los que el número de pestañas sea inferior al número de fragmentos. Hay dos tipos de selección:
- una selección realizada por el usuario al hacer clic en una pestaña. En este caso, por lo general, el fragmento mostrado debe cambiar;
- una selección por parte del software mediante el método [Tablayout.Tab.select()]. En este caso, no siempre es deseable que cambie el fragmento mostrado. He aquí dos ejemplos:
- al girar el dispositivo, se vuelve a crear la actividad y también las pestañas. Sin embargo, cuando se crea la primera pestaña, se le aplica automáticamente una operación de software [select]. Por lo tanto, no es conveniente cambiar el fragmento mostrado, ya que nos encontramos en una fase de recreación de la actividad en la que el fragmento que finalmente se muestre no será necesariamente el asociado a la primera pestaña;
- dado que la gestión de las pestañas está separada de la de los fragmentos, puede que se desee actualizar las pestañas (eliminación, adición) sin interferir con sus fragmentos asociados. Sin embargo, algunas de estas operaciones pueden, una vez más, desencadenar una operación de software implícita [select] en una de las pestañas. Esta selección no tiene por qué traducirse necesariamente en una navegación hacia el fragmento asociado;
- línea 21: el campo [navigationOnTabSelectionNeeded] no está destinado a guardarse durante las operaciones de guardado de la actividad y sus fragmentos. La anotación [@JsonIgnore] hace que el campo se ignore durante las serializaciones y deserializaciones jSON;
- líneas 25-31: el constructor inicializa la matriz de estados de los fragmentos [FRAGMENTS_COUNT] de la aplicación. Los elementos de esta matriz se inicializan con el campo [hasBeeenVisited=false]. Esta información se utiliza para determinar si se trata o no de la primera visita al fragmento;
La clase [Session] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// datos que deben compartirse entre los propios fragmentos y entre los fragmentos y la actividad
// los elementos que no se pueden serializar en jSON deben llevar la anotación @JsonIgnore
// No olvidar los métodos getter y setter necesarios para la serialización/deserialización en jSON
}
- línea 5: la clase [Session] amplía la clase [AbstractSession] que acabamos de ver. El desarrollador colocará en ella los elementos que se compartirán entre los propios fragmentos y entre los fragmentos y la actividad. Cabe señalar que la clase [Session] ya no está anotada con la anotación AA [@EBean]. Se ha convertido en una clase normal;
2.5.7. La clase abstracta [AbstractActivity]
![]() |
2.5.7.1. Squelette
La clase [AbstractActivity] tiene más de 300 líneas. La analizaremos por etapas. Su estructura básica es la siguiente:
package client.android.architecture;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// capa [DAO]
private IDao dao;
// la sesión
protected Session session;
// el contenedor de fragmentos
protected MyPager mViewPager;
// la barra de herramientas
private Toolbar toolbar;
// la imagen de espera
private ProgressBar loadingPanel;
// barra de pestañas
protected TabLayout tabLayout;
// el gestor de fragmentos o secciones
private FragmentPagerAdapter mSectionsPagerAdapter;
// nombre de la clase
protected String className;
// mapeador jSON
private ObjectMapper jsonMapper;
// constructor
public AbstractActivity() {
// nombre de la clase
className = getClass().getSimpleName();
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implementación IMainActivity --------------------------------------------------------------------
...
// ciclo de vida: copia de seguridad y restauración de la actividad ------------------------------------
...
// gestión de la imagen de espera ---------------------------------
...
// interfaz IDao -----------------------------------------------------
...
// el gestor de fragmentos --------------------------------
...
// clases hijas
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
}
La clase [AbstractActivity]:
- implementa la interfaz [IMainActivity] (líneas 21 y 55);
- gestiona el guardado y la restauración de la actividad y sus fragmentos durante una rotación del dispositivo (línea 58);
- gestiona la imagen de espera durante un intercambio con el servidor web / jSON (línea 61);
- implementa la interfaz IDao de la capa [DAO] (línea 64);
- implementa el gestor de fragmentos (línea 67);
- exige a sus clases hijas la presencia de seis métodos (líneas 71-81);
2.5.7.2. Implementar la interfaz [IMainActivity]
La implementación de la interfaz [IMainActivity] (véase el apartado 2.5.4) es la siguiente:
// Implementación IMainActivity --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
}
// visualización de un nuevo fragmento
mViewPager.setCurrentItem(position);
// se indica la acción en curso durante este cambio de vista
session.setAction(action);
}
2.5.7.3. Guardar el estado de la actividad y sus fragmentos
El estado de la actividad y de sus fragmentos se encuentra íntegramente en la sesión. Por lo tanto, se trata de guardar dicha sesión. Repetimos aquí lo que se ha hecho en el proyecto [Exemple-22] (véase el apartado 1.23):
// gestión de la copia de seguridad y la restauración de la actividad ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// padre
super.onSaveInstanceState(outState);
// guardado de la sesión en forma de cadena jSON
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// registro
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. Restauración del estado de la actividad y de sus fragmentos
Se trata de restaurar la sesión. Procedemos tal y como se ha mostrado en [Exemple-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// padre
super.onCreate(savedInstanceState);
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// ¿Hay algo que restaurar?
if (savedInstanceState != null) {
// recuperación de sesión
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// registro
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// sesión
session = new Session();
}
...
- líneas 10-26: si el parámetro [Bundle savedInstanceState] de la línea 2 no es null, entonces se restaura la sesión (líneas 12-17);
- líneas 26-29: el caso en el que el parámetro [Bundle savedInstanceState] de la línea 2 es null corresponde al primer inicio de la actividad. En ese caso, se crea una sesión vacía;
2.5.7.5. Inicialización de la capa [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// padre
super.onCreate(savedInstanceState);
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// capa [DAO]
dao = getDao();
if (dao != null) {
// configuración de la capa [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// clases hijas
protected abstract IDao getDao();
....
}
- línea 11: se solicita a la actividad hija (línea 21) una referencia a la capa [DAO];
- líneas 14-17: si existe la capa [DAO], se configura a partir de la información contenida en la interfaz [IMainActivity];
2.5.7.6. Inicialización de la vista asociada a la actividad
La vista asociada a la actividad se ha presentado en el apartado 2.5.1:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- contenedor de fragmentos -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
Esta vista se inicializa con el siguiente código:
@Override
protected void onCreate(Bundle savedInstanceState) {
// clase principal
super.onCreate(savedInstanceState);
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// vista asociada
setContentView(R.layout.activity_main);
// componentes de la vista ---------------------
// barra de herramientas
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// ¿Imagen de carga?
if (IS_WAITING_ICON_NEEDED) {
// se añade la imagen de espera
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// creación de ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// se añade ProgressBar a la barra de herramientas
toolbar.addView(loadingPanel);
}
...
- línea 11: la vista XML [activity_main] está asociada a la actividad;
- líneas 14-15: la barra de herramientas está integrada y es compatible;
- líneas 17-27: posible adición de una imagen de espera: si el valor booleano [IS_WAITING_ICON_NEEDED] es verdadero en la interfaz [IMainActivity];
- línea 23: creación de la imagen de espera de tipo [ProgressBar] a la que hace referencia el campo [loadingPanel];
- línea 24: inicialmente, esta imagen está oculta;
- línea 26: se añade a la barra de herramientas;
2.5.7.7. Gestión de pestañas
La interfaz [IMainActivity] puede requerir una barra de pestañas. Esta se añade y se gestiona de la siguiente manera:
// barra de pestañas
protected TabLayout tabLayout;
...
// ¿Barra de pestañas?
if (ARE_TABS_NEEDED) {
// se añade la barra de pestañas
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// sin navegación por selección hasta que se muestre un fragmento
session.setNavigationOnTabSelectionNeeded(false);
// creación de la barra de pestañas
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// Añadir la barra de pestañas a la barra de aplicaciones
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// gestor de eventos de la barra de pestañas
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// se ha seleccionado una pestaña
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// posición de la pestaña
int position = tab.getPosition();
// memoria
session.setPreviousTab(position);
// ¿Se muestra el fragmento asociado?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// clases hijas
protected abstract void navigateOnTabSelected(int position);
...
- líneas 12-48: adición y gestión de una barra de pestañas;
- línea 6: la barra de pestañas se añade si la constante [ARE_TABS_NEEDED] está establecida en vrai en la interfaz [IMainActivity];
- línea 12: al crear la barra de pestañas, pueden producirse operaciones [Tablayout.Tab.select] implícitas (no es el usuario quien las provoca). Se establece el valor booleano [session.navigationOnTabSelectionNeeded] en faux para evitar cualquier navegación durante estas selecciones falsas. Será el desarrollador quien seleccione el fragmento que se va a mostrar mediante el método [navigateToView]. La variable booleana [session.navigationOnTabSelectionNeeded] se restablecerá a vrai cuando se muestre este fragmento (véase la clase AbstractFragment);
- línea 14: creación de una barra de pestañas referenciada por el campo [tabLayout]. Utilizamos una barra de pestañas personalizada [CustomTabLayout], sobre la que volveremos más adelante;
- línea 15: establecemos los colores de los títulos de las pestañas. Estos se encuentran en el siguiente archivo [res / color / tab_txt.xml]:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="#FFFF00" />
<item android:state_selected="false" android:color="#FFFFFF" />
</selector>
- línea (c): el color del título de la pestaña cuando está seleccionada;
- línea (d): el color del título de la pestaña cuando esta no está seleccionada;
Por supuesto, este archivo se puede modificar. Los códigos hexadecimales de los colores se pueden consultar, por ejemplo, aquí.
- líneas 17-18: añadir esta barra de pestañas a la barra de aplicaciones presente en la vista XML [activity_main];
- líneas 20-47: gestor de eventos de la barra de pestañas;
- líneas 22-36: solo se gestiona el evento [onTabSelected]. Corresponde a un clic en la pestaña [Tab tab] pasada como parámetro al método o bien a una operación de software [TabLayout.Tab.select];
- línea 30: posición de la pestaña seleccionada;
- línea 32: esta posición se guarda en la sesión;
- línea 34: ahora se trata de mostrar el fragmento asociado a esta pestaña. Solo la clase hija (línea 52) puede realizar esta asociación. Cabe señalar que no se asocia la barra de pestañas con el contenedor de fragmentos [mViewPager], como se ha hecho en algunos de los ejemplos estudiados. Aquí se separa totalmente la gestión de la barra de pestañas de la de los fragmentos. Por eso, cuando se hace clic en una pestaña, es necesario indicar qué vista se desea que se muestre;
- línea 28: se distingue entre la selección de una pestaña con o sin navegación. En general, cuando el usuario hace clic en una pestaña, se desea que haya navegación, mientras que en una selección por parte del software no se desea. Es el desarrollador quien distingue estos dos casos mediante el elemento [session.navigationOnTabSelectionNeeded]. Cuando no se realiza la navegación, el número de la última pestaña seleccionada no se guarda en la sesión. Será el desarrollador quien deba hacerlo;
2.5.7.8. El gestor de pestañas [CustomTabLayout]
![]() |
Utilizamos un gestor de pestañas personalizado para poder mostrar el título de las pestañas con diferentes tipos de letra. La clase [CustomTabLayout] es la siguiente:
package client.android.architecture.custom;
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class CustomTabLayout extends TabLayout {
private Typeface mTypeface;
public CustomTabLayout(Context context) {
super(context);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
}
@Override
public void addTab(Tab tab) {
super.addTab(tab);
ViewGroup mainView = (ViewGroup) getChildAt(0);
ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
int tabChildCount = tabView.getChildCount();
for (int i = 0; i < tabChildCount; i++) {
View tabViewChild = tabView.getChildAt(i);
if (tabViewChild instanceof TextView) {
((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
}
}
}
}
- La personalización de la fuente de los títulos de las pestañas se realiza en las líneas 30 y 44;
El archivo [fonts] es el siguiente:
![]() |
Fuentes:
- el código de la clase [CustomTabLayout] se ha encontrado en URL y [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- las fuentes se han encontrado en URL y [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Últimas inicializaciones
@Override
protected void onCreate(Bundle savedInstanceState) {
// padre
super.onCreate(savedInstanceState);
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// instanciación del gestor de fragmentos
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// el contenedor de fragmentos está asociado al gestor de fragmentos
// es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor de fragmentos
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// se desactiva el deslizamiento entre fragmentos
mViewPager.setSwipeEnabled(false);
// adyacencia de los fragmentos
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// se muestra la primera vista
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// se pasa el control a la actividad hija
onCreateActivity();
}
...
// clases hijas
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- líneas 10-19: aquí encontramos código que aparece a menudo en los ejemplos estudiados;
- líneas 21-23: visualización de la primera vista. Sin duda, hay varias formas de distinguir este caso. Aquí hemos aprovechado el hecho de que, para la primera vista, el valor de la acción que provoca el cambio de vista es NONE;
- línea 22: no hacemos ninguna suposición sobre el primer fragmento que se va a mostrar. En nuestros ejemplos, a menudo ha sido el fragmento n.º 0, pero no siempre (véase el Ejemplo 22). Por lo tanto, le pediremos a la actividad hija (línea 30) que nos indique cuál es esa primera vista;
- línea 25: aquí hemos factorizado todo lo que se podía. Ahora, la clase hija tiene que realizar sus propias inicializaciones (línea 29);
2.5.7.10. Gestión de la imagen de espera
En la clase [AbstractActivity], la imagen de espera se gestiona mediante los dos métodos siguientes:
// gestión de la imagen de espera ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. Implementación de la interfaz [IDao]
En la clase [AbstractActivity], la interfaz [IDao] (véase el apartado 2.5.5) se implementa de la siguiente manera:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// capa [DAO]
private IDao dao;
...
// Interfaz IDao -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String mdp) {
dao.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
dao.setBasicAuthentification(isBasicAuthentificationNeeded);
}
@Override
public void setDebugMode(boolean isDebugEnabled) {
dao.setDebugMode(isDebugEnabled);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
- línea 3: cabe recordar que el valor de este campo lo ha proporcionado 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:
...
// el gestor de fragmentos --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragmentos de la clase hija
fragments = getFragments();
}
// debe mostrar el fragmento n.º posición
@Override
public AbstractFragment getItem(int position) {
// se muestra el fragmento
return fragments[position];
}
// muestra el número de fragmentos que hay que gestionar
@Override
public int getCount() {
return fragments.length;
}
// muestra el título del fragmento n.º posición
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// clases hijas
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- línea 5: el array de fragmentos asociados a la actividad. Todos los fragmentos se derivarán de la clase [AbstractFragment];
- líneas 8-12: es el constructor el que inicializa la matriz de fragmentos. Solicita estos a la clase hija de la actividad (línea 35);
- líneas 28-31: los títulos de los fragmentos se pueden utilizar en una aplicación en la que haya tantas pestañas como fragmentos. En este caso, se puede asignar a la pestaña el título del fragmento. Aquí, estos títulos se solicitan a la clase hija (línea 37);
2.5.7.13. El método [onResume]
El método [onResume] se ejecuta poco antes de que la vista asociada a la actividad se haga visible. Aquí se utiliza para seleccionar una pestaña tras un guardado o una restauración:
@Override
public void onResume() {
// clase principal
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// si se restaura, hay que restaurar la última pestaña seleccionada
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- línea 10: selección de la pestaña que estaba seleccionada antes del proceso de guardado/restauración. Hay que tener en cuenta 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 desactivado la navegación al seleccionar una pestaña. Por lo tanto, aquí se selecciona una pestaña, pero no se produce ningún cambio de fragmento;
2.5.7.14. Résumé
La clase abstracta [AbstractActivity] será la clase padre de la única actividad de la aplicación.
La actividad hija deberá implementar los seis métodos siguientes:
// clases hijas
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
Además, la actividad hija tiene acceso a los siguientes elementos protegidos de su clase padre:
// la sesión
protected ISession session;
// el contenedor de fragmentos
protected MyPager mViewPager;
// barra de pestañas
protected CustomTabLayout tabLayout;
// nombre de la clase
protected String className;
2.5.8. La actividad [MainActivity]
![]() |
La clase [MainActivity] puede tener otro nombre. Su única restricción es implementar la interfaz [IMainActivity]. La clase básica proporcionada es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesión
private Session session;
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesión
this.session = (Session) super.session;
// tareas pendientes: continuamos con las inicializaciones iniciadas por la clase padre
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// pendiente: definir aquí los fragmentos
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// Tareas pendientes: definir aquí los títulos de los fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// Tareas pendientes: navegación por pestañas — definir la vista que se va a mostrar
}
@Override
protected int getFirstView() {
// Tareas pendientes: navegación por pestañas: definir la primera vista que se mostrará
return 0;
}
}
- línea 14: para que se entienda la notación AA [@Bean] de la línea 19, es necesario que la actividad tenga la notación AA [@EActivity];
- línea 15: la actividad está asociada al menú XML [menu_main]. Actualmente, este menú está vacío. El desarrollador deberá completarlo si lo necesita;
- línea 16: la clase hereda de la clase [AbstractActivity];
- líneas 19-20: una referencia a la capa [DAO]. Esta será instanciada por la biblioteca AA antes de que se inicialice este campo. Esto implica que el bean AA [Dao] debe existir. Esto siempre es así con la aplicación de estructura básica que proporcionamos. Incluso en una aplicación sin la capa [DAO], se puede dejar que el paquete [dao] exista. Esto no supone ninguna complicación;
- línea 22: la sesión como instancia del tipo [Session]. La sesión existe en la clase padre [AbstractActivity], pero como instancia de la interfaz [ISession] (línea 32);
- líneas 24-63: los seis métodos impuestos por la clase padre [AbstractActivity];
- líneas 36-39: el método [getDao] devuelve una referencia a la capa [DAO]. En este caso, dicha referencia nunca es null. Sin embargo, en la clase padre [AbstractActivity] se ha previsto el caso en el que la clase hija devuelva una referencia null para indicar que no existe la capa [DAO]. Si se desea hacer uso de esta posibilidad (que, en mi opinión, no es muy útil), es aquí donde hay que devolver el puntero null;
2.6. La capa [DAO]

![]() |
2.6.1. La interfaz IDao
Se presentó en el apartado 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL del servicio web
void setUrlServiceWebJson(String url);
// usuario
void setUser(String user, String mdp);
// tiempo de espera del cliente
void setTimeout(int timeout);
// autenticación básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuración
void setDebugMode(boolean isDebugEnabled);
// tiempo de espera del cliente en milisegundos antes de la solicitud
void setDelay(int delay);
// Tarea pendiente: declara tu interfaz aquí
}
El desarrollador añadirá los métodos de su capa [DAO] a partir de la línea 24.
2.6.2. La interfaz [WebClient]
![]() |
La interfaz [WebClient] es la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// Tarea pendiente: declara aquí los URL que se deben alcanzar
}
El desarrollador añadirá los métodos que se comunican con los URL expuestos por el servidor jSON a partir de la línea 17.
2.6.3. El interceptor de autenticación [MyAuthInterceptor]
![]() |
La clase [MyAuthInterceptor] es la siguiente:
package client.android.dao.service;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// usuario
private String user;
// contraseña
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// encabezados HTTP de la solicitud HTTP interceptada
HttpHeaders headers = request.getHeaders();
// el encabezado HTTP de autenticación básica
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// adición a los encabezados HTTP
headers.setAuthorization(auth);
// se continúa con el ciclo de vida de la solicitud HTTP
return execution.execute(request, body);
}
// elementos de la autenticación
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Esta clase genera el siguiente encabezado de autenticación HTTP:
donde [code] es el código Base64 de la cadena «user:mp». Esta clase solo se utiliza si el servidor jSON espera este tipo de autenticación. Existen otras.
Nota: el uso de esta clase se ilustra en el apartado 3.6.3.1.
2.6.4. La clase [AbstractDao]
![]() |
La clase [AbstractDao] es la siguiente:
package client.android.dao.service;
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapeador jSON
private ObjectMapper mapper = new ObjectMapper();
// modo de depuración
protected boolean isDebugEnabled;
// nombre de la clase
protected String className;
// tiempo de espera antes de ejecutar la consulta
private int delay;
// constructor
public AbstractDao() {
// nombre de la clase
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// métodos protegidos ----------------------------------------------------------
// interfaz genérica
protected interface IRequest<T> {
T getResponse();
}
// solicitud genérica a un servicio web / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// registro
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// ejecución del servicio: se espera una única respuesta
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// ejecución del servicio
try {
// ¿En espera?
if (delay > 0) {
Thread.sleep(delay);
}
// se está ejecutando la consulta sincrónica
T response = request.getResponse();
// registro
if (isDebugEnabled) {
String log;
if (response instanceof String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
}
// se envía la respuesta al observador
subscriber.onNext(response);
// se notifica el fin del observable
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// registro
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Erreur jSON imprévue"));
}
}
// se lanza una excepción
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// modo de depuración
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- líneas 35-81: el método [getResponse] utiliza la biblioteca RxAndroid para representar un tipo [Observable<T>]. A diferencia de algunos ejemplos vistos anteriormente, no se representa un tipo [Response<T>], que es un tipo propietario, sino cualquier tipo T;
- línea 35: el método [getResponse] recibe como parámetro una instancia del tipo [IRequest<T>] de las líneas 30-32, cuyo método [IRequest.getReponse()] obtiene el tipo T mediante una operación HTTP síncrona;
- líneas 48-50: se espera artificialmente [delay] milisegundos. En producción se establecerá [delay=0]. En la fase de depuración se establecerá [delay=qqs secondes] para dar al usuario la oportunidad de cancelar la operación asíncrona y ver así cómo se comporta entonces el código;
- línea 52: la respuesta esperada se solicita mediante una consulta sincrónica;
- línea 64: una vez recibida la respuesta, se pasa al observador;
- línea 66: se indica que no habrá más emisiones. Nos encontramos aquí en el caso particular de una acción asíncrona que solo devuelve un elemento;
- líneas 67-78: en caso de excepción, se envía la excepción al observador (línea 77);
2.6.5. La clase [Dao]
![]() |
La clase [Dao] es la siguiente:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// cliente del servicio web
@RestService
protected WebClient webClient;
// seguridad
@Bean
protected MyAuthInterceptor authInterceptor;
// el RestTemplate
private RestTemplate restTemplate;
// fábrica de RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// registro
Log.d(className, "afterInject");
// se fabrica el restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// se instala el convertidor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// se establece el restTemplate del cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// se establece el URL del servicio web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// se registra el usuario en el interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuración de fábrica
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// ¿Interceptor de autenticación?
if (isBasicAuthentificationNeeded) {
// se añade el interceptor de autenticación
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// pendiente: implementación IDao
}
- líneas 21-22: inyección del bean AA [WebClient], que se encargará de las comunicaciones con el servidor web / jSON;
- líneas 24-25: inyección del interceptor de autenticación;
- líneas 31-42: método ejecutado tras la inyección de los campos de las líneas 21-25;
- línea 37: el objeto [RestTemplate], que gestiona las comunicaciones entre el cliente y el servidor, se crea a partir de un factory. No es imprescindible, pero es a través del factory como se pueden configurar los tiempos de espera de las comunicaciones. Por eso no utilizamos el constructor sin parámetros [RestTemplate()];
- línea 39: añadimos un convertidor jSON a los convertidores del [RestTemplate]. Este será el único convertidor. Asimismo, cuando un método del cliente [WebClient] reciba una cadena jSON del servidor, esta se deserializará automáticamente en el objeto que el método debe devolver;
- línea 41: el objeto [RestTemplate], así configurado, se pasa al cliente web, que se encargará de gestionar las comunicaciones entre el cliente y el servidor gracias a él;
- líneas 44-48: se establece el URL raíz del servidor web / jSON. Todas las URL declaradas en la clase [WebClient] son URL relativas a esta URL raíz;
- líneas 50-54: este método permite especificar el propietario de la conexión cuando esta está controlada por una autorización de tipo básico (véase el apartado 2.6.3);
- líneas 56-64: establecen los timeouts de los intercambios entre cliente y servidor. Esto se realiza a través del factory del objeto [RestTemplate] que rige los intercambios;
- líneas 66-78: este método permite indicar que el servidor está protegido mediante una autenticación de tipo básico;
- líneas 72-77: si se solicita una autenticación de tipo básico, el interceptor de autenticación inyectado en la línea 25 se añade a los interceptores del objeto [RestTemplate]. Este interceptor añadirá automáticamente a todas las solicitudes del cliente web la línea HTTP de autenticación básica que espera el servidor;
- el desarrollador implementará la interfaz [IDao] a partir de la línea 87;
2.7. Los fragmentos
![]() |
2.7.1. La clase [MenuItemState]
La clase [MenuItemState] encapsula el estado de una opción de menú:
package client.android.architecture;
public class MenuItemState {
// identificador de la opción de menú
private int menuItemId;
// visibilidad de la opción
private boolean isVisible;
// constructores
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getter y setter
...
}
2.7.2. La clase [Utils]
La clase [Utils] reúne métodos estáticos de utilidad:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// lista de mensajes de una excepción - versión 1
static public List<String> getMessagesFromException(Throwable ex) {
// se crea una lista con los mensajes de error de la pila de excepciones
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// lista de mensajes de una excepción - versión 2
static public String getMessageForAlert(Throwable th) {
// Se construye el texto que se va a mostrar
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// resultado
return texte.toString();
}
// Lista de mensajes de una excepción - versión 3
static public String getMessageForAlert(List<String> messages) {
// Se genera el texto que se va a mostrar
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// resultado
return texte.toString();
}
}
2.7.3. La clase padre [AbstractFragment]
La clase [AbstractFragment] agrupa lo que es común a todos los fragmentos de la aplicación. Al igual que en la clase [AbstractActivity], su código es complejo. También en este caso lo analizaremos por etapas.
2.7.3.1. El esqueleto
package client.android.architecture.core;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// datos privados ------------------------------------------------------------
// suscripciones a observables
private List<Subscription> abonnements = new ArrayList<>();
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estado del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// tareas asíncronas
private boolean runningTasksHaveBeenCanceled;
// datos accesibles a las clases hijas ---------------------------------------
// modo de depuración
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nombre de la clase
protected String className;
// tareas asíncronas
protected int numberOfRunningTasks;
// actividad
protected IMainActivity mainActivity;
protected Activity activity;
// sesión
protected Session session;
// Actualización de fragmento ----------------------------------------------------------------------------------
...
// gestión del menú ------------------------------------------
...
// gestión de la espera -------------------------------------------------------------
...
// gestión de operaciones asíncronas --------------------------------------------------------------------
...
// gestión de excepciones -------------------------------------------------------------------
....
// gestión del ciclo de vida del fragmento --------------------------------------------------------
...
// clases hijas -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
}
- líneas 28-45: los datos privados de la clase;
- líneas 47-58: los datos protegidos a los que pueden acceder las clases hijas;
- líneas 61-62: código que actualiza el fragmento que se va a mostrar;
- líneas 64-65: código auxiliar para gestionar el posible menú;
- líneas 67-68: código auxiliar para gestionar la espera durante una operación asíncrona;
- líneas 70-71: código para facilitar la comunicación del fragmento con la capa [DAO];
- líneas 73-74: código de utilidad para gestionar 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:
// nombre de la clase
protected String className;
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
...
// constructor ----------------------
public AbstractFragment() {
// inicialización
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// registro
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- línea 9: se indica el nombre de la clase hija que se está instanciando aquí. Este nombre se utiliza en todos los registros de la clase padre;
- línea 10: se indica que el fragmento está en proceso de construcción. Esta información se utilizará cuando se solicite al fragmento hijo que se actualice;
2.7.3.3. Gestión del menú
En nuestra arquitectura, todo fragmento debe tener un menú, aunque esté vacío. De hecho, los registros han mostrado que, cuando se ejecuta el método [onCreateOptionsMenu] —que se ejecuta cuando el fragmento tiene un menú—, el fragmento ya se ha asociado a su actividad, su vista y su menú, y va a hacerse visible. Por lo tanto, este es el momento en el que se puede realizar la actualización de la interfaz visual y del menú. Es en este método [onCreateOptionsMenu] donde solicitamos al fragmento hijo que se actualice.
La gestión del menú agrupa métodos de utilidad que permiten al fragmento hijo mostrar o no elementos del menú:
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// gestión del menú ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// recorre todos los elementos del menú
for (int i = 0; i < menu.size(); i++) {
// elemento n.º i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// si el elemento n.º i es un submenú, se vuelve a empezar
if (menuItem.hasSubMenu()) {
// recursividad
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// resultado
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// se recuperan los identificadores de las opciones del menú
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// se transfieren las opciones del menú a una matriz
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identificador de la opción
int id = menuOptionsIds.get(i);
// estado de la opción
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// resultado
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// estados de las opciones del menú
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// estado
MenuItemState state = this.menuOptionsStates[i];
// ID del menú
int id = state.getMenuItemId();
// inicialización del estado
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// resultado
return menuOptionsStates;
}
// Visualización de opciones de menú -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// se actualizan todas las opciones del menú
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// se actualizan algunas opciones del menú
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- líneas 6-18: este método permite obtener los identificadores numéricos de todas las opciones del menú;
- línea 6: el método [getMenuOptions] recibe dos parámetros:
- [Menu menu]: el menú del fragmento;
- [List<Integer> menuOptionsIds]: la lista de identificadores de Android de las opciones del menú. Inicialmente, esta lista está vacía. A continuación, se rellena mediante un recorrido recursivo (línea 15) del árbol del menú;
- líneas 20-40: a partir del menú, construye la matriz de estados (identificador, visibilidad) de las opciones del menú. Esta matriz se almacena en la línea 3. La clase [MenuItemState] se ha descrito en el apartado 2.7.1;
- líneas 43-55: una variante del método anterior. Realiza la misma tarea, pero en lugar de volver a calcular los identificadores de todas las opciones del menú —lo cual ya se ha hecho—, utiliza los identificadores de la tabla de estados de la línea 3;
- líneas 58-63: el método [setAllMenuOptionsStates] permite ocultar o mostrar todas las opciones del menú del fragmento;
- líneas 65-69: el método [setMenuOptionsStates] permite, de forma selectiva, mostrar u ocultar algunas de las opciones del menú;
- los métodos [getMenuOptions, getMenuOptionsStates] se declaran privados, ya que solo se utilizan en [AbstractFragment]. Los métodos [setAllMenuOptionsStates] (línea 58) y [setMenuOptionsStates] (línea 65) se declaran protegidos para que estén disponibles para las clases hijas;
2.7.3.4. Gestión de la espera hasta la finalización de una tarea asíncrona
// suscripciones a observables
private List<Subscription> abonnements = new ArrayList<>();
// tareas asíncronas
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// gestión de la espera hasta que finalice una operación asíncrona -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// se anota el número de tareas que se van a ejecutar
this.numberOfRunningTasks = numberOfRunningTasks;
// se establece la imagen de espera
mainActivity.beginWaiting();
// se vacía la lista de suscripciones
abonnements.clear();
// Aún no se ha producido ninguna cancelación
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// se oculta la imagen de espera
mainActivity.cancelWaiting();
}
- líneas 9-18: para iniciar una o varias operaciones 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 el fragmento hijo va a iniciar;
- línea 11: se almacena el parámetro del método;
- línea 13: se hace visible la imagen de espera;
- línea 15: se borra la lista de suscripciones a las operaciones asíncronas. Estas aún no han sido creadas por el fragmento hijo;
- línea 17: se mantiene un valor booleano para indicar que las tareas asíncronas solicitadas por el fragmento hijo se han cancelado. Inicialmente, este valor booleano tiene el valor false;
- líneas 20-25: el fragmento hijo llama al método padre [cancelWaitingTasks] para indicar que quiere cancelar las tareas que ha iniciado;
- línea 22: se oculta la imagen de espera;
2.7.3.5. Gestión de excepciones
// gestión de excepciones -------------------------------------------------------------------
// Mostrar alerta de excepción
protected void showAlert(Throwable th) {
// se muestran los mensajes de la pila de excepciones del Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// Visualización de la lista de mensajes
protected void showAlert(List<String> messages) {
// se muestra la lista de mensajes
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
- líneas 4-7: el método [showAlert(Throwable)] permite a un fragmento hijo mostrar en una ventana los mensajes de la pila de excepciones del Throwable pasado como parámetro;
- líneas 10-13: el método [showAlert(List<String>] permite que un fragmento secundario muestre en una ventana la lista de mensajes pasada como parámetro;
- La clase [Utils], utilizada en las líneas 6 y 12, se ha descrito en el apartado 2.7.2;
2.7.3.6. Gestión de operaciones asíncronas
...
// suscripciones a observables
private List<Subscription> abonnements = new ArrayList<>();
// tareas asíncronas
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// ejecución de una tarea asíncrona con RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// proceso: el observable que se va a ejecutar u observar
// consumeResult: el método que procesa la respuesta obtenida
//
// solo se crean nuevas suscripciones si no se ha producido ninguna cancelación
if (!runningTasksHaveBeenCanceled) {
// ejecución en el hilo de E/S y observación en el hilo de la interfaz de usuario
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
//: se ejecuta el observable
try {
abonnements.add(process.subscribe(
// Consumo del resultado
consumeResult,
// consumo de excepción
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// fin de la tarea
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// una operación asíncrona ha generado una excepción
// o se ha producido una excepción durante la ejecución de una operación asíncrona
private void consumeThrowable(Throwable th) {
...
}
- líneas 9-41: ejecutan una tarea asíncrona;
- línea 9: el método [executeInBackground] espera dos parámetros:
- [Observable<T> process]: el proceso asíncrono que se va a ejecutar;
- [Action1<T> consumeResult]: el método del fragmento secundario al que hay que llamar para transmitirle los elementos emitidos por el proceso. En nuestros ejemplos anteriores, los procesos siempre han emitido un único elemento. El tipo T de [Action1<T>] es el tipo T del resultado devuelto por el proceso observado;
- línea 14: la tarea asíncrona solo se inicia si aún no se ha producido una cancelación por parte del usuario o del programa (debido a una excepción);
- línea 16: el proceso está configurado para ejecutarse en un hilo de E/S y se observa en el hilo de la interfaz de usuario;
- línea 16: la instrucción [process.subscribe] inicia la ejecución del proceso en el hilo de E/S. Dentro de este hilo, las operaciones se ejecutan de forma síncrona porque utilizamos una biblioteca HTTP que es síncrona;
- línea 19: el método [process.subscribe] tiene tres parámetros:
- línea 21: [consumeResult]: el método del fragmento secundario que va a consumir los elementos emitidos por el proceso;
- líneas 22-28: el método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. El procesamiento se delega al método [consumeThrowable] de la línea 49;
- líneas 29-36: el método que se ejecuta cuando la tarea emite la notificación de fin de emisión. El procesamiento se delega al método [endOfTask] de la línea 43;
- línea 19: la tarea asíncrona que acaba de iniciarse se registra en el campo [abonnements], que almacena todas las tareas asíncronas iniciadas. Esto permitirá cancelarlas si fuera necesario;
- líneas 37-39: método que se ejecuta cuando se ha producido una excepción durante el procesamiento de la tarea asíncrona. El procesamiento se delega al método [consumeThrowable] de la línea 49;
El método [endOfTask] es el siguiente:
// tareas asíncronas
protected int numberOfRunningTasks;
...
private void endOfTask() {
// una tarea menos pendiente
numberOfRunningTasks--;
// ¿Terminado?
if (numberOfRunningTasks == 0) {
// fin de la espera
cancelWaitingTasks();
// se notifica el final de las tareas a la clase hija
notifyEndOfTasks(false);
}
}
...
// clases hijas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- línea 6: acaba de finalizar una tarea asíncrona. Se decrementa el contador de tareas activas;
- línea 8: si ya no hay tareas activas, el fragmento hijo ha obtenido todas sus respuestas;
- línea 10: se cancela la espera;
- línea 12: se notifica al fragmento hijo que todas las tareas que ha iniciado han finalizado llamando a su método [notifyEndOfTasks]. El parámetro de este método indica cómo han finalizado las tareas: de forma normal, por cancelación del usuario o del código, o bien porque se ha producido una excepción. En la línea 12, se indica un final normal. Cabe señalar que el fragmento hijo no tiene que preocuparse de llevar la cuenta de las tareas aún activas. Su clase padre lo hace por él;
El método [consumeThrowable] es el siguiente:
// tareas asíncronas
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// una operación asíncrona ha generado una excepción
// o se ha producido una excepción durante la ejecución de una operación asíncrona
private void consumeThrowable(Throwable th) {
// th: la excepción que hay que tratar
//
// registro
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// se cancelan las tareas ya iniciadas
cancelRunningTasks();
// se muestran los mensajes de error
showAlert(th);
}
// cancelación de tareas
protected void cancelRunningTasks() {
// registro
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// se cancelan todas las tareas asíncronas registradas
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// se registra la cancelación
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// fin de la espera
cancelWaitingTasks();
// se notifica la cancelación de las tareas al fragmento hijo
notifyEndOfTasks(true);
}
...
// clases hijas -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- línea 3: el método [consumeThrowable] recibe la excepción que se ha producido;
- línea 15: se cancelan todas las tareas que siguen activas;
- línea 17: se muestra el texto de la excepción;
- líneas 21-37: se cancelan todas las tareas;
- líneas 27-29: se cancelan todas las suscripciones;
- línea 31: se registra que se ha producido una cancelación;
- línea 32: se pone a cero el contador de tareas;
- línea 34: se cancela la espera;
- línea 36: se notifica al fragmento hijo el fin de las tareas tras la cancelación;
2.7.3.7. Gestión del ciclo de vida del fragmento
// ciclo de vida --------------------------------------------------------
@Override
public void onDestroyView() {
// padre
super.onDestroyView();
// registro
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// padre
super.onDestroy();
// registro
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- líneas 2-20: los métodos [onDestroyView, onDestroy] solo están ahí para los registros. Estos permiten al desarrollador comprender mejor el ciclo de vida de los fragmentos;
El almacenamiento del fragmento al girar el dispositivo se lleva a cabo mediante los siguientes métodos [setUserVisibleHint, onSaveInstanceState, saveState]:
// ciclo de vida del fragmento
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// padre
super.setUserVisibleHint(isVisibleToUser);
// ¿copia de seguridad?
if (this.isVisibleToUser && !isVisibleToUser) {
// el fragmento va a quedar oculto; se guarda
if (!saveFragmentDone) {
saveState();
}
}
// memoria
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// registro
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// padre
super.onSaveInstanceState(outState);
// guardar el fragmento solo si está visible
if (isVisibleToUser) {
// quizá ya se haya guardado
if (!saveFragmentDone) {
saveState();
}
// se debe realizar la restauración en cualquier caso
session.setAction(ISession.Action.RESTORE);
}
}
- líneas 6-19: el fragmento se guarda si pasa del estado «mostrado» al estado «oculto» (línea 11). Es el método [setUserVisibleHint] el que nos 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: al girar el dispositivo, se invocará el método [onSaveInstanceState]. El fragmento se guarda bajo dos condiciones:
- que sea visible (línea 34);
- aún no se haya guardado (línea 36). Es posible que los métodos [setUserVisibleHint, onSaveInstanceState] no puedan ejecutarse ambos cuando el fragmento está visible y que, por lo tanto, la gestión del valor booleano [saveFragmentDone] sea innecesaria. En caso de duda, he preferido utilizar este;
- línea 40: tras el guardado vendrá la restauración. Cabe señalar que, la próxima vez que el fragmento tenga que actualizarse, deberá hacerlo mediante una operación [RESTORE];
Cabe señalar los dos momentos en los que se solicita una copia de seguridad del fragmento:
- cuando este pasa del estado visible al estado oculto;
- cuando se produce una rotación del dispositivo;
El método privado [saveState] es el siguiente:
...
private void saveState() {
// ¿Hay que cancelar las tareas?
if (numberOfRunningTasks != 0) {
// Se cancelan las tareas
cancelRunningTasks();
}
// se guarda el estado del fragmento
CoreState currentState = saveFragment();
// Se ha visitado el fragmento
currentState.setHasBeenVisited(true);
// se guarda el estado del menú
currentState.setMenuOptionsState(getMenuOptionsStates());
// inicio de sesión
session.setCoreState(getNumView(), currentState);
// guardado realizado
saveFragmentDone = true;
// registro
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// clases hijas -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- líneas 4-7: la rotación del dispositivo puede producirse mientras se están realizando operaciones asíncronas. En este caso, se toma la decisión de cancelarlas todas. No es una buena decisión para el usuario, que tendrá que volver a realizar una nueva solicitud, potencialmente larga, cuando lo único que ha hecho es mover su teléfono o su tableta, o bien recibir una llamada telefónica. Es posible mantener las conexiones de red a lo largo de un ciclo de copia de seguridad/restauración. Sin embargo, las soluciones no son evidentes y he decidido no abordarlas en este curso para principiantes. La solución consiste en establecer estas conexiones de red a través de un fragmento sin interfaz gráfica asociada y que no se elimine durante el ciclo de copia de seguridad y restauración. Para ello, basta con utilizar la instrucción [Fragment.setRetainInstance(true)];
- línea 9: se le pide al fragmento hijo que guarde su estado en un tipo derivado de [CoreState] (línea 31);
- línea 11: se registra que se ha visitado el fragmento. Esta información es útil. Cuando se visita un fragmento por primera vez, su actualización puede ser diferente a las siguientes, ya que entonces no tiene un estado anterior en la sesión;
- línea 13: se guarda el estado del menú, lo que nos permitirá restaurarlo automáticamente;
- línea 15: este estado actual se guarda en la sesión. En ella, los estados se agrupan por vista/fragmento, y cada uno de ellos tiene un estado. El número de la vista lo proporciona el fragmento hijo (línea 33);
- línea 17: se indica que se ha guardado el fragmento. Esto se debe a que hay dos métodos que pueden llamar al método [saveState] y no tiene sentido realizar dos guardados;
La regeneración de la vista asociada al fragmento se lleva a cabo mediante el siguiente método:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// clase principal
super.onActivityCreated(savedInstanceState);
// registro
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// hay que restaurar la vista
viewHasToBeInitialized = true;
}
En el ciclo de vida, el método [onActivityCreated] se ejecuta justo después del método [onCreateView]. La llamada a este último método indica que la vista asociada al fragmento debe reconstruirse. Nos limitamos a señalarlo en la línea 10.
2.7.3.8. Actualización del fragmento
La actualización del fragmento es la última operación que se realiza sobre el fragmento antes de que sea visible y quede a la espera de las acciones del usuario. Se lleva a cabo mediante el siguiente código:
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// Estados del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// Actualización del fragmento ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// registro
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memoria
this.menu = menu;
// se recuperan las # opciones del menú si aún no se ha hecho
if (fragmentHasToBeInitialized) {
// se recuperan las # opciones del menú
getMenuOptionsStates(menu);
// actividad
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
previousState = session.getCoreState(getNumView());
// actualización del fragmento secundario en varios pasos
// paso 1: ¿es la primera visita?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// No es la primera visita
// paso 2: ¿hay que inicializar el fragmento?
...
// paso 3: ¿hay que inicializar la vista?
...
}
// paso 4: ¿un envío, una navegación, una restauración?
...
// paso 5: actualizaciones de los terminales ----------------------
...
}
...
// clases hijas -----------------------------------------------------
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
- línea 19: se utiliza el método [onCreateOptionsMenu] para actualizar el fragmento. Por este motivo, el fragmento debe tener un menú, vacío si es necesario. Cuando se ejecuta este método, el fragmento ya se ha asociado a su vista y a su actividad, y además está visible;
- línea 25: se almacena el menú que se ha pasado como parámetro (línea 22) al método;
- líneas 27-34: si hay que inicializar el fragmento:
- línea 29: los estados de las opciones del menú se guardan en la matriz [menuOptionsStates] de la línea 3;
- línea 31: la actividad se almacena como instancia del tipo Android [Activity];
- línea 32: la actividad se almacena como una instancia de la interfaz [IMainActivity];
- línea 33: se almacena la sesión. El cambio de tipo es necesario, ya que el método [mainActivity.getSession()] devuelve un tipo [ISession];
- línea 36: se recupera de la sesión el estado anterior del fragmento. Si se trata de la primera visita al fragmento, solo el valor booleano [previousState.hasBeenVisited] tiene significado;
- líneas 39-44: código que se ejecuta cuando es la primera visita al fragmento. En este caso, su estado anterior no es relevante;
- líneas 44-50: código que se ejecuta cuando no es la primera vez que se visita el fragmento;
- líneas 46-47: código que se ejecuta si se ha llamado al constructor del fragmento (fragmentHasToBeInitialized==true);
- líneas 48-49: código que se ejecuta si se ha reconstruido la vista asociada al fragmento (viewHasToBeInitialized==true);
- líneas 51-52: código que se ejecuta en función de la acción (SUBMIT, NAVIGATION, RESTORE) en curso;
- líneas 54-55: código que se ejecuta siempre;
Las cinco etapas de la actualización son las siguientes:
paso 1
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// Ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// Estados del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
previousState = session.getCoreState(getNumView());
// actualización del fragmento secundario en varias etapas
// paso 1: ¿es la primera visita?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// Inicialización del fragmento y de la vista
initFragment(null);
initView(null);
// borrado de previousState para continuar
previousState = null;
} else {
// No es la primera visita
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- línea 19: se recupera el estado anterior del fragmento en la sesión;
- líneas 22-31: código ejecutado si nunca se ha visitado el fragmento;
- línea 27: se solicita a la clase hija que inicialice el fragmento. El parámetro del método [initFragment] de la línea 35 es el estado anterior del fragmento. Aquí se pasa null para indicar al fragmento hijo que se trata de la primera visita;
- línea 28: se le pide a la clase hija que inicialice la vista asociada al fragmento. El parámetro del método [initView] de la línea 37 es el estado anterior del fragmento. Aquí se pasa null para indicar al fragmento hija que se trata de la primera visita;
- línea 30: se establece el estado anterior en null para los pasos siguientes;
pasos 2 y 3
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// Estados del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
previousState = session.getCoreState(getNumView());
// actualización del fragmento secundario en varias etapas
// paso 1: ¿es la primera visita?
if (!previousState.getHasBeenVisited()) {
...
} else {
// No es la primera visita
// etapa 2: ¿hay que inicializar el fragmento?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// fragmento secundario
initFragment(previousState);
}
// paso 3: ¿hay que inicializar la vista?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// fragmento secundario
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- líneas 24-42: se ejecutan cuando no es la primera visita al fragmento;
- líneas 27-33: si el fragmento acaba de reconstruirse, se reinicia llamando al método [initFragment] de la clase hija (líneas 32, 46). Se le pasa el estado anterior del fragmento;
- líneas 35-51: si la vista asociada al fragmento debe inicializarse o reinicializarse, se le pide al fragmento hijo que lo haga (líneas 40, 48). Una vez más, se le pasa el último estado conocido del fragmento;
paso 4
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// Estados del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// se recupera el estado anterior del fragmento (la primera vez, solo el valor booleano hasBeenVisited tiene significado)
previousState = session.getCoreState(getNumView());
// Actualización del fragmento secundario en varias etapas
...
// etapa 4: ¿un envío, una navegación, una restauración?
// registro
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// acción en curso
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// fragmento secundario
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// restauración del menú
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragmento secundario
updateOnRestore(previousState);
} else {
// se trata de una primera visita: no hay que hacer nada
}
break;
case RESTORE:
// restauración
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// menú de restauración (previousState no puede ser nulo)
setMenuOptionsStates(previousState.getMenuOptionsState());
// fragmento «hija»
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- líneas 34-66: se procesa la acción en curso, que puede ser una de las tres siguientes:
- RESTORE: se está restaurando el fragmento tras una rotación del dispositivo;
- NAVIGATION: se vuelve al fragmento con la intención de encontrarlo en el estado en el que se dejó la última vez que se utilizó;
- SUBMIT: todos los demás casos;
- línea 34: se recupera la acción en curso;
- líneas 36-42: para una acción de tipo SUBMIT, se llama al método [updateOnSubmit] del fragmento hijo (líneas 41, 68) pasándole el último estado conocido del fragmento;
- líneas 43-55: para una acción de tipo NAVIGATION;
- líneas 47-54: queremos restablecer el fragmento a su último estado conocido. La operación NAVIGATION puede coincidir con una primera visita. Este sería el caso, por ejemplo, en una aplicación con pestañas: si paso de la pestaña 1 a la pestaña 4:
- debo inicializar el fragmento de la pestaña 4 si se trata de la primera visita;
- restablecer el fragmento de la pestaña 4 a su estado anterior si no se trata de la primera visita;
- líneas 52-54: no se hace nada si se trata de la primera visita. Será el método secundario [initView(CoreState previousState)] el que realice esta inicialización. La primera visita se caracteriza por la condición [previousState==null];
- línea 49: si no es la primera visita al fragmento, se le devuelve su menú;
- línea 51: se solicita a la clase hija que se actualice llamando al método de la línea 70. Se le pasa el estado anterior del fragmento para que pueda realizar su trabajo;
- líneas 56-66: en el caso de una operación de restauración del fragmento, se hace lo mismo que en el caso de una navegación que no sea la primera visita;
paso 5
// menú del fragmento
private Menu menu;
private MenuItemState[] menuOptionsStates;
// ciclo de vida del fragmento
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// estados del fragmento
private CoreState previousState;
// mapeador jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// ciclo de vida del fragmento
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// etapa 5: actualizaciones finales ----------------------
// Se ha cambiado de vista
session.setPreviousView(getNumView());
// ya no hay ninguna acción en curso
session.setAction(ISession.Action.NONE);
// cuando se salga de este fragmento, deberá guardarse
saveFragmentDone = false;
// mientras el fragmento no se haya reconstruido, no es necesario inicializarlo
fragmentHasToBeInitialized = false;
// Mientras no se haya reconstruido la vista, no es necesario inicializarla
viewHasToBeInitialized = false;
// se vuelve al funcionamiento normal de la selección de pestañas
session.setNavigationOnTabSelectionNeeded(true);
// se notifica al fragmento que la vista está lista
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- líneas 18-30: al llegar aquí, el fragmento se ha inicializado y está listo para mostrarse. A continuación, se restablecen a su estado inicial todos los indicadores utilizados en la gestión del ciclo de vida del fragmento;
- línea 20: se ha cambiado de vista: se anota en la sesión;
- línea 22: ya no hay ninguna acción en curso;
- línea 24: cuando se vaya a salir del fragmento que se muestra actualmente, habrá que guardarlo al salir de él;
- línea 26: ya no es necesario reconstruir el fragmento. Este indicador se restablecerá a vrai cuando se vuelva a ejecutar el constructor del fragmento;
- línea 28: ya no es necesario inicializar la vista asociada al fragmento. Este indicador se restablecerá a vrai cuando se vuelva a ejecutar el método [onActivityCreated];
- línea 30: el fragmento se muestra, posiblemente, en una aplicación con pestañas. En ese caso, cuando el usuario haga clic en una de ellas, debe producirse un cambio de fragmento;
- línea 36: se indica a la clase hija que el fragmento está listo. Esta puede incorporar en el método [notifyEndOfUpdates] las actualizaciones que haya que realizar en todos los casos, iniciar una operación asíncrona para obtener nuevos datos, etc.
2.7.4. Un ejemplo de fragmento
![]() |
Se ha incluido en el proyecto [client-android-skel] un ejemplo de fragmento para mostrar al lector la estructura típica de un fragmento de una aplicación basada en este proyecto.
La clase [DummyFragment] es la siguiente:
package client.android.fragments.behavior;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
public class DummyFragment extends AbstractFragment {
// campos heredados de la clase padre -------------------------------------------------------
// modo de depuración
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// nombre de la clase
//-- String protegido className;
// tareas asíncronas
//-- int protegido numberOfRunningTasks;
// actividad
//-- protegido IMainActivity mainActivity;
//-- actividad protegida;
// sesión
//-- protegido Session session;
// métodos heredados de la clase padre -------------------------------------------------------
// visualización de opciones de menú
//-- protegido void setAllMenuOptionsStates(booleano isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// gestión de la espera hasta que finalice una serie de tareas asíncronas
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// ejecución de una tarea asíncrona con RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// cancelación de tareas
//-- protegido void cancelRunningTasks() {
// visualización de alerta ante una excepción
//-- protected void showAlert(Throwable th) {
// visualización de la lista de mensajes
//-- protected void showAlert(List<String> mensajes) {
// métodos impuestos por la clase padre -------------------------------------------------------
@Override
public CoreState saveFragment() {
// hay que guardar el fragmento
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// sino hay nada que guardar, hay que ejecutar [return new CoreState();] y eliminar la clase [DummyFragmentState]
}
@Override
protected int getNumView() {
// hay que devolver el número del fragmento a la tabla de fragmentos gestionados por la actividad (véase MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// el fragmento se hace visible y se ha creado en este paso o en uno anterior
// Esto ocurre al iniciar la aplicación y cada vez que se gira el dispositivo Android
// va seguida necesariamente de la ejecución de [initView]
//: hay que inicializar los campos del fragmento que se ha reconstruido
// previousState es la última copia de seguridad del fragmento; toma el valor null si es la primera vez que se visita el fragmento
}
@Override
protected void initView(CoreState previousState) {
// el fragmento se hace visible y la vista asociada se ha reconstruido en este paso o en uno anterior
// esto ocurre cada vez que se ejecuta [initFragment] y cada vez que el fragmento sale de la adyacencia del fragmento mostrado
// Es necesario inicializar los componentes de la vista que se ha reconstruido
// previousState es la última copia de seguridad del fragmento; toma el valor null si es la primera visita al fragmento
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// se ejecuta después de [initFragment, initView] si se ejecutan estos métodos
//: la vista se mostrará tras una operación del tipo SUBMIT
//: por lo general, es necesario inicializar el fragmento y la vista asociada a partir de la sesión
// previousState es la última copia de seguridad del fragmento; toma el valor null si es la primera visita al fragmento
// no hay nada que hacer si no se puede acceder al fragmento mediante una operación SUBMIT
// si se puede llegar al fragmento mediante operaciones SUBMIT desde diferentes fragmentos, se puede conocer la vista anterior mediante [session.getPreviousView]
// si se puede llegar al fragmento mediante varias operaciones SUBMIT a partir del mismo fragmento, entonces hay que activar un indicador para diferenciar los distintos tipos de SUBMIT a partir de ese fragmento
}
@Override
protected void updateOnRestore(CoreState previousState) {
// se ejecuta después de [initFragment, initView] si se ejecutan estos métodos
//: la vista se mostrará tras una operación del tipo RESTORE o NAVIGATION
// previousState es la última copia de seguridad del fragmento; nunca es nulo
//: hay que restablecer la vista a su estado anterior
}
@Override
protected void notifyEndOfUpdates() {
// se ejecuta después de los métodos [updateOnSubmit, updateOnRestore]
// al llegar aquí, la vista ya se ha creado e inicializado
// A menudo no hay nada que hacer aquí, pero también se pueden agrupar en este punto las acciones que habría que realizar independientemente de cómo se acceda a esta vista
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// se invoca cuando las tareas asíncronas iniciadas por el fragmento se han completado o se han cancelado
// Estos dos casos se pueden diferenciar gracias al parámetro runningTasksHaveBeenCanceled
// por lo general, es necesario restablecer la vista a un estado diferente al que tenía mientras esperaba las respuestas de las tareas asíncronas
}
}
Es posible que la clase [DummyFragment] no tenga ningún estado. En este caso, se ha incluido uno para recordar lo que se espera de ella:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// estado del fragmento [DummyFragment]
// incluir únicamente campos serializables en jSON
// Añadir la anotación @JsonIgnore a los demás, aunque no queda claro para qué podrían servir
// No olvides los getters y setters: se utilizan para la serialización y deserialización
}
Para ilustrar el uso del proyecto [client-android-skel], utilizaremos primero algunos ejemplos sencillos antes de pasar a un caso práctico más completo.
2.8. Ejercicios ilustrativos
Vamos a empezar por refactorizar ejemplos ya escritos.
2.8.1. Ejemplo 17B
Retomamos el ejemplo 17 estudiado en el apartado 1.18. Se trata de una aplicación con un único fragmento, sin tareas asíncronas ni pestañas. La examinamos para ver cómo se comporta al girar el dispositivo. Introducimos los siguientes datos:

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

Si comparamos las vistas, se ha conservado todo excepto la lista [2], que ahora está vacía.
Por otra parte, si hacemos clic en el botón [Valider], aparece un cuadro de diálogo que muestra los datos introducidos en el formulario. Si en ese momento giramos el dispositivo, se cierra el cuadro de diálogo.
Por lo tanto, al girar el dispositivo, tendremos que regenerar:
- la lista desplegable y su elemento seleccionado;
- el cuadro de diálogo, si estaba visible en el momento de la rotación;
2.8.1.1. El proyecto [Exemple-17B]
Duplicamos el proyecto [client-android-skel] en ejemplos/Ejemplo-17B. A continuación, cargamos el nuevo proyecto [1]:
![]() | ![]() | ![]() |
- en [2-3], dentro de la carpeta [behavior], pegamos el fragmento [Vue1Fragment] del proyecto [Exemple-17];
![]() | ![]() | ![]() |
- en [4-5], dentro de la carpeta [layout] de [Exemple-17B], se pega la vista [vue1.xml] de [Exemple-17]. Esta es la vista asociada al fragmento;
- en [6], la carpeta [values] de [Exemple-17B] se sustituye por la carpeta [values] de [Exemple-17];
Se modificará el margen superior de la vista [vue1.xml] a 80 dp:
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="80dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
En este punto, podemos intentar una primera compilación para ver los errores. Los primeros errores señalados provienen de imports, debido a que algunos paquetes han cambiado de ubicación. Los corregimos (Ctrl-Shift-O). Otros errores se deben a que la vista [Vue1Fragment] no implementa todos los métodos exigidos por su clase padre [AbstractParent]:

Generamos los métodos que faltan (Alt-Intro).
Otro error de compilación señalado es el siguiente:

Esto se corrige en el archivo [build.gradle] del módulo (línea 20 a continuación):
![]() |
En este punto, podemos volver a compilar para ver los errores restantes. El único error que aparece es en el método [Vue1Fragment.updateFragment]:
![]() |
Hay que eliminar la anotación [@Override] de la línea 135. Ya no hay errores. Partiremos de ahí para modificar el proyecto.
2.8.1.2. El estado del fragmento [Vue1Fragment]
El fragmento [Vue1Fragment] necesita guardar información al girar el dispositivo para que pueda restaurarse por completo. Para ello, creamos una clase [Vue1FragmentState]:
![]() |
Por el momento, esta clase está vacía:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. Personalización del proyecto
![]() |
En la carpeta [custom] se encuentran los elementos de arquitectura que el desarrollador puede personalizar.
Las constantes de la interfaz [IMainActivity] serán las siguientes:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// Acceso a la sesión
ISession getSession();
// cambio de vista
void navigateToView(int position, ISession.Action action);
// gestión de la espera
void beginWaiting();
void cancelWaiting();
// constantes de la aplicación -------------------------------------
// modo de depuración
boolean IS_DEBUG_ENABLED = true;
// tiempo máximo de espera de la respuesta del servidor
int TIMEOUT = 1000;
// tiempo de espera antes de ejecutar la solicitud del cliente
int DELAY = 0;
// autenticación básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adyacencia de fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de pestañas
boolean ARE_TABS_NEEDED = false;
// imagen de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos de la aplicación
int FRAGMENTS_COUNT = 1;
}
- líneas 24-31: la aplicación no utiliza aquí su capa [DAO]. Estas constantes no se utilizarán;
- línea 34: una adyacencia de fragmentos de 1, que es el valor por defecto. Como la aplicación solo tiene un fragmento (línea 43), este valor no tiene importancia;
- líneas 39-40: como no hay operaciones con la capa [DAO], no es necesario tener una imagen de espera;
- línea 37: no se trata de una aplicación con pestañas;
- línea 43: solo hay un fragmento;
La clase [Session] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// los elementos que no se pueden serializar en jSON deben llevar la anotación @JsonIgnore
}
Está vacía. De hecho, como solo hay un fragmento, no es necesario prever una comunicación entre fragmentos con una sesión.
Por último, la clase [CoreState] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragmento visitado o no
protected boolean hasBeenVisited = false;
// estado del posible menú del fragmento
protected MenuItemState[] menuOptionsState;
// getters y setters
...
}
- líneas 11-13: debemos incluir todas las clases derivadas de [CoreState] que almacenan el estado de los distintos fragmentos. En este caso, solo hay una (línea 12);
2.8.1.4. La actividad [MainActivity]
La actividad [MainActivity] es actualmente la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesión
private Session session;
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesión
this.session = (Session) super.session;
// tareas pendientes: continuamos con las inicializaciones iniciadas por la clase padre
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// pendiente: definir aquí los fragmentos
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// pendiente: definir aquí los títulos de los fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// pendiente: navegación por pestañas: definir la vista que se mostrará al seleccionar la pestaña n.º [position]
}
@Override
protected int getFirstView() {
// pendiente: definir el n.º de la primera vista (fragmento) que se mostrará
return 0;
}
}
Los comentarios de [//todo] indican lo que debe hacer el desarrollador. La clase [MainActivity] evoluciona de la siguiente manera:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesión
private Session session;
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// sesión
this.session = (Session) super.session;
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
}
Solo hay que modificar el método de las líneas 41-44. Debe devolver la matriz de fragmentos de la aplicación. En la línea 43, no hay que olvidar poner el guión bajo detrás del nombre del fragmento.
2.8.1.5. El estado del fragmento [FragmentState]
Tras las pruebas de rotación realizadas en el proyecto [Exemple-17], se decide memorizar los siguientes elementos del fragmento:
- la lista de valores del menú desplegable;
- la posición del elemento seleccionado en dicha lista;
- el mensaje que muestra el cuadro de diálogo si este está presente en el momento de la rotación;
La clase [Vue1FragmentState] será la siguiente:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// los valores de la lista desplegable
private List<String> list;
// el elemento seleccionado en la lista desplegable
private int listSelectedPosition;
// el mensaje que se muestra en el cuadro de diálogo
private String message;
// métodos getter y setter
...
}
2.8.1.6. El fragmento [AbstractFragment]
Actualmente, el ciclo de vida del fragmento se gestiona mediante dos métodos (líneas 6 y 32):
// lista desplegable
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// se marca el primer botón
radioButton1.setChecked(true);
// el calendario
datePicker1.setCalendarViewShown(false);
// el seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// la lista desplegable
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// inicialización del adaptador del menú desplegable
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
El código de estos dos métodos se migrará a los métodos definidos por la clase [AbstractFragment] de la siguiente manera:
// gestión del ciclo de vida del fragmento ---------------------------------------------------------------------
@Override
public CoreState saveFragment() {
Vue1FragmentState state = new Vue1FragmentState();
state.setList(list);
state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
state.setMessage(message);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// ¿Primera visita?
if (previousState == null) {
// se crean los valores de la lista desplegable
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// se recuperan los valores de la lista desplegable
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// y el mensaje del cuadro de diálogo
message = state.getMessage();
}
// Inicialización del adaptador de la lista desplegable
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// el calendario
datePicker1.setCalendarViewShown(false);
// el seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// inicialización del adaptador de la lista desplegable
dropDownList.setAdapter(dataAdapter);
// ¿Es tu primera visita?
if (previousState == null) {
// se marca el primer botón
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// valor de la barra de desplazamiento
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// elemento seleccionado en la lista desplegable
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// ¿Se muestra el cuadro de diálogo?
if (message != null) {
// se muestra
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- líneas 2-9: el método [saveFragment] debe colocar los elementos del fragmento que se van a almacenar en una clase derivada de [CoreState] y devolver la instancia de esta;
- líneas 11-14: el método [getNumView] debe devolver el número del fragmento. En este caso, solo hay un fragmento, cuyo número es 0;
- líneas 16-34: el método [initFragment] debe inicializar los campos del fragmento. Recibe el estado anterior del fragmento. Si [previousState] es igual a null, entonces se trata de la primera visita;
- líneas 19-25: durante la primera visita, se crean los valores de la lista desplegable;
- líneas 26-30: si no se trata de la primera visita, los campos [list, message] del fragmento se restauran a partir del estado anterior;
- líneas 33-34: inicialización del campo [dataAdapter] del fragmento. Es la fuente de datos de la lista desplegable;
- líneas 37-62: el método [initView] sirve para inicializar los componentes de la interfaz visual. Recibe como parámetro el estado anterior [previousState]. Si es [previousState==null], entonces se trata de la primera visita;
- aquí se reproduce lo que había anteriormente en el método [@AfterViews];
- líneas 57-61: en la primera visita, se comprueba que el primer botón de opción esté marcado;
- líneas 64-67: el método [updateOnSubmit] se ejecuta cuando la acción en curso es [SUBMIT]. Aquí no hay navegación entre fragmentos y, por lo tanto, no hay ninguna acción en curso;
- líneas 69-81: el método [updateOnRestore] se ejecuta cuando la acción en curso es [NAVIGATION] o [RESTORE]. Aquí no hay navegación entre fragmentos y, por lo tanto, no es posible la acción [NAVIGATION];
- línea 72: se vuelve a calcular (no se restaura) el valor de TextView seekBarValue. De hecho, durante las rotaciones, a veces se perdía su valor;
- líneas 74-75: se coloca la lista en el elemento que estaba seleccionado antes de la rotación. De lo contrario, la lista se situaba en su primer elemento;
- líneas 76-80: se vuelve a mostrar el cuadro de diálogo si el mensaje del estado anterior no es null. Volveremos sobre el método [showMessage] (línea 79);
- líneas 83-86: el método [notifyEndOfUpdates] es el último método invocado por la clase padre antes de dejar en paz al fragmento hijo. Aquí no hay nada que hacer;
- líneas 88-91: el método [notifyEndOfTasks] indica el final de las tareas asíncronas iniciadas por el fragmento. Aquí no hay ninguna;
La restauración del cuadro de diálogo se realiza de la siguiente manera:
// el mensaje del cuadro de diálogo
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// lista de mensajes que se van a mostrar
List<String> messages = new ArrayList<>();
...
// Visualización
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// se genera el texto que se va a mostrar
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// se almacena el mensaje
message = texte.toString();
// se muestra
showMessage();
}
private void showMessage() {
// se muestra
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// se restablece el mensaje
message = null;
}
}).show();
}
Cuando el usuario valida el formulario, el método [doValider] (línea 5) crea una lista de mensajes que, a continuación, 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 este mismo mensaje el que el método [updateOnRestore] hace que se muestre;
- línea 27: el segundo parámetro del método [setNeutralButton] es el método que se ejecuta cuando el usuario hace clic en el botón [Fermer] del cuadro de diálogo;
- línea 31: al cerrar el cuadro de diálogo, se devuelve el mensaje a null para indicar que el cuadro de diálogo ya no está presente;
2.8.1.7. Tests
Se invita al lector a probar este proyecto y a comprobar que el fragmento se conserva correctamente 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:

El URL tiene el siguiente formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} donde:
- city: la ciudad de la que queremos conocer la información meteorológica, en este caso Angers;
- country: el país de la ciudad, en este caso Francia (fr);
- APPID: una clave que se obtiene al registrarse en la página web [https://home.openweathermap.org/users/sign_up];
2.8.2.1. El proyecto
![]() |
El proyecto se ha desarrollado a partir del proyecto [client-android-skel]. Presenta las siguientes características:
- solo tiene un fragmento cuyo estado no es necesario conservar;
- realiza consultas asíncronas;
2.8.2.2. Personalización del proyecto
![]() |
La interfaz [IMainActivity] permite especificar ciertas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// acceso a la sesión
ISession getSession();
// cambio de vista
void navigateToView(int position, ISession.Action action);
// gestión de la cola
void beginWaiting();
void cancelWaiting();
// constantes de la aplicación -------------------------------------
// modo de depuración
boolean IS_DEBUG_ENABLED = true;
// tiempo máximo de espera de la respuesta del servidor
int TIMEOUT = 1000;
// tiempo de espera antes de ejecutar la solicitud del cliente
int DELAY = 5000;
// autenticación básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adyacencia de fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de pestañas
boolean ARE_TABS_NEEDED = false;
// imagen de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos de la aplicación
int FRAGMENTS_COUNT = 1;
}
- líneas 25, 28, 31, 40: características de la capa [DAO]. Línea 31: no es necesaria la autenticación básica;
- línea 34: adyacencia de los fragmentos. En este caso, esta constante no tiene importancia, ya que solo hay un fragmento;
- línea 37: no es una aplicación con pestañas;
- línea 43: solo hay un fragmento;
La clase [CoreState], que almacena el estado de los fragmentos, será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// tarea pendiente: añadir aquí las subclases de [CoreState]
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragmento visitado o no
protected boolean hasBeenVisited = false;
// estado del posible menú del fragmento
protected MenuItemState[] menuOptionsState;
// getters y setters
...
}
- líneas 10-13: no hay nada que declarar, ya que en esta aplicación solo hay un fragmento cuyo estado no se conserva;
La clase [Session] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// los elementos que no se pueden serializar en jSON deben llevar la anotación @JsonIgnore
}
Está vacía, ya que en esta aplicación no hay comunicación entre fragmentos.
2.8.2.3. La capa [DAO]
![]() |
En la capa [DAO], hay que personalizar tres clases:
- la interfaz IDao;
- su implementación Dao;
- la interfaz WebClient de comunicación con el servidor web / jSON;
La interfaz [WebClient] será la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// servicio meteorológico
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- líneas 18-19: el URL del servicio meteorológico. Cabe recordar que esta se refiere al URL raíz (RestClientRootUrl, línea 12) del cliente. En este caso, dicho URL raíz será [http://api.openweathermap.org/];
La interfaz [IDao] será la siguiente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL del servicio web
void setUrlServiceWebJson(String url);
// usuario
void setUser(String user, String mdp);
// tiempo de espera del cliente
void setTimeout(int timeout);
// autenticación básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuración
void setDebugMode(boolean isDebugEnabled);
// tiempo de espera del cliente en milisegundos antes de la solicitud
void setDelay(int delay);
// servicio meteorológico
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Cabe recordar que los métodos de las líneas 6-22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
- línea 25: el método [getWeatherForecast] permite obtener la cadena jSON con la información meteorológica de la ciudad [city] del país [country]. El tercer parámetro es la clave obtenida en la página web [https://home.openweathermap.org/users/sign_up];
La interfaz [IDao] está implementada por la siguiente clase [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// cliente del servicio web
@RestService
protected WebClient webClient;
// seguridad
@Bean
protected MyAuthInterceptor authInterceptor;
// el RestTemplate
private RestTemplate restTemplate;
// fábrica de RestTemplate
private SimpleClientHttpRequestFactory factory;
// tiempo de espera
private int timeout;
@AfterInject
public void afterInject() {
// registro
Log.d(className, "afterInject");
// se construye el restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// se establece el convertidor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// se establece el restTemplate del cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// se establece el URL del servicio web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// se registra el usuario en el interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// memoria
this.timeout = timeout;
// configuración de fábrica
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// ¿Interceptor de autenticación?
if (isBasicAuthentificationNeeded) {
// se añade el interceptor de autenticación
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// servicio meteorológico ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// registro
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// resultado
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Cabe recordar que las líneas 17-90 están presentes por defecto en la clase [Dao] del proyecto [client-android-skel]. Solo hay que añadir los métodos de implementación de la interfaz [IDao], específicos de la aplicación (línea 92);
- líneas 93-105: implementación del método [getWeatherForecast]. Esta es muy sencilla y se realiza en 6 líneas, las líneas 100-105;
- línea 100: el método [getResponse] es un método de la clase padre [AbstractDao]. Espera un parámetro de tipo [IRequest<T>], donde T es el tipo de la respuesta esperada del servidor; en este caso, un String, ya que se espera una cadena jSON. El tipo T de [IRequest<T>] debe ser el tipo T del método [Observable<T> getWeatherForecast];
- la interfaz [IRequest<T>] solo tiene un método: getResponse. Este tiene la función de proporcionar la respuesta de tipo T que debe devolver el método [Observable<T> getWeatherForecast];
- línea 103: es la interfaz [WebClient] la que proporciona esta respuesta. Se le pasan los tres parámetros recibidos en la línea 94. Por este motivo, estos deben tener el atributo «final»;
2.8.2.4. La actividad [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// interfaz IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- Recordemos que las líneas 15-55 están presentes por defecto en el proyecto [client-android-skel]. Solo hay que personalizarlas;
- líneas 37-40: la tabla de fragmentos. Aquí solo hay uno;
- líneas 43-46: no se necesitan títulos de fragmentos;
- líneas 48-50: aquí no hay pestañas;
- líneas 52-55: la primera vista que se debe mostrar es la vista n.º 0, la de [MeteoFragment];
- líneas 58-61: implementación de la interfaz [IDao]. Aquí no hay nada más que hacer que delegar el trabajo a la capa [DAO] de la línea 21;
2.8.2.5. El fragmento [MeteoFragment]
![]() |
El fragmento [MeteoFragment] consulta el servicio web /jSON de meteorología. Su estructura es la siguiente:
package client.android.fragments;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
- línea 14: la vista [res / layout / meteo_fragment.xml] es la siguiente:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Construisez votre interface visuelle"
android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="120dp"/>
</RelativeLayout>
La vista solo muestra el texto de la línea 10;
- línea 15: el menú [res / menu / menu_meteo.xml] es el siguiente:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionMeteo"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionTerminer"
android:title="@string/actionTerminer"/>
</menu>
</item>
</menu>
- líneas 10-12: esta opción del menú sirve para consultar el tiempo de una ciudad;
- líneas 14-15: esta opción del menú sirve para cancelar esta consulta si está en curso;
- líneas 16-18: esta opción del menú cierra la aplicación;
El código completo del fragmento es el siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
// datos locales
private int nbReponsesRecues;
// gestión de eventos ---------------------------------------------------------------------------------------
// ciudades de las que se quiere conocer la información meteorológica
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// su país
String country = "fr";
// Consigue un identificador API creando una cuenta [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL del servicio web / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// Inicio de la espera de las tareas asíncronas [paysDeLoire.length]
beginWaiting(paysDeLoire.length);
// número de respuestas recibidas
nbReponsesRecues = 0;
// las llamadas asíncronas se realizan en paralelo
for (String city : paysDeLoire) {
// el tiempo
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// procesamiento de la respuesta
consumeResponse(response);
// una respuesta de +
nbReponsesRecues++;
}
});
}
}
// procesamiento de la respuesta del servidor
private void consumeResponse(String response) {
// registro
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// Inicio de la espera
protected void beginWaiting(int numberOfRunningTasks) {
// registro
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// padre
beginRunningTasks(numberOfRunningTasks);
// se muestra la opción [Annuler]
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionAnnuler, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menú
initMenu();
// visualización de resultados
String message;
switch (nbReponsesRecues) {
case 0:
message = "Aucune réponse n'a été reçue";
break;
case 1:
message = "Une réponse a été reçue. Consultez vos logs...";
break;
default:
message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
break;
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
}
// métodos privados -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menú
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// gestión del ciclo de vida ---------------------------------------------------------------------------------------
...
}
- líneas 25-50: gestión del clic en la opción de menú [Météo];
- línea 32: construcción de la URL del servicio web / jSON del servicio meteorológico. A continuación, esta se pasa a la capa [DAO] a través de la actividad;
- línea 34: se inicia la espera. Se pasa el número de tareas que se van a lanzar, para que la clase padre pueda notificarnos cuando estas hayan finalizado. En este caso, hay cinco tareas, ya que vamos a solicitar la información meteorológica de las cinco ciudades de la línea 23;
- línea 16: vamos a contar el número de respuestas recibidas para poder mostrarlo;
- líneas 38-50: se realiza un bucle por las ciudades de las que queremos la información meteorológica;
- línea 40: vamos a realizar 5 solicitudes HTTP en paralelo;
- línea 40: se le pide a la clase padre [AbstractParent] que consulte el servicio web /jSON;
- líneas 40-48: el método [executeInBackground] espera dos parámetros:
- línea 40: el proceso que se va a observar y ejecutar lo proporciona el método [mainActivity.getWeatherForecast];
- líneas 40-48: la instancia [Action1] que se ejecutará al recibir la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getWeatherForecast];
- línea 44: se ha recibido una respuesta. Se pasa al método [consumeResponse] de la línea 53;
- línea 46: se incrementa el contador de respuestas recibidas;
- líneas 53-56: se procesa una respuesta jSON del servicio meteorológico;
- línea 55: simplemente se registra la cadena jSON;
- líneas 59-72: código ejecutado antes del inicio de las tareas asíncronas;
- línea 65: se pasa el número de tareas a ejecutar a la clase principal [AbstractParent]. Esto es lo que permite que dicha clase nos avise cuando todas hayan finalizado;
- líneas 67-70: preparación del menú para una espera. Solo se mantiene la opción [Actions/Annuler], que permitirá al usuario cancelar las tareas iniciadas;
- líneas 74-92: código que se ejecuta cuando la clase padre nos avisa de que todas las tareas iniciadas han finalizado;
- línea 77: se restablece el menú a su estado inicial. El método [initMenu] (líneas 95-102) muestra el menú con todas sus opciones, excepto la opción [Actions/Annuler], que permanece oculta;
- líneas 80-91: se muestra el número de respuestas recibidas;
Al hacer clic en la opción de menú [Annuler], se ejecuta el siguiente código:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// se cancelan las tareas asíncronas
cancelRunningTasks();
}
- línea 7: se solicita a la clase padre que cancele las tareas aún activas;
Al hacer clic en la opción de menú [Terminer] se ejecuta el siguiente código:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// se detiene todo
System.exit(0);
}
La gestión del ciclo de vida del fragmento se lleva a cabo mediante los siguientes métodos:
// gestión del ciclo de vida ---------------------------------------------------------------------------------------
@Override
public CoreState saveFragment() {
return new CoreState();
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// ¿Primera visita?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- líneas 3-6: sirven para almacenar el estado del fragmento en una clase derivada de [CoreState]. Si el fragmento no tiene ningún estado que almacenar, como en este caso, basta con devolver una instancia de [CoreState]. No se debe devolver null, ya que esto provocaría un fallo del sistema más adelante;
- líneas 8-11: deben devolver el número de la vista. En este caso, el fragmento [MeteoFragment] tiene el número 0;
- líneas 13-16: sirven para inicializar el fragmento una vez que se ha construido (previousState==null) o reconstruido (previousState!=null). Aquí no hay nada que hacer. El único campo susceptible de inicialización es el siguiente:
// ciudades de las que se quiere conocer el tiempo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
pero se inicializa por sí solo;
- líneas 18-24: sirven para inicializar la vista asociada al fragmento una vez que se ha construido (previousState==null) o reconstruido (previousState!=null);
- líneas 21-23: si es la primera vez que se visita el fragmento, se inicializa su menú para ocultar la opción [Annuler];
- líneas 27-30: se ejecutan si, para llegar al fragmento, se ha realizado una navegación con una acción de tipo [SUBMIT]. En este caso, no hay navegación entre fragmentos, ya que solo hay un fragmento;
- líneas 32-35: se ejecutan durante un ciclo de copia de seguridad/restauración debido a una rotación del dispositivo u otra causa. En este caso, como no se ha guardado ningún estado, no hay nada que hacer;
- líneas 37-40: se invocan cuando se han realizado todas las actualizaciones anteriores. En este caso, no hay nada que hacer;
2.8.2.6. Tests
Ahora ejecutamos el ejemplo:


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

Los registros son entonces los siguientes:
07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
- líneas 3-6, 10: las 5 llamadas a HTTP han generado 5 excepciones;
- línea 7: el fragmento [MeteoFragment] recibe la primera excepción. A continuación, cancelará todas las tareas;
Ahora establezcamos un tiempo de espera de 5 segundos en [IMainActivity.DELAY] y cancelemos la operación. Los registros son entonces los siguientes:
07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
- línea 3: solicitud de cancelación;
- línea 4: la espera se cancela porque se ha producido una cancelación;
- líneas 6-10: la cancelación de las tareas provoca una excepción en cada uno de los subprocesos de las cinco tareas. El tipo de excepción depende de las aplicaciones. La excepción en este caso es [java.lang.InterruptedException] porque las tareas se interrumpieron mientras ejecutaban la instrucción [Thread.sleep(delay)], que las hace esperar artificialmente [delay] milisegundos;
2.8.3. Ejemplo 16B
Aquí refactorizamos el ejemplo 16 del apartado 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:

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

Se observa que se han perdido todos los mensajes de error. Vamos a intentar mejorar esto.
2.8.3.1. El proyecto Ejemplo-16B
Copiamos el proyecto [client-android-skel] en el proyecto [exemples/Exemple-16B] y, a continuación, cargamos el nuevo proyecto:
![]() |
Del proyecto inicial [Exemple-16], copiamos en [Exemple-16B] los siguientes elementos:
- el archivo [res/layout/vue1.xml], la carpeta [res/values]:
![]() |
Modificaremos el margen superior de la vista [vue1.xml] a 80 dp:
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
- el fragmento [Vue1Fragment]:
![]() |
- la clase [dao / service / Response]:
![]() |
En este punto, podemos intentar una primera compilación:
- un primer tipo de errores es el de las clases imports. Algunas clases han cambiado de paquete en la migración a [Exemple-16B]. Empezamos por corregir este tipo de errores;
- se señala un segundo tipo de error en la clase [Vue1Fragment] porque no implementa los métodos exigidos por la clase padre [AbstractParent]. Se genera automáticamente dicha implementación;
Intentamos una segunda compilación:
- todos los errores restantes se concentran ahora en la clase [Vue1Fragment], la clase que va a sufrir más modificaciones;
2.8.3.2. Creación de un estado para el fragmento [Vue1Fragment]
Hemos visto que cierta información del fragmento deberá guardarse durante una rotación para poder restaurar el fragmento tal y como estaba antes de la rotación. Por lo tanto, creamos un estado [Vue1FragmentState] vacío por el momento:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. Personalización del proyecto
![]() |
La interfaz [IMainActivity] permite especificar ciertas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// Acceso a la sesión
ISession getSession();
// cambio de vista
void navigateToView(int position, ISession.Action action);
// gestión de la espera
void beginWaiting();
void cancelWaiting();
// constantes de la aplicación -------------------------------------
// modo de depuración
boolean IS_DEBUG_ENABLED = true;
// tiempo máximo de espera de la respuesta del servidor
int TIMEOUT = 1000;
// tiempo de espera antes de ejecutar la solicitud del cliente
int DELAY = 5000;
// autenticación básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adyacencia de fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de pestañas
boolean ARE_TABS_NEEDED = false;
// imagen de espera
boolean IS_WAITING_ICON_NEEDED = true;
// número de fragmentos de la aplicación
int FRAGMENTS_COUNT = 1;
}
- líneas 25, 28, 31, 40: características de la capa [DAO]. No es necesaria la autenticación básica;
- línea 34: adyacencia de los fragmentos. En este caso, esta constante no tiene importancia, ya que solo hay un fragmento;
- línea 37: no es una aplicación con pestañas;
- línea 43: solo hay un fragmento;
La clase [CoreState], que almacena el estado de los fragmentos, será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragmento visitado o no
protected boolean hasBeenVisited = false;
// estado del posible menú del fragmento
protected MenuItemState[] menuOptionsState;
// getters y setters
...
}
- línea 12: declaramos la clase del estado del fragmento [Vue1Fragment];
La clase [Session] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// los elementos que no se pueden serializar en jSON deben llevar la anotación @JsonIgnore
}
Está vacía, ya que en esta aplicación no hay comunicación entre fragmentos.
2.8.3.4. La capa [DAO]
![]() |
En la capa [DAO], hay que personalizar tres clases:
- la interfaz IDao;
- su implementación Dao;
- la interfaz WebClient de comunicación con el servidor web / jSON;
La clase [Response] procede del proyecto [Exemple-16], que la utiliza:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- propiedades
// estado de la operación
private int status;
// posibles mensajes de error
private List<String> messages;
// el cuerpo de la respuesta
private T body;
// constructores
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters y setters
...
}
La interfaz [WebClient] será la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// 1 número aleatorio en el intervalo [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- líneas 18-19: el URL del servicio de números aleatorios. Recordemos que esta se refiere al URL raíz (RestClientRootUrl, línea 12) del cliente. En este caso, dicho URL raíz será [http://localhost:8080];
La interfaz [IDao] será la siguiente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// URL del servicio web
void setUrlServiceWebJson(String url);
// usuario
void setUser(String user, String mdp);
// tiempo de espera del cliente
void setTimeout(int timeout);
// autenticación básica
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// modo de depuración
void setDebugMode(boolean isDebugEnabled);
// tiempo de espera del cliente en milisegundos antes de la solicitud
void setDelay(int delay);
// servicio de números aleatorios
Observable<Response<Integer>> getAlea(int a, int b);
}
- Cabe recordar que los métodos de las líneas 6-22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
- línea 25: el método [getAlea] permite obtener un número aleatorio en el intervalo [a,b]. Este número se obtiene en una respuesta de tipo [Response<Integer>], en la que el número aleatorio se encuentra en el campo [body] de dicho tipo;
La interfaz [IDao] está implementada por la siguiente clase [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// cliente del servicio web
@RestService
protected WebClient webClient;
// seguridad
@Bean
protected MyAuthInterceptor authInterceptor;
// el RestTemplate
private RestTemplate restTemplate;
// fábrica de RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// registro
Log.d(className, "afterInject");
// se fabrica el restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// se instala el convertidor jSON
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// se establece el restTemplate del cliente web
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// se establece el URL del servicio web
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// se registra el usuario en el interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuración de fábrica
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// ¿Interceptor de autenticación?
if (isBasicAuthentificationNeeded) {
// se añade el interceptor de autenticación
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// métodos privados -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// servicio de números aleatorios
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// ejecución del cliente web
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- Cabe recordar que las líneas 17-85 están presentes por defecto en la clase [Dao] del proyecto [client-android-skel]. Solo hay que añadir los métodos de implementación de la interfaz [IDao];
- líneas 88-97: implementación del método [getAlea]. Esta es muy sencilla y se realiza en 6 líneas, las 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 tipo Response<Integer>. El tipo T de [IRequest<T>] (línea 91) debe ser el tipo T del método [Observable<T> getAlea] (línea 89);
- la interfaz [IRequest<T>] solo tiene un método: getResponse. La función de este método 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 este motivo, estos deben tener el atributo «final»;
2.8.3.5. La actividad [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos con las inicializaciones iniciadas por la clase padre
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// definir aquí los fragmentos
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// definir aquí los títulos de los fragmentos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// navegación por pestañas: definir la vista que se va a mostrar
}
@Override
protected int getFirstView() {
return 0;
}
// Interfaz IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Recordemos que las líneas 15-61 están presentes por defecto en el proyecto [client-android-skel]. Solo hay que personalizarlas;
- líneas 40-44: la tabla de fragmentos. Aquí solo hay uno;
- líneas 47-51: no se necesitan títulos de fragmentos;
- líneas 53-56: aquí no hay pestañas;
- líneas 58-61: la primera vista que se debe mostrar es la vista n.º 0, la de [Vue1Fragment];
- líneas 64-67: implementación de la interfaz [IDao]. Aquí no hay nada más que hacer que delegar el trabajo a la capa [DAO] de la línea 23;
2.8.3.6. El estado del fragmento [Vue1Fragment]
![]() |
La clase [Vue1FragmentState] será la siguiente:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// Estado del fragmento ------------------------
// lista de respuestas
private List<String> reponses = new ArrayList<>();
// estado de la vista ------------------------
// mensaje de error sobre el número de números aleatorios solicitados
private boolean txtErrorAleasVisible = false;
// mensaje de error sobre el intervalo de generación [a,b]
private boolean txtErrorIntervalleVisible = false;
// mensaje de error sobre el URL del servicio web
private boolean txtMsgErreurUrlServiceWebVisible = false;
// mensaje de error sobre el tiempo de espera
private boolean textViewErreurDelayVisible = false;
// Estado visible o no del botón «Ejecutar»
private boolean btnExecuterVisible = true;
// Métodos getter y setter
...
}
Para determinar qué era necesario almacenar en el fragmento, se hicieron girar el dispositivo en diversas situaciones y se observó qué había desaparecido al restaurarlo. Se llegó a la conclusión de que había que almacenar la información de las líneas 10-23.
2.8.3.7. El fragmento [Vue1Fragment]
![]() |
Actualmente, la vista [Vue1Fragment] presenta diversos 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.
La estructura básica del fragmento es la siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {
...
}
- En la línea 26 se recuerda que todo fragmento debe tener un menú, aunque esté vacío. Este es el caso aquí.
2.8.3.7.1. Gestión del clic en el botón [Exécuter]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// se comprueban los datos introducidos
if (!isPageValid()) {
return;
}
// Se borran las respuestas anteriores
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// Se pone a 0 el contador de respuestas
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// inicialización de la actividad
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// se prepara la tarea aleatoria
beginWaiting(1);
// se solicitan los números aleatorios
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// se crea el proceso a observar
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// se solicitan los números aleatorios
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// se procesa la respuesta
consumeAleaResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// registro
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// una respuesta de +
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// se analiza la respuesta
// ¿Error?
if (response.getStatus() != 0) {
// visualización
showAlert(response.getMessages());
// cancelación
doAnnuler();
// volver a la interfaz de usuario
return;
}
// se añade la información a la lista de respuestas
reponses.add(0, String.valueOf(response.getBody()));
// se actualizan las respuestas
dataAdapterReponses.notifyDataSetChanged();
}
// cancelación ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// se cancelan las tareas asíncronas
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// se muestra el reloj de arena
beginRunningTasks(nbRunningTasks);
// el botón [Annuler] sustituye al botón [Exécuter]
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- líneas 4-6: primero se comprueba que los datos introducidos sean válidos. En ese caso, pueden aparecer mensajes de error;
- líneas 8-9: se vacía la lista de respuestas. Este cambio se refleja en el ListView, que las muestra;
- líneas 11-12: el número de respuestas recibidas se pone a cero;
- línea 14: se establece el URL del servicio de números aleatorios. Esta información se transmitirá a la capa [DAO];
- línea 15: se establece el tiempo de espera antes de realizar la solicitud al servicio de números aleatorios. Esta información se transmitirá a la capa [DAO];
- línea 17: nos preparamos para iniciar una tarea asíncrona (y no N, ya veremos por qué);
- líneas 24-27: de las N tareas asíncronas, se convierte cada una en una secuencia de operaciones [merge];
- líneas 29-36: se solicita a la clase padre [AbstractParent] que consulte el servicio web /jSON para obtener números aleatorios;
- líneas 29-36: el método [executeInBackground] espera dos parámetros:
- línea 29: el proceso que se va a observar y ejecutar es el que se ha calculado en las líneas anteriores;
- líneas 29-36: la instancia [Action1] que se ejecutará al recibir la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getAlea], es decir, un tipo [Response<Integer>];
- línea 34: cuando llega una respuesta (un número aleatorio), se procesa en el método de la línea 39;
- líneas 49-50: se anota y se indica que se ha recibido una nueva respuesta;
- líneas 53-60: el tipo [Response<T>] tiene un campo [status] que es un código de error. Si este código es distinto de cero, significa que el servidor ha detectado un problema;
- línea 55: se muestra un mensaje de error. El método [showAlert] pertenece a la clase padre;
- línea 57: se invoca el método de las líneas 68-75. Este 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 de ListView;
- línea 64: se actualiza el ListView;
- líneas 77-83: el método [beginWaiting(int nbRunningTasks)] prepara la vista para la espera (líneas 81-82) y comunica a la clase padre que las tareas de [nbRunningTasks] se ejecutarán en breve (línea 79);
2.8.3.7.2. El ciclo de vida del fragmento
El ciclo de vida del fragmento se gestiona mediante los siguientes métodos:
// datos locales
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// gestión del ciclo de vida ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// estado actual de la vista
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
state.setReponses(reponses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// ¿Primera visita?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// fuente de datos de listView
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// N.º de respuestas
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// enlace entre la vista de lista y el adaptador
listReponses.setAdapter(dataAdapterReponses);
// ¿Es tu primera visita?
if (previousState == null) {
// se ocultan los mensajes de error
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// los botones
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// estado anterior de la vista
Vue1FragmentState state = (Vue1FragmentState) previousState;
// Mostrar/ocultar los mensajes de error
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// botones
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// Número de respuestas
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// el botón [Exécuter] sustituye al botón [Annuler]
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- líneas 7-18: se encargan de guardar el fragmento cuando la clase padre lo solicita;
- línea 11: muestra el mensaje de error sobre el tiempo de espera;
- línea 12: muestra el mensaje de error sobre el número de números aleatorios solicitados;
- línea 13: muestra el mensaje de error sobre el URL del servicio web / jSON;
- línea 14: visibilidad del mensaje de error sobre el intervalo [a,b] de generación de números aleatorios;
- línea 15: visibilidad del botón [Exécuter];
- línea 16: la lista de respuestas recibidas;
- líneas 20-23: deben devolver el número de la vista. El número del fragmento es aquí 0, ya que solo hay uno;
- líneas 25-38: inicialización de los campos del fragmento, ya sea en una primera visita (previousState==null) o en una visita posterior;
- líneas 29-30: si no es la primera visita, el campo [reponses] se restaura a partir del estado anterior del fragmento;
- líneas 31-33: si se trata de la primera visita, el campo [reponses] se inicializa con una lista vacía;
- líneas 34-37: a partir del campo [reponses] se puede construir la fuente de datos del campo ListView del fragmento (línea 35), así como el número de respuestas (línea 37);
- líneas 40-55: se ejecutan para inicializar la vista asociada al fragmento, ya sea en una primera visita (previousState==null) o en una visita posterior;
- línea 43: se asocia el ListView del fragmento a la fuente de datos que se acaba de crear en el método [initFragment];
- líneas 45-54: si se trata de la primera visita, se prepara la vista para su primera visualización;
- líneas 57-60: se ejecutan durante una navegación entre fragmentos asociada a una acción de tipo [SUBMIT]. En este caso, solo hay un fragmento y, por lo tanto, no hay navegación entre fragmentos;
- líneas 63-76: se ejecutan durante una navegación entre fragmentos asociada a una acción de tipo [NAVIGATION] o bien durante un ciclo de guardado/restauración debido a una rotación del dispositivo u otra causa. En este caso, solo puede darse esta última situación. Hay que recordar que, en cualquier caso, [previousState] nunca es null;
- línea 65: se convierte el estado anterior al tipo del estado del fragmento;
- líneas 66-75: se utiliza el contenido del estado anterior para restaurar la vista;
- líneas 78-81: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada que hacer;
- líneas 83-89: se ejecutan cuando todas las tareas asíncronas han finalizado. Aquí se oculta el botón [Annuler] para sustituirlo por el botón [Exécuter];
2.8.3.8. Las pruebas
Se invita al lector a realizar las siguientes pruebas:
- generar errores y poner en marcha el dispositivo: los mensajes de error deben permanecer en pantalla;
- obtener números aleatorios y poner en marcha el dispositivo: los números aleatorios obtenidos deben permanecer en pantalla;
- establecer una espera de varios segundos y poner en marcha el dispositivo durante la espera: las tareas deben haberse cancelado (esto se puede comprobar en los registros);
2.8.4. Ejemplo-22B
Retomamos aquí el ejemplo 22 para refactorizarlo según el modelo del proyecto [client-android-skel]. Recordemos que el proyecto [Exemple-22] gestiona correctamente el ciclo de guardado y restauración de fragmentos durante una rotación y que fue este el que sirvió de base para el proyecto [client-android-skel].
Duplicamos el proyecto [client-android-skel] en [exemples/Exemple-22B] y cargamos este último proyecto:
![]() |
A continuación, copiamos diversos elementos del proyecto [Exemple-22] al proyecto [Exemple-22B].
En primer lugar, copiamos elementos de la carpeta [res]:
- [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, la carpeta [values];
![]() |
Modificaremos el margen superior de ambas vistas a 120 dp:
[vue1.xml]:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
A continuación, copiamos los elementos [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
En este punto, podemos intentar una primera compilación. Aparece un primer tipo de errores: los de los imports incorrectos porque algunas clases han cambiado de paquete. Corregimos estos imports. Un segundo tipo de errores se debe a que los fragmentos no implementan todos los métodos de su clase padre [AbstractFragment]. Se corrigen pulsando (Alt+Intro).
Los errores restantes se deben a las diferencias existentes entre la clase antigua y la nueva [AbstractFragment]. Por el momento, se ignoran.
2.8.4.1. Personalización del proyecto
![]() |
En la carpeta [custom] se encuentran los elementos de arquitectura que el desarrollador puede personalizar.
La interfaz [IMainActivity] permite especificar ciertas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// acceso a la sesión
ISession getSession();
// cambio de vista
void navigateToView(int position, ISession.Action action);
// gestión de la cola
void beginWaiting();
void cancelWaiting();
// modo de depuración
boolean IS_DEBUG_ENABLED = true;
// tiempo máximo de espera de la respuesta del servidor
int TIMEOUT = 1000;
// tiempo de espera antes de ejecutar la solicitud del cliente
int DELAY = 0;
// autenticación básica
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// adyacencia de fragmentos
int OFF_SCREEN_PAGE_LIMIT = 1;
// barra de pestañas
boolean ARE_TABS_NEEDED = true;
// imagen de espera
boolean IS_WAITING_ICON_NEEDED = false;
// número de fragmentos
int FRAGMENTS_COUNT = 5;
}
- líneas 23, 26, 29, 38: características de la capa [DAO]. Aquí no hay ninguna;
- línea 41: aquí hay cinco fragmentos;
- línea 32: adyacencia de los fragmentos. Esta constante puede tener aquí un valor en [1,4]. Se recomienda al lector que varíe este valor para comprobar si la aplicación sigue funcionando;
- línea 35: se trata de una aplicación con pestañas;
La clase [CoreState], que almacena el estado de los fragmentos, será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
// fragmento visitado o no
protected boolean hasBeenVisited = false;
// estado del posible menú del fragmento
protected MenuItemState[] menuOptionsState;
// getters y setters
...
}
- línea 12: declaramos la clase del estado del fragmento [PlaceHolderFragment]. El fragmento [Vue1Fragment], por su parte, no tiene estado;
La clase [Session] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// datos que se deben compartir entre los propios fragmentos y entre los fragmentos y la actividad
// los elementos que no se pueden serializar en jSON deben llevar la anotación @JsonIgnore
// No olvidar los métodos getter y setter necesarios para la serialización/deserialización en jSON
// número de fragmentos visitados
private int numVisit;
// N.º del fragmento de tipo [PlaceholderFragment] mostrado en la segunda pestaña
private int numFragment = -1;
// getters y setters
...
}
Es la sesión del proyecto [Exemple-22].
2.8.4.2. La actividad [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// capa [DAO]
@Bean(Dao.class)
protected IDao dao;
// sesión
private Session session;
// gestión del menú-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// Implementación de métodos de la clase padre ---------------------------------------------------
...
}
En este caso, la clase [MainActivity] es más extensa que la de los ejemplos anteriores por dos razones:
- hay pestañas que gestionar;
- hay un menú que gestionar;
2.8.4.2.1. Implementación de los métodos de la clase padre
// métodos de la clase padre -----------------------
@Override
protected void onCreateActivity() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos con las inicializaciones iniciadas por la clase padre
// sesión
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// n.º de fragmento
final String ARG_SECTION_NUMBER = "section_number";
// inicialización de la matriz de fragmentos
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// se crea un fragmento
fragments[i] = new PlaceholderFragment_();
// se pueden pasar argumentos al fragmento
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// un fragmento de +
fragments[i] = new Vue1Fragment_();
// resultado
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// aquí no hay títulos
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- líneas 2-12: el método [onCreateActivity] es invocado por la clase padre [AbstractActivity] cuando la actividad se crea por primera vez o se vuelve a crear durante un ciclo de guardado/restauración. Cuando se invoca este método, la clase padre ya ha restaurado la sesión;
- línea 10: se recupera una referencia local de la sesión. El cambio de tipo se debe a que la sesión de la clase padre es de tipo [AbstractSession];
- líneas 19-38: el método [getFragments] debe devolver a la clase padre la matriz de fragmentos gestionados por la aplicación. En este caso hay [FRAGMENTS_COUNT], número definido en [IMainActivity]. Los primeros fragmentos [FRAGMENTS_COUNT-1] son de tipo [PlaceHolderFragment] y el último, de tipo [Vue1Fragment];
- líneas 41-45: el método [getFragmentTitle] debe devolver los títulos de los fragmentos cuando esta información pueda resultar útil. No es el caso aquí;
- líneas 47-50: la clase principal invoca este método cuando el usuario hace clic en una pestaña. Volveremos sobre ello en el siguiente apartado;
- líneas 52-55: devuelve el número de la primera vista que se debe mostrar al iniciar la aplicación. En este caso, es el fragmento [Vue1Fragment] el que 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() {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continuamos con las inicializaciones iniciadas por la clase padre
// sesión
this.session = (Session) super.session;
// 1.ª pestaña
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// ¿Segunda pestaña?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// n.º del fragmento que se va a mostrar
int numFragment;
switch (position) {
case 0:
// n.º de fragmento [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// N.º de fragmento [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// visualización del fragmento
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- líneas 1-20: el método [onCreateActivity] es invocado por la clase padre [AbstractActivity] cuando la actividad se crea por primera vez o se vuelve a crear durante un ciclo de guardado/restauración. Cuando se invoca este método, la clase padre ya ha restaurado la sesión;
- línea 9: se recupera una referencia local de la sesión. El cambio de tipo se debe a que la sesión de la clase padre es de tipo [AbstractSession];
- líneas 11-13: se crea la primera pestaña;
- líneas 15-20: se crea la segunda pestaña si hay un número de fragmento registrado en la sesión (línea 15). Este número tiene inicialmente el valor -1 cuando se crea la actividad por primera vez;
- líneas 23-39: la clase padre invoca este método cuando el usuario hace clic en una pestaña;
- líneas 28-31: si se hace clic en la pestaña 0, se debe mostrar [Vue1Fragment]. Sabemos que es la primera vista que se mostró al iniciar la aplicación;
- líneas 32-35: si se hace clic en la pestaña 1, hay que mostrar el fragmento cuyo número está registrado en la sesión;
- líneas 37-39: se navega hacia el fragmento seleccionado. La acción asociada es [SUBMIT]. ¿Podría haber sido [NAVIGATION]? En este documento, se utiliza [NAVIGATION] únicamente cuando la visualización del nuevo fragmento solo requiere conocer su estado anterior. En este caso, no es así, ya que la visualización del fragmento debe cambiar con respecto a su estado anterior para mostrar una visita más;
2.8.4.2.3. Gestión del menú
La actividad está asociada al siguiente menú [menu_main.xml]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
que muestra lo siguiente:
![]() |
La gestión del menú se lleva a cabo mediante los siguientes métodos:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// registro
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// procesamiento de las opciones del menú
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// elemento procesado
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// no hay navegación al seleccionar una pestaña mediante el software
session.setNavigationOnTabSelectionNeeded(false);
// Se vuelven a crear las dos pestañas por un problema con la fuente de los títulos
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// el n.º del fragmento que se va a mostrar se establece en la sesión
session.setNumFragment(i);
// se selecciona la pestaña n.º 2 con navegación
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- líneas 16-31: gestión del clic en una opción de menú de tipo [Fragmenti];
- líneas 37-50: muestran el fragmento n.º i (se trata de fragmentos de tipo PlaceHolderFragment) en la pestaña n.º 1 (segunda pestaña);
- líneas 42-44: se decide eliminar las pestañas existentes para crear dos nuevas. Esta decisión se tomó para sortear el siguiente problema: cuando nos limitamos a mostrar el fragmento en la pestaña 1 existente (sin eliminarla, por tanto), curiosamente su título tiene un aspecto (tipo de letra, tamaño) diferente al del título de la pestaña 0;
- líneas 43-44: se crean las dos pestañas, pero no se seleccionan (último parámetro en false);
- línea 40: las operaciones de las líneas 42-44 pueden provocar operaciones [select] en las pestañas, lo que activará el gestor [onTabSelected]. Si no se hace nada, se producirá la navegación hacia un fragmento. Esto se evita estableciendo el valor booleano [navigationOnTabSelectionNeeded] en faux en la sesión. La clase [AbstractFragment] restablece automáticamente este valor booleano a vrai cuando un fragmento se vuelve visible;
- línea 46: se anota el número del fragmento que se va a mostrar en la sesión;
- líneas 48-50: se selecciona la pestaña n.º 2 con navegación (línea 48). Esto activará el procedimiento [onTabSelected], que:
- mostrará el fragmento cuyo número se ha introducido en la sesión;
- almacenará en la sesión el número de la pestaña seleccionada;
2.8.4.3. El fragmento [Vue1Fragment]
A continuación, presentamos la versión final del fragmento:
package client.android.fragments.behavior;
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
// los elementos de la interfaz visual
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// gestor de eventos
@Click(R.id.buttonValider)
protected void doValider() {
// se muestra el nombre introducido
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// Ciclo de vida del fragmento -----------------------------------------------
private void initFragment() {
// No hay nada que hacer
}
// guardar estado del fragmento
@Override
public CoreState saveFragment() {
// Estado de la vista: no hay nada que guardar
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// No hay nada que hacer
}
@Override
protected void initView(CoreState previousState) {
// ¿Primera visita?
if (previousState == null) {
// se muestra el n.º de la visita
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// se muestra el número de visita
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// métodos privados -------------------------------------
// Visualización del número de visita
private void showNumVisit() {
// incrementar el número de visita
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// se muestra el número de visita
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
La clase está prácticamente vacía.
- Líneas 35-39: llamadas por la clase padre cuando el fragmento debe guardar su estado. El fragmento [Vue1Fragment] no tiene ningún estado que guardar. Simplemente se devuelve una instancia de la clase base [CoreState] (recordatorio: no se debe devolver null);
- líneas 41-44: deben devolver el número del fragmento. El fragmento [Vue1Fragment] tiene, por definición, el número [FRAGMENTS_COUNT-1];
- líneas 51-59: llamadas por la clase padre cuando el fragmento se construye por primera vez (previousState==null) o en las siguientes ocasiones (previousState!=null);
- líneas 54-57: si se trata de la primera visita, se incrementa el número de visita y se muestra (líneas 85-92);
- líneas 61-65: se ejecutan cuando el fragmento va a mostrarse asociado a una acción [SUBMIT]. Se incrementa el número de visita y se muestra. En este caso, no es posible que el número de visita se incremente dos veces durante el ciclo de vida. De hecho, la primera visita al fragmento [Vue1Fragment] se realiza al iniciar la aplicación, cuando la acción es [NONE] por definición en la sesión. Esto garantiza que no se llame al método [updateOnSubmit]. A partir de entonces, ya no será nunca la primera visita y el método [initView] no hará nada;
- líneas 68-71: se invocan en un ciclo de copia de seguridad/restauración. Dado que el fragmento no tiene estado, aquí no hay nada que restaurar;
- líneas 73-76: se invocan cuando se han realizado todas las actualizaciones anteriores. Aquí no hay nada más que hacer;
- líneas 78-81: se invocan cuando todas las tareas asíncronas iniciadas 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 {
// texto
private String text;
// constructores
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters y setters
...
}
- cuando haya que guardar el estado del fragmento, se guardará el texto que mostraba (línea 7);
2.8.4.5. El fragmento [PlaceHolderFragment]
El fragmento [PlaceHolderFragment] será el siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
// componentes de la interfaz visual
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// datos
private String text;
// n.º de fragmento
private static final String ARG_SECTION_NUMBER = "section_number";
// Implementación de los métodos de la clase padre ----------------------------
@Override
public CoreState saveFragment() {
// se guarda el estado del fragmento
PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
placeHolderFragmentState.setText(textViewInfo.getText().toString());
return placeHolderFragmentState;
}
@Override
protected int getNumView() {
return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// texto original
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// se actualiza el texto mostrado
// se incrementa el número de visitas
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// texto modificado
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// registro
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// se restaura el texto mostrado
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 solicita al fragmento que guarde su estado, se guarda el texto mostrado por el fragmento (línea 34);
- líneas 38-41: devuelven el número del fragmento. Este depende del número de sección que se le haya pasado como argumento al crearlo;
- líneas 43-47: se invocan durante la primera construcción del fragmento (previousState == null) o durante las siguientes (previousState != null);
- línea 46: aquí no se aprovecha el estado anterior. El texto inicial [text] (línea 24), que se muestra en la primera visita, se vuelve a calcular cada vez. Esto es discutible. Se podría haber optado por incluir también esta información en el estado del fragmento;
- líneas 49-51: se invocan durante la primera construcción de la vista asociada al fragmento (previousState==null) o durante las siguientes (previousState!=null). No hay nada que hacer;
- líneas 53-56: se invocan cuando el fragmento va a mostrarse asociado a una acción [SUBMIT]. Esto ocurre siempre, salvo en el ciclo de guardado/restauración, donde la acción es [RESTORE]. Por lo tanto, se incrementa el número de visita y se muestra;
- líneas 68-74: se ejecutan en un ciclo de guardado/restauración. Se restaura el texto que se había guardado en el estado del fragmento;
- líneas 76-79: se invocan cuando se han realizado todas las actualizaciones anteriores. En este caso, no hay nada más que hacer;
- líneas 82-83: se ejecutan cuando todas las tareas asíncronas iniciadas han finalizado. En este caso, no hay tareas asíncronas;
2.8.4.6. Tests
Se invita al lector a probar la aplicación girando el dispositivo para comprobar que el fragmento mostrado no pierde su estado. También se revisarán los registros.
2.9. Conclusion
Al finalizar este capítulo, disponemos de un proyecto modelo [client-android-skel] de cliente Android que se comunica con un servicio web / jSON con las siguientes características:
- la comunicación asíncrona con el servidor web / jSON se realiza mediante la biblioteca RxJava;
- El ciclo de vida de un fragmento (actualización, guardado, restauración) lo gestiona su clase padre [AbstractFragment], que invoca en momentos concretos determinados métodos de sus clases hijas. De este modo, el fragmento hijo no tiene que preocuparse por las etapas del ciclo de vida, sino únicamente de implementar ciertos métodos impuestos por su clase padre;
- el ciclo de vida de la actividad (guardar/restaurar) lo gestiona una clase abstracta, [AbstractActivity], que también exige a la actividad hija que implemente determinados métodos;
- la clase [AbstractActivity] es capaz de gestionar una aplicación con o sin pestañas, con o sin imagen de espera, con o sin autenticación básica en el servidor web / jSON. La presencia o ausencia de estos elementos se configura mediante la configuración;
A continuación, vamos a presentar un caso práctico más complejo que los ejemplos anteriores. La nueva aplicación se basará en el proyecto modelo [client-android-skel].



















































