2. Esqueleto de un cliente Android comunicándose con un servicio web / JSON
Ahora proporcionamos un esqueleto para una aplicación Android que se comunica con uno o más servicios web / JSON. Se trata del proyecto [client-android-skel], que se encuentra en la carpeta [architecture] de los ejemplos:
![]() |
El estudio de este esqueleto de aplicación nos dará la oportunidad de repasar ciertos puntos que hemos encontrado en los ejemplos anteriores. Esta aplicación servirá de esqueleto para todas las aplicaciones futuras. Se ha creado tras numerosas iteraciones. Su objetivo es integrar en clases abstractas el mayor número posible de elementos de las aplicaciones que construiremos próximamente, para evitar tener que escribir una y otra vez el mismo tipo de código, diferenciándose únicamente en los detalles. Sus características son las siguientes:
- la comunicación asíncrona con el servidor web/JSON se gestiona mediante la biblioteca RxJava;
- el ciclo de vida de un fragmento (actualizar, guardar, restaurar) es gestionado por su clase padre [AbstractFragment], que llama a determinados métodos de sus clases hijo en momentos específicos. De este modo, la clase hija no tiene que preocuparse de las etapas del ciclo de vida, sino que sólo necesita implementar ciertos métodos requeridos por su clase padre;
- el ciclo de vida de la actividad (guardar / restaurar) es gestionado por una clase abstracta [AbstractActivity], que también requiere que la actividad hija implemente ciertos métodos;
- La clase [AbstractActivity] es capaz de gestionar una aplicación con o sin pestañas, con o sin imagen de carga, y con o sin autenticación básica contra el servidor web/JSON. La presencia o ausencia de estos elementos viene determinada por la configuración;
Este esqueleto se utilizó para todos los ejemplos posteriores. Debido a su diversidad, lo que funcionaba para un ejemplo podía no funcionar para el siguiente. Como el esqueleto se utilizó para un total de siete ejemplos, se produjeron numerosas iteraciones. Si tuviéramos que utilizarlo para un octavo ejemplo, es posible que volviéramos a encontrarnos con que la naturaleza específica de este nuevo ejemplo genera nuevos errores. No obstante, la utilización de este esqueleto simplificará considerablemente la escritura de futuros ejemplos. En efecto, la gestión del ciclo de vida de un fragmento (actualizar, guardar, restaurar) combinada con el concepto de adyacencia de fragmentos es especialmente compleja. Aquí, está completamente oculto dentro de la clase [AbstractFragment].
2.1. Arquitectura cliente Android
El cliente Android propuesto se basa en la siguiente arquitectura:
![]() |
- la capa [DAO] implementa una interfaz [IDao]. Es responsable de la comunicación con el servidor web/JSON;
- sólo hay una actividad que también implementa la interfaz [IDao]. Las vistas recurren a ella para acceder al servidor;
- las vistas se implementan por fragmentos;
El proyecto Android refleja esta arquitectura:
![]() |
Presentaremos uno a uno los distintos elementos de este proyecto.
2.2. La configuración de Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you must use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// packaging options required to generate the APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- Todos los números de versión están sujetos a cambios. No obstante, puede empezar con los números actuales si configura Android Studio para asegurarse de que estas versiones de las herramientas de Android (líneas 15-16, 47-48) están presentes (consulte la sección 6.11);
2.3. El manifiesto de la aplicación
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="client.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- Línea 3: Cambiar el paquete de aplicación;
- Líneas 10, 15: Estableceremos el valor del elemento [app_name] en el archivo [res/values/strings.xml]. Por ahora, es el siguiente:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- application name -->
<string name="app_name">[Name your app]</string>
</resources>
2.4. La organización del código Java
![]() |
- la [arquitectura] agrupa los principales elementos de organización del código;
- [activity] contiene la actividad única de la aplicación;
- [fragmentos] agrupa los fragmentos o vistas de la aplicación;
- [dao] agrupa los elementos para la comunicación con el servidor web / JSON;
2.5. Elementos de la actividad
![]() | ![]() |

2.5.1. La vista asociada a la actividad
La vista [activity_main.xml] asociada a la actividad es la siguiente:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- fragment container -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
- Línea 29: Se utiliza un contenedor de fragmentos específico;
La actividad también tiene un menú [res/menu/menu_main.xml] para su vista:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
</menu>
For now, it is empty. The developer will fill it in as needed.
2.5.2. El contenedor de fragmentos [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// controls swiping
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
// constructors
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to override to handle swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// Is swiping allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Is swiping allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// scroll control
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position, isScrollingEnabled);
}
// setters
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
Esta clase extiende la clase estándar de Android [ViewPager] únicamente para manejar el deslizamiento (línea 11) y el desplazamiento (línea 13) entre vistas.
- líneas 26-43: métodos que desactivan el barrido si se ha desactivado;
- líneas 46-49: redefinición del método [setCurrentItem], que se utiliza para cambiar la vista mostrada. Si se ha desactivado el desplazamiento, la vista cambiará sin desplazamiento. Tenga en cuenta que el desarrollador puede anular este comportamiento utilizando el método [setCurrentItem(int position, boolean smoothScrolling)], que le permite especificar el comportamiento de desplazamiento deseado;
2.5.3. La clase [CoreState]
![]() |
La clase [CoreState] es la clase padre de los estados de los distintos fragmentos:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add the subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- línea 16: Cada fragmento tiene un booleano [hasBeenVisited] en su estado que indica si ya ha sido visitado o no. Esto es necesario porque a veces, cuando un fragmento se muestra por primera vez, hay acciones específicas que deben realizarse;
- línea 18: el proyecto [client-android-skel] guarda y restaura automáticamente los menús de fragmentos si tienen uno. En el MenuItemState[] menuOptionsState almacenamos el estado visible u oculto de todas las opciones del menú;
- Líneas 10-13: Como se hizo en [Ejemplo-22], el estado de la actividad y sus fragmentos se guardarán en la sesión, que a su vez se guardará como una cadena JSON. Veremos que la sesión almacena un array de elementos de tipo [CoreState]. Si no hacemos nada, se guardará la cadena JSON de tipo [CoreState]. Sin embargo, queremos guardar los estados de los fragmentos, que se derivan de [CoreState]. Para asegurar que se genera la cadena JSON del tipo derivado en lugar de la del tipo padre, los tipos derivados deben declararse como se muestra en las líneas 10-13. La clase [CoreState] es una de las clases de arquitectura que el desarrollador debe modificar para cada nueva aplicación (líneas 10-13);
2.5.4. La interfaz [IMainActivity]
![]() |
La interfaz [IMainActivity] define lo que los fragmentos pueden solicitar a la actividad en la siguiente arquitectura:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants (to be modified) -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for server response
int TIMEOUT = 1000;
// wait time before executing the client request
int DELAY = 0;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// loading icon
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 0;
// todo: add your constants and other methods here
}
- línea 6: la interfaz [IMainActivity] extiende la interfaz [IDao] de la capa [DAO];
- Línea 9: Esta es la actividad que proporciona acceso a la sesión en forma de una instancia de la interfaz [ISession];
- línea 12: es la actividad utilizada para cambiar de vista. El segundo parámetro es la acción que desencadena este cambio de vista, uno de los valores SUBMIT, NAVIGATION, o RESTORE;
- líneas 15-17: es la actividad que gestiona la pantalla de carga;
- línea 22: para depurar la aplicación;
- línea 25: para evitar esperar demasiado si el servidor deja de responder;
- línea 28: durante la depuración, ajuste esto a unos pocos segundos para dar tiempo a cancelar la operación con el servidor y ver qué pasa;
- línea 31: ajustado a verdadero si el servicio JSON requiere autenticación básica;
- línea 34: adyacencia de fragmentos;
- línea 37: establecer en verdadero si la aplicación tiene pestañas;
- línea 39: ajustado a verdadero si la aplicación se comunica con un servidor web/JSON y desea mostrar una imagen de carga durante los intercambios;
- línea 43: número de fragmentos gestionados por la aplicación;
La interfaz [IMainActivity] es el segundo elemento de la arquitectura que el desarrollador debe implementar (línea 45).
2.5.5. La interfaz [IDao]
La interfaz [IMainActivity] amplía la siguiente interfaz [IDao]:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service URL
void setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
- línea 24: el desarrollador completará la interfaz aquí;
2.5.6. La Sesión
![]() |
La clase [Session] encapsula elementos compartidos por la actividad y los fragmentos. Implementa la siguiente interfaz [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// number of the last view displayed
int getPreviousView();
void setPreviousView(int numView);
// Last state of a view
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// current action
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// States of all views -
// Not used by the code but required for JSON serialization/deserialization
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// number of the last selected tab
int getPreviousTab();
void setPreviousTab(int position);
// navigation on tab selection
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introducimos la interfaz [ISession] para exigir la presencia de determinados métodos en la sesión:
- líneas 7-10: el número de la última vista (fragmento) visualizada;
- líneas 12-15: el estado de una vista concreta;
- líneas 17-24: introducimos el concepto de acción en curso. Hay cuatro (línea 17):
- RESTORE: una operación de guardar/restaurar está en curso. No hay ningún cambio de vista;
- NAVIGATION: la navegación está en curso. Aquí, definimos la navegación como un cambio de vista en el que la nueva vista se puede restaurar desde su último estado guardado durante la sesión;
- SUBMIT: asignamos el tipo [SUBMIT] a una acción pendiente cuando hay un cambio de vista y la nueva vista depende del estado global de la actividad, no sólo de su propio estado. A veces, es difícil distinguir entre NAVIGATION y SUBMIT. En tales casos, utilizaremos el caso más general de SUBMIT;
- NONE: el valor de la acción cuando aún no ha recibido su primer valor;
- líneas 26-30: los estados de la actividad y los fragmentos se almacenarán en un CoreState[] array. Para asegurar que se maneja correctamente durante la serialización/deserialización JSON, debe tener un getter y un setter;
- líneas 32-35: número de la última pestaña seleccionada. Se utiliza durante el ciclo de guardar/restaurar para volver a seleccionar la pestaña que estaba seleccionada antes de girar el dispositivo;
- líneas 37-40: gestiona un booleano que indica si la selección de una pestaña debe ir acompañada de un cambio de fragmento;
La interfaz [ISession] está implementada por la siguiente clase abstracta [AbstractSession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class AbstractSession implements ISession {
// Previous view ID
private int previousView;
// View state
private CoreState[] coreStates = new CoreState[0];
// current action
private Action action = Action.NONE;
// previously selected tab
private int previousTab;
// navigate to selected tab
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// constructor
public AbstractSession() {
// initialize the fragment state array
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// ISession interface ---------------------------------------------------------
@Override
public int getPreviousView() {
return previousView;
}
@Override
public void setPreviousView(int numView) {
this.preViousView = numView;
}
@Override
public CoreState getCoreState(int numView) {
return coreStates[numView];
}
@Override
public void setCoreState(int numView, CoreState coreState) {
coreStates[numView] = coreState;
}
@Override
public Action getAction() {
return action;
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public CoreState[] getCoreStates() {
return coreStates;
}
@Override
public void setCoreStates(CoreState[] coreStates) {
this.coreStates = coreStates;
}
@Override
public int getPreviousTab() {
return previousTab;
}
@Override
public void setPreviousTab(int position) {
this.previousTab = position;
}
@Override
public boolean isNavigationOnTabSelectionNeeded() {
return navigationOnTabSelectionNeeded;
}
@Override
public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
}
}
- línea 9: el ID de la vista que se mostró antes de la que se muestra actualmente. Esta información es útil cuando se puede acceder a una vista desde varias ubicaciones. Este es el caso típico de la navegación basada en pestañas. La vista mostrada puede entonces determinar qué vista se mostró anteriormente;
- línea 12: la matriz de estados para todos los fragmentos mostrados por la actividad;
- línea 18: el ID de la pestaña previamente seleccionada. Desempeña un papel similar al de la vista anterior ID en la línea 9. Esta información es útil cuando se rota el dispositivo y se necesita volver a la pestaña que estaba seleccionada antes de la rotación;
- línea 22: un booleano que indica si la selección de una pestaña debe provocar un cambio en el fragmento mostrado. Tenga en cuenta que el proyecto [client-android-skel] gestiona las pestañas y los fragmentos por separado para que pueda utilizarse en los casos en que el número de pestañas sea inferior al número de fragmentos. Existen dos tipos de selección:
- una selección realizada por el usuario al hacer clic en una pestaña. En este caso, suele ser necesario cambiar el fragmento mostrado;
- una selección por software mediante el método [Tablayout.Tab.select()]. En este caso, no siempre es deseable cambiar el fragmento visualizado. He aquí dos ejemplos:
- cuando se gira el dispositivo, la actividad se vuelve a crear, al igual que las pestañas. Sin embargo, cuando se crea la primera pestaña, ésta se somete automáticamente a una operación [select] de software. Por lo tanto, no es conveniente cambiar el fragmento visualizado, ya que nos encontramos en una fase de recreación de la actividad en la que el fragmento visualizado finalmente no será necesariamente el asociado a la primera pestaña;
- dado que la gestión de pestañas es independiente de la gestión de fragmentos, es posible que desee actualizar las pestañas (eliminar, añadir) sin interferir con sus fragmentos asociados. Sin embargo, algunas de estas operaciones pueden desencadenar de nuevo una operación de software [seleccionar] implícita en una de las pestañas. Esta selección no implica necesariamente la navegación hacia el fragmento asociado;
- línea 21: el campo [navigationOnTabSelectionNeeded] no debe guardarse durante las operaciones de guardado de la actividad y sus fragmentos. La anotación [@JsonIgnore] hace que el campo sea ignorado durante la serialización/deserialización JSON;
- líneas 25-31: El constructor inicializa el array de estados para los fragmentos [FRAGMENTS_COUNT] de la aplicación. Los elementos de este array se inicializan con el campo [hasBeenVisited=false]. Esta información se utiliza para determinar si se trata o no de la primera visita al fragmento;
La clase [Sesión] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and the activity
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
// Don't forget the getters and setters required for JSON serialization/deserialization
}
- línea 5: la clase [Session] extiende la clase [AbstractSession] que acabamos de ver. El desarrollador colocará aquí los elementos a compartir entre los propios fragmentos y entre los fragmentos y la actividad. Observa que la clase [Session] ya no está anotada con la anotación [@EBean]. Se ha convertido en una clase normal;
2.5.7. La clase abstracta [AbstractActivity]
![]() |
2.5.7.1. Esqueleto
La clase [AbstractActivity] es una clase con más de 300 líneas. La examinaremos paso a paso. Su esqueleto es el siguiente:
package client.android.architecture;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// [DAO] layer
private IDao dao;
// the session
protected Session session;
// fragment container
protected MyPager mViewPager;
// the toolbar
private Toolbar toolbar;
// the loading image
private ProgressBar loadingPanel;
// tab bar
protected TabLayout tabLayout;
// the fragment or section manager
private FragmentPagerAdapter mSectionsPagerAdapter;
// class name
protected String className;
// JSON mapper
private ObjectMapper jsonMapper;
// constructor
public AbstractActivity() {
// class name
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructor");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// IMainActivity implementation --------------------------------------------------------------------
...
// lifecycle - saving/restoring the activity ------------------------------------
...
// handling the splash screen ---------------------------------
...
// IDao interface -----------------------------------------------------
...
// the fragment manager --------------------------------
...
// child classes
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
}
La clase [AbstractActivity]:
- implementa la interfaz [IMainActivity] (líneas 21, 55);
- se encarga de guardar y restaurar la actividad y sus fragmentos cuando el dispositivo gira (línea 58);
- maneja la pantalla de carga durante la comunicación con el servidor web / JSON (línea 61);
- implementa la interfaz IDao de la capa [DAO] (línea 64);
- implementa el gestor de fragmentos (línea 67);
- requiere que sus clases hijas tengan seis métodos (líneas 71-81);
2.5.7.2. Implementación de la interfaz [IMainActivity]
La implementación de la interfaz [IMainActivity] (véase la sección 2.5.4) es la siguiente:
// IMainActivity implementation --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigating to view %s on action %s", position, action));
}
// display new fragment
mViewPager.setCurrentItem(position);
// Record the current action during this view change
session.setAction(action);
}
2.5.7.3. Guardar el estado de la actividad y sus fragmentos
El estado de la actividad y de sus fragmentos está totalmente contenido en la sesión. Por lo tanto, necesitamos guardar la sesión. Aquí, reutilizamos lo que se hizo en el proyecto [Ejemplo-22] (ver sección 1.23):
// Activity save/restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// Save session as a JSON string
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. Restauración del estado de la actividad y sus fragmentos
Se trata de restaurar la sesión. Se procede como se muestra en [Ejemplo-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// Anything to restore?
if (savedInstanceState != null) {
// restore session
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// session
session = new Session();
}
...
- líneas 10-26: si el parámetro [Bundle savedInstanceState] de la línea 2 no es null, se restablece la sesión (líneas 12-17);
- líneas 26-29: si el parámetro [Bundle savedInstanceState] de la línea 2 es null, esto corresponde a la primera vez que se lanza la actividad. A continuación, se crea una sesión vacía;
2.5.7.5. Inicialización de la capa [DAO]
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// [DAO] layer
dao = getDao();
if (dao != null) {
// [DAO] layer configuration
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentication(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// child classes
protected abstract IDao getDao();
....
}
- línea 11: se solicita a la actividad hija una referencia a la capa [DAO] (línea 21);
- líneas 14-17: si la capa [DAO] existe, se configura utilizando la información contenida en la interfaz [IMainActivity];
2.5.7.6. Inicialización de la vista asociada a la actividad
La vista asociada a la actividad se presentó en la sección 2.5.1:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- fragment container -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
Esta vista se inicializa con el siguiente código:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// associated view
setContentView(R.layout.activity_main);
// view components ---------------------
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// loading icon?
if (IS_WAITING_ICON_NEEDED) {
// add the loading image
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// Create ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// Add the ProgressBar to the toolbar
toolbar.addView(loadingPanel);
}
...
- línea 11: la vista XML [activity_main] está asociada a la actividad;
- líneas 14-15: la barra de herramientas está integrada y es compatible;
- líneas 17-27: adición opcional de un icono de carga: si el booleano [IS_WAITING_ICON_NEEDED] es verdadero en la interfaz [IMainActivity];
- línea 23: creación de la imagen de carga de tipo [ProgressBar]a la que hace referencia el campo [loadingPanel];
- línea 24: inicialmente, esta imagen está oculta;
- línea 26: se añade a la barra de herramientas;
2.5.7.7. Gestión de pestañas
La interfaz [IMainActivity] puede solicitar una barra de pestañas. Esta se añade y gestiona de la siguiente manera:
// tab bar
protected TabLayout tabLayout;
...
// tab bar?
if (ARE_TABS_NEEDED) {
// add the tab bar
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// no navigation on selection until a fragment is displayed
session.setNavigationOnTabSelectionNeeded(false);
// create tab bar
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// Add the tab bar to the app bar
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// Event handler for the tab bar
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// A tab has been selected
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("onTabSelected #%s, action=%s, tabCount=%s, isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// tab position
int position = tab.getPosition();
// memory
session.setPreviousTab(position);
// display associated fragment?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// child classes
protected abstract void navigateOnTabSelected(int position);
...
- líneas 12-48: añadir y gestionar una barra de pestañas;
- línea 6: la barra de tabulación se añade si la constante [ARE_TABS_NEEDED] se establece en verdadero en la interfaz [IMainActivity];
- línea 12: al crear la barra de pestañas, pueden producirse operaciones implícitas [Tablayout.Tab.select] (no son activadas por el usuario). Establecemos el booleano [session.navigationOnTabSelectionNeeded] a falso para evitar cualquier navegación durante estas falsas selecciones. Dependerá del desarrollador seleccionar el fragmento a mostrar utilizando el método [navigateToView]. El booleano [session.navigationOnTabSelectionNeeded] será devuelto a verdadero cuando se muestra este fragmento (véase AbstractFragment clase);
- línea 14: creación de una barra de pestañas referenciada por el campo [tabLayout]. Utilizamos una barra de pestañas personalizada [CustomTabLayout], de la que hablaremos más adelante;
- línea 15: establecemos los colores de los títulos de las pestañas. Estos se encuentran en el siguiente archivo [res/color/tab_txt.xml]:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="#FFFF00" />
<item android:state_selected="false" android:color="#FFFFFF" />
</selector>
- línea (c): el color del título de la pestaña cuando ésta está seleccionada;
- línea (d): el color del título de la pestaña cuando no está seleccionada;
Este archivo es, por supuesto, editable. Puede encontrar los códigos de color hexadecimales aquí, por ejemplo.
- líneas 17-18: añadir esta barra de pestañas a la barra de aplicaciones en la vista [activity_main] XML;
- líneas 20-47: manejador de eventos para la barra de pestañas;
- líneas 22-36: sólo se gestiona el evento [onTabSelected]. Corresponde a un clic en la [pestaña tab] pasada como parámetro al método o a una operación software [TabLayout.Tab.select];
- línea 30: posición de la pestaña seleccionada;
- línea 32: esta posición se almacena en la sesión;
- línea 34: ahora debe mostrarse el fragmento asociado a esta pestaña. Sólo la clase hija (línea 52) puede hacer esta asociación. Nótese que no asociamos la barra de pestañas al contenedor de fragmentos [mViewPager] como se hacía en algunos de los ejemplos estudiados. Aquí, separamos completamente la gestión de la barra de pestañas de la de los fragmentos. Por eso, cuando se hace clic en una pestaña, debemos especificar qué vista queremos que se muestre;
- línea 28: distinguimos entre selección de pestaña con o sin navegación. Generalmente, cuando el usuario hace clic en una pestaña, se espera que haya navegación, mientras que durante una selección programática, no. El desarrollador distingue entre estos dos casos utilizando el elemento [session.navigationOnTabSelectionNeeded]. Cuando no se realiza la navegación, el número de la última pestaña seleccionada no se guarda en la sesión. Corresponde al promotor hacerlo;
2.5.7.8. El gestor de pestañas [CustomTabLayout]
![]() |
Utilizamos un gestor de pestañas personalizado para mostrar los títulos de las pestañas en diferentes fuentes. La clase [CustomTabLayout] es la siguiente:
package client.android.architecture.custom;
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class CustomTabLayout extends TabLayout {
private Typeface mTypeface;
public CustomTabLayout(Context context) {
super(context);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
}
@Override
public void addTab(Tab tab) {
super.addTab(tab);
ViewGroup mainView = (ViewGroup) getChildAt(0);
ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
int tabChildCount = tabView.getChildCount();
for (int i = 0; i < tabChildCount; i++) {
View tabViewChild = tabView.getChildAt(i);
if (tabViewChild instanceof TextView) {
((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
}
}
}
}
- La fuente para los títulos de las pestañas se personaliza en las líneas 30 y 44;
La carpeta [fonts] es la siguiente:
![]() |
Fuentes:
- El código de la clase [CustomTabLayout] se encuentra en la clase URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- Las fuentes se encontraron en el URL [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Últimas inicializaciones
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// instantiate the fragment manager
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e., fragment #i in the fragment container is fragment #i returned by the fragment manager
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// Disable swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment adjacency
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// display the first view
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// Handle over to the child activity
onCreateActivity();
}
...
// child classes
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- líneas 10-19: Este código se encuentra habitualmente en los ejemplos que hemos estudiado;
- líneas 21-23: Visualización de la primera vista. Sin duda hay varias formas de distinguir este caso. Aquí, hemos utilizado el hecho de que para la primera vista, el valor de la acción que desencadena el cambio de vista es NONE;
- línea 22: no hacemos ninguna suposición sobre el primer fragmento a mostrar. En nuestros ejemplos, a menudo ha sido el fragmento #0, pero no siempre (ver Ejemplo-22). Por lo tanto, pediremos a la actividad hija (línea 30) que nos diga de qué vista se trata;
- línea 25: hemos factorizado todo lo que podíamos aquí. Ahora, la clase hija tiene que realizar sus propias inicializaciones (línea 29);
2.5.7.10. Tratamiento de la imagen de carga
En la clase [AbstractActivity], la imagen del marcador de posición se gestiona mediante los dos métodos siguientes:
// managing the waiting image ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. Implementación de la interfaz [IDao]
En la clase [AbstractActivity], la interfaz [IDao] (véase la sección 2.5.5) se aplica del siguiente modo:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// [DAO] layer
private IDao dao;
...
// IDao interface -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String password) {
dao.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
dao.setBasicAuthentication(isBasicAuthenticationNeeded);
}
@Override
public void setDebugMode(boolean isDebugEnabled) {
dao.setDebugMode(isDebugEnabled);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
- Línea 3: Recordemos que el valor de este campo fue proporcionado por la actividad hija en el método [onCreate];
2.5.7.12. Implementación del gestor de fragmentos
En la clase [AbstractActivity], el gestor de fragmentos se implementa de la siguiente manera:
...
// the fragment manager --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// constructor
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// fragments of the child class
fragments = getFragments();
}
// must return the fragment at position
@Override
public AbstractFragment getItem(int position) {
// return the fragment
return fragments[position];
}
// Returns the number of fragments to manage
@Override
public int getCount() {
return fragments.length;
}
// returns the title of the fragment at position
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// child classes
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- línea 5: el array de fragmentos asociados a la actividad. Todos los fragmentos derivarán de la clase [AbstractFragment];
- líneas 8-12: es el constructor que inicializa el array de fragmentos. Los solicita a la clase hija de la actividad (línea 35);
- líneas 28-31: los títulos de los fragmentos pueden utilizarse en una aplicación en la que haya tantas pestañas como fragmentos. En este caso, se puede dar a la pestaña el título del fragmento. Aquí, estos títulos se solicitan a la clase hija (línea 37);
2.5.7.13. El método [onResume]
El método [onResume] se ejecuta poco antes de que la vista asociada a la actividad se haga visible. Se utiliza aquí para seleccionar una pestaña después de una operación de guardar/restaurar:
@Override
public void onResume() {
// parent
super.onResume();
if (IS_DEBUG_ENABLED) {
log.d(className, "onResume");
}
// if restoring, then restore the last selected tab
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- Línea 10: Selección de la pestaña que estaba seleccionada antes del proceso de guardar/restaurar. Es importante señalar aquí que en el método [onCreate] -que, en el ciclo de vida de la actividad, se ejecuta antes que el método [onResume]- se ha deshabilitado la navegación al seleccionar una pestaña. Por lo tanto, aquí se selecciona una pestaña pero no hay cambio de fragmento;
2.5.7.14. Resumen
La clase abstracta [AbstractActivity] será la clase padre de la única actividad de la aplicación.
La actividad infantil debe aplicar los seis métodos siguientes:
// child classes
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
La actividad hija también tiene acceso a los siguientes miembros protegidos de su clase padre:
// the session
protected ISession session;
// the fragment container
protected MyPager mViewPager;
// tab bar
protected CustomTabLayout tabLayout;
// class name
protected String className;
2.5.8. La actividad [MainActivity]
![]() |
La clase [MainActivity] puede tener un nombre diferente. Su único requisito es implementar la interfaz [IMainActivity]. La clase proporcionada por defecto es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define the fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tab navigation - define the view to display
}
@Override
protected int getFirstView() {
// todo: tab navigation - set the first view to display
return 0;
}
}
- línea 14: para que la anotación AA [@Bean] de la línea 19 sea válida, la actividad debe tener la anotación AA [@EActivity];
- línea 15: la actividad está asociada al menú XML [menu_main]. Actualmente, este menú está vacío. El desarrollador tendrá que rellenarlo si es necesario;
- línea 16: la clase extiende la clase [AbstractActivity];
- Líneas 19-20: una referencia a la capa [DAO]. Esta será instanciada por la librería AA antes de que este campo sea inicializado. Esto significa que el bean AA [Dao] debe existir. Este es siempre el caso con la aplicación esqueleto que proporcionamos. Incluso en una aplicación sin capa [DAO], puedes dejar el paquete [dao] en su lugar. Esto no causa ninguna complicación;
- línea 22: la sesión como una instancia del tipo [Session]. La sesión existe en la clase padre [AbstractActivity] pero como una instancia de la interfaz [ISession] (línea 32);
- líneas 24-63: los seis métodos requeridos por la clase padre [AbstractActivity];
- Líneas 36-39: El método [getDao] devuelve una referencia a la capa [DAO]. Aquí, esta referencia nunca es null. Sin embargo, en la clase padre [AbstractActivity], hemos previsto el caso en que la clase hija devuelva un null para indicar que no hay capa [DAO]. Si desea utilizar esta opción (no muy útil en mi opinión), aquí es donde debe establecer el puntero a nulo;
2.6. La capa [DAO]

![]() |
2.6.1. La interfaz IDao
Se introdujo en la sección 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service URL
void setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before sending a request
void setDelay(int delay);
// todo: declare your interface here
}
El desarrollador añadirá los métodos para su capa [DAO] a partir de la línea 24.
2.6.2. La interfaz [WebClient]
![]() |
La interfaz [WebClient] es la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// todo: declare the URLs to be accessed here
}
El desarrollador añadirá los métodos que se comunican con el URLs expuesto por el servidor JSON a partir de la línea 17.
2.6.3. El interceptor de autenticación [MyAuthInterceptor]
![]() |
La clase [MyAuthInterceptor] es la siguiente:
package client.android.dao.service;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// user
private String user;
// password
private String password;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// HTTP headers of the intercepted HTTP request
HttpHeaders headers = request.getHeaders();
// Basic HTTP authentication header
HttpAuthentication auth = new HttpBasicAuthentication(user, password);
// Add to HTTP headers
headers.setAuthorization(auth);
// continue the HTTP request lifecycle
return execution.execute(request, body);
}
// authentication elements
public void setUser(String user, String password) {
this.user = user;
this.password = password;
}
}
Esta clase genera la siguiente cabecera de autenticación HTTP:
donde [code] es la cadena 'user:mp' codificada en Base64. Esta clase sólo se utiliza si el servidor JSON espera esta forma de autenticación. Existen otras formas.
Nota: El uso de esta clase se ilustra en la sección 3.6.3.1.
2.6.4. La clase [AbstractDao]
![]() |
La clase [AbstractDao] es la siguiente:
package client.android.dao.service;
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// JSON mapper
private ObjectMapper mapper = new ObjectMapper();
// debug mode
protected boolean isDebugEnabled;
// class name
protected String className;
// delay before executing the request
private int delay;
// constructor
public AbstractDao() {
// class name
className = getClass().getName();
Log.d("AbstractDao", String.format("constructor, thread=%s", Thread.currentThread().getName()));
}
// protected methods ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
T getResponse();
}
// Generic request to a web service / JSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// service execution - waiting for a single response
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// wait?
if (delay > 0) {
Thread.sleep(delay);
}
// execute the synchronous request
T response = request.getResponse();
// log
if (isDebugEnabled) {
String log;
if (response is of type String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s on thread [%s]", log, Thread.currentThread().getName()));
}
// send the response to the observer
subscriber.onNext(response);
// signal the end of the observable
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Server communication exception: %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Unexpected JSON error"));
}
}
// throw an exception
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// debug mode
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- líneas 35-81: el método [getResponse] utiliza la librería RxAndroid para devolver un tipo [Observable<T>]. A diferencia de algunos ejemplos vistos anteriormente, no devuelve un tipo [Response<T>] -que es un tipo propietario- sino cualquier tipo T;
- línea 35: el método [getResponse] toma como parámetro una instancia del tipo [IRequest<T>] de las líneas 30-32, cuyo método [IRequest.getResponse()] obtiene el tipo T mediante una operación síncrona HTTP;
- líneas 48-50: artificialmente, esperamos [delay] milisegundos. En producción, estableceremos [delay=0]. Durante la depuración, estableceremos [delay=unos segundos] para dar al usuario la oportunidad de cancelar la operación asíncrona y ver así cómo se comporta el código en ese caso;
- línea 52: se solicita la respuesta esperada con una petición síncrona;
- línea 64: una vez recibida la respuesta, se pasa al observador;
- línea 66: indicamos que no habrá más emisiones. Este es el caso específico de una acción asíncrona que devuelve un solo elemento;
- líneas 67-78: en caso de excepción, ésta se propaga al observador (línea 77);
2.6.5. La clase [Dao]
![]() |
La clase [Dao] es la siguiente:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// Web service client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuration factory
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// todo: implement IDao
}
- líneas 21-22: inyección del bean AA [WebClient], que se encargará de la comunicación con el servidor web / JSON;
- líneas 24-25: inyección del interceptor de autenticación;
- líneas 31-42: método ejecutado tras la inyección de los campos de las líneas 21-25;
- línea 37: el objeto [RestTemplate], que gestiona la comunicación cliente/servidor, se crea a partir de un objeto fábrica. Esto no es estrictamente necesario, pero el fábrica nos permite configurar los tiempos de espera de la comunicación. Por eso no usamos el constructor sin parámetros [RestTemplate()];
- línea 39: añadimos un conversor JSON a los conversores de [RestTemplate]. Éste será el único convertidor. Así, cuando un método del [WebClient] reciba una cadena JSON del servidor, será automáticamente deserializada en el objeto que el método debe devolver;
- línea 41: el objeto [RestTemplate] configurado de esta forma se pasa al cliente web, que gestionará la comunicación cliente/servidor utilizándolo;
- Líneas 44-48: Establecemos la raíz URL del servidor web/JSON. Todos los URLs declarados en la clase [WebClient] son relativos a este URL raíz;
- líneas 50-54: este método permite especificar el propietario de la conexión cuando ésta se controla mediante autenticación básica (véase la sección 2.6.3);
- líneas 56-64: establecer el tiempos de espera para los intercambios cliente/servidor. Esto se hace a través del objeto [RestTemplate] fábrica, que rige los intercambios;
- líneas 66-78: este método especifica que el servidor está protegido por autenticación básica;
- líneas 72-77: si se requiere autenticación básica, el interceptor de autenticación inyectado en la línea 25 se añade a los interceptores del objeto [RestTemplate]. Este interceptor añadirá automáticamente la cabecera de autenticación básica HTTP esperada por el servidor a todas las peticiones del cliente web;
- El desarrollador implementará la interfaz [IDao] a partir de la línea 87;
2.7. Fragmentos
![]() |
2.7.1. La clase [MenuItemState]
La clase [MenuItemState] encapsula el estado de una opción de menú:
package client.android.architecture;
public class MenuItemState {
// menu item ID
private int menuItemId;
// visibility of the option
private boolean isVisible;
// constructors
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
2.7.2. La clase [Utils]
La clase [Utils] contiene métodos de utilidad estáticos:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// list of messages from an exception - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list containing the error messages from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// List of messages for an exception - version 2
static public String getMessageForAlert(Throwable th) {
// build the text to display
StringBuilder text = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
text.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return text.toString();
}
// list of messages for an exception - version 3
static public String getMessageForAlert(List<String> messages) {
// build the text to display
StringBuilder text = new StringBuilder();
int n = messages.size();
for (String message : messages) {
text.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return text.toString();
}
}
2.7.3. La clase padre [AbstractFragment]
La clase [AbstractFragment] contiene los elementos comunes a todos los fragmentos de la aplicación. Al igual que la clase [AbstractActivity], su código es complejo. También lo analizaremos paso a paso.
2.7.3.1. El esqueleto
package client.android.architecture.core;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// private data ------------------------------------------------------------
// subscriptions to observables
private List<Subscription> subscriptions = new ArrayList<>();
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment state
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// Asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
// data accessible to child classes ---------------------------------------
// debug mode
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
protected String className;
// asynchronous tasks
protected int numberOfRunningTasks;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// update Fragment ----------------------------------------------------------------------------------
...
// Menu management ------------------------------------------
...
// Queue management -------------------------------------------------------------
...
// asynchronous operation management --------------------------------------------------------------------
...
// exception handling -------------------------------------------------------------------
....
// fragment lifecycle management --------------------------------------------------------
...
// child classes -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
}
- líneas 28-45: los datos privados de la clase;
- líneas 47-58: datos protegidos accesibles por las clases hijas;
- líneas 61-62: código que actualiza el fragmento a mostrar;
- líneas 64-65: código de utilidad para manejar el menú, si está presente;
- líneas 67-68: código de utilidad para gestionar la espera durante una operación asíncrona;
- líneas 70-71: código para facilitar la comunicación entre el fragmento y la capa [DAO];
- líneas 73-74: código de utilidad para manejar cualquier excepción de forma estándar;
- líneas 76-77: código que gestiona el ciclo de vida del fragmento;
- líneas 80-94: la clase padre impone 8 métodos a sus clases hijas;
2.7.3.2. El constructor
El constructor de la clase es el siguiente:
// class name
protected String className;
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
...
// constructor ----------------------
public AbstractFragment() {
// init
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructor");
}
}
- Línea 9: Aquí se anota el nombre de la clase hija que se está instanciando. Este nombre se utiliza en todos los registros de la clase padre;
- línea 10: observamos que el fragmento se está construyendo. Esta información se utilizará cuando se pida al fragmento hijo que se actualice;
2.7.3.3. Gestión de menús
En nuestra arquitectura, cada fragmento debe tener un menú, aunque esté vacío. Efectivamente, los registros han mostrado que cuando se ejecuta el método [onCreateOptionsMenu] -que se ejecuta cuando el fragmento tiene un menú-, el fragmento ya se ha asociado con su actividad, vista y menú y está a punto de hacerse visible. Este es, por tanto, el momento en el que la interfaz visual y el menú pueden actualizarse. Es dentro de este método [onCreateOptionsMenu] donde ordenamos al fragmento hijo que se actualice.
La gestión de menús incluye métodos de utilidad que permiten al fragmento hijo mostrar u ocultar elementos del menú:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// menu management ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// iterate through all menu items
for (int i = 0; i < menu.size(); i++) {
// item #i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item #i is a submenu, then we start over
if (menuItem.hasSubMenu()) {
// recursion
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// result
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// retrieve the IDs of the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer the menu options to an array
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// option ID
int id = menuOptionsIds.get(i);
// option state
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
if (isDebugEnabled) {
Log.d(className, String.format("Number of menu options=%s", menuOptionsStates.length));
}
}
// menu option states
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// state
MenuItemState state = this.menuOptionsStates[i];
// menu ID
int id = state.getMenuItemId();
// initialize state
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
return menuOptionsStates;
}
// display menu options -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// update all menu options
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- líneas 6-18: este método recupera los identificadores numéricos de todas las opciones del menú;
- línea 6: el método [getMenuOptions] toma dos parámetros:
- [Menú]: el menú del fragmento;
- [Lista<Integer> menuOptionsIds]: la lista de Android IDs para las opciones del menú. Inicialmente, esta lista está vacía. Se rellena mediante un recorrido recursivo (línea 15) del árbol de menús;
- líneas 20-40: basándose en el menú, construye el array de estados (ID, visibilidad) para las opciones del menú. Este array se almacena en la línea 3. La clase [MenuItemState] se describió en la sección 2.7.1;
- líneas 43-55: una variante del método anterior. Hace lo mismo, pero en lugar de recalcular los identificadores de todas las opciones del menú -lo que ya se ha hecho- utiliza los identificadores de la matriz de estados de la línea 3;
- líneas 58-63: el método [setAllMenuOptionsStates] permite ocultar o mostrar todas las opciones del menú del fragmento;
- líneas 65-69: el método [setMenuOptionsStates] permite mostrar u ocultar selectivamente ciertas opciones del menú;
- Los métodos [getMenuOptions, getMenuOptionsStates] se declaran privados porque sólo se usan dentro de [AbstractFragment]. Los métodos [setAllMenuOptionsStates] (línea 58) y [setMenuOptionsStates] (línea 65) se declaran protected para que estén disponibles para las clases hijas;
2.7.3.4. Gestión de la espera de finalización de una tarea asíncrona
// subscriptions to observables
private List<Subscription> subscriptions = new ArrayList<>();
// asynchronous tasks
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// Handling the wait for the completion of an asynchronous operation -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// note the number of tasks that will be executed
this.numberOfRunningTasks = numberOfRunningTasks;
// display the loading image
mainActivity.beginWaiting();
// clear the list of subscriptions
subscriptions.clear();
// No cancellations yet
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// hide the loading image
mainActivity.cancelWaiting();
}
- líneas 9-18: Para iniciar una o más operaciones asíncronas, el fragmento hijo llamará al método padre [beginRunningTasks]. El parámetro de este método es el número de tareas asíncronas que lanzará el fragmento hijo;
- línea 11: almacenamos el parámetro del método;
- línea 13: se hace visible la pantalla de carga;
- línea 15: se borra la lista de suscripciones a operaciones asíncronas. Estas aún no han sido creadas por el fragmento hijo;
- línea 17: se mantiene un booleano para indicar que se han cancelado las tareas asíncronas solicitadas por el fragmento hijo. Inicialmente, el booleano tiene el valor falso;
- líneas 20-25: el fragmento hijo llama al método padre [cancelWaitingTasks] para indicar que quiere cancelar las tareas que ha lanzado;
- línea 22: se oculta la imagen en espera;
2.7.3.5. Gestión de excepciones
// exception handling -------------------------------------------------------------------
// display exception alert
protected void showAlert(Throwable th) {
// display messages from the exception stack of Throwable th
new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Close", null).show();
}
// display list of messages
protected void showAlert(List<String> messages) {
// Display the list of messages
new android.app.AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Close", null).show();
}
- líneas 4-7: el método [showAlert(Throwable)] permite que un fragmento hijo muestre los mensajes de la pila de excepciones del método Throwable pasado como parámetro en una ventana;
- líneas 10-13: el método [showAlert(List<String>)] permite a un fragmento hijo mostrar en una ventana la lista de mensajes pasada como parámetro;
- La clase [Utils] utilizada en las líneas 6 y 12 se describió en la sección 2.7.2;
2.7.3.6. Gestión de operaciones asíncronas
...
// subscriptions to observables
private List<Subscription> subscriptions = new ArrayList<>();
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// executing an asynchronous task with RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// process: the observable to execute/observe
// consumeResult: the method that processes the response
//
// new subscriptions are created only if there has been no cancellation
if (!runningTasksHaveBeenCanceled) {
// Execute on the I/O thread and observe on the UI thread
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// execute the observable
try {
subscriptions.add(process.subscribe(
// consume result
consumeResult,
// consume exception
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// end of task
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// an asynchronous operation threw an exception
// or an exception occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
...
}
- líneas 9-41: ejecutar una tarea asíncrona;
- línea 9: el método [executeInBackground] espera dos parámetros:
- [Observable<T> process]: el proceso asíncrono a ejecutar;
- [Acción1<T> consumeResult]: el método del fragmento hijo a llamar para pasarle los elementos emitidos por el proceso. En nuestros ejemplos anteriores, los procesos siempre han emitido un solo elemento. El tipo T de [Action1<T>] es el tipo T del resultado devuelto por el proceso observado;
- línea 14: la tarea asíncrona sólo se lanza si aún no ha sido cancelada por el usuario o por el programa (debido a una excepción);
- línea 16: el proceso está configurado para ejecutarse en un hilo de E/S y observado en el hilo UI;
- línea 16: la sentencia [process.subscribe] lanza el proceso en el hilo de E/S. Dentro de este hilo, las operaciones se ejecutan de forma síncrona porque estamos utilizando una librería HTTP síncrona;
- línea 19: el método [process.subscribe] tiene tres parámetros:
- línea 21: [consumeResult]: el método del fragmento hijo que consumirá los elementos emitidos por el proceso;
- líneas 22-28: el método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. La gestión se delega en el método [consumeThrowable] de la línea 49;
- líneas 29-36: el método que se ejecuta cuando la tarea emite la notificación de fin de emisión. La gestión se delega en el método [endOfTask] de la línea 43;
- línea 19: la tarea asíncrona que se acaba de lanzar se registra en el campo [subscriptions], que realiza un seguimiento de todas las tareas asíncronas lanzadas. Esto permitirá cancelarlas en caso necesario;
- líneas 37-39: método que se ejecuta cuando se produce una excepción durante el procesamiento de la tarea asíncrona. La gestión se delega en el método [consumeThrowable] de la línea 49;
El método [endOfTask] es el siguiente:
// asynchronous tasks
protected int numberOfRunningTasks;
...
private void endOfTask() {
// one less task to wait for
numberOfRunningTasks--;
// Done?
if (numberOfRunningTasks == 0) {
// end wait
cancelWaitingTasks();
// signal the end of tasks to the child class
notifyEndOfTasks(false);
}
}
...
// child classes -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- línea 6: una tarea asíncrona acaba de finalizar. Se decrementa el contador de tareas activas;
- línea 8: si no hay más tareas activas, el subproceso hijo ha recibido todas sus respuestas;
- línea 10: se cancela la espera;
- línea 12: notificamos al fragmento hijo que todas las tareas que lanzó han finalizado llamando a su método [notifyEndOfTasks]. El parámetro de este método indica cómo terminaron las tareas-normalmente, o debido a la cancelación por parte del usuario o del código porque se produjo una excepción. En la línea 12, indicamos un final normal. Tenga en cuenta que el fragmento hijo no necesita realizar un seguimiento de las tareas que siguen activas. Su clase padre lo hace por él;
El método [consumeThrowable] es el siguiente:
// asynchronous tasks
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// an asynchronous operation threw an exception
// or an exception occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
// th: the exception to be handled
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception received");
}
// cancel tasks that have already been started
cancelRunningTasks();
// display error messages
showAlert(th);
}
// cancel tasks
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Canceling running tasks");
}
// cancel all registered asynchronous tasks
for (Subscription subscription : subscriptions) {
subscription.unsubscribe();
}
// note the cancellation
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// end of wait
cancelWaitingTasks();
// Notify the child fragment of the task cancellation
notifyEndOfTasks(true);
}
...
// child classes -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- Línea 3: El método [consumeThrowable] captura la excepción ocurrida;
- línea 15: se cancelan todas las tareas que siguen activas;
- línea 17: se muestra el texto de excepción;
- líneas 21-37: se cancelan todas las tareas;
- líneas 27-29: se cancelan todas las suscripciones;
- línea 31: se anota que se ha producido una cancelación;
- línea 32: el contador de tareas se pone a cero;
- línea 34: se cancela la espera;
- línea 36: se notifica al fragmento hijo que las tareas han finalizado al cancelarse;
2.7.3.7. Gestión del ciclo de vida de los fragmentos
// Lifecycle --------------------------------------------------------
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// parent
super.onDestroy();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- Líneas 2-20: Los métodos [onDestroyView, onDestroy] se incluyen únicamente con fines de registro. Permiten al desarrollador comprender mejor el ciclo de vida del fragmento;
Guardar el fragmento cuando el dispositivo gira se gestiona mediante los siguientes métodos: [setUserVisibleHint, onSaveInstanceState, saveState]:
// fragment lifecycle
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// save?
if (this.isVisibleToUser && !isVisibleToUser) {
// the fragment will be hidden - save it
if (!saveFragmentDone) {
saveState();
}
}
// memory
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// parent
super.onSaveInstanceState(outState);
// save the fragment only if it is visible
if (isVisibleToUser) {
// maybe the save has already been done
if (!saveFragmentDone) {
saveState();
}
// restore in any case
session.setAction(ISession.Action.RESTORE);
}
}
- líneas 6-19: el fragmento se guarda si pasa del estado visible al estado oculto (línea 11). El método [setUserVisibleHint] proporciona esta información;
- línea 14: el guardado se realiza mediante el método privado de las líneas 21-23;
- líneas 25-41: Cuando el dispositivo gira, se llama al método [onSaveInstanceState]. El fragmento se guarda bajo dos condiciones:
- es visible (línea 34);
- aún no se ha guardado (línea 36). Es posible que los métodos [setUserVisibleHint] y [onSaveInstanceState] no se ejecuten ambos cuando el fragmento es visible, y que por tanto gestionar el booleano [saveFragmentDone] sea innecesario. En caso de duda, he optado por utilizarlo;
- línea 40: después de guardar viene restaurar. Ten en cuenta que la próxima vez que el fragmento necesite actualizarse, lo hará mediante una operación [RESTORE];
Observe los dos momentos en los que se solicita un fragmento guardado:
- cuando pasa del estado visible al estado oculto;
- cuando el aparato gira;
El método privado [saveState] es el siguiente:
...
private void saveState() {
// tasks to cancel?
if (numberOfRunningTasks != 0) {
// cancel the tasks
cancelRunningTasks();
}
// Save the fragment's state
CoreState currentState = saveFragment();
// the fragment has been visited
currentState.setHasBeenVisited(true);
// save menu state
currentState.setMenuOptionsState(getMenuOptionsStates());
// set session
session.setCoreState(getNumView(), currentState);
// Save complete
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// child classes -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- líneas 4-7: La rotación del dispositivo puede producirse mientras se realizan operaciones asíncronas. Aquí se toma la decisión de cancelarlas todas. No es una buena decisión para el usuario, que tendrá que hacer una nueva solicitud, que puede llevar mucho tiempo, simplemente porque ha movido su teléfono o tableta o ha recibido una llamada. Es posible mantener las conexiones de red mediante un ciclo de guardar/restaurar. Sin embargo, las soluciones no son sencillas, y he decidido no tratarlas en este curso para principiantes. El camino a seguir es establecer estas conexiones de red a través de un fragmento que no tiene ningún UI adjunto y no se destruye durante el ciclo de guardar/restaurar. Para ello, basta con utilizar la instrucción [Fragment.setRetainInstance(true)];
- línea 9: pedimos al fragmento hijo que guarde su estado en un tipo derivado de [CoreState] (línea 31);
- línea 11: observamos que el fragmento ha sido visitado. Esta información es útil. Cuando se visita un fragmento por primera vez, su actualización puede diferir de las posteriores porque no tiene ningún estado previo en la sesión;
- línea 13: guardamos el estado del menú, lo que nos permitirá restaurarlo automáticamente;
- línea 15: este estado actual se guarda en la sesión. En la sesión, los estados se agrupan por vista/fragmento, cada uno de los cuales tiene un estado. El número de vista lo proporciona el fragmento hijo (línea 33);
- línea 17: observamos que el fragmento se ha guardado. Esto se debe a que dos métodos pueden llamar al método [saveState], y es innecesario realizar dos guardados;
La vista asociada al fragmento se regenera mediante el siguiente método:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the view must be restored
viewHasToBeInitialized = true;
}
En el ciclo de vida, el método [onActivityCreated] se ejecuta inmediatamente después del método [onCreateView]. La llamada a este último método indica que la vista asociada al fragmento debe ser reconstruida. Simplemente anotamos esto en la línea 10.
2.7.3.8. Actualización del fragmento
La actualización del fragmento es la última operación que se realiza en el fragmento antes de que se haga visible y espere la entrada del usuario. Se realiza mediante el siguiente código:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// Update Fragment ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// log
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memory
this.menu = menu;
// retrieve the menu options if this hasn't already been done
if (fragmentHasToBeInitialized) {
// retrieve the # menu options
getMenuOptionsStates(menu);
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// this is not the first visit
// step 2: should the fragment be initialized?
...
// Step 3: Should the view be initialized?
...
}
// Step 4: A submit, a navigation, a restore?
...
// Step 5: Terminal updates ----------------------
...
}
...
// child classes -----------------------------------------------------
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
- línea 19: se utiliza el método [onCreateOptionsMenu] para actualizar el fragmento. Por esta razón, el fragmento debe tener un menú, aunque esté vacío. Cuando se ejecuta este método, el fragmento se ha asociado a su vista y actividad y también es visible;
- línea 25: se almacena el menú pasado como parámetro (línea 22) al método;
- líneas 27-34: si es necesario inicializar el fragmento:
- línea 29: los estados de las opciones del menú se almacenan en el array [menuOptionsStates] de la línea 3;
- línea 31: la actividad se almacena como una instancia del tipo Android [Activity];
- línea 32: la actividad se almacena como una instancia de la interfaz [IMainActivity];
- línea 33: se almacena la sesión. El type cast es necesario porque el método [mainActivity.getSession()] devuelve un tipo [ISession];
- línea 36: se recupera de la sesión el estado anterior del fragmento. Si se trata de la primera visita al fragmento, sólo es relevante el booleano [previousState.hasBeenVisited];
- líneas 39-44: código ejecutado cuando se trata de la primera visita al fragmento. En este caso, su estado anterior no es relevante;
- líneas 44-50: código ejecutado cuando no es la primera visita al fragmento;
- líneas 46-47: código ejecutado si se ha llamado al constructor del fragmento (fragmentHasToBeInitialized == true);
- líneas 48-49: código ejecutado si la vista asociada al fragmento ha sido reconstruida (viewHasToBeInitialized==true);
- líneas 51-52: código ejecutado en función de la acción actual (SUBMIT, NAVIGATION, RESTORE);
- líneas 54-55: código siempre ejecutado;
Los cinco pasos de la actualización son los siguientes:
paso 1
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// Initialize fragment and view
initFragment(null);
initView(null);
// reset previousState for later
previousState = null;
} else {
// this is not the first visit
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- línea 19: se recupera de la sesión el estado anterior del fragmento;
- líneas 22-31: código ejecutado si el fragmento nunca ha sido visitado;
- línea 27: se pide a la clase hija que inicialice el fragmento. El parámetro del método [initFragment] de la línea 35 es el estado anterior del fragmento. Aquí, null para indicar al fragmento hijo que se trata de la primera visita;
- línea 28: se pide a la clase hija que inicialice la vista asociada al fragmento. El parámetro del método [initView] de la línea 37 es el estado anterior del fragmento. Aquí, null para indicar al fragmento hijo que se trata de la primera visita;
- línea 30: establecemos el estado anterior en null para los pasos siguientes;
Pasos 2 y 3
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
// step 1 - is this the first visit?
if (!previousState.getHasBeenVisited()) {
...
} else {
// This is not the first visit
// Step 2: Does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initializing fragment");
}
// child fragment
initFragment(previousState);
}
// Step 3: Does the view need to be initialized?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "view initialization");
}
// child fragment
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- líneas 24-42: se ejecuta cuando no es la primera visita al fragmento;
- líneas 27-33: si el fragmento se acaba de reconstruir, se reinicializa llamando al método [initFragment] de la clase hija (líneas 32, 46). Se le pasa el estado anterior del fragmento;
- líneas 35-51: si es necesario inicializar o restablecer la vista asociada al fragmento, se pide al fragmento hijo que lo haga (líneas 40, 48). De nuevo, se le pasa el último estado conocido del fragmento;
Paso 4
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited has any meaning)
previousState = session.getCoreState(getNumView());
// Update the child fragment in several steps
...
// step 4: a submit, a navigation, a restore?
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("previous state=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// current action
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// child fragment
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// restore menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// child fragment
updateOnRestore(previousState);
} else {
// this is a first visit - nothing to do
}
break;
case RESTORE:
// restore
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// restore menu (previousState cannot be null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// child fragment
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- líneas 34-66: procesamos la acción actual, que puede ser una de las tres siguientes:
- RESTORE: Estamos restaurando el fragmento después de una rotación del dispositivo;
- NAVIGATION: Volvemos al fragmento, con la intención de encontrarlo en el estado en que lo dejamos la última vez que lo usamos;
- SUBMIT: todos los demás casos;
- línea 34: recuperar la acción actual;
- líneas 36-42: para una acción de tipo SUBMIT, llamamos al método [updateOnSubmit] del fragmento hijo (líneas 41, 68), pasándole el último estado conocido del fragmento;
- líneas 43-55: para una acción de tipo NAVEGACIÓN;
- líneas 47-54: queremos restaurar el fragmento a su último estado conocido. La operación NAVIGATION puede coincidir con una primera visita. Este sería el caso, por ejemplo, en una aplicación con pestañas: si paso de la pestaña 1 a la pestaña 4:
- Debo inicializar el fragmento para la pestaña 4 si es la primera visita;
- restaurar el fragmento de la pestaña 4 a su estado anterior si no es la primera visita;
- líneas 52-54: no hace nada si es la primera visita. El método hijo [initView(CoreState previousState)] se encargará de esta inicialización. La primera visita se identifica por la condición [previousState == null];
- línea 49: si no es la primera visita al fragmento, restaura su menú;
- línea 51: pedimos a la clase hija que se actualice llamando al método de la línea 70. Le pasamos el estado anterior del fragmento para que haga su trabajo. Le pasamos el estado previo del fragmento para que pueda hacer su trabajo;
- líneas 56-66: en el caso de una operación de restauración de fragmentos, hacemos lo mismo que en el caso de la navegación fuera de la primera visita;
Paso 5
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment lifecycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// JSON mapper
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment lifecycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// Step 5: Terminal updates ----------------------
// we have changed views
session.setPreviousView(getNumView());
// no more actions in progress
session.setAction(ISession.Action.NONE);
// When we leave this fragment, it must be saved
saveFragmentDone = false;
// as long as the fragment hasn't been rebuilt, it doesn't need to be initialized
fragmentHasToBeInitialized = false;
// as long as the view is not rebuilt, it does not need to be initialized
viewHasToBeInitialized = false;
// We return to normal tab selection behavior
session.setNavigationOnTabSelectionNeeded(true);
// Notify the fragment that the view is ready
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- líneas 18-30: cuando llegamos a este punto, el fragmento se ha inicializado y está listo para mostrarse. A continuación, restablecemos todos los indicadores utilizados en la gestión del ciclo de vida del fragmento a su estado inicial;
- línea 20: la vista ha cambiado; se hace constar en la sesión;
- línea 22: no hay más acciones en curso;
- línea 24: cuando salgamos del fragmento visualizado actualmente, tendremos que guardarlo al salir;
- línea 26: ya no es necesario reconstruir el fragmento. Esta bandera se pondrá a verdadero cuando se vuelva a ejecutar el constructor del fragmento;
- línea 28: ya no es necesario inicializar la vista asociada al fragmento. Esta bandera se pondrá a verdadero de nuevo cuando se vuelva a ejecutar el método [onActivityCreated];
- línea 30: el fragmento puede mostrarse en una aplicación con pestañas. En este caso, cuando el usuario hace clic en una de las pestañas, debe producirse un cambio de fragmento;
- línea 36: se notifica a la clase hija que el fragmento está listo. Puede utilizar el método [notifyEndOfUpdates] para realizar actualizaciones que habría que hacer en cualquier caso, lanzar una operación asíncrona para obtener nuevos datos, etc.
2.7.4. Un ejemplo de fragmento
![]() |
Hemos incluido un fragmento de ejemplo en el proyecto [client-android-skel] para mostrar al lector la estructura típica de un fragmento en una aplicación basada en este proyecto.
La clase [DummyFragment] es la siguiente:
package client.android.fragments.behavior;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
public class DummyFragment extends AbstractFragment {
// fields inherited from the parent class -------------------------------------------------------
// debug mode
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
//-- protected String className;
// asynchronous tasks
//-- protected int numberOfRunningTasks;
// activity
//-- protected IMainActivity mainActivity;
//-- protected Activity activity;
// session
//-- protected Session session;
// methods inherited from the parent class -------------------------------------------------------
// display menu options
//-- protected void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// managing the wait for the completion of a series of asynchronous tasks
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// executing an asynchronous task with RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// Cancel tasks
//-- protected void cancelRunningTasks() {
// display an alert on exception
//-- protected void showAlert(Throwable th) {
// display list of messages
//-- protected void showAlert(List<String> messages) {
// methods required by the parent class -------------------------------------------------------
@Override
public CoreState saveFragment() {
// the fragment must be saved
DummyFragmentState state = new DummyFragmentState();
// ...
return state;
// if there is nothing to save, use [return new CoreState();] and remove the [DummyFragmentState] class
}
@Override
protected int getNumView() {
// Return the fragment number in the array of fragments managed by the activity (see MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// The fragment becomes visible and has been constructed in this step or a previous step
// This occurs when the application starts and every time the Android device rotates
// is necessarily followed by the execution of [initView]
// the fields of the fragment that has been reconstructed must be initialized
// previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
}
@Override
protected void initView(CoreState previousState) {
// The fragment becomes visible and the associated view has been reconstructed in this step or a previous step
// this occurs every time [initFragment] is executed and every time the fragment leaves the vicinity of the displayed fragment
// The components of the view that has been reconstructed must be initialized
// previousState is the fragment's last saved state—is null if this is the first time the fragment is visited
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after a SUBMIT operation
// You generally need to initialize the fragment and the associated view from the session
// previousState is the last saved state of the fragment—is null if this is the first visit to the fragment
// there is nothing to do if the fragment cannot be reached via a SUBMIT operation
// if the fragment can be reached via SUBMIT operations from different fragments, the previous view can be obtained via [session.getPreviousView]
// if the fragment can be reached via multiple SUBMIT operations from the same fragment, then a flag must be set in the session to distinguish between the different types of SUBMIT operations originating from that fragment
}
@Override
protected void updateOnRestore(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after a RESTORE or NAVIGATION operation
// previousState is the last saved state of the fragment—never null
// the view must be restored to its previous state
}
@Override
protected void notifyEndOfUpdates() {
// occurs after the [updateOnSubmit, updateOnRestore] methods
// At this point, the view has been constructed and initialized
// There is often nothing to do here, but you can also factor in actions that need to be performed regardless of how you arrive at this view
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called when the asynchronous tasks launched by the fragment have either completed or been canceled
// These two cases can be distinguished using the runningTasksHaveBeenCanceled parameter
// generally, the view must be reset to a state different from the one it was in while waiting for responses from the asynchronous tasks
}
}
La clase [DummyFragment] puede no tener un estado. Aquí, hemos incluido uno para recordarnos lo que se espera dentro de ella:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// state of the [DummyFragment] fragment
// include only fields serializable as JSON
// Add the @JsonIgnore annotation to the others, though it's unclear what purpose they might serve
// Don't forget the getters/setters—they are used for serialization/deserialization
}
Para ilustrar el uso del proyecto [client-android-skel], utilizaremos primero ejemplos sencillos antes de pasar a un estudio de caso más exhaustivo.
2.8. Ejercicios ilustrativos
Empezaremos por refactorizar ejemplos ya escritos.
2.8.1. Ejemplo 17B
Volveremos al ejemplo 17 de la sección 1.18. Se trata de una aplicación con un único fragmento, sin tareas asíncronas ni pestañas. La examinaremos para ver cómo se comporta cuando se gira el dispositivo. Introduciremos lo siguiente:

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

If we compare the views, everything has been preserved except for list [2], which is now empty.
Además, si hace clic en el botón [Enviar], aparece un cuadro de diálogo que muestra las entradas realizadas en el formulario. Si gira el dispositivo en ese momento, el cuadro de diálogo desaparece.
Por lo tanto, durante una rotación, necesitaremos regenerarnos:
- la lista desplegable y su elemento seleccionado;
- el cuadro de diálogo si se mostró durante la rotación;
2.8.1.1. El proyecto [Ejemplo-17B
Duplicamos el proyecto [client-android-skel] en examples/Example-17B. A continuación, cargamos el nuevo proyecto [1]:
![]() | ![]() | ![]() |
- en [2-3], en la carpeta [behavior], pegamos el fragmento [Vue1Fragment] del proyecto [Example-17];
![]() | ![]() | ![]() |
- en [4-5], en la carpeta [layout] de [Ejemplo-17B], pegamos la vista [vue1.xml] de [Ejemplo-17]. Esta es la vista asociada al fragmento;
- en [6], la carpeta [valores] de [Ejemplo-17B] se sustituye por la carpeta [valores] de [Ejemplo-17];
Cambiaremos el margen superior de la vista [vue1.xml] a 80 dp:
<TextView
android:id="@+id/textViewFormTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="80dp"
android:text="@string/view1_title"
android:textSize="30sp"/>
En este punto, podemos intentar una compilación inicial para comprobar si hay errores. Los primeros errores notificados proceden del paquete importaciones que se han movido. Los corregimos (Ctrl-Shift-O). Otros errores, como , surgen porque la vista [Vue1Fragment] no implementa todos los métodos requeridos por su clase padre [AbstractParent]:

Generar los métodos que faltan (Alt-Enter).
Otro error de compilación notificado es el siguiente

Arreglamos esto en el archivo [build.gradle] del módulo (línea 20 más abajo):
![]() |
En este punto, podemos recompilar para ver los errores restantes. El único error reportado es en el método [Vue1Fragment.updateFragment]:
![]() |
Debe eliminar la anotación [@Override] de la línea 135. Ahora ya no hay errores. Usaremos esto como punto de partida para modificar el proyecto.
2.8.1.2. El estado del fragmento [Vue1Fragment]
El fragmento [Vue1Fragment] necesita guardar información cuando el dispositivo gira para que pueda ser restaurado completamente. Para ello creamos una clase [Vue1FragmentState]:
![]() |
For now, this class is empty:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. Personalización de proyectos
![]() |
La carpeta [custom] contiene elementos de arquitectura que pueden ser personalizados por el desarrollador.
Las constantes de la interfaz [IMainActivity] serán las siguientes:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for the server response
int TIMEOUT = 1000;
// timeout before executing the client request
int DELAY = 0;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// loading icon
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- líneas 24-31: La aplicación no utiliza su capa [DAO] aquí. Estas constantes no se utilizarán;
- línea 34: una adyacencia de fragmento de 1, que es el valor por defecto. Dado que la aplicación sólo tiene un fragmento (línea 43), este valor es irrelevante;
- líneas 39-40: dado que no hay operaciones con la capa [DAO], no hay necesidad de una imagen de marcador de posición;
- línea 37: esto no es una aplicación con pestañas;
- línea 43: sólo hay un fragmento;
La clase [Sesión] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
}
Está vacía. De hecho, como sólo hay un fragmento, no es necesario prever la comunicación entre fragmentos mediante una sesión.
Por último, la clase [CoreState] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Líneas 11-13: Necesitamos listar todas las clases derivadas de [CoreState] que almacenan el estado de los distintos fragmentos. Aquí sólo hay una (línea 12);
2.8.1.4. El [MainActivity]
La actividad [MainActivity] tiene actualmente este aspecto:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define the fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// To-do: tab navigation - define the view to display when tab #[position] is selected
}
@Override
protected int getFirstView() {
// todo: define the number of the first view (fragment) to display
return 0;
}
}
Los comentarios [//todo] indican lo que debe hacer el desarrollador. La clase [MainActivity] evoluciona como sigue:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
}
Sólo el método de las líneas 41-44 necesita ser modificado. Debe devolver el array de los fragmentos de la aplicación. En la línea 43, no olvides añadir el guión bajo después del nombre del fragmento.
2.8.1.5. El estado del fragmento [FragmentState]
Tras las pruebas de rotación realizadas en el proyecto [Ejemplo-17], decidimos almacenar los siguientes elementos del fragmento:
- la lista de valores de la lista desplegable;
- la posición del elemento seleccionado en esta lista;
- el mensaje mostrado por el cuadro de diálogo si está presente en el momento de la rotación;
La clase [Vue1FragmentState] será la siguiente:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// the values of the dropdown list
private List<String> list;
// the selected item in the dropdown list
private int listSelectedPosition;
// the message displayed in the dialog box
private String message;
// getters and setters
...
}
2.8.1.6. El fragmento [AbstractFragment]
Actualmente, el ciclo de vida del fragmento se gestiona mediante dos métodos (líneas 6 y 32):
// dropdown list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// Check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// the seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// the dropdown list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// initialize the dropdown list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
El código de estos dos métodos se trasladará a los métodos definidos por la clase [AbstractFragment] de la siguiente manera:
// Fragment lifecycle management ---------------------------------------------------------------------
@Override
public CoreState saveFragment() {
Vue1FragmentState state = new Vue1FragmentState();
state.setList(list);
state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
state.setMessage(message);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// First visit?
if (previousState == null) {
// Create the values for the dropdown list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// restore the values from the dropdown list
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// and the dialog message
message = state.getMessage();
}
// initialize the dropdown list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// the calendar
datePicker1.setCalendarViewShown(false);
// the seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// Initialize the drop-down list adapter
dropDownList.setAdapter(dataAdapter);
// First visit?
if (previousState == null) {
// check the first button
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// seekbar value
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// selected item in dropdown list
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// Is the dialog visible?
if (message != null) {
// display it
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- líneas 2-9: el método [saveFragment] debe colocar los elementos del fragmento a guardar en una clase derivada de [CoreState] y devolver una instancia de esa clase;
- líneas 11-14: el método [getNumView] debe devolver el número de fragmento. Aquí sólo hay un fragmento, cuyo número es 0;
- líneas 16-34: El método [initFragment] debe inicializar los campos del fragmento. Recibe el estado previo del fragmento. Si [previousState] es null, entonces esta es la primera visita;
- líneas 19-25: En la primera visita, se crean los valores para la lista desplegable;
- líneas 26-30: si no es la primera visita, los campos [lista, mensaje] del fragmento se restauran desde el estado anterior;
- líneas 33-34: inicialización del campo [dataAdapter] del fragmento. Esta es la fuente de datos para la lista desplegable;
- líneas 37-62: el método [initView] se utiliza para inicializar los componentes de la interfaz visual. Recibe el estado anterior [previousState] como parámetro. Si [previousState == null], entonces es la primera visita;
- Aquí vemos lo que había antes en el método [@AfterViews];
- líneas 57-61: en la primera visita, nos aseguramos de que se selecciona el primer botón de opción;
- líneas 64-67: el método [updateOnSubmit] se ejecuta cuando la acción actual es [SUBMIT]. Aquí no hay navegación entre fragmentos y, por tanto, no hay acción actual;
- líneas 69-81: el método [updateOnRestore] se ejecuta cuando la acción actual es [NAVIGATION] o [RESTORE]. Aquí no hay navegación entre fragmentos y, por tanto, no hay acción [NAVIGATION] posible;
- línea 72: recalculamos (no restauramos) el valor del TextView seekBarValue. Esto se debe a que, durante las rotaciones, a veces se perdía su valor;
- líneas 74-75: La lista se posiciona en el elemento que estaba seleccionado antes de la rotación. Sin esto, la lista se situaría por defecto en su primer elemento;
- líneas 76-80: el cuadro de diálogo se muestra de nuevo si el mensaje del estado anterior no es null. Volveremos al método [showMessage] (línea 79);
- líneas 83-86: el método [notifyEndOfUpdates] es el último método llamado por la clase padre antes de dejar solo al fragmento hijo. Aquí no hay nada que hacer;
- líneas 88-91: el método [notifyEndOfTasks] señala el fin de las tareas asíncronas lanzadas por el fragmento. Aquí no hay ninguna;
El cuadro de diálogo se restablece de la siguiente manera:
// the dialog message
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValidate() {
// list of messages to display
List<String> messages = new ArrayList<>();
...
// display
doDisplay(messages);
}
private void display(final List<String> messages) {
// construct the text to be displayed
StringBuilder text = new StringBuilder();
for (String message : messages) {
text.append(String.format("%s\n", message));
}
// store the message
message = text.toString();
// display it
showMessage();
}
private void showMessage() {
// display it
new AlertDialog.Builder(activity).setTitle("Entered values").setMessage(message).setNeutralButton("Close", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// reset the message
message = null;
}
}).show();
}
Cuando el usuario envía el formulario, el método [doValider] (línea 5) construye una lista de mensajes, que luego muestra (línea 10) en el cuadro de diálogo.
- líneas 14-20: La lista de mensajes se concatena en un único mensaje, que se almacena en la línea 2;
- líneas 25-33: este es el mensaje que muestra el cuadro de diálogo, y es el mismo mensaje que muestra el método [updateOnRestore];
- línea 27: el segundo parámetro del método [setNeutralButton] es el método que se ejecuta cuando el usuario hace clic en el botón [Cerrar] del cuadro de diálogo;
- línea 31: cuando se cierra el cuadro de diálogo, el mensaje se establece en null para indicar que el cuadro de diálogo ya no está presente;
2.8.1.7. Pruebas
Invitamos a los lectores a probar este proyecto y comprobar que el fragmento se conserva tras una o varias rotaciones sucesivas.
2.8.2. Ejemplo-23: Cliente meteorológico
Algunos sitios web ofrecen información meteorológica en forma de cadenas JSON. He aquí un ejemplo:

El URL tiene la forma: http://api.openweathermap.org/data/2.5/weather?q={ciudad},{país}&APPID={APPID} donde:
- ciudad: la ciudad para la que desea el tiempo, aquí Angers;
- país: el país de la ciudad, en este caso Francia (fr);
- APPID: una clave obtenida al registrarse en el sitio [https://home.openweathermap.org/users/sign_up];
2.8.2.1. El proyecto
![]() |
El proyecto se construyó basándose en el proyecto [client-android-skel]. Tiene las siguientes características:
- sólo tiene un fragmento cuyo estado no es necesario mantener;
- realiza peticiones asíncronas;
2.8.2.2. Personalización de proyectos
![]() |
La interfaz [IMainActivity] permite especificar determinadas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for server response
int TIMEOUT = 1000;
// wait time before executing the client request
int DELAY = 5000;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// loading icon
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Líneas 25, 28, 31, 40: características de la capa [DAO]. Línea 31: La autenticación básica no es necesaria;
- línea 34: adyacencia del fragmento. Aquí, esta constante es irrelevante ya que sólo hay un fragmento;
- línea 37: esto no es una aplicación con pestañas;
- línea 43: sólo hay un fragmento;
La clase [CoreState] que almacena el estado de los fragmentos será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add the subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- líneas 10-13: no hay nada que declarar ya que esta aplicación sólo tiene un fragmento cuyo estado no se guarda;
La clase [Sesión] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
}
Está vacío porque no hay comunicación entre fragmentos en esta aplicación.
2.8.2.3. La capa [DAO]
![]() |
En la capa [DAO], deben personalizarse tres clases:
- el IDao interfaz;
- su Dao aplicación;
- el WebClient interfaz para la comunicación con el servidor web / JSON;
La interfaz [WebClient] será la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// weather service
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- líneas 18-19: el URL del servicio meteorológico. Nótese que esto es relativo al URL raíz del cliente (RestClientRootUrl, línea 12). Aquí, este URL raíz será [http://api.openweathermap.org/];
La interfaz [IDao] será la siguiente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service URL
void setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// weather service
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Ten en cuenta que los métodos de las líneas 6-22 están incluidos por defecto en la interfaz IDao del proyecto [client-android-skel];
- Línea 25: El método [getWeatherForecast] recupera la cadena JSON para el tiempo en la ciudad [city] del país [country]. El tercer parámetro es la clave obtenida del sitio web [https://home.openweathermap.org/users/sign_up];
La interfaz [IDao] es implementada por la siguiente clase [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// Web service client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
// timeout
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// memory
this.timeout = timeout;
// factory configuration
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// weather service ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// result
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Ten en cuenta que las líneas 17-90 están incluidas por defecto en la clase [Dao] del proyecto [client-android-skel]. Sólo tienes que añadir los métodos de implementación de la interfaz [IDao], específicos para la aplicación (línea 92);
- líneas 93-105: implementación del método [getWeatherForecast]. Esto es muy sencillo y ocupa 6 líneas, líneas 100-105;
- línea 100: el método [getResponse] es un método de la clase padre [AbstractDao]. Espera un parámetro de tipo [IRequest<T>], donde T es el tipo de la respuesta esperada del servidor; aquí, es un Cadena ya que esperamos una cadena JSON. El tipo T de [IRequest<T>] debe ser el tipo T del método [Observable<T> getWeatherForecast];
- la interfaz [IRequest<T>] sólo tiene un método: getResponse. Su función es proporcionar la respuesta de tipo T que debe devolver el método [Observable<T> getWeatherForecast];
- línea 103: es la interfaz [WebClient] la que proporciona esta respuesta. Le pasamos los tres parámetros recibidos en la línea 94. Por esta razón, éstos deben tener el final atributo;
2.8.2.4. El [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// Parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// IDao interface ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- Ten en cuenta que las líneas 15-55 están incluidas por defecto en el proyecto [client-android-skel]. Sólo tienes que personalizarlas;
- Líneas 37-40: la matriz de fragmentos. Aquí sólo hay uno;
- Líneas 43-46: No se requieren títulos de fragmentos;
- líneas 48-50: aquí no hay pestañas;
- Líneas 52-55: La primera vista a mostrar es la vista #0, la de [MeteoFragment];
- líneas 58-61: implementación de la interfaz [IDao]. Aquí, no hay nada que hacer aparte de delegar el trabajo a la capa [DAO] en la línea 21;
2.8.2.5. El fragmento [MeteoFragment]
![]() |
El [MeteoFragment] consulta el servicio web meteorológico / JSON. Su esqueleto es el siguiente:
package client.android.fragments;
import android.util.Log;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
- Línea 14: La vista [res/layout/meteo_fragment.xml] es la siguiente:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Build your visual interface"
android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="120dp"/>
</RelativeLayout>
La vista sólo muestra el texto de la línea 10;
- línea 15: el menú [res / menu / menu_meteo.xml] es el siguiente:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionWeather"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionCancel"
android:title="@string/actionCancel"/>
<item
android:id="@+id/actionFinish"
android:title="@string/actionFinish"/>
</menu>
</item>
</menu>
- líneas 10-12: esta opción de menú se utiliza para solicitar el tiempo de una ciudad;
- líneas 14-15: esta opción de menú se utiliza para cancelar la solicitud si está en curso;
- líneas 16-18: esta opción de menú cierra la aplicación;
El código completo del fragmento es el siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
// local data
private int countOfReceivedResponses;
// event handling ---------------------------------------------------------------------------------------
// cities for which we want the weather
final String[] paysDeLoire = new String[]{"Angers", "Le Mans", "Nantes", "Laval", "La Roche-sur-Yon"};
@OptionsItem(R.id.actionMeteo)
protected void getWeather() {
// its country
String country = "fr";
// Get an API key by creating an account [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// Web service URL / JSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// Start waiting for [paysDeLoire.length] asynchronous tasks
beginWaiting(paysDeLoire.length);
// Number of responses received
nbResponsesReceived = 0;
// make asynchronous calls in parallel
for (String city : paysDeLoire) {
// weather
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// process the response
consumeResponse(response);
// a response received
nbReceivedResponses++;
}
});
}
}
// process server response
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// start waiting
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// parent
beginRunningTasks(numberOfRunningTasks);
// display the [Cancel] option
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionCancel, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// display results
String message;
switch (numberOfResponsesReceived) {
case 0:
message = "No responses were received";
break;
case 1:
message = "One response was received. Check your logs...";
break;
default:
message = String.format("%s responses were received. Check your logs...", nbReponsesRecues);
break;
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
}
// private methods -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionCancel, false)});
}
// lifecycle management ---------------------------------------------------------------------------------------
...
}
- líneas 25-50: manejo del clic en la opción de menú [Tiempo];
- línea 32: construcción del servicio web URL / JSON para el servicio meteorológico. A continuación, se pasa a la capa [DAO] a través de la actividad;
- línea 34: iniciamos la espera. Pasamos el número de tareas a lanzar para que la clase padre nos avise cuando estén completas. Aquí, hay cinco tareas porque estamos solicitando el tiempo para las cinco ciudades listadas en la línea 23;
- línea 16: contamos el número de respuestas recibidas para poder mostrarlo;
- líneas 38-50: recorremos en bucle las ciudades de las que queremos conocer el tiempo;
- línea 40: haremos 5 peticiones HTTP en paralelo;
- línea 40: pedimos a la clase padre [AbstractParent] que consulte al servicio web / JSON;
- líneas 40-48: el método [executeInBackground] espera dos parámetros:
- línea 40: el proceso a observar y ejecutar es proporcionado por el método [mainActivity.getWeatherForecast];
- líneas 40-48: la instancia [Action1] que se ejecutará cuando se reciba la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getWeatherForecast];
- línea 44: se ha recibido una respuesta. Se pasa al método [consumeResponse] en la línea 53;
- línea 46: se incrementa el contador de respuestas recibidas;
- líneas 53-56: consumir una respuesta JSON del servicio meteorológico;
- línea 55: simplemente registramos la cadena JSON;
- líneas 59-72: código ejecutado antes de lanzar las tareas asíncronas;
- línea 65: pasamos el número de tareas a ejecutar a la clase padre [AbstractParent]. Esto permite que nos notifique cuando hayan terminado todas;
- líneas 67-70: preparación del menú para una espera. Mantenemos únicamente la opción [Acciones/Cancelar], que permitirá al usuario cancelar las tareas lanzadas;
- líneas 74-92: código que se ejecuta cuando la clase padre nos notifica que todas las tareas lanzadas han finalizado;
- línea 77: reseteamos el menú a su estado inicial. El método [initMenu] (líneas 95-102) muestra el menú con todas sus opciones excepto la opción [Acciones/Cancelar], que está oculta;
- líneas 80-91: se muestra el número de respuestas recibidas;
Al hacer clic en la opción [Cancelar] del menú, se utiliza el siguiente código:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnul() {
if (isDebugEnabled) {
Log.d(className, "Cancelation requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
- línea 7: pedimos a la clase padre que cancele las tareas que aún están activas;
Al hacer clic en la opción [Finalizar] del menú, se utiliza el siguiente código:
@OptionsItem(R.id.actionTerminer)
protected void doTerminate() {
// shut everything down
System.exit(0);
}
El ciclo de vida del fragmento se gestiona mediante los siguientes métodos:
// lifecycle management ---------------------------------------------------------------------------------------
@Override
public CoreState saveFragment() {
return new CoreState();
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// First visit?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- líneas 3-6: se utiliza para almacenar el estado del fragmento en una clase derivada de [CoreState]. Si el fragmento no tiene estado que almacenar, como en este caso, simplemente devolvemos una instancia de [CoreState]. No devuelva nulo, ya que esto acabaría provocando un accidente;
- líneas 8-11: debe devolver la vista ID. Aquí, el [MeteoFragment] tiene ID 0;
- líneas 13-16: se utiliza para inicializar el fragmento una vez que ha sido construido (previousState == null) o reconstruido (previousState != null). Aquí no hay nada que hacer. El único campo que se puede inicializar es el siguiente:
// cities for which we want the weather
final String[] paysDeLoire = new String[]{"Angers", "Le Mans", "Nantes", "Laval", "La Roche-sur-Yon"};
pero se inicializa sola;
- líneas 18-24: se utilizan para inicializar la vista asociada al fragmento una vez que ha sido construido (previousState == null) o reconstruido (previousState != null);
- líneas 21-23: si es la primera vez que se visita el fragmento, se inicializa su menú para ocultar la opción [Cancelar];
- líneas 27-30: llamada si la navegación al fragmento implicó una acción [SUBMIT]. Aquí no hay navegación entre fragmentos, ya que solo hay un fragmento;
- líneas 32-35: llamada durante un ciclo de guardar/restaurar debido a la rotación del dispositivo u otra razón. Aquí, como no se ha guardado ningún estado, no hay nada que hacer;
- líneas 37-40: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada que hacer;
2.8.2.6. Pruebas
Ahora ejecutamos el ejemplo:


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

Los registros son los siguientes:
07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception received
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Canceling launched tasks
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Server communication exception: [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
- líneas 3-6, 10: las 5 llamadas HTTP generaron 5 excepciones;
- línea 7: el fragmento [MeteoFragment] recibe la primera excepción. A continuación, cancelará todas las tareas;
Ahora establezcamos un tiempo de espera de 5 segundos [IMainActivity.DELAY] y cancelemos la operación. Los registros son entonces los siguientes:
07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Cancellation requested
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Canceling launched tasks
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Server communication exception: [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Server communication exception: [java.lang.InterruptedException,[null]]
- línea 3: solicitud de anulación;
- línea 4: la espera se anula porque se ha producido una cancelación;
- líneas 6-10: La cancelación de las tareas provoca una excepción en cada uno de los cinco hilos de tareas. El tipo de excepción depende de las aplicaciones. La excepción aquí es [java.lang.InterruptedException] porque las tareas fueron interrumpidas mientras se ejecutaba la instrucción [Thread.sleep(delay)], lo que hace que esperen artificialmente durante [delay] milisegundos;
2.8.3. Ejemplo-16B
Aquí refactorizamos el Ejemplo 16 de la Sección 1.17. Presenta un fragmento que realiza llamadas asíncronas a un servidor de números aleatorios. Veamos cómo se comporta durante una rotación del dispositivo:

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

Vemos que hemos perdido todos los mensajes de error. Intentaremos mejorar esto.
2.8.3.1. El Proyecto Ejemplo-16B
Copiamos el proyecto [client-android-skel] en el proyecto [examples/Example-16B] y, a continuación, cargamos el nuevo proyecto:
![]() |
Del proyecto inicial [Ejemplo-16], copiamos los siguientes elementos en [Ejemplo-16B]:
- el archivo [res/layout/vue1.xml], la carpeta [res/values]:
![]() |
Cambiaremos el margen superior de la vista [vue1.xml] a 80 dp:
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
- el fragmento [View1Fragment]:
![]() |
- la clase [DAO / servicio / Respuesta]:
![]() |
En esta fase, podemos intentar una compilación inicial:
- El primer tipo de error consiste en importaciones. Algunas clases se han movido a paquetes diferentes durante la migración a [Ejemplo-16B]. Comenzamos corrigiendo estos errores;
- Un segundo tipo de error se reporta en la clase [Vue1Fragment] porque no implementa los métodos requeridos por la clase padre [AbstractParent]. Generamos automáticamente estos métodos;
Intentamos una segunda recopilación:
- todos los errores restantes se concentran ahora en la clase [Vue1Fragment], la clase que sufrirá más cambios;
2.8.3.2. Creación de un estado para el fragmento [Vue1Fragment]
Hemos visto que cierta información del fragmento tendrá que ser guardada durante una rotación con el fin de restaurar el fragmento a su estado anterior a la rotación. Por lo tanto, creamos un estado [Vue1FragmentState], que por ahora está vacío:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. Personalización de proyectos
![]() |
La interfaz [IMainActivity] permite especificar determinadas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// Maximum wait time for the server response
int TIMEOUT = 1000;
// wait time before executing the client request
int DELAY = 5000;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// loading icon
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- líneas 25, 28, 31, 40: características de la capa [DAO]. La autenticación básica no es necesaria;
- línea 34: adyacencia del fragmento. Aquí, esta constante es irrelevante ya que sólo hay un fragmento;
- línea 37: esto no es una aplicación con pestañas;
- línea 43: sólo hay un fragmento;
La clase [CoreState] que almacena el estado de los fragmentos será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Línea 12: Declaramos la clase de estado del fragmento [Vue1Fragment];
La clase [Sesión] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
}
Está vacío porque no hay comunicación entre fragmentos en esta aplicación.
2.8.3.4. La capa [DAO]
![]() |
En la capa [DAO], deben personalizarse tres clases:
- el IDao interfaz;
- su Dao aplicación;
- el WebClient interfaz para comunicarse con el servidor web / JSON;
La clase [Response] proviene del proyecto [Example-16], que la utiliza:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// response body
private T body;
// constructors
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
La interfaz [WebClient] será la siguiente:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// 1 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getRandom(@Path("a") int a, @Path("b") int b);
}
- Líneas 18-19: El URL para el servicio de números aleatorios. Observa que este URL es relativo al URL raíz del cliente (RestClientRootUrl, línea 12). Aquí, el URL raíz es [http://localhost:8080];
La interfaz [IDao] será la siguiente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service URL
void setWebServiceJsonUrl(String url);
// User
void setUser(String user, String password);
// Client timeout
void setTimeout(int timeout);
// Basic authentication
void setBasicAuthentication(boolean isBasicAuthenticationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// random number service
Observable<Response<Integer>> getRandom(int a, int b);
}
- Ten en cuenta que los métodos de las líneas 6-22 están presentes por defecto en la interfaz IDao del proyecto [client-android-skel];
- Línea 25: El método [getAlea] devuelve un número aleatorio en el rango [a,b]. Este número se devuelve en una respuesta [Response<Integer>], donde el número aleatorio está contenido en el campo [body] de ese tipo;
La interfaz [IDao] es implementada por la siguiente clase [Dao]:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// Web service client
@RestService
protected WebClient webClient;
// security
@Bean
protected MyAuthInterceptor authInterceptor;
// the RestTemplate
private RestTemplate restTemplate;
// RestTemplate factory
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// create the RestTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// Set the JSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// Set the RestTemplate for the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// Set the web service URL
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String password) {
// Register the user in the interceptor
authInterceptor.setUser(user, password);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// configuration factory
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentication(boolean isBasicAuthenticationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentication thread=%s, isBasicAuthenticationNeeded=%s", Thread.currentThread().getName(), isBasicAuthenticationNeeded));
}
// authentication interceptor?
if (isBasicAuthenticationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// private methods -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// random number service
@Override
public Observable<Response<Integer>> getRandom(final int a, final int b) {
// web client execution
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getRandom(a, b);
}
});
}
}
- Ten en cuenta que las líneas 17-85 están incluidas por defecto en la clase [Dao] del proyecto [client-android-skel]. Sólo tienes que añadir los métodos para implementar la interfaz [IDao];
- líneas 88-97: implementación del método [getAlea]. Esto es muy sencillo y ocupa 6 líneas, líneas 91-96;
- línea 91: el método [getResponse] es un método de la clase padre [AbstractDao]. Espera un parámetro de tipo [IRequest<T>], donde T es el tipo de la respuesta esperada, en este caso un Respuesta<Integer> tipo. El tipo T de [IRequest<T>] (línea 91) debe ser el tipo T del método [Observable<T> getAlea] (línea 89);
- la interfaz [IRequest<T>] sólo tiene un método: getResponse. Su función es proporcionar la respuesta de tipo T que debe devolver el método [Observable<T> getAlea];
- línea 94: es la interfaz [WebClient] la que proporciona esta respuesta. Se le pasan los dos parámetros recibidos en la línea 89. Por esta razón, éstos deben tener el valor final atributo;
2.8.3.5. El [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// Parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// Continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// define fragments here
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tab navigation - set the view to display
}
@Override
protected int getFirstView() {
return 0;
}
// IDao interface ------------------------------------------
@Override
public Observable<Response<Integer>> getRandom(int a, int b) {
return dao.getRandom(a, b);
}
}
- Ten en cuenta que las líneas 15-61 están incluidas por defecto en el proyecto [client-android-skel]. Sólo tienes que personalizarlas;
- líneas 40-44: la matriz de fragmentos. Aquí sólo hay uno;
- líneas 47-51: no se necesitan títulos de fragmentos;
- líneas 53-56: aquí no hay pestañas;
- líneas 58-61: la primera vista a mostrar es la vista #0, la de [Vue1Fragment];
- líneas 64-67: implementación de la interfaz [IDao]. Aquí, no hay nada que hacer aparte de delegar el trabajo a la capa [DAO] en la línea 23;
2.8.3.6. El estado del fragmento [Vue1Fragment]
![]() |
La clase [Vue1FragmentState] será la siguiente:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// fragment state ------------------------
// list of responses
private List<String> answers = new ArrayList<>();
// view status ------------------------
// error message regarding the number of random numbers requested
private boolean txtErrorRandomVisible = false;
// error message regarding the [a,b] generation interval
private boolean txtErrorIntervalVisible = false;
// error message regarding the web service URL
private boolean txtWebServiceURLErrorVisible = false;
// error message regarding the wait time
private boolean textViewErrorDelayVisible = false;
// Visibility of the Run button
private boolean btnExecuteVisible = true;
// getters and setters
...
}
Para determinar qué había que guardar en el fragmento, giramos el dispositivo en varias situaciones y observamos qué se perdía al restaurarlo. Llegamos a la conclusión de que había que guardar la información de las líneas 10-23.
2.8.3.7. El fragmento [View1Fragment]
![]() |
Actualmente, la vista [Vue1Fragment] contiene varios errores debido a que la clase padre [AbstractFragment] de la que deriva ha cambiado. En lugar de describir uno a uno los cambios que hay que realizar, comentaremos directamente la versión final.
El esqueleto del fragmento es el siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_empty)
public class Vue1Fragment extends AbstractFragment {
...
}
- Línea 26: Tenga en cuenta que cada fragmento debe tener un menú, incluso si está vacío. Este es el caso aquí.
2.8.3.7.1. Manejo del clic en el botón [Ejecutar
@Click(R.id.btn_Executer)
protected void doExecute() {
// Check the entered data
if (!isPageValid()) {
return;
}
// clear previous answers
answers.clear();
dataAdapterAnswers.notifyDataSetChanged();
// reset the response counter to 0
nbAnswers = 0;
infoAnswers.setText("List of answers (0)");
// initialize activity
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepare the random task
beginWaiting(1);
// request random numbers
getRandomNumbersInBackground(nbRandomNumbers, a, b);
}
void getRandomNumbersInBackground(int nbRandomNumbers, int a, int b) {
// create the observable process
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getRandom(a, b));
}
// request the random numbers
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// process the response
consumeRandomResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// log
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// a response of +
nbReponses++;
infoReponses.setText(String.format("List of answers (%s)", nbReponses));
// analyze the response
// error?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// Cancel
doCancel();
// return to UI
return;
}
// add the information to the list of responses
responses.add(0, String.valueOf(response.getBody()));
// refresh the responses
dataAdapterResponses.notifyDataSetChanged();
}
// Cancel ----------
@Click(R.id.btn_Cancel)
protected void cancel() {
if (isDebugEnabled) {
Log.d(className, "Cancelation requested");
}
// cancel asynchronous tasks
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// start the timer
beginRunningTasks(nbRunningTasks);
// The [Cancel] button replaces the [Run] button
btnExecute.setVisibility(View.INVISIBLE);
btnCancel.setVisibility(View.VISIBLE);
}
- líneas 4-6: En primer lugar, se comprueba que las entradas son válidas. A continuación pueden aparecer mensajes de error;
- líneas 8-9: se borra la lista de respuestas. Este cambio se refleja en ListView que los muestra;
- líneas 11-12: El número de respuestas recibidas se pone a cero;
- línea 14: Establecemos el URL para el servicio de números aleatorios. Esta información se pasará a la capa [DAO];
- línea 15: se establece el tiempo de espera antes de enviar la petición al servicio de números aleatorios. Esta información se pasará a la capa [DAO];
- línea 17: nos preparamos para lanzar 1 tarea asíncrona (no N; ya veremos por qué);
- líneas 24-27: Combinamos las N tareas asíncronas en una única secuencia de operaciones [fusión];
- líneas 29-36: pedimos a la clase padre [AbstractParent] que consulte el servicio web de números aleatorios / JSON;
- líneas 29-36: el método [executeInBackground] espera dos parámetros:
- línea 29: el proceso a observar y ejecutar es el calculado en las líneas anteriores;
- líneas 29-36: la instancia [Action1] que se ejecutará cuando se reciba la respuesta del servicio asíncrono. El tipo T de [Action1<T>] debe ser el tipo T del resultado del método [getAlea], i.e., un tipo [Response<Integer>];
- línea 34: cuando llega una respuesta (un número aleatorio), se consume en el método de la línea 39;
- líneas 49-50: registramos y señalamos que se ha recibido una nueva respuesta;
- líneas 53-60: el tipo [Response<T>] tiene un campo [status] que es un código de error. Si este código es distinto de cero, el servidor ha encontrado un problema;
- línea 55: se muestra un mensaje de error. El método [showAlert] pertenece a la clase padre;
- línea 57: se llama al método de las líneas 68-75. Cancelará las tareas que aún estén activas (línea 74);
- línea 62: la respuesta se añade a la lista de respuestas, que es la fuente de datos para el ListView;
- línea 64: el ListView se actualiza;
- líneas 77-83: el método [beginWaiting(int nbRunningTasks)] prepara la vista para la espera (líneas 81-82) y notifica a la clase padre que las tareas [nbRunningTasks] se ejecutarán en breve (línea 79);
2.8.3.7.2. Ciclo de vida del fragmento
El ciclo de vida del fragmento se gestiona mediante los siguientes métodos:
// local data
private List<String> responses;
private ArrayAdapter<String> dataAdapterAnswers;
private int numberOfAnswers = 0;
...
// lifecycle management ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// current state of the view
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErrorDelayVisible(textViewErrorDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorRandomVisible(txtErrorRandom.getVisibility() == View.VISIBLE);
state.setTxtMsgWebServiceErrorUrlVisible(txtMsgWebServiceErrorUrl.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalVisible(txtErrorInterval.getVisibility() == View.VISIBLE);
state.setExecuteButtonVisible(executeButton.getVisibility() == View.VISIBLE);
state.setResponses(responses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// First visit?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
answers = state.getAnswers();
} else {
answers = new ArrayList<>();
}
// listView data source
dataAdapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
// number of answers
nbAnswers = answers.size();
}
@Override
protected void initView(CoreState previousState) {
// Link listview / adapter
listAnswers.setAdapter(dataAdapterAnswers);
// First visit?
if (previousState == null) {
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorInterval.setVisibility(View.INVISIBLE);
txtWebServiceErrorMessage.setVisibility(View.INVISIBLE);
textViewErrorDelay.setVisibility(View.INVISIBLE);
// buttons
btnCancel.setVisibility(View.INVISIBLE);
btnExecute.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// previous state of the view
View1FragmentState state = (View1FragmentState) previousState;
// show/hide error messages
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorInterval.setVisibility(state.isTxtErrorIntervalVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErrorDelay.setVisibility(state.isTextViewErrorDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// buttons
btnCancel.setVisibility(state.isBtnExecuteVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecute.setVisibility(state.isBtnExecuteVisible() ? View.VISIBLE : View.INVISIBLE);
// number of answers
infoAnswers.setText(String.format("List of answers (%s)", numberOfAnswers));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// The [Run] button replaces the [Cancel] button
btnCancel.setVisibility(View.INVISIBLE);
btnExecute.setVisibility(View.VISIBLE);
}
- líneas 7-18: asegurar que el fragmento se guarda cuando la clase padre lo solicita;
- línea 11: muestra el mensaje de error relativo al tiempo de espera;
- línea 12: visibilidad del mensaje de error relativo al número de números aleatorios solicitados;
- línea 13: visibilidad del mensaje de error relativo al servicio web URL / JSON;
- línea 14: muestra el mensaje de error relativo al rango [a,b] para la generación de números aleatorios;
- línea 15: visibilidad del botón [Ejecutar];
- línea 16: la lista de respuestas recibidas;
- líneas 20-23: debe devolver la vista ID. El fragmento ID aquí es 0 ya que sólo hay uno;
- líneas 25-38: inicialización de los campos del fragmento, ya sea en una primera visita (previousState == null) o en una visita posterior;
- líneas 29-30: si no es la primera visita, el campo [reponses] se restaura desde el estado anterior del fragmento;
- líneas 31-33: si se trata de la primera visita, el campo [reponses] se inicializa con una lista vacía;
- líneas 34-37: utilizando el campo [reponses], podemos construir la fuente de datos del fragmento ListView (línea 35), así como el número de respuestas (línea 37);
- líneas 40-55: se ejecuta para inicializar la vista asociada al fragmento, ya sea en una primera visita (previousState == null) o en una visita posterior;
- línea 43: el fragmento ListView se vincula a la fuente de datos que se acaba de construir en el método [initFragment];
- líneas 45-54: si se trata de la primera visita, la vista se prepara para su primera visualización;
- líneas 57-60: se ejecuta durante la navegación entre fragmentos asociada a una acción [SUBMIT]. Aquí solo hay un fragmento y, por lo tanto, no hay navegación entre fragmentos;
- líneas 63-76: se ejecuta durante la navegación entre fragmentos asociada a una acción [NAVIGATION] o durante un ciclo de guardar/restaurar debido a la rotación del dispositivo u otro motivo. Aquí sólo puede darse este último caso. Recuerde que aquí, en todos los casos, [previousState] es siempre no nulo;
- línea 65: el estado anterior se convierte al tipo de estado del fragmento;
- líneas 66-75: el contenido del estado anterior se utiliza para restaurar la vista;
- líneas 78-81: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada que hacer;
- líneas 83-89: se ejecuta cuando se completan todas las tareas asíncronas. Aquí, el botón [Cancelar] se oculta y se sustituye por el botón [Ejecutar];
2.8.3.8. Pruebas
Se invita al lector a realizar las siguientes pruebas:
- crear errores y ejecutar el dispositivo: los mensajes de error deben permanecer en pantalla;
- Genere números aleatorios y ejecute el dispositivo: los números aleatorios generados deben permanecer en pantalla;
- set a wait of several seconds and run the device during the wait: the tasks must have been canceled (this can be seen in the logs);
2.8.4. Ejemplo-22B
Aquí revisitamos el Ejemplo 22 para refactorizarlo según el modelo de proyecto [client-android-skel]. Recordemos que el proyecto [Ejemplo-22] gestiona correctamente el ciclo de guardar/restaurar fragmentos durante la rotación y que sirvió de base para el proyecto [client-android-skel].
Duplicamos el proyecto [client-android-skel] en [examples/Example-22B] y cargamos este último proyecto:
![]() |
A continuación, copiamos varios elementos del proyecto [Ejemplo-22] en el proyecto [Ejemplo-22B].
En primer lugar, copiamos los elementos de la carpeta [res]:
- [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, la carpeta [values];
![]() |
Cambiaremos el margen superior de ambas vistas a 120 dp:
[view1.xml]:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/title_view1"
android:id="@+id/textViewTitleView1"
android:layout_marginTop="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
Next, we copy the elements [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
En este punto, podemos intentar una primera compilación. Aparece un primer tipo de error: incorrecto importaciones porque las clases han cambiado de paquete. Corregimos estos importaciones. Un segundo tipo de error se debe a que los fragmentos no implementan todos los métodos de su clase padre [AbstractFragment]. Corregimos esto pulsando (Alt+Enter).
Los errores restantes se deben a diferencias entre las clases antiguas y las nuevas [AbstractFragment]. Por ahora, los ignoramos.
2.8.4.1. Personalización de proyectos
![]() |
La carpeta [custom] contiene elementos de arquitectura que pueden ser personalizados por el desarrollador.
La interfaz [IMainActivity] permite especificar determinadas características del proyecto:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// access to the session
ISession getSession();
// change view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum wait time for the server response
int TIMEOUT = 1000;
// wait time before executing the client request
int DELAY = 0;
// Basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = true;
// loading icon
boolean IS_WAITING_ICON_NEEDED = false;
// number of fragments
int FRAGMENTS_COUNT = 5;
}
- líneas 23, 26, 29, 38: características de la capa [DAO]. Aquí no hay ninguna;
- línea 41: aquí hay cinco fragmentos;
- línea 32: adyacencia del fragmento. Esta constante puede tomar aquí un valor comprendido entre [1,4]. Se anima al lector a variar este valor para ver si la aplicación sigue funcionando;
- línea 35: se trata de una aplicación con pestañas;
La clase [CoreState] que almacena el estado de los fragmentos será la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
// whether the fragment has been visited
protected boolean hasBeenVisited = false;
// state of the fragment's menu (if any)
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Línea 12: Declaramos la clase de estado del fragmento [PlaceHolderFragment]. El fragmento [Vue1Fragment] en sí no tiene estado;
La clase [Sesión] es la siguiente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// Elements that cannot be serialized to JSON must have the @JsonIgnore annotation
// Don't forget the getters and setters required for JSON serialization/deserialization
// number of fragments visited
private int numVisit;
// Fragment ID of type [PlaceholderFragment] displayed in the second tab
private int numFragment = -1;
// getters and setters
...
}
Esta es la sesión del proyecto [Ejemplo-22].
2.8.4.2. El [MainActivity]
![]() |
La actividad [MainActivity] es la siguiente:
package client.android.activity;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// [DAO] layer
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// menu management-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// Implementation of methods from the parent class ---------------------------------------------------
...
}
En este caso, la clase [MainActivity] es mayor que en los ejemplos anteriores por dos motivos:
- hay pestañas que gestionar;
- hay un menú que gestionar;
2.8.4.2.1. Implementación de los métodos de la clase padre
// parent class methods -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// Continue the initializations started by the parent class
// session
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// fragment number
final String ARG_SECTION_NUMBER = "section_number";
// initialize the fragment array
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// Pass arguments to the fragment
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[i] = new Vue1Fragment_();
// result
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// no titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- líneas 2-12: El método [onCreateActivity] es llamado por la clase padre [AbstractActivity] cuando la actividad es creada por primera vez o recreada durante un ciclo de guardar/restaurar. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
- línea 10: se recupera una referencia local a la sesión. La conversión de tipo es necesaria porque la sesión de la clase padre es de tipo [AbstractSession];
- líneas 19-38: el método [getFragments] debe devolver a la clase padre el array de fragmentos gestionados por la aplicación. Aquí hay [FRAGMENTS_COUNT] fragmentos, un número definido en [IMainActivity]. Los primeros [FRAGMENTS_COUNT-1] fragmentos son de tipo [PlaceHolderFragment] y el último es de tipo [Vue1Fragment];
- líneas 41-45: el método [getFragmentTitle] debe devolver los títulos de los fragmentos cuando esta información sea útil. Este no es el caso aquí;
- líneas 47-50: este método es llamado por la clase padre cuando el usuario hace clic en una pestaña. Volveremos sobre ello en la siguiente sección;
- líneas 52-55: devuelve el número de la primera vista a mostrar cuando se inicia la aplicación. Aquí, el fragmento [Vue1Fragment] debe mostrarse en primer lugar. El método [getFirstView] podría sustituirse ventajosamente por una constante en [IMainActivity];
2.8.4.2.2. Gestión de pestañas
Las pestañas se gestionan mediante los siguientes métodos:
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// Continue the initializations started by the parent class
// session
this.session = (Session) super.session;
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("View 1");
tabLayout.addTab(tab);
// 2nd tab?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment #%s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// Fragment ID to display
int fragmentNumber;
switch (position) {
case 0:
// fragment number [View1Fragment]
numFragment = getFirstView();
break;
default:
// fragment number [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// display fragment
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- líneas 1-20: El método [onCreateActivity] es llamado por la clase padre [AbstractActivity] cuando la actividad es creada por primera vez o recreada durante un ciclo de guardar/restaurar. Cuando se llama a este método, la clase padre ya ha restaurado la sesión;
- línea 9: se recupera una referencia local a la sesión. La conversión de tipo es necesaria porque la sesión de la clase padre es de tipo [AbstractSession];
- líneas 11-13: se crea la primera pestaña;
- líneas 15-20: la segunda pestaña se crea si se almacena un fragmento ID en la sesión (línea 15). Este ID se establece inicialmente en -1 cuando la actividad se construye por primera vez;
- líneas 23-39: este método es llamado por la clase padre cuando el usuario hace clic en una pestaña;
- líneas 28-31: si se hace clic en la pestaña 0, debe mostrarse [Vue1Fragment]. Sabemos que esta es la primera vista que se mostró cuando se inició la aplicación;
- líneas 32-35: si se hace clic en la pestaña 1, debe mostrarse el fragmento cuyo número está almacenado en la sesión;
- líneas 37-39: Navegamos hasta el fragmento seleccionado. La acción asociada es [SUBMIT]. ¿Podría haber sido [NAVIGATION]? En este documento, utilizamos [NAVIGATION] sólo cuando la visualización del nuevo fragmento requiere conocer únicamente su estado anterior. Aquí no es el caso, ya que la visualización del fragmento debe cambiar de su estado anterior para mostrar una visita más;
2.8.4.2.3. Gestión de menús
La actividad está asociada al siguiente menú [menu_main.xml]:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="examples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
que muestra lo siguiente:
![]() |
El menú se gestiona mediante los siguientes métodos:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// processing menu options
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// no navigation on software tab selection
session.setNavigationOnTabSelectionNeeded(false);
// recreate both tabs due to font issues with the titles
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("View1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment #%s", (i + 1))), false);
// The fragment number to display is set in the session
session.setNumFragment(i);
// Select tab #2 with navigation
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- líneas 16-31: manejar un clic en un [Fragmenti];
- líneas 37-50: mostrar el fragmento #i (son fragmentos de tipo PlaceHolderFragment) en la pestaña #1 (la segunda pestaña);
- líneas 42-44: decidimos eliminar las pestañas existentes para crear dos nuevas. Esta decisión se tomó para solucionar el siguiente problema: cuando simplemente mostramos el fragmento en la pestaña 1 existente (sin eliminarla), curiosamente su título (fuente, tamaño) diferente al del título de la pestaña 0;
- líneas 43-44: las dos pestañas se crean pero no se seleccionan (último parámetro establecido en falso);
- Línea 40: Las operaciones de las líneas 42-44 pueden desencadenar operaciones [select] en las pestañas, que llamarán al manejador [onTabSelected]. Si no se toma ninguna acción, esto resultará en la navegación a un fragmento. Esto se evita estableciendo el booleano [navigationOnTabSelectionNeeded] a falso en la sesión. Este booleano se restablece automáticamente a verdadero por la clase [AbstractFragment] cuando un fragmento se hace visible;
- línea 46: almacenamos el número del fragmento que se mostrará en la sesión;
- líneas 48-50: Seleccione la pestaña #2 con la navegación (línea 48). Esto activará el procedimiento [onTabSelected], que:
- mostrar el fragmento cuyo número se almacenó en la sesión;
- almacena el número de la pestaña seleccionada en la sesión;
2.8.4.3. El fragmento [Vue1Fragment]
Aquí está la versión final del fragmento:
package client.android.fragments.behavior;
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
// UI elements
@ViewById(R.id.editTextName)
protected EditText editTextName;
// event handler
@Click(R.id.buttonValider)
protected void validate() {
// display the entered name
Toast.makeText(activity, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment lifecycle -----------------------------------------------
private void initFragment() {
// nothing to do
}
// save fragment state
@Override
public CoreState saveFragment() {
// view state - nothing to save
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nothing to do
}
@Override
protected void initView(CoreState previousState) {
// First visit?
if (previousState == null) {
// display the visit number
showVisitNumber();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// display the visit number
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// private methods -------------------------------------
// display number of visits
private void showVisitCount() {
// increment visit count
int visitCount = session.getVisitCount();
numVisit++;
session.setNumVisit(numVisit);
// display the visit count
Toast.makeText(activity, String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
}
}
La clase está casi vacía.
- líneas 35-39: llamada por la clase padre cuando el fragmento necesita guardar su estado. El fragmento [Vue1Fragment] no tiene ningún estado que guardar. Simplemente devolvemos una instancia de la clase base [CoreState] (recordatorio: no debemos volver null);
- líneas 41-44: debe devolver el fragmento ID. Por diseño, el fragmento [Vue1Fragment] tiene el ID [FRAGMENTS_COUNT-1];
- líneas 51-59: llamada por la clase padre cuando el fragmento se construye por primera vez (previousState == null) o en visitas posteriores (previousState != null);
- líneas 54-57: si se trata de la primera visita, incrementa el recuento de visitas y lo muestra (líneas 85-92);
- líneas 61-65: se llama cuando el fragmento está a punto de mostrarse en asociación con una acción [SUBMIT]. El recuento de visitas se incrementa y se muestra. En este caso, no es posible que el recuento de visitas se incremente dos veces durante el ciclo de vida. De hecho, la primera visita al fragmento [Vue1Fragment] se produce al inicio de la aplicación cuando la acción se establece en [NONE] por diseño en la sesión. Esto asegura que el método [updateOnSubmit] no será llamado. Después de eso, nunca será la primera visita de nuevo, y el método [initView] no hará nada;
- líneas 68-71: llamada durante un ciclo de guardar/restaurar. Como el fragmento no tiene estado, no hay nada que restaurar aquí;
- líneas 73-76: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí ya no queda nada por hacer;
- líneas 78-81: llamada cuando todas las tareas asíncronas lanzadas han finalizado. Aquí no hay tareas asíncronas;
2.8.4.4. El estado [PlaceHolderFragmentState]
El estado del fragmento [PlaceHolderFragment] será el siguiente:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class PlaceHolderFragmentState extends CoreState {
// text
private String text;
// constructors
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters and setters
...
}
- Cuando necesitemos guardar el estado del fragmento, guardaremos el texto que estaba mostrando (línea 7);
2.8.4.5. El fragmento [PlaceHolderFragment]
El fragmento [PlaceHolderFragment] será el siguiente:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
// UI components
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// data
private String text;
// fragment ID
private static final String ARG_SECTION_NUMBER = "section_number";
// implementation of parent class methods ----------------------------
@Override
public CoreState saveFragment() {
// save the fragment state
PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
placeHolderFragmentState.setText(textViewInfo.getText().toString());
return placeHolderFragmentState;
}
@Override
protected int getNumView() {
return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// original text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// update the displayed text
// increment visit count
int visitCount = session.getVisitCount();
numVisit++;
session.setNumVisit(numVisit);
// updated text
textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, displayed text=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore the displayed text
PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
textViewInfo.setText(state.getText());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
}
- líneas 30-36: cuando la clase padre pide al fragmento que guarde su estado, se guarda el texto mostrado por el fragmento (línea 34);
- líneas 38-41: devuelve el ID del fragmento. Esto depende de la sección ID pasada como argumento cuando se creó el fragmento;
- líneas 43-47: llamada durante la primera construcción del fragmento (previousState == null) o durante construcciones posteriores (previousState != null);
- línea 46: aquí no se utiliza el estado anterior. El texto inicial [texto] (línea 24) mostrado en la primera visita se recalcula cada vez. Esto es discutible. Podríamos haber optado por incluir también esta información en el estado del fragmento;
- líneas 49-51: llamada durante la primera renderización de la vista asociada al fragmento (previousState == null) o durante renderizaciones posteriores (previousState != null). No hay nada que hacer;
- líneas 53-56: llamada cuando el fragmento está a punto de mostrarse en asociación con una acción [SUBMIT]. Este es siempre el caso excepto durante el ciclo guardar/restaurar, donde la acción es [RESTORE]. Por lo tanto, incrementamos el número de visita y lo mostramos;
- líneas 68-74: llamada durante un ciclo de guardar/restaurar. Restauramos el texto que se guardó en el estado del fragmento;
- líneas 76-79: se ejecuta cuando se han completado todas las actualizaciones anteriores. Aquí no hay nada más que hacer;
- líneas 82-83: llamada cuando todas las tareas asíncronas lanzadas han finalizado. Aquí no hay tareas asíncronas;
2.8.4.6. Pruebas
Invitamos al lector a probar la aplicación girando el dispositivo para comprobar que el fragmento visualizado no pierde su estado. También examinaremos los registros.
2.9. Conclusión
Al final de este capítulo, tenemos un proyecto de ejemplo [client-android-skel] para un cliente Android que se comunica con un servicio web / JSON con las siguientes características:
- La comunicación asíncrona con el servidor web/JSON se gestiona mediante la biblioteca RxJava;
- el ciclo de vida del fragmento (actualizar, guardar, restaurar) es gestionado por su clase madre [AbstractFragment], que llama a métodos específicos de sus clases hijas en momentos precisos. De este modo, el fragmento hijo no necesita preocuparse de las etapas del ciclo de vida, sino únicamente de implementar determinados métodos requeridos por su clase padre;
- el ciclo de vida de la actividad (guardar / restaurar) es gestionado por una clase abstracta [AbstractActivity], que también requiere que la actividad hija implemente ciertos métodos;
- La clase [AbstractActivity] puede manejar una aplicación con o sin pestañas, con o sin imagen de carga, y con o sin autenticación básica contra el servidor web / JSON. La presencia o ausencia de estos elementos viene determinada por la configuración;
A continuación presentaremos un caso práctico más complejo que los ejemplos anteriores. La nueva aplicación se basará en el proyecto de plantilla [client-android-skel].



















































