Skip to content

1. Aprender a programar para Android

El PDF del documento está disponible |AQUÍ|.

Ejemplos del documento disponibles |AQUÍ|.

1.1. Introducción

1.1.1. Contenido

Este documento es una reescritura de varios documentos existentes:

  1. Android para J2EE Desarrolladores;
  1. Introducción a la programación de tablets Android a través de ejemplos;
  2. Controlar un Arduino con una tableta Android;
  3. Introducción a la programación de tabletas Android a través de ejemplos - Versión 2

e introduce las siguientes novedades:

  • El documento 1 presentaba una arquitectura denominada AVAT (Actividad-Vistas-Acciones-Tareas) para facilitar la programación asíncrona en una aplicación Android. En este documento, se utiliza la librería estándar RxJava para gestionar acciones asíncronas;
  • El documento 2 utilizaba el IDE de Eclipse con un plugin de Android. Este documento utiliza Android Studio;
  • El documento 3 se incluye tal cual;
  • El Documento 4 utilizaba la librería [Android Annotations] (AA) con la IntelliJ IDEA Community Edition IDE. Este documento reproduce íntegramente el Documento 4 con las siguientes diferencias:
    • el IDE es ahora Android Studio;
    • el sistema de compilación es Gradle para todos los proyectos cliente o servidor (en el documento 4, a veces se utilizaba Maven)
    • la programación asíncrona se implementa utilizando la biblioteca RxJava (en el Documento 4, se utilizaba la biblioteca AA);
  • este documento explora áreas no tratadas, o tratadas sólo brevemente, en los documentos anteriores:
    • el concepto de adyacencia de fragmentos;
    • guardar/restaurar la actividad y sus fragmentos;
    • el ciclo de vida del fragmento;

Por último, se presenta el esqueleto de un cliente Android que se comunica con un servicio web/JSON, en el que factorizamos un gran número de elementos habituales en este tipo de clientes. Este esqueleto se utiliza en todos los ejemplos a partir del capítulo 2. Esta es la parte verdaderamente innovadora del documento.

Se presentan los siguientes ejemplos:

Example
Naturaleza
1
Importar un proyecto Android existente
2
Un proyecto Android básico
3
Un proyecto básico [Android Annotations]
4
Vistas y eventos
5
Navegación entre vistas
6
Navegación por pestañas
7
Uso de la biblioteca [Android Annotations] con Gradle
8–12
Gestión de fragmentos en una aplicación Android
13
Revisited View Navigation
14
Arquitectura de dos capas
15
Arquitectura cliente/servidor
16
Manejo de la asincronía con RxJava
17, 17B
Componentes de entrada de datos
18
Uso de un patrón de vista
19
El componente ListView
20
Utilizar un menú
21
Uso de una clase padre para fragmentos
22, 22B
Guardar y restaurar el estado de la actividad y los fragmentos
23
Cliente meteorológico
Chap 2
Esqueleto de un cliente Android que se comunica con un servicio web / JSON. Factoriza un gran número de elementos que se encuentran comúnmente en este tipo de cliente Android.
Chap 3
Gestión de citas para una consulta médica
Chapter 4
Ejercicio práctico - Gestión básica de nóminas
Chapter 5
Ejercicio práctico - Pedir placas Arduino

Este documento se utilizó en el último curso de la escuela de ingeniería IstiA de la Universidad de Angers [istia.univ-angers.fr]. Esto explica el tono a veces un tanto inusual del texto. Los dos ejercicios prácticos son tareas de laboratorio para las que sólo se ofrecen las líneas generales de la solución. La solución debe ser desarrollada por el lector.

El código fuente de los ejemplos está disponible |HERE. Para ejecutar estos ejemplos, debe seguir el procedimiento de la sección 6.12.

Este documento es una guía introductoria a la programación en Android. No pretende ser exhaustivo. Está dirigido principalmente a principiantes.

El sitio de referencia para la programación Android está en el URL [http://developer.android.com/guide/components/index.html]. Ahí es donde usted debe ir para obtener una visión general de la programación de Android.

1.1.2. Requisitos previos

Para sacar el máximo provecho de este documento, debe tener una sólida comprensión del lenguaje de programación Java.

1.1.3. Herramientas utilizadas

Los siguientes ejemplos se han probado en el siguiente entorno:

  • Máquina con Windows 10 Pro de 64 bits;
  • JDK 1.8;
  • Android SDK API 23;
  • Android Studio, versión 2.1;
  • Emulador Genymotion, versión 2.6.0;

Para seguir este documento, debe instalarlo:

  • a JDK (véase la sección 6.8);
  • el gestor de emuladores Android Genymotion (véase la sección 6.9);
  • el gestor de dependencias de Maven (véase la sección 6.10);
  • el [Android Studio] IDE (véase la sección 6.11);

1.2. Ejemplo-01: Importación de un ejemplo Android

1.2.1. Creación del proyecto

Vamos a crear nuestro primer proyecto Android utilizando Android Studio. En primer lugar, vamos a crear una carpeta [examples] vacía donde se almacenarán todos nuestros proyectos:

  

y luego crear un proyecto con Android Studio. Primero importaremos uno de los ejemplos incluidos con el IDE [1-5]:

 

Image

La importación del proyecto puede dar lugar a errores debidos a un desajuste entre el entorno utilizado cuando se creó el proyecto y el que se utiliza aquí para ejecutarlo. Esta es una oportunidad para ver cómo resolver este tipo de error. Aquí tenemos el siguiente error:

El proyecto importado se configura mediante el siguiente archivo [build.gradle] [2]:


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
 
apply plugin: 'com.android.application'
 
repositories {
    jcenter()
}
 
dependencies {
    compile "com.android.support:support-v4:23.3.0"
    compile "com.android.support:support-v13:23.3.0"
    compile "com.android.support:cardview-v7:23.3.0"
}
 
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
    'main',     // main sample code; look here for the interesting stuff.
    'common',   // components that are reused by multiple samples
    'template'] // boilerplate code generated by the sample template process
 
android {
    compileSdkVersion 21
    buildToolsVersion "23.0.3"
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    sourceSets {
        main {
            dirs.each { dir ->
                java.srcDirs "src/${dir}/java"
                res.srcDirs "src/${dir}/res"
            }
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
 
    aaptOptions {
        noCompress "pdf"
    }
}
  • El error reportado se debe a las líneas 31, 34-35: no tenemos SDK 21. Sustituimos esta versión por la 23, que sí tenemos.

En el archivo [build.gradle], Android Studio hace sugerencias como se muestra a continuación:

 

Para aceptar las sugerencias, pulse [Alt-Enter] en la sugerencia:

 

También puede encontrar un error relacionado con la versión de Gradle:

 

Este error proviene de un desajuste entre la versión de Gradle requerida por el archivo [build.gradle] del proyecto (versión 2.10 en la línea 6 de abajo):


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

y el que aparece en el archivo [<project>/gradle/wrapper/gradle-wrapper.properties]:


#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-2.8-all.zip

En la línea 6, sustitúyase 2.8 por 2.10.

Para acceder al archivo [<project>/gradle/wrapper/gradle-wrapper.properties], utilice la vista del proyecto:

Una vez corregido, puedes compilar la aplicación [1], iniciar el emulador Genymotion [2] y ejecutar el proyecto [3]:

 

Image

Detengamos la aplicación:

  

Ahora puedes cerrar el proyecto. Vamos a crear uno nuevo.

  

1.2.2. Algunas notas sobre el IDE

1.2.2.1. Vistas

Android Studio (AS) IDE ofrece diferentes vistas para trabajar con un proyecto. Nosotros utilizaremos principalmente dos:

  • la vista [Android] [1]:
  • la vista [Proyecto] [4];
 
  

La mayor parte del tiempo, trabajaremos con la vista [Android]. Cuando clonemos un proyecto en otro, necesitaremos la vista [Proyecto].

1.2.2.2. Gestión de la ejecución

Hay varias formas de ejecutar, detener o volver a ejecutar un proyecto AS. En primer lugar, están los botones de la barra de herramientas:

El botón [Reejecutar] [3] detiene el proyecto [2] y luego lo reinicia [1].

1.2.2.3. Gestión de la caché

Android Studio mantiene una caché de los proyectos que gestiona para que el IDE responda lo mejor posible. Con la versión 2.1 de Android Studio (mayo de 2016), esta caché a menudo no reflejaba los cambios de código que se acababan de realizar. En este caso, debes invalidar la caché:

Con Android 2.1 (mayo de 2016), había que realizar el paso anterior varias veces, y a veces eso no era suficiente para resolver el problema detectado. La solución era desactivar [Instant Run]:

  • en [3-4], todo estaba desactivado;

En todos los casos siguientes, hemos trabajado con esta configuración de caché y no hemos encontrado ningún problema.

1.2.2.4. Gestión de registros

Cuando se ejecuta un proyecto, los registros se muestran en el Monitor de Android:

En la pestaña [Android Monitor] [1], los registros se muestran en la pestaña [logcat] [2]. El botón [3] permite borrar los registros. Este botón es útil cuando desea ver los registros de una acción específica:

  • borrar los registros;
  • en el dispositivo Android, realice la acción para la que desea los registros;
  • los registros que aparecen son los relacionados con la acción realizada;

Existen varios niveles de registro [4]. Por defecto, está seleccionado el modo [Verbose]. Esto significa que se muestran los registros de todos los niveles. Puede utilizar [4] para seleccionar un nivel específico.

Los logs son muy útiles para determinar en qué momentos de la ejecución de un proyecto se ejecutan determinados métodos. Los utilizaremos con frecuencia. Veamos el código de la clase [MainActivity] del proyecto [Ejemplo-01]:

 

package com.example.android.pdfrendererbasic;
 
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
 
public class MainActivity extends Activity {
 
    public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_real);
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction()
                    .add(R.id.container, new PdfRendererBasicFragment(),
                            FRAGMENT_PDF_RENDERER_BASIC)
                    .commit();
        }
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_info:
                new AlertDialog.Builder(this)
                        .setMessage(R.string.intro_message)
                        .setPositiveButton(android.R.string.ok, null)
                        .show();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Arriba, los métodos [onCreate, línea 14] y [onCreateOptionsMenu, línea 26] son métodos de la clase padre [Activity] (línea 9). Son llamados en diferentes puntos del ciclo de vida de la aplicación. A veces se ejecutan varias veces. Incluso leyendo la documentación, puede ser difícil saber si un determinado método del ciclo de vida se ejecutará antes o después de un método que hemos escrito nosotros mismos. Sin embargo, a menudo es importante conocer esta información. Por lo tanto, podemos añadir registros como se muestra a continuación:


public class MainActivity extends Activity {
 
  public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    super.onCreate(savedInstanceState);
    ...
  }
 
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("MainActivity", "onCreateOptionsMenu");
    getMenuInflater().inflate(R.menu.main, menu);
   ...
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("MainActivity", "onOptionsItemSelected");
    switch (item.getItemId()) {
      ...
  }
}
  • Las líneas 7, 14 y 21 utilizan la clase [Log]. Esta clase le permite escribir registros en la consola de Android [logcat]. Los registros se clasifican en varios niveles (info, warning, debug, verbose, error). [Log.d] muestra registros de nivel [debug]. Su primer argumento es la fuente del mensaje de registro. De hecho, varias fuentes pueden enviar mensajes a la consola de registro. Para distinguirlas, utilizamos este primer argumento. El segundo argumento es el mensaje que se escribirá en la consola de registro;

If we run the [Example-01] project again, we get the following logs:


05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
 
                                                                                      [ 05-28 08:37:12.781 23881:23881 D/         ]
                                                                                      HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu

Podemos ver que el método [onCreate], que crea la actividad Android, se ejecuta antes que el método [onCreateOptionsMenu], que crea el menú de la aplicación.

Ahora, si hacemos clic en la opción de menú en el emulador de Android [1]:

  

se añade el siguiente registro a la consola de registro:


05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected

En el futuro, a menudo añadiremos declaraciones de registro al código de Android. La mayoría de las veces, no las comentaremos. Simplemente están ahí para animar al lector a mirar la consola de registro con el fin de comprender gradualmente el ciclo de vida de una aplicación Android.

1.2.2.5. Gestión del emulador [Genymotion]

A veces, el emulador Genymotion se bloquea y no puede reiniciarse. Esto se debe a que los procesos VirtualBox siguen ejecutándose en el Administrador de tareas. Abra el Administrador de tareas [Ctrl-Alt-Supr] y elimine todos los procesos VirtualBox:

Una vez hecho esto, reinicia el emulador Genymotion desde Android Studio.

1.2.2.6. Gestión del binario APK creado

Al compilar el proyecto se genera un archivo binario con extensión .apk:

Existen dos versiones: una llamada [debug] y la otra [debug-unaligned]. Deberías usar la primera; la otra es una versión intermedia. El binario .apk producido en [4] puede transferirse directamente a un emulador o a un dispositivo Android. Para transferirlo a un emulador, basta con arrastrarlo y soltarlo en el emulador con el ratón.

1.3. Ejemplo-02: Un proyecto Android básico

Vamos a crear un nuevo proyecto Android utilizando Android Studio [1-12]:

 

En [13], ejecutamos la aplicación. A continuación, vemos la pantalla mostrada en [14] en el emulador Genymotion.

1.3.1. Configuración de Gradle

El proyecto creado se configura mediante el siguiente archivo [build.gradle]:

 

apply plugin: 'com.android.application'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "examples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
  compile 'com.android.support:appcompat-v7:23.4.0'
}

Este fichero ha sido generado por el IDE utilizando sus parámetros de configuración. Es un archivo mínimo que iremos ampliando gradualmente.

  • líneas 3-12: las características de la aplicación Android;
  • líneas 22-25: sus dependencias. Aquí es donde principalmente haremos cambios basados en los ejemplos estudiados;

1.3.2. El manifiesto de la aplicación

  

El fichero [AndroidManifest.xml] [1] define las características del binario de la aplicación Android. Su contenido es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • línea 3: el paquete del proyecto Android;
  • línea 10: el nombre de la actividad;

Estos dos datos proceden de las entradas realizadas al crear el proyecto:

  • La línea 3 del manifiesto (paquete) procede de la entrada [4] anterior. En este paquete se generan automáticamente varias clases;
  • la línea 10 del manifiesto (nombre de la actividad) procede de la entrada [1] anterior;

Volvamos al manifiesto:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • Línea 10: Actividad principal de la aplicación. Hace referencia a la clase [1] anterior;
  • línea 6: el icono de la aplicación [2]. Se puede cambiar;
  • línea 7: la etiqueta de la aplicación. Se encuentra en el archivo [strings.xml] [3]:

<resources>
  <string name="app_name">Example-02</string>
</resources>

El fichero [strings.xml] contiene las cadenas utilizadas por la aplicación. Línea 2: el nombre de la aplicación proviene de la entrada realizada al construir el proyecto [4]:

 
  • línea 10: una etiqueta de actividad. Una aplicación Android puede tener múltiples actividades;
  • línea 12: la actividad se designa como actividad principal;
  • línea 13: y debe aparecer en la lista de aplicaciones que se pueden lanzar en el dispositivo Android.

1.3.3. Actividad principal

 

Una aplicación Android se basa en una o varias actividades. Aquí se ha generado una actividad [1]: [MainActivity]. Una actividad puede mostrar una o más vistas dependiendo de su tipo. La clase [MainActivity] generada es la siguiente:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}
  • línea 6: la clase [MyActivity] extiende la clase [AppCompatActivity] de Android. Este será el caso para todas las actividades futuras;
  • línea 9: El método [onCreate] se ejecuta cuando se crea la actividad. Esto ocurre antes de que se muestre la vista asociada a la actividad;
  • línea 10: se llama al método [onCreate] de la clase padre. Esto debe hacerse siempre;
  • línea 11: el fichero [activity_main.xml] [2] es la vista asociada a la actividad. La definición XML de esta vista es la siguiente:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  <Activity>
 
  <TextView
    android:text="Hello World!"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</RelativeLayout>
  • líneas b-k: el gestor de diseño. La elección por defecto es el tipo [RelativeLayout]. En este tipo de contenedor, los componentes se posicionan unos respecto a otros (a la derecha de, a la izquierda de, debajo de, encima de);
  • líneas m-p: un componente [TextView] utilizado para mostrar texto;
  • línea n: el texto mostrado. No se recomienda codificar el texto directamente en las vistas. Es preferible mover este texto al fichero [res/values/strings.xml] [3]:

Por lo tanto, el texto mostrado será [Hola a todos]. ¿Dónde se mostrará? El contenedor [RelativeLayout] llenará la pantalla. El [TextView], que es su único elemento, se mostrará en la parte superior e izquierda de este contenedor y, por tanto, en la parte superior e izquierda de la pantalla;

¿Qué significa [R.layout.activity_main] en la línea 11? A cada recurso Android (vistas, fragmentos, componentes, etc.) se le asigna un identificador. Así, una vista [V.xml] ubicada en la carpeta [res/layout] será identificada como [R.layout.V]. R es una clase generada en la carpeta [app/build/generated] [1-3]:

 

La clase [R] es la siguiente:


...............
    public static final class string {
        public static final int abc_action_bar_home_description=0x7f060000;
        public static final int abc_action_bar_home_description_format=0x7f060001;
        public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
        ...
        public static final int app_name = 0x7f060014;
    }
 
    public static final class layout {
        public static final int abc_action_bar_title_item=0x7f040000;
        public static final int abc_action_bar_up_container = 0x7f040001;
...
        public static final int activity_main = 0x7f040019;
...
    }
 
    public static final class mipmap {
        public static final int ic_launcher = 0x7f030000;
}
  • línea 14: el atributo [R.layout.activity_main] es el identificador de la vista [res/layout/activity_main.xml];
  • Línea 7: El atributo [R.string.app_name] corresponde a la cadena ID [app_name] del fichero [res/values/string.xml]:
  • Línea 19: El atributo [R.mipmap.ic_launcher] es el identificador de la imagen [res/mipmap/ic_launcher];

Así pues, recuerde que cuando hace referencia a [R.layout.activity_main] en el código, está haciendo referencia a un atributo de la clase [R]. El IDE le ayuda a identificar los diferentes elementos de esta clase:

1.3.4. Ejecutar la aplicación

Para ejecutar una aplicación Android, necesitamos crear una configuración de ejecución:

  • En [1], seleccione [Editar configuraciones];
  • El proyecto se creó con una configuración [app], que borraremos [2] para volver a crearla;
  • en [3], cree una nueva configuración de ejecución;
  
  • en [4], seleccione [Aplicación Android];

Image

  • en [5], seleccione el módulo [app] de la lista desplegable;
  • En [6-8], mantenga los valores por defecto;
  • En [7], la actividad por defecto es la definida en el archivo [AndroidManifest.xml] (línea 1 abajo):

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
</activity>
  • En [8], selecciona [Show Chooser Dialog] para elegir el dispositivo en el que se ejecutará la app (emulador, tablet);
  • En [9], especifica que esta elección debe guardarse;
  • Confirme la configuración;
  
  • En [11], inicie el gestor de emuladores [Genymotion] (consulte la sección 6.9);
  • En [12], selecciona un emulador de tableta e inícialo [13];
  • en [14], ejecute la configuración de ejecución [app];
  • en [15], se muestra el formulario de selección del dispositivo de ejecución. Aquí sólo hay una opción disponible: el emulador [Genymotion] lanzado anteriormente;

Al cabo de un momento, el emulador de software muestra la siguiente vista:

Image

1.3.5. El ciclo de vida de una actividad

Volvamos al código de la actividad [MainActivity]:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

El método [onCreate] en las líneas 8-12 es uno de los métodos que pueden ser llamados durante el ciclo de vida de una actividad. La documentación de Android enumera estos métodos:

 
  • [1]: El método [onCreate] es llamado cuando se inicia la actividad. Es en este método donde se asocia la actividad a una vista y se recuperan las referencias a sus componentes;
  • [2-3]: A continuación se llaman a los métodos [onStart] y [onResume]. Tenga en cuenta que el método [onResume] es el último método que se ejecuta antes de que la actividad que se está ejecutando alcance el estado [4];

1.4. Ejemplo-03: Reescribir el proyecto [Example-02] utilizando la librería [Android Annotations]

A continuación introduciremos el [Anotaciones Android], que facilita la escritura de aplicaciones Android. Para ello, duplique el ejemplo [Example-02] en [Example-03] siguiendo los pasos [1-16].

  • En [1], seleccione la vista [Proyecto] para ver el proyecto Android completo;

Nota: Entre [14] y [15], cambiamos de la vista [Android] a la vista [Proyecto] (véase la sección 1.2.2.1).

A continuación, modificamos el archivo [res/values/strings.xml] [17]:

 

El archivo [strings.xml] se modifica como sigue:


<resources>
  <string name="app_name">Example-03</string>
</resources>

Ahora, ejecutamos la nueva aplicación, que ha conservado toda la configuración de [Ejemplo-02]:

 

En [19], obtenemos el mismo resultado que con [Ejemplo-02] pero con un nuevo nombre.

Ahora introduciremos la biblioteca [Android Annotations], que llamaremos AA para abreviar. Esta biblioteca introduce nuevas clases para anotar el código fuente de Android. Estas anotaciones serán utilizadas por un procesador que creará nuevas clases Java en el módulo; estas clases participarán en la compilación del módulo al igual que las clases escritas por el desarrollador. Así pues, tenemos la siguiente cadena de compilación:

En primer lugar, añadiremos las dependencias del compilador de anotaciones AA (el procesador mencionado anteriormente) al archivo [build.gradle]:


def AAVersion = '4.0.0'
 
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
}
  • Las líneas 4-5 añaden las dos dependencias que componen la biblioteca AA;

El archivo [build.gradle] se modifica de nuevo para utilizar un plugin llamado [android-apt], que divide el proceso de compilación en dos pasos:

  • procesamiento de anotaciones de Android, que genera nuevas clases;
  • compilación de todas las clases del proyecto;

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'
  • línea 8: versión del plugin [android-apt] que se buscará en el repositorio central de Maven (línea 3);
  • línea 13: activación de este plugin;

En este punto, compruebe que la configuración de ejecución [app] sigue funcionando.

Ahora introduciremos la primera anotación de la biblioteca AA en la clase [MainActivity]:

  

La clase [MainActivity] tiene actualmente este aspecto:


package examples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

Ya hemos explicado este código en la sección 1.3.3. Lo modificamos como sigue:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}
  • Línea 7: La anotación [@EActivity] es una anotación AA (línea 3). Su parámetro es la vista asociada a la actividad;

Esta anotación generará un [MainActivity_derivada de la clase [MainActivity], y esta clase será la actividad real. Por lo tanto, debemos modificar el manifiesto del proyecto [AndroidManifest.xml] de la siguiente manera:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity_">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
 
</manifest>
  • línea 11: la nueva actividad;

Una vez hecho esto, podemos compilar el proyecto [1]:

 
  • En [2], vemos la clase [MainActivity_] generada en la carpeta [app/build/generated/source/apt/debug];

La clase [MainActivity_] generada es la siguiente:


//
// DO NOT EDIT THIS FILE.
// Generated using AndroidAnnotations 4.0.0.
// 
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
 
 
package exemples.android;
 
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
 
public final class MainActivity_
    extends MainActivity
    implements HasViews
{
    private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
        init_(savedInstanceState);
        super.onCreate(savedInstanceState);
        OnViewChangedNotifier.replaceNotifier(previousNotifier);
        setContentView(R.layout.activity_main);
    }
...
  • líneas 24-25: la clase [MainActivity_] extiende la clase [MainActivity];

No intentaremos explicar el código de las clases generadas por AA. Ellas manejan la complejidad que las anotaciones pretenden ocultar. Pero a veces puede ser útil examinarlo cuando se quiere entender cómo se "traducen" las anotaciones que se utilizan

Ahora podemos volver a ejecutar la configuración [app]. Obtenemos el mismo resultado que antes. Ahora usaremos este proyecto como punto de partida y lo duplicaremos para introducir los conceptos clave de la programación en Android.

1.5. Ejemplo-04: Vistas y Eventos

1.5.1. Creación del proyecto

Seguiremos el procedimiento descrito para duplicar [Ejemplo-02] en [Ejemplo-03] en la sección 1.4:

Nosotros:

  • duplique el proyecto [Ejemplo-03] en [Ejemplo-04] (después de borrar la carpeta [app/build] de [Ejemplo-03]);
  • cargue el proyecto [Example-04];
  • cambiar el nombre del proyecto en el archivo [app / res / valores / strings.xml] (perspectiva Android);
  • Elimine el archivo [Example-04 / Example-04.iml] (vista Proyecto);
  • compilar y luego ejecutar el proyecto;
 

1.5.2. Construir una vista

Ahora utilizaremos el editor gráfico para modificar la vista que muestra el proyecto [Example-04]:

  • En [1-4], cree una nueva vista XML;
  • En [5], nombra la vista;
  • En [6], especifica la etiqueta raíz de la vista. Aquí, elegimos un contenedor [RelativeLayout]. Dentro de este contenedor, los componentes se posicionan unos respecto a otros: "a la derecha de", "a la izquierda de", "debajo de", "encima de";
  

El archivo [vue1.xml] generado [7] es el 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">
 
</RelativeLayout>
  • Línea 2: un contenedor [RelativeLayout] vacío que ocupará toda la anchura de la tableta (línea 3) y toda su altura (línea 4);
  • En [1], seleccione la pestaña [Diseño] en la vista [vue1.xml] mostrada;
  • en [2-4], cambia al modo tableta;
  • en [5], ajuste la escala a 1 para la tableta;
  • En [6], selecciona el modo "horizontal" para la tableta;
  • La captura de pantalla [7] resume las opciones elegidas.
  • En [1], seleccione un [Texto grande] y arrástrelo a la vista [2];
  • En [3], haga doble clic en el componente;
  • En [4], edite el texto mostrado. En lugar de codificarlo en la vista XML, lo externalizaremos en el archivo [res/values/string.xml]
  • En [5], añada un nuevo valor al archivo [strings.xml];
  • en [8], asignar un identificador a la cadena;
  • en [9], asigna el valor de la cadena;
  • en [10], la nueva vista tras validar el paso anterior;
  • tras hacer doble clic en el componente, cambiamos su ID [11];
  • en [12], en las propiedades del componente, cambie el tamaño de la fuente [50pt];
  • en [13], el nuevo punto de vista;

El archivo [vue1.xml] ha cambiado de la siguiente manera:


<?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="@string/view1_title"
    android:id="@+id/textViewTitleView1"
    android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
    android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
  • Los cambios realizados en el GUI se encuentran en las líneas 10, 11 y 14. Los otros atributos del [TextView] son valores por defecto o resultan del posicionamiento del componente dentro de la vista;
  • líneas 7-8: el tamaño del componente coincide con el del texto que contiene (wrap_content) tanto en altura como en anchura;
  • línea 13: la parte superior del componente se alinea con la parte superior de la vista (línea 13), 50 píxeles por debajo (línea 13);
  • línea 12: el lado izquierdo del componente se alinea con el lado izquierdo de la vista (línea 13), 213 píxeles a la derecha (línea 12);

Generalmente, los tamaños exactos de los márgenes izquierdo, derecho, superior e inferior se establecerán directamente en el XML.

Siguiendo el mismo procedimiento, cree la siguiente vista [1]:

 

Los componentes son los siguientes:

No.
Id
Type
Role
1
textViewTitleView1
TextView
View title
2
textView1
TextView
a question
3
editTextName
EditText
enter a name
4
buttonValidate
Button
to confirm the entry
5
buttonView2
Button
to switch to view #2

Posicionar los componentes entre sí puede ser frustrante, ya que el comportamiento del editor gráfico es a veces impredecible. Puede ser mejor utilizar las propiedades de los componentes:

El [textView1debe colocarse 50 píxeles por debajo del título y a 50 píxeles del borde izquierdo del contenedor:

  • en [1], el borde superior del componente se alinea con el borde inferior del [textViewTitreVue1a una distancia de 50 píxeles [3] (arriba);
  • en [2], el borde izquierdo (izquierda) del componente se alinea con el borde izquierdo del contenedor a una distancia de 50 píxeles [3] (izquierda);

El [editTextNom] debe situarse 60 píxeles a la derecha del componente [textView1] y alineado en la parte inferior con ese mismo componente;

 
  • en [1], el borde izquierdo del componente se alinea con el borde derecho del [textView1] a una distancia de 60 píxeles [2] (izquierda). Está alineado con el borde inferior (bottom:bottom) del componente [textView1] [1];

El [buttonValider] debe situarse 60 píxeles a la derecha del componente [editTextNom] y alineado en la parte inferior con ese mismo componente;

 
  • En [1], el borde izquierdo del componente está alineado con el borde derecho del [editTextNoma una distancia de 60 píxeles [2] (izquierda). Está alineado con el borde inferior del componente [editTextNomcomponente ] (bottom:bottom) [1];

El [buttonVue2] debe situarse 50 píxeles por debajo del componente [textView1y alineado a la izquierda de dicho componente;

 
  • en [1], el borde izquierdo del componente se alinea con el borde izquierdo del [textView1] y se sitúa debajo de él (arriba:abajo) a una distancia de 50 píxeles [2] (arriba);

El archivo XML generado es el 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="@string/view1_title"
    android:id="@+id/textViewTitleView1"
    android:layout_marginTop="49dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"/>
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/txt_name"
    android:id="@+id/textView1"
    android:layout_below="@+id/textViewTitleView1"
    android:layout_alignParentLeft="true"
    android:layout_marginLeft="50dp"
    android:layout_marginTop="50dp"
    android:textSize="30sp"/>

  <EditText
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/editTextName"
    android:minWidth="200dp"
    android:layout_toRightOf="@+id/textView1"
    android:layout_marginLeft="60dp"
    android:layout_alignBottom="@+id/textView1"
    android:inputType="textCapCharacters"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_validate"
    android:id="@+id/buttonValider"
    android:layout_alignBottom="@+id/editTextNom"
    android:layout_toRightOf="@+id/editTextNom"
    android:textSize="30sp"
    android:layout_marginLeft="60dp"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_vue2"
    android:id="@+id/buttonVue2"
    android:layout_below="@+id/textView1"
    android:layout_alignLeft="@+id/textView1"
    android:layout_marginTop="50dp"
    android:textSize="30sp"/>
 
</RelativeLayout>

Contiene todos los elementos gráficos. Otra forma de crear una vista es editar directamente este archivo. Una vez que te acostumbras, esto puede ser más rápido que usar el editor gráfico.

  • En la línea 38, hay información que no hemos mostrado. Se proporciona a través de las propiedades de la [editTextNom] [1]:
 

Todo el texto procede del siguiente archivo [strings.xml] [2]:


<resources>
  <string name="app_name">Example-04</string>
  <string name="titre_vue1">View #1</string>
  <string name="txt_nom">What is your name?</string>
  <string name="btn_submit">Submit</string>
  <string name="btn_view2">View #2</string>
</resources>

Ahora, vamos a modificar el [MainActivity] para que esta vista se muestre cuando se inicie la aplicación:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
 
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}
  • Línea 7: La vista [vue1.xml] es ahora mostrada por la actividad;

Modifique el archivo [AndroidManifest.xml] como sigue:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • Línea 12: Esta línea de configuración impide que aparezca el teclado en cuanto se muestra la vista [vue1]. Esto se debe a que la vista tiene un campo de entrada que tiene el foco cuando se muestra la vista. Por defecto, este foco hace que aparezca el teclado virtual;

Ejecute la aplicación y compruebe que efectivamente se muestra la vista [view1.xml]:

Image

1.5.3. Gestión de eventos

Ahora vamos a manejar el clic en el botón [Validar] en la vista [Vista1]:

Image

El código de [MainActivity] cambia como sigue:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
 
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
 
  // UI elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextName;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews(){
    Log.d("MainActivity", "afterViews");
  }
 
  // event handler
  @Click(R.id.buttonValider)
  protected void doValidate() {
    // display the entered name
    Toast.makeText(this, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
}
  • Líneas 17-18: Asociamos el campo [protegido EditText editTextNom] con el componente identificado por [R.id.editTextNom] en la interfaz visual. El campo asociado al componente debe ser accesible en la clase derivada [MainActivity_] y por esta razón no puede tener un ámbito [privado]. El campo identificado por [R.id.editTextNom] proviene de la vista [vue1.xml]:

  <EditText
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/editTextName"
    android:minWidth="200dp"
    android:layout_toRightOf="@+id/textView1"
    android:layout_marginLeft="60dp"
    android:layout_alignBottom="@+id/textView1"
    android:inputType="textCapCharacters"/>

Nota: No utilice caracteres acentuados en los identificadores [id]. AA no los maneja correctamente.

  • Línea 32: La anotación [@Click(R.id.buttonValider)] especifica el método que maneja el evento 'Click' en el botón con ID [R.id.buttonValider]. Este ID también procede de la vista [vue1.xml]:

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_valider"
    android:id="@+id/buttonValider"
    android:layout_alignBottom="@+id/editTextNom"
    android:layout_toRightOf="@+id/editTextNom"
    android:textSize="30sp"
    android:layout_marginLeft="60dp"/>
  • Línea 35: muestra el nombre introducido:
    • Toast.makeText(...).show(): muestra texto en la pantalla,
    • el primer parámetro de makeText es la actividad,
    • el segundo parámetro es el texto a mostrar en el cuadro de diálogo que se mostrará por makeText,
    • el tercer parámetro es la duración del cuadro visualizado: Toast.LENGTH_LONG o Toast.LENGTH_SHORT;
  • línea 26, la anotación [@AfterViews] marca el método a ejecutar una vez que todos los campos anotados con [@ViewById] han sido inicializados. Es importante saber cuándo se inicializan estos campos. Por ejemplo, ¿podemos utilizar la referencia de la línea 18 en el método [onCreate]? Para responder a esta pregunta, hemos añadido registros;

Ejecute el proyecto [Example-04] y compruebe que ocurre algo al hacer clic en el botón [Validate]. Obtenemos los siguientes logs:

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

Concluimos que cuando el método [onCreate] se ejecuta, los campos anotados con [@ViewById] aún no se han inicializado. Una vez más, se anima a los lectores principiantes a incluir este tipo de registro en los métodos que gestionan el ciclo de vida de la aplicación.

1.6. Ejemplo-05: Navegación entre vistas

En el proyecto anterior no se utilizó el botón [Ver 2]. Proponemos hacer uso de él creando una segunda vista y demostrando cómo navegar entre vistas. Hay varias formas de resolver este problema. El método propuesto aquí consiste en asociar cada vista a una actividad. Otro método es tener un único [AppCompatActivity] que muestre las vistas [Fragment]. Este será el método utilizado en futuras aplicaciones.

1.6.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-04] en [Ejemplo-05]. Para ello, seguiremos el procedimiento descrito para duplicar [Ejemplo-02] en [Ejemplo-03] en la Sección 1.4, que se reprodujo en la sección 1.5.

1.6.2. Añadir una segunda actividad

Para gestionar una segunda vista, crearemos una segunda actividad. Esta actividad gestionará la vista nº 2. Estamos siguiendo un una vista por actividad aquí. Otros modelos son posibles.

Image

  • En [1-4], creamos una nueva actividad;

Image

  • en [5], el nombre de la clase que se generará;
  • en [6], el nombre de la vista (view2.xml) asociada a la nueva actividad;
  
  • en [7-8], los ficheros afectados por la configuración anterior;

La actividad [SecondActivity] es la siguiente:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class SecondActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.vue2);
  }
}
  • Línea 11: La actividad está asociada a la vista [vue2.xml];

La vista [vue2.xml] es la siguiente:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="examples.android.SecondActivity">
 
</RelativeLayout>

Actualmente es una vista vacía con un gestor de diseño [RelativeLayout] (línea 2). En la línea 11, podemos ver que se ha asociado a la nueva actividad.

El manifiesto del módulo Android [AndroidManifest.xml] ha cambiado como sigue:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity android:name=".SecondActivity">
    </activity>
  </application>
 
</manifest>

Línea 20: se ha registrado una segunda actividad.

1.6.3. Navegar de la vista 1 a la vista 2

Volvamos al código de la clase [MainActivity], que muestra la Vista 1. Actualmente no se gestiona la transición a la Vista 2:

  

Lo gestionamos de la siguiente manera:


  // navigate to View 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // navigate to View 2 by passing it the name entered in View 1
    // create an Intent
    Intent intent = new Intent();
    // associate this Intent with an activity
    intent.setClass(this, SecondActivity.class);
    // We associate information with this Intent
    intent.putExtra("NAME", editTextName.getText().toString().trim());
    // launch the [SecondActivity] activity by passing it the Intent
    startActivity(intent);
}
  • líneas 2-3: el [navigateToView2maneja el "clic" en el botón identificado por [R.id.buttonVue2definido en la vista [vue1.xml]:

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_vue2"
    android:id="@+id/buttonVue2"
    android:layout_below="@+id/textView1"
    android:layout_alignLeft="@+id/textView1"
    android:layout_marginTop="50dp"
android:textSize="30sp"/>

Los comentarios describen los pasos a seguir para el cambio de vista:

  1. línea 6: crear un objeto de tipo [Intent]. Este objeto especificará tanto la actividad a lanzar como la información a pasarle;
  2. línea 8: asociar el Intent con una actividad, en este caso una actividad de tipo [SecondActivity] que será la responsable de mostrar la vista #2. Recuerda que la [MainActivity] muestra la vista #1. Así que tenemos una vista = una actividad. Necesitaremos definir el tipo [SecondActivity];
  3. Línea 10: Opcionalmente, añada información al objeto [Intent]. Esta información está destinada al [SecondActivity] que se lanzará. Los parámetros para [Intent.putExtra] son (Clave de objeto, Valor de objeto). Tenga en cuenta que el método [EditText.getText()], que devuelve el texto introducido en el campo de texto, no devuelve un tipo [String] sino un tipo [Editable]. Debe utilizar el método [toString] para obtener el texto introducido;
  4. Línea 12: Lanzar la actividad definida por el objeto [Intent].

Ejecute el proyecto [Example-05] y verifique que ve la Vista #2 (vacía por ahora):

1.6.4. Vista del edificio nº 2

 
  • En [1-2], eliminamos la vista [main.xml], que ya no necesitamos, y modificamos la vista [vue2.xml] como sigue:
 

Los componentes son los siguientes:

No.
Id
Type
Role
1
textViewTitleView2
TextView
View title
2
textViewHello
TextView
some text
5
btn_view1
Button
to go to view #1

El archivo XML [vue2.xml] es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="examples.android.SecondActivity">
 
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/title_view2"
    android:id="@+id/textViewTitleView2"
    android:layout_marginTop="50dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"/>
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/textViewHello"
    android:layout_centerVertical="true"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/textViewTitleView2"
    android:layout_marginTop="50dp"
    android:layout_marginLeft="50dp"
    android:textSize="30sp"
    android:text="Hello!"
    android:textColor="#ffffb91b"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_view1"
    android:id="@+id/buttonVue1"
    android:layout_marginTop="50dp"
    android:textSize="30sp"
    android:layout_alignLeft="@+id/textViewHello"
    android:layout_below="@+id/textViewHello"/>
 
</RelativeLayout>

Ejecute el proyecto [Example-05] y compruebe que ve la nueva vista al hacer clic en el botón [View #2].

1.6.5. La actividad [SecondActivity]

En [MainActivity], escribimos el siguiente código:


    // navigate to View 2
    protected void navigateToView2() {
        // navigate to View 2 by passing it the name entered in View 1
        // create an Intent
        Intent intent = new Intent();
        // associate this Intent with an activity
        intent.setClass(this, SecondActivity.class);
        // attach information to this Intent
        intent.putExtra("NAME", edtName.getText().toString().trim());
        // launch the [SecondActivity] activity by passing it the Intent
        startActivity(intent);
}

En la línea 9, pasamos información a [SecondActivity] que no se utilizó. Ahora la estamos usando, y esto ocurre en el código de [SecondActivity]:

  

El código de [SecondActivity] cambia como sigue:


package exemples.android;
 
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
 
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
 
  // UI components
  @ViewById
  protected TextView textViewHello;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews() {
    // Retrieve the intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // retrieve the name
        String name = extras.getString("NAME");
        if (name != null) {
          // display it
          textViewHello.setText(String.format("Hello %s!", name));
        }
      }
    }
  }
 
}
  • línea 11: utilizamos la anotación [@EActivity] para indicar que la clase [SecondActivity] es una actividad asociada a la vista [vue2.xml];
  • líneas 15-16: recuperamos una referencia al componente [TextView] identificado por [R.id.textViewBonjour]. Aquí, no escribimos [@ViewById(R.id.textViewBonjour)]. En este caso, AA asume que el identificador del componente es idéntico al campo anotado, aquí el campo [textViewBonjour];
  • línea 23: la anotación [@AfterViews] marca un método que debe ejecutarse después de que se hayan inicializado los campos anotados con [@ViewById]. En el método [OnCreate] (línea 19), estos campos no se pueden utilizar porque aún no se han inicializado. En el proyecto [Ejemplo-05], pasamos de una actividad a otra, y al principio no estaba claro si el método anotado con [@AfterViews] se ejecutaría una vez durante la instanciación inicial de la actividad o cada vez que se iniciara la actividad. Las pruebas demostraron que la segunda hipótesis era correcta;
  • línea 26: la clase [AppCompatActivity] tiene un método [getIntent] que devuelve el objeto [Intent] asociado a la actividad;
  • línea 28: el método [Intent.getExtras] devuelve un objeto [Bundle], que es una especie de diccionario que contiene información asociada al objeto [Intent] de la actividad;
  • línea 31: recuperamos el nombre almacenado en el objeto [Intent] de la actividad;
  • línea 34: lo mostramos.

Recordatorio: campos anotados con la anotación [@ViewById] no debe contener caracteres acentuados.

Volvamos a la clase [SecondActivity]. Porque escribimos:


@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {

AA generará una clase [SecondActivity_] derivada de [SecondActivity], y esta clase será la actividad real. Esto nos lleva a hacer cambios en:

[MainActivity]


  // navigate to view #2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // associate this Intent with an activity
    intent.setClass(this, SecondActivity.class);
    ...
}
  • En la línea 6, debemos sustituir [SecondActivity] por [SecondActivity_];

[AndroidManifest.xml]


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity android:name=".SecondActivity_">
    </activity>
  </application>
 
</manifest>
  • En la línea 20, sustituya [SecondActivity] por [SecondActivity_];

Pruebe esta nueva versión. Escriba un nombre en la vista nº 1 y compruebe que la vista nº 2 lo muestra correctamente.

1.6.6. Navegar de la vista 2 a la vista 1

Para navegar desde la Vista #2 a la Vista #1, seguiremos el procedimiento visto anteriormente:

  • Coloque el código de navegación en la actividad [SecondActivity] que muestra la Vista 2;
  • escribe el método [@AfterViews] en el [MainActivity] que muestra la Vista 1;

El código de [SecondActivity] cambia como sigue:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // create an Intent for the [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity.class);
    // Retrieve the Intent for the current activity [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // Set the name in the Intent of [MainActivity]
        intent1.putExtra("NAME", extras2.getString("NAME"));
      }
      // launch [MainActivity]
      startActivity(intent1);
    }
}
  • líneas 1-2: asociar el método [navigateToView1] a un clic sobre el botón [btn_vue1];
  • línea 4: creamos una nueva [Intent];
  • línea 5: asociada a la actividad [MainActivity_];
  • línea 7: recuperar la intención asociada a [SecondActivity];
  • línea 9: recuperar la información de este Intent;
  • línea 12: la clave [NAME] se recupera de [intent2] y se coloca en [intent1] con el mismo valor asociado;
  • línea 15: se lanza la actividad [MainActivity_].

En el código de [MainActivity], añadimos el siguiente método [@AfterViews]:


  @AfterViews
  protected void afterViews() {
    // retrieve the intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // retrieve the name
        String name = extras.getString("NAME");
        if (name != null) {
          // display it
          editTextName.setText(name);
        }
      }
    }
}

Realice estos cambios y pruebe su aplicación. Ahora, cuando vuelva de la Vista 2 a la Vista 1, debería aparecer el nombre que introdujo originalmente, lo que no ocurría hasta ahora.

1.6.7. Ciclo de vida de la actividad

En la sección 1.3.5, hemos introducido el ciclo de vida de una actividad. Aquí tenemos dos actividades, y cambiamos entre ellas durante la ejecución. Estas actividades contienen dos métodos -[onCreate] y [afterViews]- y no está claro cuándo se llama a uno en relación con el otro. Es importante saberlo. Para averiguarlo, añadiremos registros a ambas actividades:

Así que en la clase [MainActivity], escribimos:


  // constructor
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    ...
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
    ...
    }
}
  • líneas 2-4: queremos saber si la clase [MainActivity] se instancia una o varias veces;
  • línea 8: queremos saber si el método [onCreate] es llamado una o varias veces;
  • línea 14: queremos saber si el método [afterViews] es llamado una o varias veces;

Hacemos exactamente lo mismo en la clase [SecondActivity].

Cuando se inicia la aplicación, vemos los siguientes registros:

1
2
3
05-28 09:38:09.429 26711-26711/examples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/examples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/examples.android D/MainActivity: afterViews

Los métodos [onCreate, afterViews] de la primera actividad se ejecutaron en este orden. Al hacer clic en el botón [Ver #2], los nuevos registros son los siguientes:

1
2
3
05-28 09:39:26.607 26711-26711/examples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/examples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/examples.android D/SecondActivity: afterViews

Los métodos [onCreate, afterViews] de la segunda actividad se ejecutaron en este orden. Al hacer clic en el botón [Ver #1], los nuevos registros son los siguientes:

1
2
3
05-28 09:39:56.393 26711-26711/examples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/examples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/examples.android D/MainActivity: afterViews

Por lo tanto, la clase [MainActivity] se instanciará de nuevo. Al hacer clic en el botón [Ver #2], los nuevos registros son los siguientes:

1
2
3
05-28 09:40:59.099 26711-26711/examples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/examples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/examples.android D/SecondActivity: afterViews

Por lo tanto, la clase [SecondActivity] se instanciará de nuevo.

Por tanto, ambas actividades se recrean sistemáticamente cada vez que se modifica la actividad.

A continuación exploraremos una arquitectura con una única actividad capaz de gestionar múltiples vistas denominada fragmentos. La actividad y las vistas se instanciarán una sola vez, a diferencia del método anterior en el que una actividad podía instanciarse varias veces.

1.7. Ejemplo-06: Navegación por pestañas

Aquí exploraremos las interfaces con pestañas. El ejemplo es complejo, pero introduce todos los elementos que utilizaremos más adelante: actividad única, gestor de fragmentos (vistas), contenedor de fragmentos, navegación entre fragmentos. El concepto de pestañas difiere del de fragmentos y es secundario para lo que queremos demostrar en este ejemplo.

1.7.1. Creación del proyecto

Creamos un nuevo proyecto:

 
  • en [7], seleccione una actividad con pestañas (Tabbed Activity);
  • en [10-14], mantenga los valores por defecto;
  • en [15], seleccione pestañas con barra de título;

El proyecto resultante es el siguiente:

 
  • en [1], la actividad;
  • en [2], las opiniones;

Se ha creado automáticamente una configuración de ejecución [app], con el nombre del módulo [2b]:

 

Puede ejecutarlo. Aparecerá una ventana con tres pestañas [3-6]:

Image

1.7.2. Configuración de Gradle

El proyecto [Ejemplo-06] se generó con el siguiente archivo [build.gradle]:

 

apply plugin: 'com.android.application'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "examples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
}

Hay un elemento nuevo respecto a lo que hemos visto antes: la línea 25. Esta librería es necesaria para los nuevos componentes que utiliza la aplicación generada.

1.7.3. La vista [activity_main]

  

La vista [activity_main] es la vista asociada al [MainActivity] del proyecto. En modo [diseño], la vista tiene este aspecto:

Image

Contiene los siguientes componentes:

  
  • [main_content] es la vista completa;
  • [appbar] (recuadro rojo, 1) es la barra de aplicaciones. Contiene dos componentes:
    • [Barra de herramientas] (recuadro amarillo 4) es la barra de herramientas;
    • [pestañas] (cuadro naranja 5) es la barra de título de las pestañas;
  • el [contenedor] (caja verde, 2) puede contener varios fragmentos. Un fragmento es una vista. Así, una misma actividad puede mostrar varias vistas (fragmentos) en este contenedor;
  • [fab] (componente 3) se denomina componente flotante;

En modo [texto], el código es el 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"
                                                 <layout id="exemples.android.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.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
 
  </android.support.design.widget.AppBarLayout>
 
  <android.support.v4.view.ViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
 
  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email"/>
 
</android.support.design.widget.CoordinatorLayout>

Vemos los elementos descritos anteriormente:

  • líneas 2-49: la definición del componente [main_content] (línea 5), que constituye toda la vista. Podemos ver que se trata de un [CoordinatorLayout] diseño (línea 2);
  • líneas 11-33: el contenedor [appbar] (línea 12). Se trata de un [AppBarLayout] (línea 11);
  • líneas 18-24: el componente [toolbar] (línea 19) de tipo [Toolbar] (línea 18);
  • líneas 28-31: el contenedor [tabs] (línea 29). Se trata de un diseño de tipo [TabLayout] (línea 28). Mostrará los títulos de las pestañas;
  • líneas 35-39: el componente [contenedor] (línea 36). Este contenedor muestra las diferentes vistas de la actividad;
  • líneas 41-47: el componente [fab] (línea 42) de tipo [FloatingActionButton] (línea 41). Se trata de un botón sobre el que se puede hacer clic. Por defecto, se sitúa en la parte inferior derecha de toda la vista;

No trataremos de entender el significado de todos los atributos de estos componentes. Los utilizaremos tal cual. Es a través de la experiencia -y a menudo en modo [diseño]- como descubrimos sus funciones. En este modo, descubrimos que los componentes tienen docenas de atributos. Generalmente, sólo se inicializan algunos, mientras que los demás conservan sus valores por defecto.

Sin embargo, aclaremos algunos puntos. La mayoría de los valores que configuran las distintas vistas se reúnen en la carpeta [res/values]:

  

Estos valores están referenciados en las líneas 15-16, 23, 39 y 46 del fichero [activity_main.xml]. Veamos un ejemplo:

  • línea 15:

    android:paddingTop="@dimen/appbar_padding_top"

La anotación [@dimen] hace referencia a los [res/values/dimens.xml]:


<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
</resources>

La línea 15 del archivo [activity_main.xml] hace referencia a la línea (f) anterior;

Del mismo modo, la anotación:

  • [@cadena] hace referencia al archivo de recursos [res/valores/cuerdas.xml];
  • [@color] hace referencia al archivo de recursos [res/values/colores.xml];
  • [@style] hace referencia al archivo de recursos [res/values/estilos.xml];

1.7.4. La actividad

  

El código generado para la actividad coincide con la complejidad de la vista descrita anteriormente: es complejo. Lo analizaremos en varios pasos.

1.7.4.1. Gestión de fragmentos y fichas

El código en [MainActivity] relacionado con fragmentos y pestañas es el siguiente:


package exemples.android;
 
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
 
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
 
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container 
  private ViewPager mViewPager;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // parent
    super.onCreate(savedInstanceState);
    // view
    setContentView(R.layout.activity_main);
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // 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 = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // the tab bar is also associated with the fragment container
    // i.e., tab #i displays fragment #i from the container
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
  }
 
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }
 
  // the fragment manager
  // this is where we request the fragments to be displayed in the main view
  // must define the [getItem] and [getCount] methods—the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • línea 28: Android proporciona un contenedor de vistas de tipo [android.support.v4.view.ViewPager] (línea 12). Este contenedor debe estar provisto de un gestor de vistas o fragmentos. El desarrollador es responsable de proporcionarlo;
  • línea 25: el gestor de fragmentos utilizado en este ejemplo. Su implementación está en las líneas 61-63;
  • línea 31: el método que se ejecuta cuando se crea la actividad;
  • línea 35: la vista [activity_main.xml] está asociada a la actividad;
  • línea 37: recuperamos la referencia al componente [toolbar] de la vista a través de su identificador;
  • línea 38: esta barra de herramientas se convierte en la barra de acciones de la actividad (un concepto de Android);
  • línea 40: se instancia el gestor de fragmentos. El parámetro del constructor es la clase de Android [android.support.v4.app.FragmentManager] (línea 10);
  • línea 44: recuperamos la referencia al contenedor de fragmentos de la vista [activity_main.xml] a través de su ID;
  • línea 45: el gestor de fragmentos está vinculado al contenedor de fragmentos. Esto significa que cuando se pida al contenedor de fragmentos que muestre el fragmento #i, lo solicitará al gestor de fragmentos;
  • línea 48: recuperamos una referencia a la barra de pestañas a través de su identificador;
  • línea 49: el gestor de pestañas está asociado al contenedor de fragmentos. Esto significa que cuando se haga clic en la pestaña #i, el contenedor mostrará el fragmento #i. La asociación entre el gestor de pestañas y el contenedor de fragmentos elimina la necesidad de gestionar las pestañas. Así, no necesitamos definir un manejador de eventos para hacer clic en una pestaña. La asociación con el contenedor de fragmentos lo proporciona por defecto. Veremos un ejemplo en el que hay más fragmentos que pestañas. En este caso, no hacemos esta asociación.

El manejador de fragmentos [SectionsPagerAdapter] es el siguiente:


// the fragment handler
  // this is what we call to retrieve the fragments to display in the main view
  // must define the [getItem] and [getCount] methods—the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment position
    @Override
    public Fragment getItem(int position) {
      // instantiate a [PlaceHolder] fragment and return it
      return PlaceholderFragment.newInstance(position + 1);
    }
 
    // returns the number of fragments managed
    @Override
    public int getCount() {
      return 3;
    }
 
    // optional - gives a title to the managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      switch (position) {
        case 0:
          return "SECTION 1";
        case 1:
          return "SECTION 2";
        case 2:
          return "SECTION 3";
      }
      return null;
    }
  }
}
  • Los fragmentos que muestra una aplicación dependen de la propia aplicación. El gestor de fragmentos lo define el desarrollador;
  • línea 5: el gestor de fragmentos extiende la clase Android [android.support.v4.app.FragmentPagerAdapter]. El constructor se nos proporciona. Debemos definir al menos los dos métodos siguientes:
    • int getCount(): devuelve el número de fragmentos a gestionar;
    • Fragmento getItem(i): devuelve el fragmento #i;

En CharSequence getPageTitle(i) que devuelve el título del fragmento #i, es opcional. Dado que el gestor de pestañas se ha asociado con el gestor de fragmentos, el título de la pestaña #i será el título del fragmento #i. Así, los títulos de las líneas 27-33 serán los títulos de las pestañas;

  • Líneas 18-21: getCount devuelve el número de fragmentos gestionados, en este caso tres;
  • líneas 11-15: getItem(i) devuelve el fragmento #i. Aquí, todos los fragmentos serán del mismo tipo, [PlaceholderFragment];
  • líneas 24-35: getPageTitle(int i) devuelve el título del fragmento #i;

1.7.4.2. Los fragmentos mostrados

  

Los fragmentos de la actividad son todos del mismo tipo aquí y están todos asociados a la siguiente vista XML [fragment_main]:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context="examples.android.MainActivity$PlaceholderFragment">
 
  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
 
</RelativeLayout>
  • líneas 1-16: a [RelativeLayout] diseño;
  • líneas 11-14: el componente único de la vista (fragmento): un [TextView] identificado por [section_label];

En [MainActivity], los fragmentos gestionados son del tipo [PlaceholderFragment] tipo:


// a fragment
  public static class PlaceholderFragment extends Fragment {
      // text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
    }
 
    // returns a fragment with information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // embedded info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // [fragment_main] view is instantiated
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // the [TextView] is found
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // its content is modified
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // return the view
      return rootView;
    }
  }
  • línea 2: el [PlaceholderFragmentextiende la clase [Fragment] de Android. Esto es generalmente siempre el caso;
  • línea 2: el [PlaceholderFragmentes estática. Su método [newInstance] (línea 10) permite obtener instancias de tipo [PlaceholderFragment];
  • líneas 10-19: el método [newInstance] crea y devuelve un objeto de tipo [PlaceholderFragment];
  • líneas 14-16: el fragmento se crea con un argumento;

Un fragmento debe definir el método [onCreateView] en la línea 22. Este método debe devolver la vista asociada al fragmento.

  • Línea 25: La vista [fragment_main.xml] está asociada al fragmento;
  • Línea 27: Esta vista contiene un componente [TextView], cuya referencia se recupera a través de su ID;
  • línea 29: el texto se muestra en el [TextView];
    • [getString] es un método de la clase padre [AppCompatActivity];
    • el primer argumento es un componente ID. [R.string.section_format] se refiere al ID del componente identificado por [section_format] en el archivo [res/values/strings.xml] (línea 4 más abajo):

<resources>
  <string name="app_name">Example-06</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>
  • (continuación)
    • la línea (d) sobre %1$d indica que el argumento #1 (%1) debe formatearse como un entero ($d);
    • el segundo argumento de [getString] es el valor que se asignará al argumento $1 en la línea (d) anterior;
    • [getArguments] devuelve la referencia al paquete de argumentos del fragmento. Es importante señalar aquí que cada argumento se creó con el siguiente paquete (líneas f-h):

    // returns a fragment with information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // embedded info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
}
  • (continuación)
    • getArguments().getInt(ARG_SECTION_NUMBER) will therefore return the value [sectionNumber] from lines (g) and (b) above;
  • línea 31: devolvemos la vista así creada;

1.7.4.3. Gestión de menús

En la aplicación generada, hay un menú:

  

El contenido del fichero [menu_main.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="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>
  • líneas 1-9: el menú;
  • líneas 5-8: un elemento de menú identificado por [action_settings] (línea 5);
  • línea 6: la etiqueta de la opción de menú. Se encuentra en el fichero [res/values/strings.xml] (línea (c) siguiente:

<resources>
  <string name="app_name">Example-06</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>

El código anterior corresponde a la siguiente imagen (el menú se encuentra en la parte superior derecha de la ventana de ejecución de Android):

 

Este menú se maneja de la siguiente manera en la actividad [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Back button, as long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      return true;
    }
 
    return super.onOptionsItemSelected(item);
}
  • Líneas 1-6: Este método es llamado cuando el sistema está listo para crear el menú de la aplicación. El parámetro de entrada [Menú menú] es un menú vacío que aún no tiene ninguna opción;
  • línea 4: se utiliza el archivo [res/menu/menu_main.xml]. Al objeto [Menu menu] pasado como parámetro se le asignan las opciones de menú definidas en este fichero;
  • línea 5: se indica que se ha creado el menú;
  • líneas 8-21: el método [onOptionsItemSelected] se ejecuta cada vez que se hace clic en una opción del menú;
  • línea 13: la referencia de la opción de menú seleccionada;
  • líneas 16-18: si la opción pulsada es la que tiene el identificador [action_settings], no se hace nada y se indica que el evento ha sido gestionado (línea 17);
  • línea 20: el evento se pasa a la clase padre;

Para ver mejor lo que ocurre con este menú, añadimos registros al código anterior:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "creating menu");
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Back button, as long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}

1.7.4.4. El botón flotante

La vista generada tiene un botón flotante:

  

Este componente se define en la vista principal [activity-main.xml]:


  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>

La línea 7 hace referencia a una imagen proporcionada por el framework de Android, concretamente a un sobre.

Este componente se gestiona en la clase [MainActivity] del siguiente modo:


    // floating button
    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
});
  • línea 2: recuperar la referencia del botón flotante en la vista asociada a la actividad (activity_main);
  • líneas 3-9: le asignamos un manejador para gestionar los clics sobre él;
  • línea 6: la clase [Snackbar] permite mostrar mensajes temporales en la vista mediante su método [Snackbar.make]. El primer argumento es una vista a partir de la cual [Snackbar] buscará una vista padre en la que mostrar el mensaje. Aquí, [view] es la vista del sobre en el que se ha hecho clic (línea 5). La vista padre que se encontrará es la vista [activity_main]. El segundo argumento es el mensaje a mostrar. El tercer argumento es la duración de la visualización (SHORT o LONG);
  • línea 7: puede hacer clic en el mensaje mostrado para desencadenar una acción. En este caso, no se asocia ninguna acción al hecho de hacer clic en el mensaje. Por último, el método [show] muestra el mensaje;

Al pulsar el botón flotante se produce el siguiente resultado visual:

 

1.7.5. Ejecución del proyecto

Ahora que hemos explicado los detalles del código generado, podemos entender mejor su ejecución:

Image

Al hacer clic en la pestaña #i, se muestra el fragmento #i en el contenedor de la vista. Esto es evidente por el texto que aparece en [4]. También puedes ver que puedes cambiar entre pestañas deslizando la vista hacia la derecha o hacia la izquierda con el ratón. Veremos que este comportamiento se puede controlar.

Al hacer clic en la opción de menú [6], se obtienen los siguientes registros:

 

1.7.6. Ciclo de vida de los fragmentos

  • En [1], vemos que el método [onCreateView] y los métodos subsiguientes se ejecutan cuando el fragmento se muestra por primera vez y cada vez que la actividad necesita redibujarlo;

Para realizar un seguimiento del ciclo de vida de la actividad y los fragmentos, añadimos los siguientes registros al código [MainActivity]:


// constructor
  public MainActivity(){
    Log.d("MainActivity", "constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
      // parent
    super.onCreate(savedInstanceState);
...
  }
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
    // text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }
 
    // returns a fragment with information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      ...
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      ...
    }
  }
 
 
}

Volvemos a ejecutar el proyecto. Los primeros registros son los siguientes:

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/examples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/examples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/examples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/examples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/examples.android D/menu: creating menu
  • línea 1: creación de la actividad;
  • línea 2: ejecución de su método [onCreate];
  • líneas 3-4: instanciación del fragmento #1;
  • líneas 5-6: instanciación del fragmento nº 2;
  • línea 7: inicializando fragmento #2;
  • línea 8: inicialización del fragmento nº 1;
  • línea 9: creación del menú de actividades;

Aquí, debemos recordar el código responsable de la creación de los fragmentos:


  // the fragment manager
  // this is what we call to retrieve the fragments to display in the main view
  // must define the [getItem] and [getCount] methods—the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment position
    @Override
    public Fragment getItem(int position) {
      // instantiate a [PlaceHolder] fragment and return it
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • líneas 11-15: un fragmento es instanciado por [newInstance] cada vez que el contenedor de fragmentos solicita uno;

Los registros anteriores muestran que los dos primeros fragmentos se han instanciado e inicializado.

Ahora, hagamos clic en la pestaña nº 2. Los nuevos registros son los siguientes:

1
2
3
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/examples.android D/PlaceholderFragment: onCreateView 3
  • Líneas 1-3: El fragmento #3 es instanciado e inicializado. Recuerda que el Fragmento #2 es el que se está mostrando;

Ahora, hagamos clic en la pestaña #3. Aquí no hay registros. Esto se debe probablemente a que el fragmento #3, que debe mostrarse, ya había sido instanciado. Ahora, volvamos a la pestaña #1. Los registros son los siguientes:

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

El fragmento #1 no se instanciará de nuevo, pero su método [onCreateView] se ejecutará de nuevo. Este comportamiento ocurre también para los otros dos fragmentos.

De estos registros, podemos concluir que:

  • la actividad fue instanciada e inicializada una vez;
  • cada fragmento se instanciaba una vez;
  • el método [onCreateView] de cada fragmento se ejecutó varias veces;

Lo que necesitas saber -y lo que confirman los registros- es que, por defecto, cuando se muestra el fragmento #i, se instancian los fragmentos i-1 e i+1, si no lo están ya. Esto explica, por ejemplo, por qué en el arranque, aunque debería mostrarse el fragmento #1, los fragmentos 1 y 2 fueron instanciados e inicializados. Los registros también muestran que el método [getItem(i)] sólo se llama una vez, aunque el fragmento #i se muestre varias veces. Por lo tanto, parece que el contenedor de fragmentos [ViewPager], que se supone que debe mostrar el fragmento [SectionsPagerAdapter] #i, lo solicita una vez al gestor de fragmentos [ ]. Después, no vuelve a solicitarlo y sigue utilizando el que obtuvo.

Por último, los registros proporcionan información sobre el método [onCreateView] de los fragmentos:

  • al inicio, los fragmentos 1 y 2 fueron instanciados y su método [onCreateView] ejecutado;
  • Al cambiar del Fragmento 1 al Fragmento 2, el método [onCreateView] del Fragmento 2 no se vuelve a ejecutar. Por lo tanto, no se puede utilizar para actualizar el Fragmento 2. Sin embargo, el usuario puede haber realizado una operación en el Fragmento 1 cuyo resultado debería ser mostrado por el Fragmento 2. Vemos que el método [onCreateView] no puede utilizarse para actualizar el Fragmento 2. Tendremos que encontrar otra solución;

1.8. Ejemplo-07: Ejemplo-06 reescrito utilizando la biblioteca [AA]

1.8.1. Creación del proyecto

Duplicaremos el proyecto [Ejemplo-06] en [Ejemplo-07] para introducir anotaciones de Android en este último. Para ello, sigue el procedimiento de la sección 1.4. Obtenemos el siguiente resultado:

1.8.2. Configuración de Gradle

 

Actualizamos el fichero [build.gradle] como sigue:


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 {
    applicationId "examples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
}

Hemos añadido la configuración necesaria para utilizar la biblioteca [Android Annotations] (véase la sección 1.4).

1.8.3. Añadir las primeras anotaciones AA

Crearemos anotaciones AA en [MainActivity]:

  

La clase [MainActivity] cambia de la siguiente manera:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;
 
 
  // constructor
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // 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 provided by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // the tab bar is also associated with the fragment container
    // i.e., tab #i displays fragment #i from the fragment container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
  }
  • línea 1: la anotación [@EActivity] convierte a [MainActivity] en una clase gestionada por AA. Su parámetro [R.layout.activity_main] es el identificador de la vista [activity_main.xml] asociada a la actividad;
  • líneas 11-12: el componente identificado por [R.id.tabs] se inyecta en el campo [tabLayout]. Se trata del gestor de pestañas;
  • líneas 14-15: el componente identificado por [R.id.fab] se inyecta en el campo [fab]. Este es el botón flotante;
  • líneas 23-50: el código que antes estaba en el método [onCreate] se traslada a un método con cualquier nombre pero anotado con [@AfterViews] (línea 23). En el método anotado de esta forma, podemos estar seguros de que todos los componentes de la interfaz visual anotados con [@ViewById] han sido inicializados;
  • También hemos añadido registros para ver el ciclo de vida de la actividad;

Recuerde que la anotación [@EActivity] generará una clase [MainActivity_], que será la actividad real del proyecto. Por lo tanto, debe modificar el archivo [AndroidManifest.xml] de la siguiente manera:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:label="@string/app_name"
      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 12: la nueva actividad.

En este punto, ejecute de nuevo el proyecto y compruebe que sigue obteniendo la interfaz con pestañas.

1.8.4. Reescribir los fragmentos

Revisaremos cómo se gestionan los fragmentos en el proyecto. Por ahora, la clase [PlaceholderFragment] es una clase interna estática de la actividad [MainActivity]. Volveremos a un caso de uso más común, en el que los fragmentos se definen en clases externas. Además, introduciremos anotaciones AA para los fragmentos.

El proyecto [Ejemplo-07] evoluciona del siguiente modo:

  

Arriba, vemos la clase [PlaceholderFragment], que se ha desplazado fuera de la clase [MainActivity]. Se reescribe como sigue:


package exemples.android;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // fragment number
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // constructor
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
  }
 
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    return super.onCreateView(inflater, container, savedInstanceState);
  }
 
  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    if (textViewInfo != null) {
      Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
    }
  }
}
  • línea 15: el fragmento se anota con la anotación [@EFragment], cuyo parámetro es el identificador de la vista XML asociada al fragmento, en este caso la vista [fragment_main.xml];
  • líneas 19-20: inyectar en el [textViewInfola referencia al componente en [fragment_main.xml] identificado por [R.id.section_label], que es del tipo [TextView] (línea (l) siguiente):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context="examples.android.MainActivity$PlaceholderFragment">
 
  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
 
</RelativeLayout>
  • líneas 42-52: El método [onResume] se ejecuta antes de que se muestre la vista asociada al fragmento. Puede utilizarse para actualizar la interfaz de usuario que se mostrará;
  • línea 47: debe llamar al método del mismo nombre en la clase padre;
  • línea 49: No está claro si el método [onResume] puede ser ejecutado antes de que el campo de la línea 20 sea inicializado. Los registros configurados para seguir el ciclo de vida del fragmento nos lo dirán. Por ahora, como precaución, realizamos una comprobación de nulos;
  • Línea 51: Actualizamos la información en el [textViewInfocon el argumento entero pasado al fragmento durante su creación;

La clase [MainActivity] pierde su clase interna [PlaceholderFragment] y ve evolucionar su gestor de fragmentos de la siguiente manera:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private Fragment[] fragments;
    // number of fragments
    private static final int FRAGMENTS_COUNT = 3;
    // fragment number
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialize the fragment array
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; 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);
      }
    }
 
    // fragment #position
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }
 
    // returns the number of managed fragments
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // optional - sets a title for the managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Tab #%s", (position + 1));
    }
  }
  • línea 4: los fragmentos se colocan en una matriz;
  • líneas 16-23: el array de fragmentos se inicializa en el constructor. Son de tipo [PlaceholderFragment_] (línea 18) y no [PlaceholderFragment]. El [PlaceholderFragmentha sido anotada con una anotación AA y generará un [PlaceholderFragment_] derivada de [PlaceholderFragment], y es esta clase la que debe utilizar la actividad. A cada fragmento creado se le pasa un argumento entero que será mostrado por el fragmento;
  • líneas 42-45: hemos cambiado los títulos de los fragmentos. Como estos son también los títulos de las pestañas, deberíamos ver un cambio en la barra de pestañas;

Vamos a compilar [Make] [1] este proyecto:

 
  • en [2], podemos ver que las clases generadas por la librería AA se encuentran en la carpeta [app / build / generated / source / apt / debug] (debes estar en la perspectiva [Project] para ver [2]);

Ejecute el proyecto [Example-07] y compruebe que sigue funcionando.

1.8.5. Revisión de los registros

Cuando se lanza la aplicación, los registros son los siguientes:

05-28 13:54:54.801 8809-8809/examples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/examples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/examples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/examples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/examples.android D/menu: creating menu
  • línea 1: construcción de la actividad única;
  • línea 2: método [afterViews] de la actividad: se inicializan sus campos anotados con [@ViewById];
  • líneas 3-5: construcción de los tres fragmentos;
  • líneas 6-7: el contenedor de fragmentos [ViewPager] solicita los dos primeros fragmentos;
  • líneas 8-9: métodos del fragmento 2;
  • líneas 10-11: métodos del fragmento 1;
  • líneas 12-13: [onResume] método del fragmento 1;
  • líneas 14-15: [onResume] método del fragmento 2;
  • línea 16: creación del menú de actividades;

Tenga en cuenta que esto responde a una pregunta formulada anteriormente: el método [onResume] del fragmento 1, por ejemplo (línea 12), se ejecuta después del método [afterViews] del fragmento (línea 11). Por lo tanto, cuando el método [onResume] se ejecuta, puede utilizar los campos anotados con [@ViewById]. Ahora podemos escribir el método [onResume] como sigue:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}

Ahora cambiemos de la pestaña 1 a la pestaña 2. Los nuevos registros son los siguientes:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/examples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3
  • línea 1: el contenedor de fragmentos [ViewPager] solicita el fragmento #3;
  • líneas 2-3: métodos del fragmento #3. Tenga en cuenta que este fragmento se instanció cuando se inició la aplicación;
  • líneas 4-5: se ejecuta el método [onResume] del fragmento #3. Tenga en cuenta que el fragmento # 2 se muestra actualmente;

Ahora pasemos de la pestaña 2 a la pestaña 3. No hay registros. Por lo tanto, no se ejecuta ninguno de los métodos [onCreateView, afterViews, onResume] del fragmento #3. Muestra correctamente el texto [Hola Mundo de la sección:3] únicamente porque este texto ya había sido creado en el paso anterior cuando se mostró el Fragmento #2. Recuerda que en ese paso se había ejecutado el método [onResume] del Fragmento #3. Podemos ver aquí que, al igual que el método [onCreateView], el método [onResume] no puede utilizarse para actualizar el Fragmento 3. Si hubiéramos necesitado cambiar el texto mostrado por el fragmento, ninguno de estos dos métodos podría haberlo hecho.

Ahora, volvamos de la pestaña #3 a la pestaña #1. Los registros son entonces los siguientes:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1

Podemos ver que se han ejecutado todos los métodos del Fragmento 1. Podemos ver que el método getItem no ha sido llamado. Como se ha mencionado, este método sólo se llama una vez por cada fragmento;

Ahora, cambiemos de la pestaña 1 a la pestaña adyacente 2. Obtenemos los siguientes registros:

1
2
3
4
05-28 14:12:59.526 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

Sorprendente, ¿verdad? Todos los métodos del fragmento #3 se vuelven a ejecutar.

Para entender estos fenómenos, recuerda que, por defecto, cuando el contenedor de fragmentos muestra el fragmento i, inicializa los fragmentos i-1, i e i+1. Revisemos los registros a la luz de esta información.

En primer lugar, los registros cuando se inicia la aplicación:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/examples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/examples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/examples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/examples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/examples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/examples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/examples.android D/menu: creating menu

Como el contenedor de fragmentos mostrará el fragmento 1, se inicializan los fragmentos 1 y 2 (líneas 8-15).

Ahora pasamos de la pestaña 1 a la pestaña 2:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/examples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

Como el contenedor de fragmentos mostrará el fragmento 2, los fragmentos 1, 2 y 3 deben inicializarse. Los fragmentos 1 y 2 ya se han inicializado en el paso anterior. El fragmento 3 se inicializa en las líneas 2-5.

Pasamos de la pestaña 2 a la pestaña 3. No hay registros. Como el contenedor de fragmentos mostrará el fragmento 3, los fragmentos 2 y 3 deben estar inicializados. Sin embargo, desde el paso anterior, ya lo están. Lo que no vemos aquí es que el fragmento 1, que no es adyacente al fragmento 3, pierde su estado, que no se conserva en memoria.

Pasamos de la pestaña 3 a la pestaña 1. Los registros son los siguientes:

1
2
3
4
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/examples.android D/PlaceholderFragment: onResume setText 1

Dado que el contenedor de fragmentos mostrará el fragmento 1, el fragmento 2 también debe inicializarse. Se ha inicializado desde el paso anterior. En ese mismo paso, el estado del fragmento 1 se perdió. Por lo tanto, se restablece en las líneas 1-4. Lo que no vemos aquí es que el fragmento 3, que no es adyacente al fragmento 1, pierde su estado, que entonces no se retiene en memoria.

Al cambiar de la pestaña 1 a la pestaña adyacente 2, obtenemos los siguientes registros:

1
2
3
4
05-28 14:12:59.526 8809-8809/examples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/examples.android D/PlaceholderFragment: onResume setText 3

Como el contenedor de fragmentos mostrará el fragmento 2, los fragmentos 1, 2 y 3 deben inicializarse. Los fragmentos 1 y 2 ya se han inicializado en el paso anterior. El fragmento 3 se inicializa en las líneas 1-4.

¿Qué hemos aprendido?

  • que la gestión de fragmentos por defecto es muy específica y que necesitas entenderla si no quieres tirarte de los pelos. Podemos cambiar este modo de gestión, y lo haremos un poco más tarde;
  • que con este manejo por defecto, ninguno de los métodos [onCreateView, onResume] puede ser usado para actualizar el fragmento que será mostrado porque no podemos estar seguros de que serán ejecutados;

1.8.6. onDestroyView

El método [onDestroyView] forma parte del ciclo de vida del fragmento (véase la sección 1.7.6):

Lo vemos en el ciclo de vida de un fragmento:

  • el método [onCreateView] puede ejecutarse varias veces;
  • antes de volver al método [onCreateView] más tarde, hay necesariamente una llamada al método [onDestroyView] [2];

Insertaremos estos métodos en los fragmentos para seguir mejor su ciclo de vida. El código del fragmento pasa a ser el siguiente:


package exemples.android;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
...
 
  @Override
  public void onDestroyView() {
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onDestroyView();
  }
 
}

Vamos a ejecutar la aplicación. Los primeros registros son los siguientes:

06-03 02:45:42.163 2346-2346/examples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/examples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/examples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/examples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/examples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/examples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/examples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/examples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/examples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/examples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/examples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/examples.android D/menu: creating menu
  • línea 1: construcción de la actividad única;
  • línea 2: método [afterViews] de la actividad: se inicializan sus campos anotados con [@ViewById];
  • líneas 3-5: construcción de los tres fragmentos;
  • líneas 6-7: el contenedor de fragmentos [ViewPager] solicita los dos primeros fragmentos;
  • líneas 8-9: se crea la vista para el fragmento 2 (no necesariamente se hace visible);
  • líneas 10-11: se crea la vista para el fragmento 1 (no necesariamente se hace visible);
  • líneas 12-13: [onResume] método del fragmento 1;
  • líneas 14-15: método [onResume] del fragmento 2;
  • línea 16: se crea el menú de actividades;

Pasa de la pestaña 1 a la pestaña 3:


06-03 02:50:02.685 2346-2346/examples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/examples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/examples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/examples.android D/PlaceholderFragment: onDestroyView 1
  • línea 1: el contenedor de fragmentos solicita el tercer fragmento;
  • líneas 2-3: se crea la vista para el fragmento 3 (no necesariamente se muestra);
  • líneas 4-5: se ejecuta el método [onResume] del fragmento 3;
  • línea 6: se ejecuta el método [onDestroyView] del fragmento 1. Esto significa que cuando el usuario vuelva al fragmento 1 o a un fragmento adyacente, se volverá a ejecutar el ciclo de vida de este fragmento;

Volver de la pestaña 3 a la pestaña 1:


06-03 02:53:46.255 2346-2346/examples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/examples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/examples.android D/PlaceholderFragment: onDestroyView 3
  • Líneas 1-4: Se reejecuta el ciclo de vida del fragmento 1 porque había sufrido un [onDestroyView];
  • línea 5: ahora se ejecuta el método [onDestroyView] del fragmento 3. De nuevo, cuando el usuario vuelva al fragmento 3 o a un fragmento adyacente, se volverá a ejecutar el ciclo de vida de este fragmento;

1.8.7. setUserVisibleHint

El método [onCreateView] del ciclo de vida instancia la vista asociada al fragmento pero no necesariamente la hace visible. Eso es lo que veremos ahora. El método [Fragment.setUserVisibleHint] se ejecuta cada vez que cambia la visibilidad del fragmento. Añadimos este método al código del fragmento:


package exemples.android;
 
....
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  ...
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

Al iniciarse, los registros son los siguientes:


06-03 03:06:13.263 20586-20586/examples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/examples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/examples.android D/menu: creating menu
  • Los registros de las líneas 7, 9-10 muestran que sólo el fragmento 1 se hace visible. También podemos ver que se hace visible antes de que se ejecute su método [onCreateView];

Pasemos de la pestaña 1 a la pestaña 2:


06-03 03:10:15.215 20586-20586/examples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume setText 3
  • El fragmento 1 se oculta (línea 3), el fragmento 2 se muestra (línea 4);

Pasemos de la pestaña 2 a la pestaña 3:


06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 1
  • El fragmento 2 está oculto (línea 1), el fragmento 3 se muestra (línea 2);

Volvamos a la pestaña 1:


06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 3
  • El fragmento 3 se oculta (línea 2), el fragmento 1 se muestra (línea 3);

¿Qué hemos aprendido?

  • El método [setUserVisibleHint] se ejecuta una vez con la propiedad [isVisibleToUser] establecida en verdadero para el fragmento que se va a mostrar;
  • No podemos determinar cuándo se ejecutará este método en relación con el ciclo de vida del fragmento. Así, para el fragmento 1, el método [setUserVisibleHint, true] se ejecutó antes que el método [onCreateView] al inicio del ciclo de vida de este fragmento, mientras que para los fragmentos 2 y 3 ocurrió lo contrario;

1.8.8. setOffscreenPageLimit

Los registros anteriores muestran que cuando el contenedor de fragmentos [ViewPager] está a punto de mostrar el fragmento #i, ejecuta, si no lo ha hecho ya, el ciclo de vida de los fragmentos adyacentes i-1 e i+1. Este comportamiento puede controlarse mediante el parámetro [ViewPager].setOffscreenPageLimit método:

// fragment offset
    [ViewPager].setOffscreenPageLimit(n);

Con la instrucción anterior,

  1. cuando el contenedor de fragmentos [ViewPager] está a punto de mostrar el fragmento #i, ejecuta, si no lo ha hecho ya, el ciclo de vida de los fragmentos adyacentes en el rango [i-n, i+n];
  2. if fragment j is then displayed:
    • se produce el mismo fenómeno para los fragmentos adyacentes en el intervalo [j-n, j+n];
    • los fragmentos inicializados en el paso 1 que ya no son adyacentes al nuevo fragmento dentro del intervalo [j-n, j+n] pueden someterse entonces a una operación [onDestroyView]. Sin embargo, he observado en otras aplicaciones, en particular la del capítulo 3, que no siempre ha sido así;

Modificamos el método [MainActivity.afterViews] como sigue:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // 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 provided by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // Disable swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // Fragment offset
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
 
    // the tab bar is also associated with the fragment container
    // i.e., tab #i displays fragment #i from the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
}
  • Línea 20: Establecemos el número de fragmentos adyacentes a inicializar al número total de fragmentos menos 1. Así, en el arranque, cuando el contenedor de fragmentos muestre el fragmento #1, inicializará simultáneamente los fragmentos 2, 3, ..., n, donde n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Esto significa que se inicializarán todos los fragmentos. Cuando el viewport se mueve a otro fragmento, el contenedor de fragmentos:
    • detectará que todos los fragmentos adyacentes al nuevo fragmento ya están inicializados y, por tanto, no los inicializará;
    • como la adyacencia del nuevo fragmento también cubre todos los fragmentos, ninguno será "desinicializado" por el contenedor de fragmentos;

En total, deberíamos ver todos los fragmentos instanciados e inicializados cuando se inicia la aplicación y luego nunca más. Esto es lo que comprobaremos ahora examinando los registros.

En el arranque, tenemos los siguientes registros:

06-03 03:30:55.411 10344-10344/examples.android W/System: ClassLoader referenced unknown path: /data/app/examples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/examples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/examples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/examples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/examples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/examples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/examples.android D/menu: creating menu
  • líneas 4-6: construcción de los tres fragmentos;
  • líneas 7, 9, 11: el contenedor de fragmentos solicita los tres fragmentos. En la versión anterior, solicitaba dos;
  • líneas 14-25: se ejecuta el ciclo de vida de los tres fragmentos;

Ahora pasemos de la pestaña 1 a la pestaña 2:

06-03 03:34:03.388 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Pasemos de la pestaña 2 a la pestaña 3:

06-03 03:34:43.292 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Luego de la pestaña 3 a la pestaña 1:

06-03 03:35:32.666 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

Los registros confirman la teoría. Todos los fragmentos se instanciaron e inicializaron al arrancar. Después de eso, sus métodos de ciclo de vida ya no se ejecutan. Este es un comportamiento muy predecible de los fragmentos, lo que los hace mucho más fáciles de usar.

Lo que queremos encontrar es una forma de actualizar un fragmento que está a punto de mostrarse, independientemente de la adyacencia de fragmentos elegida por el desarrollador. Los registros nos han mostrado dos cosas:

  • el método [setUserVisibleHint, true] se ejecuta siempre para el fragmento que se va a mostrar, pero no para los demás;
  • este evento puede producirse antes o después del ciclo de vida del fragmento. Esto depende de la adyacencia del fragmento elegida por el desarrollador. Esto es un problema porque si el ciclo de vida aún no se ha producido, significa que el fragmento no puede ser actualizado por el método [setUserVisibleHint, true];

Los registros al inicio de la aplicación cuando la adyacencia de fragmentos era 1 eran los siguientes:


06-03 03:06:13.263 20586-20586/examples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/examples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/examples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/examples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/examples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/examples.android D/menu: creating menu
  • Podemos ver que cuando el Fragmento 1 se hace visible, su vista aún no ha sido creada. Por lo tanto, no podemos interactuar con él. Esto puede hacerse durante el ciclo de vida del fragmento, por ejemplo en el método [onCreateView] (línea 11) o en el método [onResume] (líneas 13-14). Dado que estamos utilizando anotaciones AA, normalmente no necesitamos escribir el método [onCreateView]. Por lo tanto, el método [onResume] parece ser el más apropiado aquí para actualizar el Fragmento 1;

Cuando cambiamos de la pestaña 1 a la pestaña 2, los registros fueron los siguientes:


06-03 03:10:15.215 20586-20586/examples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/examples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/examples.android D/PlaceholderFragment: onResume setText 3

Esta vez, sólo tenemos el método [setUserVisibleHint, true] en la línea 4 para actualizar el fragmento 2;

Cuando cambiamos de la pestaña 2 a la pestaña 3, los registros fueron los siguientes:


06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 1

Aquí, sólo tenemos el método [setUserVisibleHint, true] en la línea 2 para actualizar el fragmento 3;

Cuando cambiamos de la pestaña 3 a la pestaña 1, los registros fueron los siguientes:


06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/examples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/examples.android D/PlaceholderFragment: onDestroyView 3

Aquí, debes usar el método [onResume] del Fragmento 1 (líneas 6-7) para actualizar el Fragmento 1.

Así que en este ejemplo, vemos que para actualizar un fragmento que está a punto de ser mostrado, tenemos dos métodos: [setUserVisibleHint] y [onResume].

Implementaremos esta solución en un nuevo proyecto en el que cada fragmento deberá mostrar el número de veces que ha sido visualizado, lo que llamaremos visita. Por lo tanto, tendremos que actualizar su visualización cada vez que se muestre. Este es el problema que intentamos resolver.

Antes, examinemos la última etapa del ciclo de vida de una actividad o fragmento: cuando se destruye. El sistema puede decidir destruir una actividad si otras actividades con mayor prioridad necesitan recursos que en ese momento no están disponibles. Para liberar estos recursos, el sistema tomará la iniciativa de destruir ciertas actividades. Entonces se llamará al método [onDestroy] de la actividad y de los fragmentos.

1.8.9. OnDestroy

Permitiremos al usuario eliminar la actividad mediante una opción de menú [5]. Para ello, añadimos una nueva opción de menú al fichero [menu_main.xml] [1]:


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

Basta con copiar y pegar la primera opción del menú y adaptar el resultado (líneas 9 y 10). La etiqueta de esta nueva opción se añade al fichero [strings.xml] [2]:


<resources>
  <string name="app_name">Example-07</string>
  <string name="action_settings">Settings</string>
  <string name="action_terminate">Terminate</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>

Finalmente, en la clase [MainActivity], manejamos el clic en la opción [Terminar]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, as long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    if (id == R.id.action_terminate) {
      Log.d("menu", "action_terminate selected");
      //end the activity
      finish();
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}
  • líneas 14-19: copia y pega las líneas 10-13 y adapta el código a la nueva opción;
  • línea 17: la actividad finaliza mediante una acción de software;

Ahora vamos a ejecutar esta nueva versión, y en cuanto aparezca la primera vista, haga clic en la opción de menú [Terminar]. Los registros son entonces los siguientes:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/examples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/examples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/examples.android D/PlaceholderFragment: onDestroy 3
  • líneas 1-2: haga clic en la opción [Terminar];
  • línea 4: se llama al método [onDestroy] de la actividad;
  • líneas 4-5: se llama al método [onDestroyView] del fragmento 1, seguido de su método [onDestroy];
  • líneas 6-9: este proceso se repite para los otros dos fragmentos;

Es importante recordar que el método [onDestroy] de la actividad y los fragmentos se llama cuando la actividad está a punto de ser destruida por el sistema, el desarrollador o el usuario. Este método puede utilizarse para guardar información -por ejemplo, localmente en la tableta- de modo que pueda recuperarse cuando el usuario reinicie la aplicación.

1.9. Ejemplo-08: Actualización de un fragmento con adyacencia de fragmento variable

1.9.1. Creación del proyecto

Duplique el proyecto [Ejemplo-07] en [Ejemplo-08]. Para ello, siga el procedimiento descrito para duplicar [Ejemplo-02] en [Ejemplo-03] en la sección 1.4.

1.9.2. Reescribir el fragmento [PlaceholderFragment]

El nuevo código para el fragmento [PlaceholderFragment] es el siguiente. Funciona independientemente de la adyacencia asignada a los fragmentos (1, parcial, total):


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;
 
  // fragment number
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // constructor
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // initialization complete
      initDone = true;
    }
    // display current text
    textViewInfo.setText(text);
  }
 
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  @Override
  public void onDestroyView() {
...
  }
 
  @Override
  public void onResume() {
...
  }
 
  // update fragment
  public void update() {
    // The task depends on the visit count
    if (visitCount > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // updated text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  private String getInfo() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
  }
}
  • líneas 34-48: el método [@AfterViews] puede ejecutarse varias veces. Solíamos utilizarlo para inicializar el texto del fragmento (línea 42). Seguimos haciéndolo, pero para asegurarnos de que sólo ocurre una vez, manejamos un booleano [initDone] (línea 44) para indicar que la inicialización se ha completado y no es necesario repetirla;
  • líneas 56-59: Introducimos el método [onDestroyView] para tener en cuenta el hecho de que la próxima vez que se vuelva a mostrar el fragmento, se volverá a ejecutar su ciclo de vida;
  • Los registros mostraron que se pueden ejecutar dos métodos después del método [@AfterViews]: los métodos [setUserVisibleHint] y [onResume]. El método [onResume] sólo se ejecuta cuando se ejecuta el ciclo de vida del fragmento. Sin embargo, el método [setUserVisibleHint] no siempre se ejecuta después del método [@AfterViews]. Los registros muestran que al menos uno de los dos se ejecuta después del método [@AfterViews]. Los registros nunca han mostrado que ambos puedan ejecutarse juntos después del método [@AfterViews]. Es uno u otro. Como precaución, estableceremos un booleano [updateDone] cuando se haya realizado una actualización;

Los métodos [setUserVisibleHint] y [onResume] son los siguientes:


  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int visitCount = 0;
 
@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // increment
      numVisit++;
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // update
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • línea 14: se almacena el estado visible del fragmento;
  • líneas 22-25: si el fragmento es visible y se ha ejecutado el método [@AfterViews], se ejecuta el método [update] y se establece el booleano [updateDone] a verdadero;
  • líneas 26-28: si el fragmento se va a ocultar, el booleano [updateDone] se restablece a falso. Necesitamos un evento para restablecer el booleano [updateDone], que está definido como verdadero en cuanto se llame al método [update] para falso para poder realizar nuevas actualizaciones. Para ello, se aprovecha el hecho de que el fragmento ya no es visible. Cuando vuelva a ser visible, el fragmento deberá actualizarse de nuevo;
  • líneas 32-42: los registros muestran que dependiendo de la adyacencia elegida para los fragmentos, el método [onResume] puede ejecutarse aunque el fragmento no sea visible. Si no es visible, no realizamos la actualización (línea 39) y, como hicimos para [setMenuVisibility], gestionamos el booleano [updateDone].

Por último, el método [onDestroyView] es el siguiente:


  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // update indicator
    afterViewsDone = false;
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

El método [onDestroyView] se ejecuta cuando finaliza el ciclo de vida de un fragmento. Otro ciclo de vida puede reanudarse más tarde.

  • Línea 6: El método [onDestroyView] elimina cualquier conexión con la vista adjunta al fragmento. Se volverá a crear durante el siguiente ciclo de vida del fragmento. Por ahora, necesitamos establecer el booleano [afterViews] a falso para indicar que la conexión con la vista ya no existe;

Ejecutaremos la aplicación con 5 fragmentos que tengan una adyacencia de 2. Los cambios se realizan en [MainActivity]:


    // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT=2;
 
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    ....
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
...
}

Los registros de inicio son los siguientes:

05-31 06:23:07.015 32551-32551/examples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/examples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/examples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/examples.android D/menu: creating menu
  • líneas 8, 10, 12: el contenedor de fragmentos solicita todos los fragmentos adyacentes al fragmento 1;
  • líneas 9, 11, 13: el método [setUserVisibleHint] de estos fragmentos se ejecuta con [visibleToUser] establecido en falso;
  • Línea 14: Se llama al método [setUserVisibleHint] del fragmento 1 con [visibleToUser] establecido en verdadero;
  • líneas 15-17: se llama al método [afterViews] de los 3 segmentos adyacentes. Aquí vemos un caso en el que se llama a este método después de que un fragmento se haya hecho visible (Fragmento 1, línea 14);
  • líneas 18-20: se llama al método [onResume] de los 3 segmentos adyacentes;

Pasar de la pestaña 1 a la pestaña 2:

1
2
3
4
5
6
05-31 06:52:36.132 32551-32551/examples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/examples.android D/PlaceholderFragment: onResume 4: numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • como la disposición de los fragmentos se desplaza una posición a la derecha, el fragmento 4 es reclamado por el contenedor de fragmentos;
  • línea 2: se llama al método [setUserVisibleHint] del fragmento 4 con [visibleToUser] establecido en falso;
  • línea 3: se llama al método [setUserVisibleHint] del fragmento 1 con [visibleToUser] establecido en falso. Como resultado, el fragmento 1 está ahora oculto;
  • línea 4: se llama al método [setUserVisibleHint] del fragmento 2 con [visibleToUser] establecido en verdadero. El fragmento 2 es ahora visible;
  • líneas 5-6: continúa el ciclo de vida del fragmento 4;

Pasamos de la pestaña 2 a la pestaña 3:

1
2
3
4
5
6
05-31 06:58:16.228 32551-32551/examples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/examples.android D/PlaceholderFragment: onResume 5: numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • como la disposición de los fragmentos se desplaza una posición a la derecha, el fragmento 5 es reclamado por el contenedor de fragmentos;
  • línea 2: se llama al método [setUserVisibleHint] del fragmento 5 con [visibleToUser] establecido en falso;
  • línea 3: se llama al método [setUserVisibleHint] del fragmento 2 con [visibleToUser] establecido en falso. Como resultado, el fragmento 2 está ahora oculto;
  • línea 4: se llama al método [setUserVisibleHint] del fragmento 3 con [visibleToUser] establecido en verdadero. El fragmento 3 es ahora visible;
  • líneas 5-6: continúa el ciclo de vida del fragmento 5;

Pasamos de la pestaña 3 a la 4:


05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • Línea 1: El fragmento 3 está ahora oculto;
  • línea 2: el fragmento 4 es ahora visible. Observe que el ciclo de vida del fragmento 4 no se ejecuta. Esto ya se hizo dos pasos antes;
  • línea 3: el fragmento 1 abandona las proximidades del fragmento 4 visualizado. Se ejecuta su método [onDestroyView]. La próxima vez que se muestre, se volverá a ejecutar su ciclo de vida de vista [onCreateView, afterViews, onResume];

Pasamos de la pestaña 4 a la 5:


05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • Línea 1: El fragmento 4 está ahora oculto;
  • línea 2: el fragmento 5 es ahora visible. Observe que el ciclo de vida del fragmento 5 no se ejecuta. Esto ya se hizo dos pasos antes;
  • línea 3: el fragmento 2 abandona las proximidades del fragmento 5 visualizado. Se ejecuta su método [onDestroyView];

Pasamos de la pestaña 5 a la pestaña 1:


05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/examples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/examples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/examples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • líneas 1, 4, 5, 6: el ciclo de vida del fragmento 1 se vuelve a ejecutar. Esto se debe a que había perdido la conexión con su vista;
  • líneas 2, 5, 8, 9: por la misma razón, se vuelve a ejecutar el ciclo de vida del fragmento 2;
  • líneas 10-11: se eliminan los fragmentos 4 y 5 de las proximidades del fragmento visualizado;
  • línea 7: se actualiza el fragmento 1;
 

Los registros nunca mostraron que los métodos [setUserVisibleHint] y [onResume] intentaran actualizar el fragmento. Es uno u otro. Invitamos al lector a que realice más pruebas y controle los registros para comprender plenamente los conceptos de adyacencia de fragmentos y ciclo de vida.

Ahora, establezcamos la adyacencia total y ejecutemos las mismas pruebas.

En [MainActivity]:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Los registros de inicio son los siguientes:


05-31 07:34:44.717 28908-28908/examples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/examples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/examples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/examples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/examples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/examples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/examples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/examples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/examples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/examples.android D/menu: creating menu
  • Los registros muestran que se está ejecutando el ciclo de vida de los 5 fragmentos;
  • El fragmento 1 aparece en la línea 18;

Pasar de la pestaña 1 a la pestaña 2:

05-31 07:38:27.780 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 1 está oculto;
  • línea 2: aparece el fragmento 2;

Pasar de la pestaña 2 a la pestaña 3:

05-31 07:39:33.059 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 2 está oculto;
  • línea 2: aparece el fragmento 3;

Pasar de la pestaña 3 a la pestaña 4:

05-31 07:40:30.362 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 3 está oculto;
  • línea 2: aparece el fragmento 4;

Pasar de la pestaña 4 a la pestaña 5:

05-31 07:41:23.479 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 4 está oculto;
  • línea 2: aparece el fragmento 5;

Pasamos de la pestaña 5 a la pestaña 1:


05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 5: numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/examples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 5 está oculto;
  • línea 2: aparece el fragmento 1;
  • línea 3: se actualiza el fragmento 1;

Pasar de la pestaña 1 a la pestaña 4:


05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 1: numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: setUserVisibleHint 4: numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/examples.android D/PlaceholderFragment: update 4: numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 1 está oculto;
  • línea 2: aparece el fragmento 4;
  • línea 3: se actualiza el fragmento 4;

Podemos ver que con adyacencia completa, el comportamiento de los fragmentos es mucho más predecible.

Ahora, pongamos la adyacencia a cero y veamos qué ocurre. La clase [MainActivity] evoluciona como sigue:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Los registros de inicio son los siguientes:


06-01 03:11:52.068 5679-5679/examples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/examples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/examples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/examples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/examples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/examples.android D/menu: creating menu
  • En las líneas 8 y 10, vemos que el contenedor de fragmentos ha solicitado 2 fragmentos, los números 1 y 2. Por lo tanto, todo procede como si hubiera una adyacencia de 1. La adyacencia de 0 se ha ignorado. Por lo tanto, todo procede como si hubiera una adyacencia de 1. La adyacencia de 0 se ha ignorado.

1.9.3. Comunicación entre fragmentos

En la arquitectura anterior, tenemos una actividad y n fragmentos. El usuario interactúa con los distintos fragmentos. Estas interacciones modifican el estado de la aplicación. Aquí, el estado de la aplicación se refiere al conjunto de información que almacena a lo largo de su vida. Se plantea entonces el siguiente problema:

  • cuando el usuario interactúa con el fragmento i, la aplicación pasa del estado E1 al estado E2;
  • una acción del usuario sobre el fragmento i hace que se muestre el fragmento j;
  • ¿cómo actualizamos el fragmento j con el estado actual de la aplicación E2?

Por los ejemplos anteriores, sabemos cómo actualizar el fragmento j. Pero, ¿dónde encontramos el estado E2 de la aplicación para actualizarlo?

Existen diferentes soluciones a este problema. Hemos visto una: el fragmento i puede pasar el estado E2 de la aplicación al fragmento j mediante argumentos. Encontramos este método en la clase [MainActivity] al crear los fragmentos:


      for (int i = 0; i < fragments.length; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // we can pass arguments to the fragment
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

Esta solución no es inmediatamente utilizable aquí. De hecho, cuando el usuario hace clic en la pestaña j, que mostrará el fragmento j, no se llama a nuestro código. Sólo se ejecuta el código del sistema. Veremos en un proyecto futuro cómo interceptar un clic en una pestaña, pero por ahora tomaremos un enfoque diferente.

Ya hemos hablado del estado de la aplicación: el conjunto de datos gestionados por la aplicación a lo largo del tiempo. Aquí, la aplicación consiste en una actividad y n fragmentos, todos instanciados una vez al inicio de la aplicación y cuyo tiempo de vida coincide con el de la aplicación. Por lo tanto, cualquiera de estos elementos -o varios de ellos juntos- puede sirven como candidatos para almacenar el estado de la aplicación. Cada fragmento tiene acceso, a través del método [Fragment.getActivity()], a la actividad que lo creó. Dado que todos los fragmentos tienen acceso a la actividad, parece natural almacenar el estado de la aplicación dentro de ella.

Sin embargo, el resultado del método [Fragment.getActivity()] depende del momento del ciclo de vida en que se llame. Ilustramos este punto añadiendo algunos registros a la clase [PlaceholderFragment]:


  // update fragment
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // updated text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  private String getInfo() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
      numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
  • líneas 14-16: el método [getInfo] muestra parte del estado de la aplicación;

Lanzamos la aplicación con una adyacencia de fragmento de 2. Los registros cuando se inicia la aplicación:


06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/examples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/examples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/examples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 2: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 3: numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/examples.android D/menu: creating menu
  • líneas 9, 10, 13, 14: vemos que en los métodos [setUserVisibleHint], tenemos [getActivity()==null] si el fragmento aún no es visible (isVisibleToUser==false);
  • línea 19: vemos que cuando el flujo de ejecución llega al método [update] del fragmento 1, el método [getActivity] devuelve correctamente la actividad;

Cuando la adyacencia de fragmentos se establece en 4 (adyacencia completa), los registros son los siguientes:


06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepting and dropping
06-01 03:35:23.900 2814-2814/examples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/examples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/examples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/examples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/examples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/examples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/examples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/examples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/examples.android D/menu: creating menu

Obtenemos los mismos resultados. Podemos concluir que en cuanto el fragmento es visible, el método [getActivity] devuelve la actividad del fragmento. También observamos que cuando la ejecución alcanza el método [update] del fragmento que está a punto de mostrarse, el método [getActivity] devuelve efectivamente un valor.

Para ilustrar la comunicación entre fragmentos, estamos construyendo un nuevo proyecto.

1.10. Ejemplo-09: Comunicación entre fragmentos, barrido y desplazamiento

1.10.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-07] en [Ejemplo-08]. Para ello, seguiremos el procedimiento descrito para duplicar [Ejemplo-02] en [Ejemplo-03] en la sección 1.4.

1.10.2. La sesión

En este nuevo proyecto, queremos que los fragmentos muestren el número total de fragmentos vistos por el usuario. Aquí, necesitamos mantener un contador que sea accesible a todos los fragmentos. Llamaremos al objeto que encapsula los datos compartidos por los fragmentos una "sesión." Esta terminología procede del desarrollo web, donde los datos que deben compartirse en diferentes vistas solicitadas por el mismo usuario se colocan en una sesión. Encapsular la información compartida por los distintos fragmentos en un único objeto hace que el código sea más legible.

La clase [Sesión] será la siguiente:

  

package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
 
  // getters and setters
 
  public int getNumVisit() {
    return numVisit;
  }

  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
}
  • línea 8: la sesión encapsulará el número de fragmentos visitados;
  • línea 5: la anotación [EBean] es una anotación AA. El atributo [scope] especifica el ámbito (o tiempo de vida) de la clase anotada. En este caso, el atributo [ámbito = EBean.Scope.Singletonhace de la clase [Session] un singleton: se instanciará una y sólo una vez cuando se inicie la aplicación. Una referencia a una clase anotada con [EBean] puede ser inyectada en otra clase. Este es el concepto de inyección de dependencia;

1.10.3. El [MainActivity]

La actividad [MainActivity] evoluciona del siguiente modo:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  ...
 
  // session injection
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");
 
    // session initialization
    session.setNumVisit(0);
  }
 
...
  • líneas 7-8: inyección de la referencia al singleton de sesión utilizando la anotación [@Bean]. El parámetro de la anotación es la clase del bean a inyectar. El campo así anotado no puede tener ámbito [private];
  • línea 15: la anotación [@AfterInject] se utiliza para designar un método que se llamará una vez que se hayan completado todas las inyecciones de la clase. Así, al entrar en el método [afterInject] en la línea 16, la referencia de la línea 8 ya ha sido inicializada;
  • línea 20: el contador de visitas se pone a cero;

1.10.4. El fragmento [PlaceholderFragment]

El fragmento [PlaceholderFragment] evoluciona como sigue:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
....
 
  // session
  protected Session session;
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // The fragment will be hidden
      updateDone = false;
    }
  }
 
  // update fragment
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // session
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // increment visit count
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
  }
  • línea 7: la sesión;
  • líneas 35-37: sabemos que cuando entramos en el método [update], el método [getActivity] devuelve correctamente la actividad. Aprovechamos para recuperar la sesión y almacenarla localmente (línea 36);
  • líneas 39-41: para incrementar el número de visita, lo recuperamos de la sesión. Podríamos haber colocado este código en el método [setUserVisibleHint] a partir de la línea 19, ya que sabemos que el método [getActivity] devuelve la actividad en ese punto. Aquí, decidimos no asignar un rol específico a este método y mover el código específico del fragmento al método [update] del fragmento, que está diseñado para ese propósito;
  • línea 43: muestra el número de visita;

Al ejecutar esta aplicación con 5 fragmentos, con 2 fragmentos adyacentes, los primeros registros son los siguientes:


05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/examples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/examples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/examples.android D/PlaceholderFragment: constructor
...
  • Líneas 2-3: Podemos ver que el método [afterInject] de la actividad se ejecuta antes que su método [afterViews];

Invitamos a los lectores a probar esta nueva aplicación.

1.10.5. Desactivar Swipe

En la aplicación anterior, al deslizar el emulador de Android con el ratón hacia la izquierda o la derecha, la vista actual se sustituye por la vista de la derecha o la izquierda, según corresponda. Este comportamiento por defecto no siempre es deseable. Aprenderemos a desactivar el deslizamiento de la vista.

Volvamos a la vista principal XML [activity_main]:

  

En el código XML de la vista, encontramos el contenedor de fragmentos:


  <android.support.v4.view.ViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

La línea 1 especifica la clase que gestiona las páginas de la actividad. Esta clase se encuentra en la actividad [MainActivity]:


import android.support.v4.view.ViewPager;
...
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

Línea 12: El contenedor de fragmentos es de tipo [android.support.v4.view.ViewPager] (línea 1). Para deshabilitar el swiping, necesitamos extender esta clase como sigue:

  

package exemples.android;
 
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;
 
  // 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;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • línea 8: la clase [MyPager] extiende la clase [ViewPager] de Android (línea 4);
  • al deslizar el dedo, se puede llamar a los manejadores de eventos de las líneas 24 y 34. Ambos devuelven un booleano. Simplemente tienen que devolver el booleano [false] para desactivar el deslizamiento;
  • línea 11: el booleano utilizado para indicar si se permite o no el gesto de deslizar.

Una vez hecho esto, ahora debemos utilizar nuestro nuevo gestor de páginas. Esto se hace en la vista XML [activity_main.xml] y en la actividad principal [MainActivity]. En [activity_main.xml] escribimos:

  

  <exemples.android.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

Línea 1: Usamos la nueva clase. En [MainActivity], el código cambia como sigue:


package examples.android;
 
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // the fragment container is associated with the fragment manager
    // i.e., fragment #i in the fragment container is fragment #i provided by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // we disable swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // the tab bar is also associated with the fragment container
...
  • línea 12: el gestor de páginas es ahora de tipo [MyPager];
  • línea 23: activamos o desactivamos el barrido.

Prueba esta nueva versión. Activa o desactiva el deslizamiento y observa la diferencia en el comportamiento de las vistas cuando las arrastras a la derecha o a la izquierda con el ratón. En todas las aplicaciones futuras, swiping estará desactivado. No volveremos a mencionarlo.

1.10.6. Desactivar el desplazamiento entre fragmentos

Sigamos con una mejora del gestor de pestañas. Al pasar de la pestaña 1 a la 4, ves pasar las dos pestañas intermedias, la 2 y la 3. En la jerga de Android, esto se llama smoothScrolling. Este comportamiento puede llegar a ser molesto si hay muchas pestañas. Se puede desactivar añadiendo el siguiente código al gestor de fragmentos [MyPager]:


// controls swiping
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
 ...
  // scrolling
  @Override
  public void setCurrentItem(int position) {
    super.setCurrentItem(position, isScrollingEnabled);
  }
 
  // setters
...
 
  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }

Debido a que el gestor de pestañas ha sido asociado con el gestor de fragmentos [MyPager], cuando se hace clic en la pestaña #i, el fragmento #i es mostrado por el contenedor de fragmentos utilizando el método [setCurrentItem] anterior (línea 9). [position] es el número del fragmento a mostrar;

  • línea 10: se llama al método [setCurrentItem] de la clase padre. El segundo argumento establecido en [false] solicita una transición inmediata entre los fragmentos antiguos y nuevos (sin desplazamiento); establecido en [true] solicita una transición mediante desplazamiento. Aquí, el segundo argumento es el valor del campo de la línea 4, un campo que el desarrollador puede establecer utilizando el método de las líneas 16-18;

If you want to disable scrolling, the [MainActivity] class will look like this:


...
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // disable swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
...

Ejecute de nuevo el proyecto y compruebe que ya no hay ninguna desplazamiento entre las pestañas 1 y 4, por ejemplo. A partir de aquí, desactivaremos siempre el desplazamiento. No volveremos sobre esto.

1.10.7. Un nuevo fragmento

En nuestro ejemplo, todos los fragmentos son del mismo tipo [PlaceHolderFragment]. Ahora aprenderemos a crear un nuevo fragmento y a visualizarlo.

En primer lugar, copie la vista [vue1.xml] del proyecto [Ejemplo-04] en el proyecto [Ejemplo-09] [1]:

 
  • en [1], la vista [vue1.xml];
  • en [3], la vista muestra errores debido a que falta texto en el archivo [res/values/strings.xml];

En [2], añadimos el texto que falta tomándolo del fichero [res/values/strings.xml] del proyecto [Example-04]


<resources>
  <string name="app_name">Example-07</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- view 1 -->
  <string name="view1_title">View #1</string>
  <string name="txt_nom">What is your name?</string>
  <string name="btn_submit">Submit</string>
  <string name="btn_view2">View 2</string>
</resources>
  • Arriba, añadimos las líneas 6-9;

Ahora, creamos la clase [Vue1Fragment], que será el fragmento responsable de mostrar la vista [vue1.xml]:

  

La clase [Vue1Fragment] será la siguiente:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
 
  // UI elements
  @ViewById(R.id.editTextName)
  protected EditText editTextName;
 
  // event handler
  @Click(R.id.buttonValider)
  protected void validate() {
    // display the entered name
    Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}
 
  • línea 10: la anotación [@EFragment] garantiza que el fragmento utilizado por la actividad será realmente la clase [Vue1Fragment_]. Tenga esto en cuenta. El fragmento está asociado a la vista [vue1.xml];
  • líneas 14-15: el componente identificado por [R.id.editTextNom] se inyecta en el campo [editTextNom] en la línea 15;
  • líneas 18-20: el método [doValider] maneja el evento 'click' en el botón identificado por [R.id.buttonValider];
  • línea 21: el primer parámetro de [Toast.makeText] es de tipo [Activity]. El método [Fragment.getActivity()] recupera la actividad en la que se encuentra el fragmento. Se trata de [MainActivity] ya que, en esta arquitectura, tenemos una única actividad que muestra diferentes vistas o fragmentos;

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


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private Fragment[] fragments;
    // fragment number
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialize the fragment array
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int 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[fragments.length - 1] = new Vue1Fragment_();
    }
 
 ...
  }
  • línea 13: hay fragmentos de tipo [FRAGMENTS_COUNT]: [FRAGMENTS_COUNT-1] fragmentos de tipo [PlaceholderFragment] (líneas 14-21) y un fragmento de tipo [Vue1Fragment_], línea 23 (nótese el guión bajo);

Compile y ejecute el proyecto [Ejemplo-09]. La pestaña 5 debería tener un aspecto diferente:

1.10.8. Derivar todos los fragmentos de la misma clase abstracta

El nuevo fragmento [Vue1Fragment] también debe actualizarse cuando se muestre. Para ello, necesitaremos crear un código similar al creado para el fragmento [PlaceholderFragment]. Para evitar repeticiones, factorizaremos lo que pueda ser en una clase abstracta de la que heredarán todos los fragmentos de la aplicación.

Para ello, creamos un nuevo proyecto.

1.11. Ejemplo-10: Derivar todos los fragmentos de una clase abstracta

1.11.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-09] en [Ejemplo-10]:

1.11.2. Gestión del modo de depuración

Añadimos al proyecto la opción de mostrar u ocultar los logs en modo depuración. Para ello, añadimos una constante estática a la clase [MainActivity]:


  // debug mode
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. La clase padre abstracta de todos los fragmentos

  

La clase [AbstractFragment] es la siguiente:


package exemples.android;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
 
public abstract class AbstractFragment extends Fragment {
 
  // private data
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;
 
  // Data accessible to child classes
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;
 
  // activity
  protected MainActivity activity;
 
  // session
  protected Session session;
 
  // constructor
  public AbstractFragment() {
    // init
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    ...
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    ...
  }
 
  // local info
  protected String getParentInfo() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }
 
  // update fragment
  protected void update() {
    ...
    // ask the child class to update itself
    updateFragment();
  }
 
  protected abstract void updateFragment();
}
  • línea 7: la clase [AbstractFragment] extiende la clase [Fragment] de Android;
  • Cada fragmento debe ser capaz de actualizarse a sí mismo. Por eso la clase padre [AbstractFragment] requiere que sus clases hijas tengan un método [updateFragment] (línea 68), al que llama (línea 65);
  • línea 19: la clase almacenará una referencia a la actividad de la aplicación;
  • línea 22: la clase almacenará una referencia a la sesión donde se recogen los datos compartidos por los fragmentos y la actividad;
  • líneas 25-33: el constructor de la clase abstracta;
  • línea 27: creación de una copia de la constante [MainActivity.IS_DEBUG_ENABLED] en el campo de la línea 16;
  • línea 28: se almacena el nombre de la clase instanciada, i.e., el nombre de una clase hija;
  • líneas 15-22: estos campos tienen el atributo [protected] para que las clases hijo puedan acceder a ellos. Nótese que las clases hijas desconocen la existencia de los booleanos [isVisibleToUser] y [updateDone] (líneas 10-11);
  • línea 57: el método [getParentInfos] tiene el atributo [protected] para que las clases hijas puedan llamarlo;

Los métodos [setUserVisibleHint, onDestroyView, onResume] siguen siendo los mismos que en la clase [PlaceholderFragment] del proyecto anterior:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // local
    this.isVisibleToUser = isVisibleToUser;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // when the fragment becomes visible
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // exit the fragment
      updateDone = false;
    }
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // update indicator
    afterViewsDone = false;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView: %s", getParentInfos()));
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // update
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

El método de [actualización] es el siguiente:


  // update fragment
  protected void update() {
    // retrieve the activity and session
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // we ask the child class to update itself
    updateFragment();
}

Según el código anterior, cuando se ejecuta el método [update] de un fragmento, el fragmento es visible. Esto es importante porque significa que el método [Fragment.getActivity] devuelve entonces una referencia a la actividad de la aplicación (véase la sección 1.10.8), que a su vez proporciona acceso a la sesión.

  • líneas 4-10: inicializar la actividad y la sesión si aún no se han inicializado;
  • línea 12: se llama al método [updateFragment] de la clase hija. Cuando este método se ejecuta, los campos [activity] y [session] a los que tiene acceso ya han sido inicializados;

1.11.4. La clase [PlaceholderFragment]

  

La clase [PlaceholderFragment] está estructurada de la siguiente manera:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // data
  private boolean initDone;
 
  // data
  private String text;
  private int visitCount;
 
  // fragment number
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // constructor
  public PlaceholderFragment() {
    super();
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
 ...
  }
 
  // update fragment
  public void updateFragment() {
  ...
  }
 
}
  • Línea 10: La clase [PlaceholderFragment] extiende la clase [AbstractFragment]. Con esta arquitectura, escribir un fragmento implica:
    • escribir el método [@AfterViews], que se utiliza para inicializar el fragmento durante su primer ciclo de vida o para restablecerlo si previamente se ha producido un [onDestroyView]. La línea 39 es necesaria para gestionar correctamente el ciclo de vida del fragmento;
    • escribir el método [updateFragment], que actualiza el fragmento justo antes de que se muestre. Este método puede utilizar la sesión de su clase padre;
    • escribiendo los manejadores de eventos del fragmento. Esto es lo que haremos en futuros proyectos;

Los métodos [@AfterViews] y [updateFragment] siguen siendo los mismos que en el proyecto anterior:


@AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // display current text
    textViewInfo.setText(text);
  }
 
  // update fragment
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // increment visit count
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
  }
 
  // local info for logs
  protected String getLocalInfo() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • Líneas 7 y 23: En los registros, mostramos información de la clase padre usando el método heredado [getParentInfos];

1.11.5. La clase [Vue1Fragment]

  

La clase [Vue1Fragment] tiene la misma estructura que la clase [PlaceholderFragment]:


package exemples.android;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // UI elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextName;
 
  // data
  private int visitCount;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }
 
  // event handler
  @Click(R.id.buttonValider)
  protected void doValidate() {
    // display the entered name
    Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // local information for logs
  protected String getLocalInfo() {
    return String.format("numVisit=%s", numVisit);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // increment visit count
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // display the visit count
    Toast.makeText(getActivity(), String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
  }
}
  • Línea 9: La clase [Vue1Fragment] extiende la clase [AbstractFragment];
  • líneas 18-26: El método [@AfterViews] no tiene nada interesante que hacer. Todavía debe ser escrito para establecer el booleano [afterViewsDone] a verdadero, ya que esta información es utilizada por la clase padre;
  • líneas 42-49: el método [updateFragment] consiste en mostrar un breve mensaje con el número de visita (línea 48) e incrementar este número en la sesión (líneas 44-46);

Invitamos a los lectores a probar este nuevo proyecto.

Utilizaremos esta arquitectura en todos los proyectos futuros:

  • una actividad y n fragmentos;
  • todos los fragmentos extienden la clase [AbstractFragment];
  • los datos que se compartirán entre fragmentos y entre fragmentos y la actividad se colocan en la clase [Session];

1.11.6. Asociación pestaña/fragmento

En la clase [MainActivity], que gestiona las pestañas, se escribe lo siguiente:


// the tab bar is also associated with the fragment container
// i.e., tab #i displays fragment #i from the container
tabLayout.setupWithViewPager(mViewPager);

La línea 3 asocia el gestor de pestañas con el contenedor de fragmentos. Hemos visto una consecuencia de esta asociación: cuando el usuario hace clic en la pestaña #i, el contenedor de fragmentos muestra el fragmento #i. No hemos visto lo contrario: cuando pedimos al contenedor de fragmentos que muestre el fragmento #i, la pestaña #i se selecciona automáticamente.

Para ilustrar este comportamiento, añadiremos las opciones [Fragmento 1, Fragmento 2, ...] al menú actual. Cuando el usuario haga clic en la opción [Fragmento i], pediremos al contenedor de fragmentos que muestre el fragmento #i. Entonces veremos si el fragmento #i ha sido seleccionado o no.

Este paso comienza con la modificación del menú de la aplicación:

 

El contenido del archivo [res/menu/menu_main.xml] cambia como sigue:


<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"/>
  <item android:id="@+id/fragment5"
        android:title="@string/fragment5"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>
  • líneas 9-28: las cinco nuevas opciones de menú;
  • las etiquetas de las opciones (líneas 10, 14, 18, 22, 26) se definen en el archivo [res/values/strings.xml] [2]:

<resources>
  <string name="app_name">Example-10</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- view 1 -->
  <string name="view1_title">View #1</string>
  <string name="txt_name">What is your name?</string>
  <string name="btn_submit">Submit</string>
  <string name="btn_view2">View 2</string>
  <!-- menu -->
  <string name="fragment1">Fragment 1</string>
  <string name="fragment2">Fragment 2</string>
  <string name="fragment3">Fragment 3</string>
  <string name="fragment4">Fragment 4</string>
  <string name="fragment5">Fragment 5</string>
</resources>

El resultado visual es el siguiente:

  

El manejo de los clics para estas opciones de menú se realiza en la clase [MainActivity]:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // process menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "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;
      }
      case R.id.fragment5: {
        showFragment(4);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
    }
  }
  • Línea 2: El método [onOptionsItemSelected] es llamado cuando se hace click en una de las opciones del menú;
  • línea 8: recuperamos el ID de la opción pulsada;
  • líneas 9-36: los diferentes casos son tratados por un sentencia switch;
  • líneas 16-36: al hacer clic en la opción [Fragmento i] se llama al método [showFragment(i-1)] en las líneas 41-45;
  • línea 43: se pide al contenedor de fragmentos que muestre el fragmento solicitado;
  • línea 42: primero comprobamos que es posible (condición 1) y que es necesario (condición 2);

Invitamos a los lectores a probar esta nueva versión. Observamos que cuando solicitamos la visualización del fragmento #i, efectivamente se visualiza y la pestaña #i se selecciona a su vez.

Ahora que hemos visto cómo funciona la asociación pestaña/fragmento, veremos otro caso: uno en el que la gestión de pestañas está desacoplada de la gestión de fragmentos. Este es el caso, por ejemplo, cuando hay menos pestañas que fragmentos. Para ilustrar este nuevo caso de uso, construiremos un nuevo proyecto.

1.12. Ejemplo-11: Fichas separadas de fragmentos

1.12.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-10] en [Ejemplo-11]:

1.12.2. Objetivos

La nueva aplicación tendrá dos pestañas:

  • La primera pestaña siempre mostrará el fragmento [Vista1];
  • la segunda pestaña mostrará un fragmento seleccionado en el menú;

Image

  • en [1], el fragmento [View1];
  • en [2], el fragmento [PlaceholderFragment] seleccionado por el usuario;
  • en [3], se siguen contando las visitas;

1.12.3. La sesión

  

La nueva sesión será la siguiente:


package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // number of [PlaceholderFragment] fragments displayed in the second tab
  private int numFragment;
 
  // getters and setters
...
}
  • Línea 10: Manejaremos los clics de las pestañas nosotros mismos. Cuando se hace clic en una pestaña, necesitamos cargar el fragmento que se mostró la última vez que se seleccionó. El campo [numFragment] almacenará el número de fragmento de la pestaña nº 2, un número en el rango [0, Fragments_COUNT-2]. Cuando se haga clic en la pestaña nº 2, recuperaremos de la sesión el número de fragmento que se mostrará;

1.12.4. El menú

  

El menú [res / menu / menu_main.xml] cambia de la siguiente manera:


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

La pestaña nº 2 mostrará uno de los cuatro fragmentos de las líneas 9-24. El quinto fragmento es el fragmento [Vue1Fragment], que siempre se mostrará en la pestaña nº 1.

1.12.5. La clase [MainActivity]

La clase [MainActivity] debe ahora gestionar las pestañas y la navegación entre ellas, cosa que no hacía anteriormente. Su código cambia como sigue:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // display View1
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
 
    // initially, there is only one tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("View 1");
    tabLayout.addTab(tab);
 
    // event handler
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // A tab has been selected - change the fragment displayed by the fragment container
        ...
      }
 
      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
 
      }
 
      @Override
      public void onTabReselected(TabLayout.Tab tab) {
 
      }
    });
 
...
 
}
  • línea 17: El primer fragmento mostrado por el contenedor de fragmentos será el fragmento [Vue1Fragment]. Por diseño, este será el último fragmento del contenedor;
  • líneas 20-22: como no hemos establecido una asociación entre las pestañas y el contenedor de fragmentos, tenemos que gestionar las pestañas nosotros mismos. Inicialmente, la barra de pestañas [tabLayout] de la línea 3 no tiene pestañas;
  • línea 20: creamos la primera pestaña;
  • línea 21: le damos un título. En los ejemplos anteriores, los títulos de las pestañas eran los mismos que los de los fragmentos. Ahora ya no es así. Como resultado, eliminamos el método [getPageTitle] del gestor de fragmentos. Ya no lo necesitamos:

    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Tab #%s", (position + 1));
}
  • línea 22: la pestaña creada se añade a la barra de pestañas. Nuestra barra de pestañas tiene ahora una pestaña. ¿Qué muestra esta pestaña? Es importante entender que las pestañas y los fragmentos son dos conceptos distintos. El fragmento mostrado es siempre el elegido por el contenedor de fragmentos. Si cambias de pestaña y no pides al contenedor que cambie el fragmento mostrado, no pasa nada: se sigue mostrando el mismo fragmento, pero la pestaña seleccionada ha cambiado. Así que aquí, el fragmento mostrado es el elegido en la línea 17: el fragmento [Vue1Fragment];
  • líneas 26-30: el método a escribir para manejar el cambio de tabulador del usuario;

El método [onTabSelected] de las líneas 26-30 se activa siempre que hay un cambio de pestaña (si el usuario hace clic en una pestaña que ya está seleccionada, no pasa nada). Su código es el siguiente:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("tabs", "onTabSelected");
        }
        // A tab has been selected - change the fragment displayed by the fragment container
        // tab position
        int position = tab.getPosition();
        // number of the fragment to display
        int fragmentNumber;
        switch (position) {
          case 0:
            // fragment number [View1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // fragment number [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // display fragment
        mViewPager.setCurrentItem(numFragment);
}
  • línea 8: recuperamos la posición de la pestaña sobre la que se ha hecho clic. Aquí, obtendremos un número 0 o 1;
  • líneas 12-15: si se ha hecho clic en la primera pestaña, nos preparamos para mostrar el fragmento [Vue1Fragment];
  • líneas 16-18: en otros casos (pestaña #2 pulsada), nos preparamos para volver a mostrar el fragmento que se mostró la última vez que se seleccionó la pestaña #2. Su ID se almacenó entonces en la sesión de la aplicación;
  • línea 21: pedimos al contenedor de fragmentos que muestre el fragmento deseado;

Ahora vamos a ver la gestión de las opciones del menú (todavía en [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // process menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "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;
}
  • líneas 16-31: manejo de las 4 opciones del menú. Cada manejador llama al método [showFragment] con el número de fragmento a mostrar;

El método [showFragment] es el siguiente:


  // tab #2
  private TabLayout.Tab tab2 = null;
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // if the second tab does not yet exist, create it
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // Set the title of the second tab
      tab2.setText(String.format("Fragment #%s", (i + 1)));
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
      // the number of the displayed fragment is saved in the session
      session.setNumFragment(i);
      // Select tab 2—do nothing if it is already selected
      tab2.select();
    }
}
  • Recuerde que cuando se inicia la aplicación, sólo hay una pestaña;
  • línea 2: una referencia a la pestaña nº 2, inicialmente null;
  • línea 5: las condiciones de visualización no han cambiado con respecto a la versión anterior;
  • líneas 7-10: si la pestaña nº 2 aún no existe, se crea (línea 8) y se añade a la barra de pestañas (línea 9);
  • línea 12: el número del fragmento que se va a mostrar se coloca en el título de la segunda pestaña, con numeración a partir de 1;
  • línea 14: se muestra el fragmento deseado;
  • línea 16: su número se almacena en la sesión;
  • línea 18: se selecciona la pestaña #2. Si ya estaba seleccionada, no pasará nada: no se ejecutará el método [onTabSelected]. Si aún no estaba seleccionada, se ejecutará el método [onTabSelected]. Este método ordena entonces al contenedor de fragmentos que muestre el fragmento ya mostrado en la línea 14. Una simple comprobación en el método [onTabSelected] evita esta situación:

        // display fragment only if necessary
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Invitamos a los lectores a probar esta nueva versión.

1.12.6. Mejoras

Ahora tenemos una sólida comprensión de los fragmentos, su ciclo de vida, el concepto de adyacencia de fragmentos y su relación con la barra de pestañas. También tenemos una arquitectura robusta que acaba de pasar la prueba del Ejemplo 11:

  • una actividad y n fragmentos;
  • todos los fragmentos extienden la clase [AbstractFragment];
  • los datos que se compartirán entre fragmentos y entre fragmentos y la actividad se colocan en la clase [Session];

En un nuevo proyecto, aclararemos las relaciones entre la actividad y los fragmentos añadiendo una interfaz.

1.13. Ejemplo 12: Definir las relaciones entre la actividad y los fragmentos

En este ejemplo, queremos definir las relaciones mínimas entre la actividad y los fragmentos. Para ello, utilizaremos

  • una interfaz [IMainActivity] que define lo que los fragmentos pueden solicitar a la actividad;
  • una clase abstracta [AbstractFragment] que definirá el estado y los métodos que debe tener cada fragmento;

1.13.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-11] en [Ejemplo-12] siguiendo el procedimiento de la sección 1.4. Obtenemos el siguiente resultado:

1.13.2. La interfaz [IMainActivity]

De los ejemplos anteriores se deduce claramente que los fragmentos necesitan acceder a la sesión instanciada por la actividad. Además, aunque no es visible en estos ejemplos, es de esperar que los manejadores de eventos en los fragmentos a veces resulten en un cambio de vista. Se pedirá a la actividad que realice este cambio. La interfaz [IMainActivity] podría entonces tener este aspecto:

  

package exemples.android;
 
public interface IMainActivity {
 
  // access to the session
  Session getSession();
 
  // view change
  void navigateToView(int position);
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
}

Línea 12: Nótese la presencia de una constante que antes estaba en la clase [MainActivity]. Queremos reducir el acoplamiento entre los fragmentos y la actividad y limitarlo a un acoplamiento entre [AbstractFragment] y [IMainActivity]. De este modo, la actividad podrá denominarse de otro modo que [MainActivity]. Dado que la constante [IS_DEBUG_ENABLED] se utiliza en los fragmentos, se traslada a la interfaz [IMainActivity].

1.13.3. La clase abstracta [AbstractFragment]

La clase abstracta [AbstractFragment] cambia muy poco:


  // data accessible to child classes
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
...
  // update fragment
  protected void update() {
    // retrieve the activity and session
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // we ask the child class to update itself
    updateFragment();
}
  • Líneas 6 y 7: mantenemos dos tipos de referencias a la actividad:
    • línea 6: una referencia a la actividad que implementa la interfaz [IMainActivity];
    • línea 7: una referencia a la actividad que hereda de la clase Android [Activity]. Este es el caso para todas las actividades;

Estas dos referencias apuntan naturalmente al mismo objeto. Sin embargo, este objeto se ve como dos tipos diferentes. Esto evitará la conversión de tipos en tiempo de ejecución;

  • línea 14: recuperamos una referencia a la actividad utilizando el método [getActivity];
  • línea 15: si esta referencia no es nula, entonces podemos acceder a la sesión;
  • Líneas 16-17: Almacenamos la actividad como una implementación de la interfaz [IMainActivity] y la sesión;

1.13.4. Modificación del gestor de fragmentos

El adaptador de fragmentos [SectionsPagerAdapter] de la clase [MainActivity] se modifica en un punto: en lugar de gestionar fragmentos de tipo [Fragment], ahora gestiona fragmentos de tipo [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private AbstractFragment[] fragments;
    // fragment number
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialize the fragment array
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // a fragment of +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }
 
    // fragment #position
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }
 
    // returns the number of fragments managed
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. Modificación de la clase [MainActivity]

La clase [MainActivity] debe implementar la interfaz [IMainActivity]:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
 
...
  // session injection
  @Bean(Session.class)
  protected Session session;
...
  // session getter
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // display the view at position
    if (mViewPager.getCurrentItem() != position) {
      // display fragment
      mViewPager.setCurrentItem(position);
    }
  }
 
  • líneas 10-12: el método [getSession] ya existía;
  • líneas 15-22: el método [navigateToView] muestra el fragmento #[posición];
  • línea 17: comprobamos si hay algo que hacer;
  • línea 19: aparece el fragmento #[posición];

En este punto, ejecute la aplicación. Debería funcionar.

1.13.6. Modificación de la visualización de fragmentos en [MainActivity]

Actualmente, la clase [MainActivity] muestra un fragmento utilizando la sentencia:


    // display View1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Dado que el método [navigateToView] hace lo mismo, sustituya este tipo de declaración en todas partes (2 lugares) por:

navigateToView(...);

A continuación, ejecute la aplicación. Debería seguir funcionando.

1.13.7. Conclusión

A partir de ahora, utilizaremos siempre la arquitectura anterior:

  • una actividad que implemente la interfaz [IMainActivity];
  • que extienden la clase [AbstractFragment], lo que requiere que implementen el método [updateFragment]. Estos también deben tener un método [@AfterViews] en el que establezcan el booleano [afterViewsDone] a verdadero;
  • una sesión que encapsula los datos que se compartirán entre los fragmentos y la actividad;

1.14. Ejemplo-13: Ejemplo-05 con fragmentos

En el proyecto [Example-05] introdujimos la navegación entre vistas. En aquel momento, se trataba de la navegación entre actividades: 1 vista = 1 actividad. Aquí proponemos tener una única actividad con múltiples vistas de tipo [AbstractFragment].

1.14.1. Creación del proyecto

Duplicamos el proyecto anterior [Ejemplo-12] en [Ejemplo-13] siguiendo el procedimiento de la sección 1.4. Obtenemos el siguiente resultado:

1.14.2. Estructura del proyecto

Empezaremos a utilizar paquetes para organizar el código. Por ahora, podemos distinguir dos dominios distintos:

  • gestión de actividades;
  • gestión de fragmentos;

Creamos dos paquetes para ellos: [examples.android.activity] y [examples.android.fragments]:

 

Hacemos lo mismo para crear el paquete [examples.android.fragments]:

En [8], creamos un tercer paquete llamado [architecture] en el que colocaremos las entidades [IMainActivity, AbstractFragment, Session, MyPager], que son los bloques de construcción de la arquitectura de nuestra app. Esto sirve como recordatorio de que hemos hecho una elección arquitectónica específica. A continuación, mueve los elementos existentes del proyecto como se muestra en [9]. Cada movimiento debe confirmarse haciendo clic en el botón [Refactorizar].

En este punto, compile la aplicación. Tenemos los siguientes errores en [MainActivity]:

 

Al mover clases a paquetes, Android Studio realizó los cambios necesarios en el código de la aplicación (líneas 18-21, por ejemplo). Las clases referenciadas en las líneas 15 y 17 no fueron movidas. Son generadas por la librería Android Annotations. Para estas clases, debe cambiar el importaciones manualmente. Por lo tanto, estas líneas se convierten en:

 

Una vez hecho esto, no hay más errores de compilación. Ejecute la aplicación. Entonces verá el siguiente error:

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}: 

Este error proviene del manifiesto de la aplicación:

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:label="@string/app_name"
      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>

Las líneas 3 y 12 especifican que la actividad designada es [examples.android.MainActivity_]. Sin embargo, dado que la actividad se ha trasladado al paquete [activity], la línea 12 debe ser ahora:


      android:name=".activity.MainActivity_"

Tenga en cuenta el . antes de [actividad]. Una vez más, Android Studio no pudo actualizar el manifiesto porque hace referencia a una clase Android Annotations que no se ha movido. El uso de la biblioteca AA por lo tanto viene con una serie de inconvenientes.

1.14.3. Limpieza del proyecto

En el nuevo proyecto:

  • ya no hay pestañas, botones flotantes ni menús;
  • los fragmentos [PlaceholderFragment] desaparecen. La aplicación gestionará dos fragmentos: [Vue1Fragment], que ya tenemos, y [Vue2Fragment], que tendremos que crear;
  • la sesión ya no es la misma;

1.14.3.1. Limpieza de fragmentos

Eliminar la clase [PlaceHolderFragment] [1]:

 

Del mismo modo, elimine la vista [res/layout/fragment_main.xml] asociada a este fragmento [2].

1.14.3.2. Limpieza de la sesión

Actualmente, la sesión es la siguiente:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // Fragment ID of type [PlaceholderFragment] displayed in the second tab
  private int fragmentCount;
 
  // getters and setters
 
  public int getNumVisit() {
    return numVisit;
  }
 
  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
 
  public int getNumFragment() {
    return numFragment;
  }
 
  public void setNumFragment(int numFragment) {
    this.numFragment = numFragment;
  }
}

No vamos a salvar nada de esta sesión.

Compile el proyecto. Las líneas que causan errores son las que utilizaban el contenido de la sesión. Elimínalas. En la clase [Vue1Fragment], eliminamos también la variable [numVisit] del código, que pasa a ser el siguiente:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // Visual interface elements
  @ViewById(R.id.editTextName)
  protected EditText editTextName;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event handler
  @Click(R.id.buttonValider)
  protected void doValidate() {
    // display the entered name
    Toast.makeText(getActivity(), String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}

1.14.3.3. Eliminar las pestañas, el botón flotante y el menú

La eliminación de las pestañas y el botón flotante se realiza en dos lugares:

  • en la vista [res/layout/activity-main.xml], que define estos elementos y su colocación en la vista;
  • en el código de actividad [MainActivity];

También se elimina el menú en dos lugares:

  • en la vista [res/menu/menu-main.xml], que define las opciones del menú;
  • en el código de actividad [MainActivity];

El código de la vista [res / layout / activity-main.xml] es actualmente el 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.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
 
  </android.support.design.widget.AppBarLayout>
 
  <examples.android.architecture.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
 
  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email"/>
 
</android.support.design.widget.CoordinatorLayout>
  • Eliminar líneas [28-31, 41-47];
  • elimine también la barra de herramientas de las líneas 18-24;

El código del menú [res / menu / menu_main.xml] es actualmente 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/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>
  • Eliminaremos las líneas 9-24. Esto deja una opción que no utilizaremos. Simplemente para proporcionar un ejemplo de una declaración de opción de menú que se puede replicar a través de copiar / pegar;

En la clase [MainActivity], elimina todo lo que haga referencia a las pestañas, el botón flotante, la barra de herramientas y el menú. La forma más fácil de encontrar estas referencias es eliminar sus declaraciones:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

y recompilar la aplicación. Las líneas con errores son las que hacen referencia a los elementos que faltan. Así que borra todas esas líneas. Además, modifica el gestor de fragmentos para que ya no haga referencia al fragmento [PlaceholderFragment] que hemos eliminado:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private AbstractFragment[] fragments;
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // returns the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • Líneas 7-10: Hemos eliminado toda la generación de fragmentos;

En este punto, ya no debería haber errores de compilación. En la clase [MainActivity], hemos llegado al siguiente código intermedio:


package exemples.android.activity;
 
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.IMainActivity;
import examples.android.architecture.MyPager;
import examples.android.architecture.Session;
import examples.android.fragments.View1Fragment_;
import org.androidannotations.annotations.*;
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
 
  // session injection
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // constructor
  public MainActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }
 
  @AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
 
    // toolbar - this is where the app name is displayed
    setSupportActionBar(toolbar);
 
    // 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 provided by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // Disable swiping between fragments
    mViewPager.setSwipeEnabled(false);

    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // display View1
    navigateToView(FRAGMENTS_COUNT - 1);
 
  }
 
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }
 
  // session getter
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // display the view at position
    if (mViewPager.getCurrentItem() != position) {
      // display fragment
      mViewPager.setCurrentItem(position);
    }
  }
 
  // the fragment manager
  // this is what we call to retrieve the fragments to display in the main view
  // Must define the [getItem] and [getCount] methods—the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private AbstractFragment[] fragments;
 
    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // returns the number of managed fragments
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Hay algunos cambios más que hacer:

  • suprimir la línea 31, que ya no es necesaria;
  • línea 33: establece la adyacencia del fragmento en 1;
  • línea 76: navegue hasta la vista 0. Esta será la primera que se muestre;
  • línea 108: inicializar el array con el fragmento [Vue1Fragment_]:

    // the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Así que sólo tenemos un fragmento. Ejecute la aplicación. Deberías obtener el siguiente resultado:

Image

El botón [Validar] debería funcionar.

1.14.4. Creación de fragmentos y vistas asociadas

La aplicación tendrá dos vistas, las del proyecto [Example-05]. Ya tenemos la vista [vue1.xml] en el proyecto actual. Ahora duplicaremos [vue2.xml] de [Ejemplo-05] a [Ejemplo-12] (abra ambos proyectos y copie y pegue entre ellos).

 
  • En [1], la nueva vista. Cuando intentamos editarla, aparecen errores [2]. Tenemos que modificar el fichero [strings.xml] [3] para añadir las cadenas a las que hace referencia esta nueva vista:

<resources>
  <string name="app_name">Example-13</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- view 1 -->
  <string name="view1_title">View 1</string>
  <string name="txt_name">What is your name?</string>
  <string name="btn_submit">Submit</string>
  <!-- view 2 -->
  <string name="btn_view2">View #2</string>
  <string name="view2_title">View #2</string>
  <string name="btn_view1">View #1</string>
</resources>

Duplicamos la clase [View1Fragment] en [View2Fragment]:

  

y modifica el código copiado como sigue


package examples.android.fragments;
 
import android.util.Log;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • línea 9: el fragmento está asociado a la vista [res/layout/view2.xml];
  • línea 10: la clase extiende la clase abstracta [AbstractFragment];
  • líneas 12-20: el método necesario [@AfterViews];
  • líneas 23-25: el método [updateFragment] requerido;

1.14.5. Implementación de fragmentos y navegación entre ellos

La actividad gestionará ahora dos fragmentos. Su clase [SectionsPagerAdapter] se actualiza como sigue:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
 
    ...
}

La interfaz [IMainActivity] gestiona la navegación entre vistas mediante su método [navigateToView]. Manejaremos el clic en el botón [Vista 2] en el fragmento [Vue1Fragment]:


package examples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // Visual interface elements
  @ViewById(R.id.editTextName)
  protected EditText editTextName;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event handlers ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValidate() {
    // display the entered name
    Toast.makeText(activity, String.format("Hello %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • líneas 37-40: el método [showVue2] maneja el evento 'click' en el botón [View #2];
  • línea 39: la navegación se realiza utilizando el método [navigateToView] de la actividad. Recordemos que la actividad se almacenó en la clase padre como:

  // activity
protected IMainActivity mainActivity;

y que esta actividad ya ha sido inicializada al entrar en cualquier manejador de eventos.

  • línea 34: la sentencia utiliza la variable [activity] de la clase padre, que es una referencia a la actividad como instancia del tipo [Activity] de Android;

protected Activity activity;

Encontramos un código similar para el fragmento [Vue2Fragment]:


package examples.android.fragments;
 
import android.util.Log;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event handlers ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showView1() {
    mainActivity.navigateToView(0);
  }
 
  // Update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Líneas 24-27: El método [showVue1] maneja el evento 'click' en el botón [View 1];

Ejecute el proyecto y compruebe que la navegación entre vistas funciona.

1.14.6. Definición de la sesión

La aplicación funciona de la siguiente manera:

  • Introduzca un nombre en Vista 1;
  • Muestra este nombre en la Vista 2;

Para permitir que la Vista 1 pase el nombre introducido a la Vista 2, utilizaremos la siguiente sesión;


package examples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // name
  private String name;
 
  // getters and setters
...
}
  • línea 8: el nombre introducido;

La clase [MainActivity] inicializará la sesión de la siguiente manera:


  // session injection
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // initialize session
    session.setName("");
}

1.14.7. Código final de los fragmentos

En el fragmento [Vue1Fragment], modificamos el código del manejador de clic del botón [Validar]:


package examples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // UI elements
  @ViewById(R.id.editTextName)
  protected EditText editTextName;
 
...
  // event handlers ----------------------------------
 
  @Click(R.id.buttonValider)
  protected void validate() {
    // store the entered name
    String name = editTextName.getText().toString();
    // display it
    Toast.makeText(activity, name, Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showView2() {
    // Store the entered name in the session
    session.setName(editTextName.getText().toString());
    // navigate to view #2
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
 
  }
}
  • líneas: 31-37: manejar el clic en el botón [Ver #2];
  • línea 34: antes de navegar a la vista 2, almacenamos el nombre introducido en la sesión para que la nueva vista pueda acceder a él;

La vista [View2Fragment] evoluciona del siguiente modo:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  // UI components
  @ViewById(R.id.textViewHello)
  protected TextView textViewHello;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event handlers ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showView1() {
    mainActivity.navigateToView(0);
  }
 
  // Update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String name = session.getName();
    // display it
    textViewHello.setText(String.format("Hello %s!", name));
  }
}

Cuando se muestre la vista #2, se debe mostrar el nombre introducido en la vista #1. Sabemos que inmediatamente después de que se muestre, se ejecutará su método [updateFragment]. Es por tanto en este método (líneas 36-42) donde podemos colocar el código para mostrar el nombre.

  • líneas 16-17: declaración del único componente visual de la vista;
  • Línea 39: El nombre introducido en la Vista 1 se recupera de la sesión;
  • Línea 41: Se actualiza la etiqueta [textViewBonjour];

Ejecute el proyecto y compruebe que funciona.

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

En el fragmento [Vue1Fragment], el método [@AfterViews] es el siguiente:


  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Este método es incompleto. De hecho, siempre debemos tener en cuenta el caso en que el fragmento se recicla después de una operación [onDestroyView]. En este caso, la vista del Fragmento 1 se regenera, y cualquier nombre que se haya introducido previamente desaparecerá de la vista. No queremos que esto ocurra. Actualmente, el nombre introducido sigue apareciendo porque la adyacencia de los fragmentos del fragmento 1 significa que el ciclo de vida del fragmento [Vue1Fragment] sólo se ejecuta una vez. Sin embargo, es preferible tener en cuenta que el fragmento se recicla.

Hay varias formas de resolver este problema:

  • podemos aprovechar que el método [update] se ejecuta sistemáticamente cada vez que se muestra el fragmento para actualizar el nombre introducido;
  • puede realizar esta actualización sólo cuando se vuelva a ejecutar el método [@AfterViews]. Adoptaremos este último enfoque;

Modificamos el código en [View1Fragment] como sigue:


    // UI elements
    @ViewById(R.id.editTextNom)
    protected EditText editTextName;
 
    // data
    private String name;
 
    @AfterViews
    protected void afterViews() {
        // memory
        afterViewsDone = true;
        // log
        if (isDebugEnabled) {
            Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // (re)initialize the displayed text
        editTextName.setText(name);
    }
 
    // event handlers ----------------------------------
 
...
 
    @Click(R.id.buttonVue2)
    protected void showView2() {
        // we store the entered name so we can retrieve it if the fragment is recycled
        name = editTextName.getText().toString();
        // store the entered name in the session
        session.setName(name);
        // navigate to view #2
        activity.navigateToView(1);
}
  • línea 27: como estamos a punto de dejar la vista 1 por la vista 2, almacenamos el nombre introducido;
  • línea 17: cada vez que se ejecuta el ciclo de vida del fragmento, se vuelve a mostrar el último nombre introducido;

For the [View2Fragment] fragment, the existing code is sufficient:


  // UI components
  @ViewById(R.id.textViewHello)
  protected TextView textViewHello;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String name = session.getName();
    // display it
    textViewHello.setText(String.format("Hello %s!", name));
}
  • El único componente visual de la vista (línea 3) se actualiza cada vez que se muestra la vista (línea 21). Por lo tanto, el método [@AfterViews] no tiene nada que añadir;

1.14.9. Conclusión

Llegados a este punto, hemos demostrado una vez más la pertinencia de nuestra arquitectura:

  • una actividad que implemente la interfaz [IMainActivity];
  • que extienden la clase [AbstractFragment], lo que requiere que implementen el método [updateFragment]. Estos también deben tener un método [@AfterViews] en el que establezcan el booleano [afterViewsDone] a verdadero;
  • una sesión que encapsula los datos que se compartirán entre los fragmentos y la actividad;

1.15. Ejemplo 14: Una arquitectura de dos capas

Construiremos una aplicación de vista única con la siguiente arquitectura:

1.15.1. Creación del proyecto

Duplicamos el proyecto anterior [Ejemplo-12] en [Ejemplo-13] siguiendo el procedimiento de la sección 1.4. Obtenemos el siguiente resultado:

1.15.2. La vista [vista1]

La aplicación sólo tendrá una vista [view1.xml]. Por lo tanto, eliminaremos la otra vista [view2.xml] junto con su fragmento asociado:

 

Compilar la aplicación. Los errores aparecen en [MainActivity]:

 

Corrija la línea 4 a continuación en el gestor de fragmentos [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

La línea 4 pasa a ser:


    // the fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Elimine las importaciones que ya no sean necesarias [Ctrl-Shift-O]. Ya no debería haber errores de compilación. Ejecuta el proyecto: debería aparecer la vista #1. Ahora vamos a modificarla.

Crearemos la vista [vue1.xml] que generará números aleatorios:

 

Sus componentes son los siguientes:

No.
ID
Type
Role
1
edtNbAleas
EditText
number of random numbers to generate in the integer interval [a,b]
2
edtA
EditText
value of a
2
edtB
EditText
value of b
4
btnExecute
Button
starts number generation
5
ListView
lstAnswers
list of generated numbers in reverse order of generation. The most recently generated number is displayed first;

Su código XML es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/RelativeLayout1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="20dp"
                android:orientation="vertical" >
 
  <TextView
    android:id="@+id/txt_Title2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:text="@string/aleas"
    android:textAppearance="?android:attr/textAppearanceLarge" />
 
  <TextView
    android:id="@+id/txt_nbaleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/txt_Titre2"
    android:layout_marginTop="20dp"
    android:text="@string/txt_nbaleas" />
 
  <EditText
    android:id="@+id/edt_nbaleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_nbaleas"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_nbaleas"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_errorNbAleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/edt_nbaleas"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_nbaleas"
    android:text="@string/txt_errorNbAleas"
    android:textColor="@color/red" />
 
  <TextView
    android:id="@+id/txt_a"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/txt_nbaleas"
    android:layout_marginTop="20dp"
    android:text="@string/txt_a" />
 
  <EditText
    android:id="@+id/edt_a"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_a"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_b"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_a"
    android:text="@string/txt_b" />
 
  <EditText
    android:id="@+id/edt_b"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_b"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_errorInterval"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/edt_b"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_b"
    android:text="@string/txt_errorInterval"
    android:textColor="@color/red" />
 
 
  <Button
    android:id="@+id/btn_Execute"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/txt_a"
    android:layout_marginTop="20dp"
    android:text="@string/btn_execute" />
 
 
  <TextView
    android:id="@+id/txt_Answers"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/btn_Execute"
    android:layout_marginTop="30dp"
    android:text="@string/list_reponses"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:textColor="@color/blue" />
 
  <ListView
    android:id="@+id/lst_answers"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/txt_Answers"
    android:layout_marginTop="40dp"
    android:background="@color/wheat"
    android:clickable="true"
    tools:listitem="@android:layout/simple_list_item_1" >
  </ListView>
 
</RelativeLayout>

La vista anterior utiliza etiquetas definidas en [res/valores/cadenas.xml]:


<resources>
  <string name="app_name">Example-14</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- view 1 -->
  <string name="view1_title">View 1</string>
  <string name="list_answers">List of answers</string>
  <string name="btn_execute">Execute</string>
  <string name="random_numbers">Generate N random numbers</string>
  <string name="txt_n_random">Value of N:</string>
  <string name="txt_a">"Generation interval [a,b], a: "</string>
  <string name="txt_b">"b: "</string>
  <string name="txt_dummy">Dummy</string>
  <string name="txt_errorNbRandom">Enter an integer >=1</string>
  <string name="txt_errorInterval">The interval bounds must be integers and b ≥ a</string>
</resources>

Los colores utilizados en [vue1.xml] se definen en el archivo [res/values/colores.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <!-- app colors -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
  <color name="floral_white">#FFFAF0</color>
</resources>

1.15.3. La sesión

  

Como aquí sólo hay un fragmento, no es necesario planificar la comunicación entre fragmentos. Por lo tanto, la sesión estará vacía:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}

En este punto, compile la aplicación. Aparecerán errores en las líneas que utilizaron elementos de la sesión ahora vacía. Elimine estas líneas y compruebe que la compilación ya no produce errores.

1.15.4. El fragmento [Vue1Fragment]

  

Modificamos el fragmento [Vue1Fragment] existente del siguiente modo:


package exemples.android.fragments;
 
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // Visual interface elements
  @ViewById(R.id.lst_reponses)
  protected ListView listAnswers;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNumberOfElements;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorRandom;
  @ViewById(R.id.txt_errorInterval)
  protected TextView txtErrorInterval;
 
  // list of responses to a command
  private List<String> responses = new ArrayList<>();
  // ListView adapter
  private ArrayAdapter<String> adapterResponses;
 
  // user input
  private int numberOfRandomNumbers;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // hide error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorInterval.setVisibility(View.INVISIBLE);
  }
 
  @Click(R.id.btn_Execute)
  void doExecute() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorInterval.setVisibility(View.INVISIBLE);
    // Check the validity of the entries
    if (!isPageValid()) {
      return;
    }
  }
 
  // Check the validity of the entered data
  private boolean isPageValid() {
...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Aquí sólo hay un fragmento cuyo ciclo de vida se ejecutará una sola vez, al inicio de la aplicación. Por esta razón, el método [@AfterViews] (líneas 46-57) y el método [updateFragment] (líneas 75-81) se ejecutarán sólo una vez al inicio de la aplicación;
  • líneas 55-56: ocultamos los dos mensajes de error de la vista (mostrados a continuación) [1-2];
 
  • líneas 59-60: el método que se ejecuta al hacer clic en el botón [Ejecutar];
  • líneas 71-73: se comprueba la validez de las entradas;

El método [isPageValid] es el siguiente:


  // the inputs
  private int nbRandom;
  private int a;
  private int b;
 
...
 
// Check the validity of the entered data
  private boolean isPageValid() {
    // Enter the number of random numbers
    nbRandom = 0;
    Boolean error;
    int numberOfErrors = 0;
    try {
      nbRandom = Integer.parseInt(edtNbRandom.getText().toString());
      error = (nbRandom = < 1);
    } catch (Exception ex) {
      error = true;
    }
    // error?
    if (error) {
      errorCount++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // Enter a
    a = 0;
    error = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      error = true;
    }
    // error?
    if (error) {
      errorCount++;
      txtErrorInterval.setVisibility(View.VISIBLE);
    }
    // Set b
    b = 0;
    error = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      error = b < a;
    } catch (Exception ex) {
      error = true;
    }
    // error?
    if (error) {
      errorCount++;
      txtErrorInterval.setVisibility(View.VISIBLE);
    }
    // return
    return (nbErrors == 0);
  }
 
  • Líneas 2-4: Estos tres campos son inicializados por el método [isPageValid]. Además, este método devuelve verdadero si todas las entradas son válidas, y falso en caso contrario. Si alguna entrada no es válida, se muestran los mensajes de error asociados;

En este punto, la aplicación es ejecutable. Verifique la funcionalidad del método [isPageValid] introduciendo datos incorrectos.

1.15.5. La capa [empresarial

  

La capa [business] proporciona la siguiente interfaz [IMetier]:


package exemples.android.metier;
 
import java.util.List;
 
public interface IBusiness {
 
    List<Object> getRandom(int a, int b, int n);
}

El método [getAleas(a,b,n)] devuelve normalmente n enteros aleatorios en el rango [a,b]. También está diseñado para lanzar una excepción una de cada tres veces, y esta excepción se incluye en los resultados devueltos por el método. En última instancia, el método devuelve una lista de objetos de tipo [Excepción] o [Entero].

La implementación [Metier] de esta interfaz es la siguiente:


package exemples.android.metier;
 
import org.androidannotations.annotations.EBean;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
@EBean(scope = EBean.Scope.Singleton)
public class BusinessImplementation implements IBusinessImplementation {
 
    public List<Object> getRandom(int a, int b, int n) {
        // the list of objects
        List<Object> responses = new ArrayList<Object>();
        // some checks
        if (n < 1) {
            answers.add(new AleaException("The number of random integers requested must be greater than or equal to 1"));
        }
        if (a < 0) {
            answers.add(new AleaException("The number a in the interval [a,b] must be greater than 0"));
        }
        if (b < 0) {
            answers.add(new AleaException("The number b in the interval [a,b] must be greater than 0"));
        }
        if (a >= b) {
            answers.add(new AleaException("In the interval [a,b], a must be less than b"));
        }
        // error?
        if (answers.size() != 0) {
            return answers;
        }
        // Generate random numbers
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // generate a random exception 1 time out of 3
            int number = random.nextInt(3);
            if (number == 0) {
                answers.add(new AleaException("Random exception"));
            } else {
                // otherwise, return a random number between two bounds [a, b]
                answers.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // result
        return answers;
    }
}
  • Línea 9: Utilizamos la anotación AA [@EBean] en la clase [Business] para poder inyectar referencias a ella en la capa [Presentation]. El atributo (ámbito = EBean.Scope.Singleton) garantiza que sólo se creará una única instancia de la clase [Business]. Por lo tanto, siempre se inyecta la misma referencia si se inyecta varias veces en la capa [Presentation];
  • el resto del código es estándar;

El tipo [AleaException] utilizado por la clase [Metier] es el siguiente:


package exemples.android.metier;
 
public class AleaException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
 
    public AleaException() {
    }
 
    public AleaException(String detailMessage) {
        super(detailMessage);
    }
 
    public AleaException(Throwable throwable) {
        super(throwable);
    }
 
    public AleaException(String detailMessage, Throwable throwable) {
        super(detailMessage, throwable);
    }
 
}
  • Línea 3: La clase [AleaException] extiende la clase del sistema [RuntimeException], convirtiéndola en una excepción no manejada: no necesita ser manejada en un bloque try/catch, ni necesita ser incluida en las firmas de los métodos;

1.15.6. El [MainActivity] revisitado

  

[Empresas] LayerActivityUserView

La actividad implementará la interfaz [IMetier] de la capa [business]. Así, el fragmento/vista sólo tendrá como contrapartida la actividad.

La actividad [MainActivity] ya implementa la interfaz [IMainActivity]. Para que también implemente la interfaz [IMetier], podemos:

  • añadir la interfaz [IMetier] a las interfaces implementadas por la actividad;
  • asegurarse de que la propia interfaz [IMainActivity] extiende la interfaz [IMetier]. Este es el enfoque que estamos adoptando;

La interfaz [IMainActivity] pasa a ser la siguiente:

  

package exemples.android.architecture;
 
import exemples.android.metier.IMetier;
 
public interface IMainActivity extends IMetier {
 
  // access to the session
  Session getSession();
 
  // change view
  void navigateToView(int position);
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
}
  • línea 5: la interfaz [IMainActivity] extiende la interfaz [IMetier]

La clase [MainActivity] evoluciona del siguiente modo:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  ...
 
  // session injection
  @Bean(Session.class)
  protected Session session;
 
  // business logic injection
  @Bean(Metier.class)
  protected IBusinessLogic businessLogic;
 
...
  // IBusiness implementation --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return business.getRandom(a, b, n);
}
  • líneas 11-12: se inyecta la capa [business] en la actividad. Para ello, utilizamos la anotación [@Bean], cuyo parámetro es la clase que lleva la anotación [@EBean];
  • línea 2: la actividad implementa la interfaz [IMainActivity] y por tanto la interfaz [IMetier] de la capa [business];
  • líneas 16-19: implementación del método único de la interfaz [IMetier]. Simplemente delegamos la llamada a la capa [business];

1.15.7. El fragmento [Vue1Fragment] revisitado

  

El código de la clase [Vue1Fragment] evoluciona de la siguiente manera:


package exemples.android.fragments;
 
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // elements of the user interface
  @ViewById(R.id.lst_reponses)
  protected ListView listAnswers;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNumberOfEntries;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorRandom;
  @ViewById(R.id.txt_errorInterval)
  protected TextView txtErrorInterval;
 
  // list of responses to a command
  private List<String> answers = new ArrayList<>();
  // ListView adapter
  private ArrayAdapter<String> adapterAnswers;
 
  // user input
  private int numberOfRandomNumbers;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
   ...
  }
 
  @Click(R.id.btn_Execute)
  void doExecute() {
  ...
  }
 
  // Check the validity of the entered data
  private boolean isPageValid() {
   ...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("View1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // will only be executed once when the application starts
    // create the ListView adapter—this requires that the [activity] variable has been initialized
    adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
    listAnswers.setAdapter(answerAdapter);
  }
}
  • Líneas 69-70: Establecer el adaptador para el componente [ListView];

El componente [ListView] se utiliza para mostrar una lista de elementos. Para ello utiliza un adaptador [ListAdapter], que a su vez está conectado a la fuente de datos que alimenta el [ListView]. Para definir el adaptador de un [ListView], utilice el siguiente método [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

[ListAdapter] es una interfaz. La clase [ArrayAdapter] es una clase que implementa esta interfaz. El constructor utilizado en la línea 69 es el siguiente:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] es la actividad que muestra el [ListView];
  • [recurso] es el número entero que identifica la vista utilizada para mostrar un elemento en el [ListView]. Esta vista puede ser de cualquier complejidad. El desarrollador la construye según sus necesidades;
  • [textViewResourceId] es el número entero que identifica un componente [TextView] en la vista [resource]. La cadena mostrada será mostrada por este componente;
  • [objetos]: la lista de objetos mostrados por el [ListView]. El método [toString] de los objetos se utiliza para mostrar el objeto en el [TextView] identificado por [textViewResourceId] dentro de la vista identificada por [resource].

La tarea del desarrollador es crear la vista [resource] que mostrará cada elemento en el [ListView]. Para el caso simple en el que sólo queremos mostrar una única cadena de caracteres, como aquí, Android proporciona la vista identificada por [android.R.layout.simple_list_item_1]. Contiene un componente [TextView] identificado por [android.R.id.texto1]. Este es el método utilizado en la línea 69 para crear el adaptador [ListView]. Este adaptador sólo necesita definirse una vez. Para permitir su reutilización, se ha definido como una variable de instancia de la clase (línea 39). Veamos de nuevo la línea 69:


adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);

El primer parámetro del constructor [ArrayAdapter] es la actividad obtenida de un fragmento a través de [getActivity] y almacenada aquí en la variable [activity] de la clase padre. Este campo no siempre tiene un valor. Así, los registros muestran que cuando llegamos al método [@AfterViews] aún no se ha inicializado, por lo que no podemos colocar las líneas 69-70 en este método. En el método [updateFragment], esto es posible porque sabemos que cuando se ejecuta este método, [activity] no es necesariamente null. El adaptador está aquí asociado a la fuente de datos [reponses] definida en la línea 37;

El método [doExecute] gestiona el clic en el botón [Ejecutar]. Su código es el siguiente:


@Click(R.id.btn_Executer)
  void doExecute() {
    // we hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorInterval.setVisibility(View.INVISIBLE);
    // clear previous responses
    responses.clear();
    adapterAnswers.notifyDataSetChanged();
    // Check the validity of the entries
    if (!isPageValid()) {
      return;
    }
    // request random numbers from the activity
    List<Object> data = mainActivity.getRandom(a, b, nbRandom);
    // create a list of Strings from this data
    for (Object o : data) {
      if (o instanceof Exception) {
        answers.add(((Exception) o).getMessage());
      } else {
        answers.add(o.toString());
      }
    }
    // refresh listview
    adapterAnswers.notifyDataSetChanged();
  }
  • líneas 7-8: queremos borrar el ListView. Para ello, borramos la fuente de datos [reponses] y pedimos al adaptador asociado con el ListView que se actualice;
  • líneas 10-12: Antes de ejecutar la acción solicitada, verificamos que los valores introducidos son correctos;
  • línea 14: se solicita a la actividad la lista de números aleatorios. Obtenemos una lista de objetos donde cada objeto es de tipo [Integer] o [AleaException];
  • líneas 16-22: a partir de la lista de objetos obtenida, se actualiza la fuente de datos [reponses] mostrada por el ListView;
  • línea 24: se pide al adaptador ListView que se actualice;

1.15.8. Ejecución

Ejecute el proyecto y compruebe que funciona correctamente.

1.16. Ejemplo 15: Arquitectura cliente/servidor

Ahora veremos una arquitectura común para una aplicación Android, una en la que la aplicación Android se comunica con servicios web remotos. Ahora tendremos la siguiente arquitectura:

Hemos añadido una capa [DAO] a la aplicación Android para comunicarse con el servidor remoto. Se comunicará con el servidor que genera los números aleatorios que muestra la tableta Android. Este servidor tendrá la siguiente arquitectura de dos capas:

Los clientes consultan URLs específicos en la capa [web/JSON] y reciben una respuesta de texto en formato JSON (JavaScript Object Notation). Aquí, nuestro servicio web manejará un único URL de la forma [/a/b], que devolverá un número aleatorio en el rango [a,b]. Describiremos la aplicación en el siguiente orden:

El servidor

  • su capa [empresarial];
  • su servicio [web/JSON] implementado con Spring MVC;

El cliente

  • su capa [DAO]. No habrá capa [empresarial];

1.16.1. El servidor [web/JSON]

Queremos construir la siguiente arquitectura:

1.16.1.1. Creación de proyectos

Construiremos el servicio web utilizando el ecosistema Spring [http://spring.io/]. Vamos a la página web [http://start.spring.io/] (junio de 2016), que nos permitirá generar un proyecto Gradle con las dependencias necesarias para nuestro proyecto-que no es un proyecto Android y para el que Android Studio no ofrece ninguna ayuda:

  • en [1]: elige un proyecto Gradle;
  • en [2-3]: las propiedades del JAR generada por el proyecto (véase más abajo);
  • en [4]: selecciona la dependencia web [5] para que los binarios necesarios para nuestro servicio web estén disponibles;
  • en [6]: generar el proyecto. A continuación, se genera un archivo ZIP de un proyecto esqueleto de Gradle y se pone a disposición para su descarga;

¿Qué deberías poner en [2-3]? Ya hemos utilizado dependencias de Gradle. Por ejemplo, la del proyecto anterior era la siguiente:

 

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
  ...
}
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
}
  • Línea 22: Se especifica una dependencia en el formato [groupId:artifactId:version]. Lo que se solicita en el formulario en [http://start.spring.io/]:
    • en [2] es [groupId];
    • en [3] es [artifactId];

Descomprima el archivo zip obtenido en la carpeta que contiene los demás proyectos:

Usando Android Studio, abre el proyecto Gradle [server-01] [1-2]. El proyecto abierto está en [3] (vista Proyecto).

1.16.1.2. Configuración de Gradle

  

El archivo Gradle generado (junio de 2016) es el siguiente:


buildscript {
 ext {
  springBootVersion = '1.3.5.RELEASE'
 }
 repositories {
  mavenCentral()
 }
 dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
 }
}
 
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot' 
 
jar {
 baseName = 'server-01'
 version = '0.0.1-SNAPSHOT'
}
 
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
repositories {
 mavenCentral()
}
 
dependencies {
 compile('org.springframework.boot:spring-boot-starter-web')
 testCompile('org.springframework.boot:spring-boot-starter-test') 
}
 
eclipse {
 classpath {
   containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
   containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
 }
}
  • Las líneas 14 y 34-38 son para el IDE de Eclipse. Las eliminamos;
  • Las líneas 1-11 y 15 se utilizan para añadir un plugin llamado [spring-boot] a nuestro proyecto Gradle. Spring Boot es un proyecto dentro del ecosistema Spring [http://projects.spring.io/spring-boot/]. Este plugin define las versiones de las dependencias más utilizadas con Spring. Esto nos permite omitir la especificación de sus versiones (líneas 30 y 31). La versión es entonces la definida por la versión de Spring Boot utilizada (línea 3);
  • líneas 22-23: la versión de Java a utilizar, aquí la versión 1.8;
  • líneas 25-27: los repositorios binarios a utilizar para descargar las dependencias;
  • línea 26: especifica el repositorio central de Maven. Este es actualmente el mayor repositorio binario de código abierto disponible;
  • líneas 29-32: las dependencias necesarias para el proyecto:
  • línea 30: esta dependencia incluye todos los binarios necesarios para crear un servicio web Spring;
  • línea 31: esta dependencia incluye todos los binarios necesarios para las pruebas, en particular las pruebas JUnit;
  • Una dependencia [compilar] indica que la dependencia es necesaria para compilar el proyecto. Una dependencia [testCompile] indica que la dependencia sólo es necesaria para ejecutar pruebas. Por lo tanto, no se incluye en el binario del proyecto;

Empezaremos por limpiar el archivo Gradle:


// spring boot
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}
 
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
 
// project binary
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}
 
// Java versions
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
// Maven repositories
repositories {
  mavenLocal()
  mavenCentral()
}
 
// dependencies
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • línea 30: hemos añadido el repositorio local de Maven para la máquina de desarrollo. Este se crea cuando se instala Maven (ver sección 6.10). Si la dependencia solicitada ya se encuentra en el repositorio local de Maven, no se obtendrá del repositorio central de Maven;
  • líneas 19-22: una tarea Gradle para generar el binario del proyecto. La usaremos para ver lo que se está haciendo;
  • En [1-4], ejecute la tarea [jar] definida en el archivo [build.gradle] ([1] se encuentra arriba a la derecha y al lado del archivo IDE);

El paso anterior crea el archivo JAR del proyecto y lo coloca en la carpeta [build/libs] [5]:

  

El nombre del archivo se deriva directamente de la información proporcionada a la tarea [jar] en el archivo [build.gradle] (líneas 19-22).

Todas las dependencias del proyecto pueden verse como sigue:

 

Podemos ver en [1] que la única dependencia del proyecto [compile('org.springframework.boot:spring-boot-starter-web')] ha traído consigo docenas de binarios. Spring Boot para la web ha incluido las dependencias que probablemente necesitará una aplicación web MVC de Spring. Esto significa que algunas pueden ser innecesarias. Spring Boot es ideal para un tutorial:

  • incluye las dependencias que probablemente necesitaremos;
  • incluye un servidor Tomcat integrado [1], lo que nos evita tener que desplegar la aplicación en un servidor web externo;

Puede encontrar muchos ejemplos que utilizan Spring Boot en el sitio web del ecosistema Spring [http://spring.io/guides].

Ahora completaremos el archivo [build.gradle] de la siguiente manera:


// spring boot
...
// dependencies
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
 
// plugin to create a Maven-compliant binary in the local Maven repository
apply plugin: 'maven-publish'
publishing {
  publications {
    maven(MavenPublication) {
      groupId 'istia.st.examples.android'
      artifactId 'server-01'
      version '0.0.1-SNAPSHOT'
      from components.java
    }
  }
  repositories {
    maven {
      // change to point to your repository, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • línea 10: importamos un plugin de Gradle llamado [maven-publish] que nos permite publicar el binario del proyecto en un repositorio de Maven de acuerdo con los estándares de Maven;
  • línea 11: una tarea Gradle llamada [publishing];
  • líneas 14-15: las características del binario Maven que se creará;
  • línea 23: el repositorio Maven en el que se publicará, en este caso un repositorio Maven local;

Al añadir el plugin [maven-publish] se han creado nuevas tareas en el proyecto Gradle:

Si en [2] ejecutamos la tarea [publish], el binario del proyecto se crea e instala en la carpeta especificada en la línea 23 del fichero [build.gradle]:

 

La tarea [jar] genera el binario del proyecto. Este binario no incluye sus dependencias y, por tanto, no es ejecutable. Es posible generar un binario ejecutable que incluya todas sus dependencias. Para ello, añadimos el siguiente código al fichero [build.gradle]:


// create a binary with all its dependencies
version = '1.0'
task fatJar(type: Jar) {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
  }
  baseName = project.name + '-all'
  from { configurations.compile.collect { if (it.isDirectory()) { it } else { zipTree(it) } }
  with jar
}
  • Línea 6: Introduzca el nombre completo de la clase ejecutable del proyecto:
  

El código de esta clase será el siguiente:


package istia.st.exemples.android;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Server01Application {
 
    public static void main(String[] args) {
        System.out.println("Server01Application running");
        //SpringApplication.run(Server01Application.class, args);
    }
}

Actualiza el proyecto Gradle y ejecuta la tarea [fatJar]:

 

El binario se genera en la carpeta [build/libs] y puede ejecutarse [1-7]:

1.16.1.3. Configuración del proyecto

La configuración de Gradle no es suficiente. También necesitamos configurar el proyecto. Dado que este no es un proyecto Android generado por el IDE, esta configuración-que no hemos hecho hasta ahora-debe hacerse aquí.

 
  • En [3-4]: use JDK 1.8;

Para compilar el proyecto, el botón disponible para los proyectos Android ya no está presente. Utilizaremos una opción de menú [1-2]:

A continuación, se pide al lector que cree el siguiente proyecto. Comentaremos el código final del proyecto [3].

1.16.1.4. La capa [empresarial

  

La capa [business] sigue el mismo enfoque que la capa [business] del ejemplo anterior. Tendrá la siguiente interfaz [IMetier]:


package exemples.android.server.metier;
 
public interface IBusiness {
  // random number in [a,b]
    int getRandom(int a, int b);
}
  • Línea 5: el método que genera 1 número aleatorio en [a,b]

El código de la clase [Metier] que implementa esta interfaz es el siguiente:


package exemples.android.server.metier;
 
import org.springframework.stereotype.Service;
 
import java.util.Date;
import java.util.Random;
 
@Service
public class BusinessImplementation implements IBusinessImplementation {
 
  @Override
  public int getRandom(int a, int b) {
    // some checks
    if (a < 0) {
      throw new RandomException("The number a in the interval [a,b] must be greater than 0", 2);
    }
    if (b < 0) {
      throw new AleaException("The number b in the interval [a, b] must be greater than 0", 3);
    }
    if (a >= b) {
      throw new AleaException("In the interval [a,b], a must be less than b", 4);
    }
    // generate result
    Random random = new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

No comentaremos la clase: es similar a la del ejemplo anterior, salvo que no lanza excepciones aleatoriamente. Observe la anotación de Spring [@Service] en la línea 8, que hace que Spring instancie la clase como una única instancia (singleton) y ponga su referencia a disposición de otros componentes de Spring. Aquí se podrían haber utilizado otras anotaciones de Spring para conseguir el mismo efecto. Los componentes Spring tienen nombres por defecto que pueden especificarse como un atributo de la anotación utilizada. Sin este atributo, como aquí, el componente Spring toma el nombre de la clase con su primer carácter en minúsculas. Así, aquí, el componente Spring se llama por defecto [metier];

La clase [Metier] lanza excepciones de tipo [AleaException]:


package exemples.android.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // constructors
  public AleaException() {
  }
 
  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }
 
  // getters and setters
....
}
  • línea 3: [AleaException] extiende la clase [RuntimeException]. Por lo tanto, es una excepción no manejada (no es necesario manejarla con un try/catch);
  • línea 6: se añade un código de error a la clase [RuntimeException];

1.16.1.5. El servicio web / JSON

 
  

El servicio web / JSON está implementado por Spring MVC. Spring MVC implementa el patrón arquitectónico MVC (Modelo-Vista-Controlador) de la siguiente manera:

La tramitación de una solicitud de cliente procede del siguiente modo:

  1. solicitar - los URLs solicitados tienen la forma http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... El [Dispatcher Servlet] es la clase de Spring que gestiona los URLs entrantes. Se "encamina" el URL a la acción que debe manejarlo. Estas acciones son métodos de clases específicas llamadas [Controladores]. El sitio C en MVC aquí se refiere a la cadena [Servlet Despachador, Controlador, Acción]. Si no se ha configurado ninguna acción para gestionar el URL entrante, el [Servlet Dispatcher] responderá que no se ha encontrado el URL solicitado (error 404 NOT FOUND);
  1. procesamiento
  • la acción seleccionada puede utilizar los parámetros que el [Servlet Dispatcher] le ha pasado. Estos pueden provenir de varias fuentes:
    • el camino [/param1/param2/...] del URL,
    • los parámetros URL [p1=v1&p2=v2],
    • a partir de los parámetros enviados por el navegador con su solicitud;
  • al procesar la solicitud del usuario, la acción puede necesitar la capa [de negocio] [2b]. Una vez procesada la petición del cliente, puede desencadenar varias respuestas. Un ejemplo clásico es:
    • una página de error si la solicitud no ha podido procesarse correctamente
    • una página de confirmación en caso contrario
  • la acción ordena que se muestre una vista específica [3]. Esta vista mostrará los datos conocidos como ver modelo. Este es el M en MVC. La acción creará este modelo M [2c] e instruirá a un V que se mostrará [3];
  1. respuesta - la vista seleccionada V utiliza el modelo M construido por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente, y luego envía esta respuesta.

For a web service / JSON, the previous architecture is slightly modified:

  • en [4a], el modelo, que es una clase Java, se convierte en una cadena JSON mediante una biblioteca JSON;
  • en [4b], esta cadena JSON se envía al navegador;

Un ejemplo de serialización de un objeto Java en una cadena JSON y de deserialización de una cadena JSON en un objeto Java se presenta en los apéndices de la Sección 6.14.

Volvamos a la capa [web] de nuestra aplicación:

En nuestra aplicación, sólo hay un controlador:

  

El servicio web/JSON enviará a sus clientes una respuesta de tipo [Response] como sigue:


package exemples.android.server.web;
 
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
...
}
  • línea 13: el campo [T body] es la respuesta esperada por el cliente. Decidimos utilizar aquí una respuesta genérica de tipo T, en lugar del tipo Entero tipo del número aleatorio esperado. Queremos poder reutilizar esta clase en otras situaciones. Mientras procesa la solicitud del cliente, el servidor puede encontrarse con un problema, que se resume en los otros dos campos;
    • línea 8: un código de estado (0 si no hay error);
    • línea 9: if estado != 0, una lista de mensajes de error -normalmente los de la pila de excepciones si se ha producido una excepción-null si no hay errores;

El controlador [WebController] es el siguiente:


package exemples.android.server.web;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import examples.android.server.business.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import java.util.ArrayList;
import java.util.List;
 
@Controller
public class WebController {
 
  // business layer
  @Autowired
  private IBusinessModel businessModel;
  // JSON mapper
  @Autowired
  private ObjectMapper mapper;
 
  // random numbers
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
  public String getRandom(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
 
    // the response
    Response<Integer> response = new Response<>();
    // we use the business layer
    try {
      response.setBody(business.getRandom(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // return the response
    return mapper.writeValueAsString(response);
  }
 
  private List<String> getMessagesFromException(Throwable e) {
    // list of messages
    List<String> messages = new ArrayList<String>();
    // iterate through the exception stack
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // return the result
    return messages;
  }
 
}
  • línea 17: la anotación [@Controller] indica que la clase es un controlador MVC cuyos métodos manejan peticiones para ciertos URLs en la aplicación web;
  • líneas 21-22: la anotación [@Autowired] indica a Spring que inyecte un componente de tipo [IMetier] en el campo. Esta será la clase [Metier] anterior. Como le hemos añadido la anotación [@Service], se trata como un componente Spring;
  • líneas 24-25: hacemos lo mismo con un mapper JSON que definiremos más adelante. Nuestro servicio web enviará su respuesta como una cadena JSON. Este mapeador se encargará de la serialización de la respuesta en JSON;
  • línea 30: el método que genera el número aleatorio. Su nombre no importa. Cuando se ejecuta, sus parámetros han sido inicializados por Spring MVC. Ya veremos cómo. Además, si se ejecuta, es porque el servidor web ha recibido una petición HTTP GET para el URL de la línea 28;
  • línea 28: la anotación [@RequestMapping] define ciertas propiedades del método anotado:
    • [valor]: el URL aceptado por el método;
    • [método]: el método HTTP aceptado por el método. Existen principalmente dos: GET y POST. El método [POST] se utiliza cuando el cliente desea adjuntar un documento a su solicitud HTTP;
    • [produce]: establece una de las cabeceras de la respuesta HTTP que se enviará al cliente. Aquí, entre las cabeceras HTTP enviadas con la respuesta al cliente, habrá una que informe al cliente de que la respuesta se está enviando en forma de cadena JSON. Esta cabecera no es obligatoria. Se proporciona al cliente con fines informativos si el cliente espera respuestas que pueden adoptar diversas formas;
    • [consume]: no está presente aquí. Especifica las cabeceras HTTP que deben acompañar a la solicitud HTTP del cliente para que sea aceptada;
  • línea 29: la anotación [@ResponseBody] indica que el resultado producido por el método debe enviarse al cliente. Sin esta anotación, la respuesta del método se trata como una clave utilizada para seleccionar la página HTML que se enviará al cliente. En un servicio web / JSON, no hay páginas HTML;
  • línea 28: el URL procesado es de la forma /{a}/{b}, donde {x} representa una variable. Las variables {a} y {b} se asignan a los parámetros del método en la línea 30. Esto se hace mediante el método @PathVariable("x") anotación. Nótese que {a} y {b} son componentes de un URL y por tanto son de tipo Cadena. La conversión de Cadena al tipo de parámetro puede fallar. Spring MVC lanza entonces una excepción. En resumen: si solicito el URL /100/200 en un navegador, el getAlea de la línea 30 se ejecutará con los parámetros enteros a=100, b=200;
  • línea 36: se pide a la capa [business] un número aleatorio en el rango [a,b]. Recordemos que la capa [business].getAlea puede lanzar una excepción;
  • línea 37: sin error;
  • línea 39: código de error;
  • línea 40: la lista de mensajes de respuesta es la de la pila de excepciones (líneas 46-57). Aquí, sabemos que la pila contiene sólo una excepción, pero queríamos demostrar un método más genérico;
  • línea 43: la respuesta de tipo [Response<Integer>] se devuelve como una cadena JSON;

1.16.1.6. Configuración del proyecto Spring

  

Hay varias formas de configurar Spring:

  • utilizando archivos XML;
  • con código Java;
  • utilizando una combinación de ambos;

Elegimos configurar nuestra aplicación web utilizando código Java. La siguiente clase [Config] se encarga de esta configuración:


package exemples.android.server.config;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
@ComponentScan(basePackages = { "examples.android.server.business", "examples.android.server.web" })
@EnableWebMvc
public class Config {
  // web configuration ------------------------------------
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet();
    return servlet;
  }
 
  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }
 
  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }
 
  // JSON mapper
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
 
}
  • Línea 12: Le decimos a Spring en qué paquetes encontrará los dos componentes que necesita gestionar:
    • el componente [Metier] anotado con [@Service] en el [exemples.android.server.metier] paquete;
    • el componente [WebController] anotado con [@Controller] en el [examples.android.server.web] paquete;
  • línea 13: la anotación [@EnableWebMvc] permite a Spring Boot gestionar automáticamente una serie de configuraciones estándar para una aplicación Spring MVC. Esto reduce significativamente la carga de trabajo del desarrollador;
  • líneas 16, 22, 27 y 33: la anotación [@Bean] también define componentes Spring (beans) del mismo modo que las dos anotaciones encontradas (@Service, @Controller). Aquí, la anotación [@Bean] anota un método en lugar de una clase, y el resultado del método es el componente Spring. En ausencia de un atributo de nombre dentro de la anotación [@Bean], el componente Spring creado toma el nombre del método anotado;
  • líneas 16-20: define el bean [dispatcherServlet]. Este es un nombre predefinido en Spring MVC que define el bean controlador frontal de la aplicación MVC, un objeto por el que pasan todas las peticiones del cliente y que las envía (de ahí su nombre) a los distintos [@Controller]s de la aplicación Spring MVC;
  • línea 18: el bean [dispatcherServlet] es una instancia de la clase [DispatcherServlet] proporcionada por Spring MVC;
  • líneas 22-25: el bean [servletRegistrationBean] se utiliza para definir qué URLs son aceptados por la aplicación. En la línea 24, se aceptan todos los URLs;
  • líneas 27-30: el bean [embeddedServletContainerFactory] se utiliza para definir el servidor embebido en las dependencias del proyecto que alojará la aplicación web. La línea 29 especifica que se trata de un servidor Tomcat y que se ejecutará en el puerto 8080. Por defecto, los binarios para este servidor web son proporcionados por la dependencia [org.springframework.boot:spring-boot-starter-web] en el archivo Gradle;

1.16.1.7. Ejecutar el servicio web / JSON

  

El proyecto se ejecuta desde la siguiente clase ejecutable [Boot]:


package exemples.android.server.boot;
 
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
 
public class Boot {
  public static void main(String[] args) {
    // Run the application
    SpringApplication.run(Config.class, args);
  }
 
}
  • La clase [Boot] es una clase ejecutable (líneas 7-10);
  • línea 9: el método estático [SpringApplication.runes un método de [Spring Boot] (línea 4) que lanzará la aplicación. Su primer parámetro es la clase Java que configura el proyecto. En este caso, es la clase [Config] que acabamos de describir. El segundo parámetro es el array de argumentos pasados al método [main] (línea 7);

La aplicación web puede lanzarse de varias formas, entre las que se incluyen las siguientes:

 

A continuación, aparecen varios registros en la consola:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.1.RELEASE)

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • líneas 12-14: se lanza el servidor embebido Tomcat;
  • líneas 15-19: se carga y configura el servlet Spring MVC [DispatcherServlet];
  • línea 20: se detecta el servidor web URL [/{a}/{b}];

Ahora, abramos un navegador y probemos el JSON URL del servicio web:

Cada vez, obtenemos la representación JSON de un objeto de tipo [Response<Integer>].

En lugar de utilizar un navegador estándar, utilicemos ahora el [ Extensión [Advanced Rest Client] para el navegador Chrome (véanse los apéndices, sección 6.13):

Image

  • en [1], el URL solicitado;
  • en [2], utilizando una petición GET;
  • en [3], se envía la solicitud;

Image

  • en [4], las cabeceras HTTP de la respuesta del servidor. Tenga en cuenta que esto indica que el documento enviado es una cadena JSON;
  • en [5], la cadena JSON recibida;

1.16.1.8. Generación del ejecutable del proyecto JAR

En la sección 1.16.1.2, mostramos cómo configurar el archivo Gradle para generar un ejecutable para la aplicación con todas sus dependencias. Adaptada a la aplicación actual, esta configuración pasa a ser la siguiente:


// create a binary with all its dependencies
version = '1.0'
task fatJar(type: Jar) {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    attributes 'Main-Class': 'examples.android.server.boot.Boot'
  }
  baseName = project.name + '-all'
  from { configurations.compile.collect { if it.isDirectory() then it else zipTree(it) } }
  with jar
}

Para generar este ejecutable, sigue estos pasos [1-5]:

Para ejecutarlo, detenga el servicio web si se está ejecutando [1] y, a continuación, ejecute el archivo [2-4]:

 

Abra un navegador y solicite el URL [localhost:8080/100/200]. Deberías obtener los mismos resultados que antes.

1.16.1.9. Gestión de registros

Cuando ejecute el archivo ejecutable, observará que los registros son diferentes de cuando ejecuta el proyecto desde IDE. Verá los registros en modo [DEBUG]:


...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
June 7, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Boot started in 1.984 seconds (JVM running for 2.206)

Puede gestionar el nivel de registro añadiendo un archivo [logback.xml] a la carpeta [resources] del proyecto:

  

Este archivo podría tener el siguiente contenido:


<configuration>
 
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

El nivel de registro se controla en la línea 12. Si ahora reconstruimos el archivo ejecutable y lo ejecutamos, sólo obtendremos registros de nivel [info]:


...
09:36:52.433 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
June 7, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]
June 7, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO  exemples.android.server.boot.Boot - Boot started in 1.865 seconds (JVM running for 2.203)

1.16.2. El cliente Android para el servidor web / JSON

El cliente Android tendrá la siguiente arquitectura:

El cliente tendrá dos componentes:

  1. una capa [Presentación] (vista+actividad) similar a la que estudiamos en el Ejemplo [Ejemplo-14];
  2. la capa [DAO] que interactúa con el servicio [web / JSON] que estudiamos anteriormente.

1.16.2.1. Creación del proyecto

Duplicamos el proyecto anterior [Ejemplo-14] en [Ejemplo-15] siguiendo el procedimiento de la sección 1.4. Obtenemos el siguiente resultado:

Next, the reader is invited to create the following project.

1.16.2.2. Configuración de Gradle

 

El archivo [build.gradle] es el siguiente:


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 {
    applicationId "examples.android"
    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 fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}
 
repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}

Sólo comentaremos lo que no se haya tratado ya:

  • líneas 46-47: inserción de un plugin AA. El plugin [rest-spring-api] permite delegar la comunicación cliente/servidor a la biblioteca AA;
  • línea 50: la librería [spring-android-rest-template] es la librería utilizada por AA para manejar la comunicación cliente/servidor. La versión [2.0.0.M3] es una versión denominada 'milestone' que no se encuentra en los repositorios habituales de Maven. Por tanto, debemos especificar, en las líneas 56-59, el repositorio a utilizar (línea 58) para encontrar la librería;
  • Línea 51: una biblioteca JSON;
  • líneas 33-39: sin esta propiedad, se producen errores al generar el binario APK del proyecto;

1.16.2.3. El manifiesto de la aplicación Android

  

Es necesario actualizar el archivo [AndroidManifest.xml]. Por defecto, el acceso a Internet está desactivado. Debe activarse mediante una directiva especial:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="examples.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 5: El acceso a Internet está permitido;

1.16.2.4. La capa [DAO]

  

1.16.2.4.1. La interfaz [IDao] de la capa [DAO]

La interfaz de la capa [DAO] será la siguiente:


package exemples.android.dao;
 
public interface IDao {
 
  // random number
  int getRandom(int a, int b);
 
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // maximum wait time (ms) for the server response
  void setTimeout(int timeout);
 
  // Client wait time in milliseconds before sending a request
  void setDelay(int delay);
 
}
  • línea 6: el servicio web / método JSON para obtener un número aleatorio en el rango [a,b] de este servicio web;
  • línea 9: el URL del servicio web / JSON para generar números aleatorios;
  • línea 12: establecemos un tiempo máximo de espera para la respuesta del servidor;
  • línea 15: queremos establecer un tiempo de espera antes de ejecutar la petición al servidor, para dar tiempo al usuario a cancelar su petición;

1.16.2.4.2. La interfaz [WebClient]
  

La interfaz [WebClient] gestiona la comunicación con el servicio web. Su código es el siguiente:


package exemples.android.dao;
 
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.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // 1 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getRandom(@Path("a") int a, @Path("b") int b);
}
  • Línea 12: [WebClient] es un interfaz que la biblioteca AA implementará por sí misma utilizando las anotaciones que le añadiremos. Esta interfaz debe implementar llamadas al URLs expuesto por el servicio web / JSON:

  // random number
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
public String getRandom(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
  • Línea 11: La anotación [@Rest] es una anotación AA. El valor del atributo [converters] es una matriz de convertidores. Aquí, el atributo [MappingJackson2HttpMessageConverter.clasegarantiza que cuando el servidor envía una cadena JSON, ésta se deserializa automáticamente. Así, vemos en la línea (d) que la cadena URL [/{a}/{b}] devuelve un Cadena que en realidad es una cadena JSON (línea b). Con esta información y la del tipo esperado en la línea 16, la instancia [WebClient] del cliente deserializará la cadena que recibe a un tipo [Response<Integer>];
  • línea 15: una anotación @Get que indica que el URL debe llamarse utilizando un método HTTP GET. El parámetro de la anotación @Get es el formato URL esperado por el servicio web. Simplemente utilice el parámetro [value] de la anotación @RequestMapping (línea b) del método llamado en el [WebController] del servidor. Las llaves {} encierran los parámetros URL que deben pasarse a los parámetros del método en la línea 16. La sintaxis [@Ruta("a") int a] hace que al parámetro [a] del método se le asigne el valor {a} del URL. Cuando el parámetro URL y el parámetro del método tienen el mismo nombre, como aquí, podemos escribir más sencillamente [@Ruta int a];

En el caso de una petición HTTP POST, el método de llamada tendría la siguiente firma:


  @Post("/{a}/{b}")
  Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);

La anotación [@Body] designa el valor publicado. Este será serializado automáticamente a JSON. En el lado del servidor, tendremos la siguiente firma:


  // random numbers
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
  • línea 2: especifica que se espera una petición HTTP POST y que el cuerpo de la petición (objeto enviado) debe transmitirse como una cadena JSON (consume );
  • línea 4: el valor publicado se recuperará en el parámetro [@RequestBody T body] del método;

Volvamos al código de la clase [WebClient]:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • Necesitamos poder especificar el URL del servicio web con el que contactar. Esto se consigue ampliando la interfaz [RestClientRootUrl] proporcionada por AA. Esta interfaz expone un método [setRootUrl(urlServiceWeb)] que nos permite establecer el URL del servicio web a contactar;
  • Además, queremos controlar la llamada al servicio web porque queremos limitar el tiempo de espera de la respuesta. Para ello, extendemos la interfaz [RestClientSupport], que expone el método [setRestTemplate] que nos permitirá:
    • crear nosotros mismos el objeto [RestTemplate], que se utiliza para gestionar los intercambios cliente/servidor;
    • configura este objeto para establecer el tiempo máximo de respuesta;

1.16.2.4.3. La clase [Respuesta

El método [getAlea] de la interfaz [IDao] devuelve una respuesta de tipo [Response] como sigue:


package exemples.android.dao;
 
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
...
}

Se trata de la clase [Response] ya utilizada en el lado del servidor (sección 1.16.1.5). De hecho, desde el punto de vista de la programación, es como si la capa [DAO] del cliente se comunicara directamente con la capa [WebController] del servicio web:

La comunicación de red entre el cliente y el servidor, así como la serialización/deserialización de objetos Java en el lado del cliente, son transparentes para el programador.

1.16.2.4.4. Implementación de la capa [DAO]
  

La interfaz [IDao] se implementa con la siguiente clase [Dao]:


package exemples.android.dao;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import examples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean
public class Dao implements IDao {
 
  // REST service client
  @RestService
  protected WebClient webClient;
 
  // JSON mapper
  private ObjectMapper mapper = new ObjectMapper();
  // delay before executing the request
  private int delay;
 
// IDao interface -------------------------------------------------------------------
  @Override
  public int getRandom(int a, int b) {
    ...
  }
 
  @Override
  public void setWebServiceJsonUrl(String webServiceJsonUrl) {
   ...
  }
 
  @Override
  public void setTimeout(int timeout) {
   ...
  }
 
  @Override
  public void setDelay(int delay) {
    this.delay = delay;
  }
 
}
  • línea 15: anotamos la clase [Dao] con la anotación [@EBean] para convertirla en un bean AA que podamos inyectar en otro lugar;
  • líneas 19-20: inyectamos la implementación de la interfaz [WebClient] que hemos descrito. La anotación [@RestService] se encarga de esta inyección;
  • los demás métodos implementan la interfaz [IDao] (líneas 27-46);

[setTimeout] método

El método [setTimeout] es el siguiente:


  @Override
  public void setTimeout(int timeout) {
    // Set the timeout for REST client requests
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // Create the RestTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // Set the JSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Set the RestTemplate for the web client
    webClient.setRestTemplate(restTemplate);
}
  • La interfaz [WebClient] será implementada por una clase AA utilizando la dependencia Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementa la comunicación del cliente con el servidor web/JSON utilizando una clase [RestTemplate];
  • línea 4: la clase [SimpleClientHttpRequestFactory] es proporcionada por la dependencia [spring-android-rest-template]. Nos permitirá establecer el tiempo máximo de espera para la respuesta del servidor (líneas 5-6);
  • línea 8: construimos el objeto [RestTemplate], que servirá de canal de comunicación con el servicio web. Le pasamos como parámetro el objeto [factory] que acabamos de construir;
  • línea 10: el diálogo cliente/servidor puede adoptar diversas formas. Los intercambios se producen a través de líneas de texto, y debemos decirle al objeto [RestTemplate] qué hacer con cada línea de texto. Para ello, le proporcionamos convertidores-clases capaces de procesar líneas de texto. La elección del conversor se hace generalmente a través de las cabeceras HTTP que acompañan a la línea de texto. En este caso, sabemos que sólo recibimos líneas de texto en formato JSON. Además, como vimos en la sección 1.16.1.7, el servidor envió la cabecera HTTP:

Content-Type: application/json;charset=UTF-8 

Línea 10: el único conversor para el [RestTemplate] será un conversor JSON implementado utilizando la biblioteca [Jackson]. Hay una peculiaridad con respecto a estos convertidores: AA requiere que lo incluyamos también en la anotación [WebClient]:


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

Línea 1: Se nos pide que especifiquemos un conversor aunque ya lo estemos especificando programáticamente.

  • Línea 12: El objeto [RestTemplate] construido de esta forma se inyecta en la implementación de la interfaz [WebClient], y es este objeto el que se encargará de la comunicación cliente/servidor;

Método [getAlea]

El método [getAlea] es el siguiente:


  @Override
  public int getAlea(int a, int b) {
    // service execution
    Response<Integer> info;
    DaoException ex;
    try {
      // wait
      waitSomeTime(delay);
      // execute service
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // return the result
        return info.getBody();
      } else {
        // log the exception
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // log the exception
      ex = new DaoException(e, 100);
    }
    // throw the exception
    throw ex;
  }
...
  // private methods -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • línea 8: espera [delay] milisegundos;
  • línea 10: simplemente llamamos al método con la misma firma en la clase que implementa la interfaz [WebClient];
  • línea 11: analizamos la respuesta recibida del servidor comprobando su [estado];
  • líneas 12-14: si no hubo error del lado del servidor (status = 0), entonces devuelve el resultado del método;
  • línea 17: si se ha producido un error del lado del servidor (status!=0), preparamos una excepción sin lanzarla. El servidor ha enviado una lista de mensajes de error. Creamos una excepción con, como único mensaje, la cadena JSON de la lista de mensajes del servidor;
  • líneas 19-22: otros casos de excepción;
  • línea 24: cuando llegamos a este punto, se ha producido necesariamente una excepción. Así que la lanzamos;

El [DaoException] utilizado por este código es el siguiente:


package exemples.android.dao;
 
import java.util.ArrayList;
import java.util.List;
 
public class DaoException extends RuntimeException {
 
  // error code
  private int code;
 
  // constructors
  public DaoException() {
  }
 
  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  // getters and setters
...
}
  • línea 6: la [DaoException] es una excepción no manejada;

Método [setUrlServiceWebJson]

El método [setUrlServiceWebJson] es el siguiente:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // Set the REST service URL
    webClient.setRootUrl(urlServiceWebJson);
}
  • Línea 4: Establecemos el servicio web URL utilizando el método [setRootUrl] de la interfaz [WebClient]. Este método existe porque esta interfaz extiende la interfaz [RestClientRootUrl];

1.16.2.5. El paquete [arquitectura

El paquete [arquitectura] contiene los elementos que estructuran la aplicación:

1.16.2.5.1. La interfaz [IMainActivity]

La interfaz [IMainActivity] enumera los métodos que debe implementar la actividad de la aplicación:


package exemples.android.architecture;
 
import exemples.android.dao.IDao;
 
public interface IMainActivity extends IDao {
 
  // access to the session
  Session getSession();

  // change view
  void navigateToView(int position);
 
  // wait
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
  // response timeout
  int TIMEOUT = 1000;
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
}
  • línea 5: la interfaz [IMainActivity] extiende la interfaz [IDao];
  • líneas 13-16: a los métodos ya presentes en los ejemplos anteriores (líneas 7-11), hemos añadido dos métodos para gestionar la pantalla de carga de la aplicación (líneas 14, 16);
  • línea 21: establecemos un tiempo máximo de espera para la respuesta del servidor de 1 segundo;

1.16.2.5.2. La clase [Utils]

En la clase [Utils] hemos agrupado métodos de utilidad estáticos que pueden ser llamados desde varias partes de la arquitectura de la aplicación:


package exemples.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 with 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 getMessagesForAlert(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();
  }
 
}
  • líneas 9-18: crea una lista de mensajes de error contenidos en un Throwable;
  • líneas 21-32: utiliza el método anterior para construir, a partir de la lista de mensajes obtenida, el texto a mostrar en un mensaje de alerta Android;
  • líneas 27-28: los mensajes están numerados. El número más pequeño (1) corresponde a la excepción inicial, y el más alto a la excepción más reciente de la pila de excepciones;

1.16.2.5.3. La clase abstracta [AbstractFragment]

La clase [AbstractFragment] tiene dos finalidades:

  1. para garantizar que el método [updateFragments] de las clases hijas se llame siempre que se muestre el fragmento, y sólo una vez;
  2. para factorizar el estado y los métodos de las clases hijas que se pueden factorizar;

Es el propósito 2 el que nos lleva a incluir operaciones de gestión de imágenes de espera en esta clase: todos los componentes de una aplicación Android asíncrona deben gestionar este tipo de cuestiones:


  // wait management
  protected void beginWaiting() {
    // display the hourglass
    mainActivity.beginWaiting();
  }
 
  protected void cancelWaiting() {
    // remove the hourglass
    mainActivity.cancelWaiting();
}

1.16.2.6. La vista

1.16.2.6.1. La vista [view1.xml]
  

En comparación con el ejemplo anterior, la vista [view1.xml] ha cambiado de la siguiente manera:

 
 
  • en [1], el usuario debe especificar el servicio web URL y el tiempo de espera [2] antes de cada llamada al servicio web;
  • en [3], se cuentan las respuestas;
  • en [4], el usuario puede cancelar su solicitud;
  • en [5], aparece un indicador de carga cuando se solicitan los números. Desaparece una vez que se han recibido todos los números o se ha cancelado la operación;

Image

  • En [6] se comprueba la validez de las entradas;

Se invita al lector a cargar el archivo [vue1.xml] de los ejemplos. Para el resto de esta sección, proporcionamos el IDs de los nuevos componentes:

Image

No.
Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNumberOfEpisodes
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorInterval
6
EditText
editTextWebServiceUrl
7
TextView
textViewErrorUrl
8
EditText
editTextDelay
9
TextView
textViewErrorDelay
10
Button
btn_Execute
11
Button
btn_Cancel
12
TextView
txt_Answers
13
ListView
lst_answers

Los botones [10-11] están físicamente uno encima del otro. En un momento dado, sólo uno de los dos estará visible.

1.16.2.6.2. El fragmento [Vue1Fragment]
  

El esqueleto del fragmento [Vue1Fragment] es el siguiente:


package exemples.android.fragments;
 
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // Visual interface elements
  @ViewById(R.id.editTextUrlServiceWeb)
  EditText edtUrlServiceRest;
  @ViewById(R.id.errorUrlTextView)
  TextView txtWebServiceUrlErrorMessage;
  @ViewById(R.id.editTextDelay)
  EditText edtDelay;
  @ViewById(R.id.textViewErrorDelay)
  TextView errorDelayTextView;
  @ViewById(R.id.lst_reponses)
  ListView listAnswers;
  @ViewById(R.id.txt_Answers)
  TextView infoAnswers;
  @ViewById(R.id.edt_nbaleas)
  EditText edtNumberOfShots;
  @ViewById(R.id.edt_a)
  EditText edtA;
  @ViewById(R.id.edt_b)
  EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  TextView txtErrorRandom;
  @ViewById(R.id.txt_errorInterval)
  TextView txtErrorInterval;
  @ViewById(R.id.btn_Execute)
  Button btnExecute;
  @ViewById(R.id.btn_Cancel)
  Button btnCancel;
...
  // local data
  private List<String> answers;
  private ArrayAdapter<String> adapterAnswers;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone = true;
    // initially no error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorInterval.setVisibility(View.INVISIBLE);
    txtWebServiceErrorUrl.setVisibility(View.INVISIBLE);
    textViewErrorDelay.setVisibility(View.INVISIBLE);
    // [Cancel] button hidden
    btnCancel.setVisibility(View.INVISIBLE);
    btnExecute.setVisibility(View.VISIBLE);
    // list of responses
    answers = new ArrayList<>();
  }
...
  • líneas 24-49: referencias a los componentes de la vista [view1.xml] (línea 20);
  • líneas 55-69: el método [@AfterViews] ejecutado cuando las referencias de las líneas 24-49 han sido inicializadas;
  • línea 58: no olvides esto, necesario para el ciclo de vida del fragmento;
  • líneas 60-63: se ocultan los mensajes de error;
  • líneas 65-66: se oculta el botón [Cancelar] (línea 65) y se muestra el botón [Ejecutar] (línea 66). Observe que están físicamente uno encima del otro;
  • Línea 68: El campo de la línea 52 contendrá la lista de cadenas que mostrará el ListView de respuestas;

Inmediatamente después del método [@AfterViews], se ejecutará el siguiente método [updateFragment]:


  @Override
  protected void updateFragment() {
    // create the adapter for the list of responses
    adapterAnswers = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, answers);
    listAnswers.setAdapter(answerAdapter);
}
  • Líneas 4-5: Crear el ListView para las respuestas. Se almacena en una variable de instancia para que esté disponible para otros métodos de la clase;

Al hacer clic en el botón [Ejecutar] se desencadena la ejecución del siguiente método:


// the inputs
  private int nbRandom;
  private int a;
  private int b;
  private String webServiceJsonUrl;
  private int delay;
 
  // local data
  private int countOfInfo;
  private List<String> responses;
  private ArrayAdapter<String> responseAdapter;
  private boolean hasBeenCanceled;
 
  @Click(R.id.btn_Execute)
  protected void doExecute() {
    // clear previous responses
    answers.clear();
    adapterAnswers.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoAnswers.setText(String.format("List of answers (%s)", nbAnswers));
    // Check the validity of the entries
    if (!isPageValid()) {
      return;
    }
    // initialize activity
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // request random numbers
    for (int i = 0; i < nbAleas; i++) {
      getRandom(a, b);
    }
    // start waiting
    beginWaiting();
  }
 
  @Background(id = "alea")
  void getRandom(int a, int b) {
    // We should do as little as possible here
    // In any case, no display—these should be handled in the UiThread
    try {
      // display the result in the UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // display the exception in the UiThread
      showAlert(e);
    }
  }
  • líneas 17-18: borramos la lista anterior de respuestas del servidor. Para ello, en la línea 17, borramos la fuente de datos [reponses] asociada a la variable ListView adaptador;
  • línea 19: un booleano que nos dirá si el usuario canceló o no su petición;
  • líneas 21-22: mostramos un contador puesto a cero para el número de respuestas;
  • líneas 24-26: Recuperamos las entradas de las líneas [2-6] y verificamos su validez. Si alguna de ellas no es válida, se aborta el método (línea 25) y se devuelve al usuario a la interfaz visual;
  • líneas 28-29: si todos los datos introducidos son válidos, se pasan a la actividad el servicio web URL (línea 28) y el tiempo de espera antes de cada llamada al servicio (línea 29). Esta información es requerida por la capa [DAO], y nótese que es la actividad la que se comunica con ella;
  • líneas 31-33: los números aleatorios se solicitan uno a uno desde el método [getAlea] en la línea 39;
  • línea 38: el método [getAlea] está anotado con la anotación AA [@Background], lo que significa que se ejecutará en un hilo (flujo de ejecución, proceso) distinto de aquel en el que se ejecuta la interfaz visual. En efecto, es obligatorio ejecutar cualquier llamada a Internet en un hilo distinto al de la interfaz visual. Así, en un momento dado, puede haber varios hilos:
    • la que muestra el UI (Interfaz de Usuario) y gestiona sus eventos,
    • los hilos [nbAleas], cada uno de los cuales solicita un número aleatorio al servicio web. Estos hilos se lanzan de forma asíncrona: el hilo UI lanza un hilo [getAlea] (línea 32) que solicita un número aleatorio al servicio web y no espera a que termine. Se le notificará la finalización mediante un evento. Así, los hilos [nbAleas] se lanzarán en paralelo. Es posible configurar la aplicación para que lance sólo un hilo a la vez. En ese caso, existe una cola de hilos a ejecutar;

Línea 38: el parámetro [id] asigna un nombre al hilo generado. Aquí, los hilos [nbAleas] tienen todos el mismo nombre [alea]. Esto nos permitirá cancelarlos todos al mismo tiempo. Este parámetro es opcional si no se gestiona la cancelación de hilos;

  • Línea 44: Se llama al método [getAlea] de la actividad. Por lo tanto, se ejecutará en un hilo separado del UI. Este hilo realizará la llamada al servicio web y no esperará la respuesta. Más tarde se le notificará mediante un evento que la respuesta está disponible. En este punto, en la línea 44, el método [showInfo] será llamado con la respuesta recibida como parámetro;
  • Líneas 45-47: La ejecución de la petición web puede lanzar una excepción. A continuación, solicitamos que los mensajes de error de la excepción se muestren en un mensaje de alerta;
  • Línea 35: Esperamos los resultados:
    • se mostrará un indicador de carga;
    • el botón [Cancelar] sustituirá al botón [Ejecutar]. Como los hilos lanzados son asíncronos, el hilo UI no los espera, y la línea 35 se ejecuta antes de que terminen. Una vez que el método [beginWaiting] ha finalizado, el UI puede responder de nuevo a las peticiones del usuario, como por ejemplo un clic en el botón [Cancelar]. Si los hilos lanzados hubieran sido síncronos, la línea 35 sólo se alcanzaría una vez que todos los hilos hubieran terminado. Cancelarlos ya no tendría sentido;

El método [showInfo] es el siguiente:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // one more piece of info
      nbInfos++;
      infoReponses.setText(String.format("List of responses (%s)", nbInfos));
      // Are we done?
      if (nbInfos == nbAleas) {
        // end the wait
        cancelWaiting();
      }
      // add the information to the list of responses
      answers.add(0, String.valueOf(alea));
      // display the responses
      adapterAnswers.notifyDataSetChanged();
    }
}
  • El método [showInfo] es llamado dentro del hilo [getAlea] anotado con [@Background]. Este método actualizará el UI. Sólo puede hacerlo si se ejecuta dentro del subproceso UI. Este es el significado de la anotación [@UiThread] en la línea 1;
  • línea 2: el método recibe un número aleatorio;
  • línea 3: el cuerpo del método se ejecuta sólo si el usuario no ha cancelado su solicitud;
  • líneas 5-6: el contador de respuestas se incrementa y se muestra;
  • líneas 8-11: si se han recibido todas las respuestas esperadas, finaliza la espera (señal de fin de espera; el botón [Ejecutar] sustituye al botón [Cancelar]);
  • líneas 12-15: el número aleatorio recibido se añade a la lista de respuestas mostradas por el componente [ListView listReponses], y la lista se actualiza;

El método [showAlert] es el siguiente:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // cancel everything
      cancelAll();
      // display it
      new AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Close", null).show();
    }
}

La lógica aquí es similar a la del método [showInfo]:

  • línea 1: la anotación [@UiThread] es obligatoria;
  • línea 2: el método recibe la excepción que se ha producido;
  • línea 3: el método se ejecuta sólo si el usuario no ha cancelado su solicitud;
  • línea 5: la solicitud del usuario se cancela como si él mismo hubiera pulsado el botón [Cancelar];
  • línea 7: la alerta se muestra utilizando la clase [AlertDialog] de Android:
    • [actividad]: es la actividad de tipo [Actividad] almacenada en la clase padre [AbstractFragment];
    • [setTitle]: establece el título de la ventana de alerta [1];
    • [setMessage]: establece el mensaje que muestra la ventana de alerta [2];
    • [setNeutral]: establece el botón que cerrará la ventana de alerta [3];
    • [mostrar]: solicita que se muestre la ventana de alerta;
 

Al hacer clic en el botón [Cancelar] se utiliza el siguiente método:


  @Click(R.id.btn_Cancel)
  protected void doCancel() {
    // memory
    hasBeenCanceled=true;
    // cancel the asynchronous task
    BackgroundExecutor.cancelAll("alea", true);
    // end of wait
    cancelWaiting();
}
  • línea 4: nota que el usuario ha cancelado su solicitud;
  • línea 6: cancela todas las tareas identificadas por la cadena [alea]. El segundo parámetro [true] significa que deben cancelarse aunque ya se hayan lanzado. El identificador [alea] es el utilizado para calificar el método [getAlea] del fragmento (línea 1 más abajo):

  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

Nota: Resultó que la línea 6 del código del método [doAnnuler] se comportaba incorrectamente. Por eso añadimos el booleano [hasBeenCanceled]. En efecto, en caso de excepción (servidor caído), aparecería la ventana de alerta n veces si hubiéramos solicitado n números aleatorios.

1.16.2.7. La actividad [MainActivity]

1.16.2.7.1. La vista [activity-main.xml]
  

En comparación con el ejemplo anterior, hemos añadido una imagen de carga a la vista asociada al [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">
      <!-- loading image -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
 
    </android.support.v7.widget.Toolbar>
    <!-- loading image -->
  </android.support.design.widget.AppBarLayout>
...
  • líneas 17-21: la imagen del marcador de posición;

1.16.2.7.2. La actividad [MainActivity]

El [MainActivity] ha cambiado poco respecto a lo que era en el [Ejemplo-14]. En primer lugar, le inyectamos la capa [DAO]:


  // DAO injection
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // configure the [DAO] layer
    setTimeout(TIMEOUT);
}
  • líneas 2-3: inyección de la capa [DAO] mediante una anotación AA;
  • líneas 5-13: código ejecutado después de esta inyección;
  • línea 12: establecer el tiempo de espera para la capa [DAO]

Además, la actividad [MainActivity] debe implementar la interfaz [IMainActivity], que a su vez extiende la interfaz [IDao]:


  // IMainActivity implementation --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // display the view at position
    if (mViewPager.getCurrentItem() != position) {
      // display fragment
      mViewPager.setCurrentItem(position);
    }
  }
 
  // Handling the loading image
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }
 
  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }
 
  // IDao implementation --------------------------------------------------------------------
 
  @Override
  public int getRandom(int a, int b) {
    // execution
    return dao.getRandom(a, b);
  }
 
  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
  }
 
  @Override
  public void setWebServiceJsonUrl(String url) {
    dao.setUrlServiceWebJson(url);
  }
 
  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
}

1.16.2.8. Ejecución del proyecto

Inicie el servicio web (sección 1.16.1.7) y, a continuación, inicie el cliente Android:

Image

Para saber qué introducir en [1], siga estos pasos. Abra un símbolo del sistema y escriba el siguiente comando:


C:\Program Files\Console2>ipconfig
 
Windows IP Configuration
 
Wireless Network Adapter Local Area Connection* 3:
 
   Media status. . . . . . . . . . . . : Media disconnected
   Connection-specific DNS suffix. . . :
 
VirtualBox Host-Only Network Ethernet adapter:
 
   Connection-specific DNS suffix. . . :
   Local loopback IPv6 address. . . . .: fe80::e481:1583:cd2a:c47%27
   IPv4 address. . . . . . . . . . . . . .: 192.168.82.2
   Subnet mask. . . . . . . . . . . . . .: 255.255.255.0
   Default gateway. . . . . . . . . :
 
VirtualBox Host-Only Network #2 Ethernet Card:
 
   Connection-specific DNS suffix. . . :
   Local link IPv6 address . . . . .: fe80::8191:14ad:407d:b840%54
   IPv4 address . . . . . . . . . . . . . .: 192.168.64.2
   Subnet mask. . . . . . . . . . . . . .: 255.255.255.0
   Default gateway. . . . . . . . . :
 
Ethernet card:
 
   Connection-specific DNS suffix: ad.univ-angers.fr
   Local link IPv6 address . . . . .: fe80::d972:ad53:3b8a:263f%28
   IPv4 address . . . . . . . . . . . . . .: 172.19.81.34
   Subnet mask. . . . . . . . . . . . . .: 255.255.0.0
   Default gateway. . . . . . . . . : 172.19.0.254
 
Wi-Fi wireless network adapter:
 
   Media status. . . . . . . . . . . . : Media disconnected
   Connection-specific DNS suffix. . . : uang ad.univ-angers.fr univ-angers.fr

Si ha instalado [GenyMotion], la máquina virtual VirtualBox ha añadido direcciones IP a su ordenador (líneas 10 y 18). Estas direcciones son particularmente convenientes porque no están bloqueadas por el cortafuegos de Windows. La línea 30 muestra la dirección IP de su ordenador en una red local. Para utilizar esta dirección, por lo general es necesario desactivar el firewall de Windows. Si está conectado a una red Wi-Fi, utilice la dirección Wi-Fi y, también en este caso, desactive el cortafuegos si lo tiene.

Pruebe la aplicación en los siguientes casos:

  • 100 números aleatorios en el rango [1000, 2000] sin tiempo de espera;
  • 2000 números aleatorios en el rango [10000, 20000] sin tiempo de espera, y cancela la espera antes de que se complete la generación;
  • 5 números aleatorios en el rango [100, 200] con un tiempo de espera de 5000 ms, y cancelar la espera antes de que se complete la generación;

1.16.2.9. Gestión de cancelaciones

Para hacer un seguimiento de lo que ocurre cuando el usuario solicita la cancelación o cuando la cancelación es provocada por una excepción, añadimos el siguiente método a la interfaz [IDao] (véase la sección 1.16.2.4.1):


package exemples.android.dao;
 
public interface IDao {
 
  ...
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}

En la clase [Dao], añadimos el siguiente código:


  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
..
  // constructor
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
...
  // IDao interface -------------------------------------------------------------------
  @Override
  public int getRandom(int a, int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] in progress", a, b));
    }
    // service execution
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • línea 9: anotamos el nombre de la clase;
  • líneas 16-18: Escribimos un log cada vez que se llama al método [getAlea];

Además, en el fragmento [Vue1Fragment], añadimos los siguientes registros:


  @UiThread
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }
 
  @UiThread
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception received");
    }
    ...
    }
}
 
  @Click(R.id.btn_Cancel)
  protected void doCancel() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Cancellation requested");
    }
   ...
}

Cada vez que el fragmento [Vue1Fragment] recibe información de la capa [DAO], se emite un registro. Además, cuando se llama al método [doAnnuler], se registra el evento.

Prueba 1

Solicitamos 5 números a pesar de que el servidor no se ha iniciado. Obtenemos los siguientes registros:

06-06 08:48:51.571 15317-16201/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.576 15317-16202/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.585 15317-16204/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.586 15317-16203/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 08:48:51.593 15317-16205/examples.android D/Dao_: getRandom [100, 200] in progress
...
06-06 08:48:53.568 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.568 15317-15317/examples.android D/View1Fragment_: Cancellation requested
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
06-06 08:48:53.587 15317-15317/examples.android D/View1Fragment_: Exception received
  • Líneas 1-5: El método [getAlea] de la clase [Dao] es llamado cinco veces. Observa que son llamadas asíncronas realizadas por el fragmento [VueFragment], y que el fragmento no espera el resultado de su llamada;
  • línea 7: se ha realizado la primera petición HTTP, y el fragmento [VueFragment] ha recibido su primera excepción;
  • línea 8: a continuación, solicita la anulación de todas las solicitudes;
  • líneas 9-12: sin embargo, vemos que recibe las cuatro excepciones siguientes. Por lo tanto, las peticiones asíncronas pendientes se ejecutaron todas;

Prueba 2

Ahora, iniciemos el servidor y solicitemos 5 números con un retardo de 5 segundos, luego hagamos clic en [Cancelar] antes de que termine el retardo. Los registros son los siguientes:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] in progress
06-06 09:12:38.360 4640-5055/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.361 4640-5056/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.362 4640-5057/examples.android D/Dao_: getRandom [100, 200] in progress
06-06 09:12:38.363 4640-5058/examples.android D/Dao_: getRandom [100, 200] in progress
...
06-06 09:12:39.895 4640-4640/examples.android D/View1Fragment_: Cancellation requested
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/examples.android D/View1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/examples.android D/View1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/examples.android D/View1Fragment_: showInfo(157)
  • líneas 1-5: el método [getAlea] de la clase [Dao] es llamado cinco veces;
  • línea 7: el usuario solicita que se anulen las solicitudes;
  • línea 8: vemos que [Vue1_Fragment] recibe 5 valores. Una vez más, las peticiones asíncronas pendientes se han ejecutado todas;

Por eso tuvimos que gestionar un booleano [hasBeenCanceled] para evitar mostrar nada cuando se había solicitado una cancelación. En el código de cancelación:


  @Click(R.id.btn_Cancel)
  protected void doCancel() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Cancellation requested");
    }
    // memory
    hasBeenCanceled = true;
    // cancel the asynchronous task
    BackgroundExecutor.cancelAll("alea", true);
    // end of wait
    cancelWaiting();
}

El código de la línea 10 no hace lo que se espera. Esto puede deberse a que las tareas asíncronas comparten el mismo método anotado con [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Ejemplo 16: Manejo de la Asincronía con RxAndroid

Ahora gestionaremos la asincronía necesaria para las aplicaciones Android utilizando una librería llamada RxJava [http://reactivex.io/] y su versión derivada para el entorno Android [RxAndroid]. Para ello, utilizaremos el curso [Introducción a RxJava. Aplicación a Entornos Swing y Android].

1.17.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-1] en [Ejemplo-16]:

1.17.2. Configuración de Gradle

  

En [build.gradle], añadimos la dependencia de la biblioteca [RxAndroid]:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. La capa [DAO]

  

1.17.4. La interfaz [IDao]

La interfaz [IDao] pasa a ser la siguiente:


package examples.android.dao;
 
import rx.Observable;
 
public interface IDao {
 
  // random number
  Observable<Integer> getRandom(int a, int b);
 
  // Web service URL
  void setWebServiceJsonUrl(String url);
 
  // maximum wait time (ms) for the server response
  void setTimeout(int timeout);
 
  // Client wait time in milliseconds before sending a request
  void setDelay(int delay);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • línea 8: el método [getAlea] devuelve ahora un tipo [Observable] de la biblioteca RxJava (línea 3). El principio es el siguiente:

Un flujo de elementos de tipo Observable<T> es observado por uno o más suscriptores (observadores, consumidores) de tipo Subscriber<T>. La librería RxJava permite que el flujo Observable<T> se ejecute en el hilo T1 y su observador Subscriber<T> en el hilo T2 sin que el desarrollador tenga que preocuparse de gestionar el ciclo de vida de estos hilos y, naturalmente, de cuestiones difíciles, como la compartición de datos entre hilos y la sincronización de hilos para ejecutar una tarea global. Por lo tanto, facilita la programación asíncrona.

1.17.5. La clase [AbstractDao]

Derivaremos la clase [Dao] de la siguiente clase [AbstractDao]:


package examples.android.dao;
 
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();

  // protected methods ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    Response<T> getResponse();
  }
 
  // Generic request
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // service execution
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // make the synchronous request and forward the response to the subscriber
          Response<T> response = request.getResponse();
          // error?
          int status = response.getStatus();
          if (status != 0) {
            // Log the exception
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // send the response
            subscriber.onNext(response.getBody());
            // signal the end of the observable
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // log the exception
          ex = new DaoException(e, 100);
        }
        // exception?
        if (ex != null) {
          // throw the exception
          subscriber.onError(ex);
        }
      }
    });
  }
 
}
  • La clase [AbstractDao] tiene como elemento principal un método genérico [getResponseutilizado para recuperar una [Response<T>] del servidor, donde T es el tipo del resultado deseado por el cliente HTTP (aquí, Integer);
  • Línea 20: El único parámetro del método genérico [getResponsees una instancia de la interfaz genérica [IRequest<T>] de las líneas 15-17. Esta interfaz sólo tiene un método [getResponse], y es este método el que devuelve la [Respuesta<T>] deseada;
  • Gracias a los dos puntos anteriores, la clase [AbstractDao] puede servir como clase padre para cualquier capa [Dao] del lado del cliente de un servidor que envíe respuestas de tipo [Response<T>];
  • línea 20: el método genérico [getResponse] devuelve un tipo [Observable<T>] que representa el resultado realmente esperado por el cliente HTTP (aquí, un tipo Observable<Integer>);
  • líneas 22-51: el método estático [rx.Observable.create] crea un tipo [Observable];
  • línea 22: el único parámetro de este método es una instancia del tipo [rx.Observable.OnSubscribe<T>], una interfaz que tiene los siguientes métodos:
    • [onNext(T elemento)]: permite emitir un elemento de tipo T a un observador;
    • [onError(Throwable th)]: permite emitir una excepción a un observador;
    • [onCompleted]: permite indicar a un observador que las emisiones han finalizado;

Un tipo [Observable<T>] obedece ciertas restricciones:

  • emite sus elementos mediante el método [onNext(T elemento)];
  • el método [onCompleted] debe ser llamado exactamente una vez en cuanto no haya más elementos que emitir al observador;
  • no se llama al método [onCompleted] si se ha llamado al método [onError(Throwable th)];

En nuestro ejemplo:

  • el observador será el fragmento [Vue1Fragment]. Es el observador que consume los elementos emitidos por el [Observable<T>] (elemento o excepción);
  • el tipo [Observable<T>] creado emitirá un único elemento (línea 37);
  • línea 29: hace un síncrono HTTP al servidor y obtiene el tipo [Response<T>]. Esta petición HTTP es gestionada por el tipo [IRequest] pasado como parámetro al método genérico [getResponse];
  • línea 31: recupera la respuesta estado;
  • líneas 32-34: si esta estado indica un error, se prepara una excepción;
  • líneas 36-39: si el estado no es un error, se envía la respuesta realmente esperada por el cliente (línea 37), y se notifica al observador que no habrá más emisiones (línea 39);
  • líneas 41-44: si la petición HTTP termina con una excepción, regístrala;
  • líneas 46-49: si la excepción [ex] no es null, entonces se emite al observador. Aquí no es necesario llamar al método [onCompleted] para indicar al observador que no se emitirán más elementos. Esto está implícito;

La clave de estas explicaciones es que:

  • el método genérico [<T> Observable<T> getResponse(final IRequest<T> request)] devuelve un tipo [Observable<T>] que emite un único elemento de tipo T o una excepción;
  • este método acepta como único parámetro un tipo [IRequest<T>] cuyo único método [getResponse()] realiza la petición HTTP que devuelve el tipo [Response<T>];

1.17.6. La clase [Dao]

La clase [Dao] evoluciona de la siguiente manera:


@EBean
public class Dao extends AbstractDao implements IDao {
 
  // REST service client
  @RestService
  protected WebClient webClient;
 
  // delay before executing the request
  private int delay;
  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
 
  // constructor
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
 
 
  // IDao interface -------------------------------------------------------------------
  @Override
  public Observable<Integer> getRandom(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] in progress", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // wait
        waitSomeTime(delay);
        // synchronous HTTP call
        return webClient.getAlea(a, b);
      }
    });
}
...
  • línea 2: la clase [Dao] extiende la clase [AbstractDao];
  • línea 24: el método [getAlea] devuelve ahora un tipo [Observable<Integer>];
  • línea 30: llamada al método genérico [getResponse] de la clase padre. Se le pasa un parámetro de tipo [IRequest<Integer>];
  • líneas 32-37: implementación de la interfaz [IRequest<Integer>];
  • línea 36: la petición HTTP se realiza a través de la interfaz AA [webClient] como se hizo anteriormente. Sabemos que recuperaremos un tipo [Respuesta<Integer>], que es efectivamente el tipo que debe devolver el método [IRequest<Integer>.getResponse()];
  • línea 36: aquí utilizamos una función llamada un cierre: la capacidad de encapsular valores externos a una instancia dentro de ella cuando se crea, en este caso los valores de [a, b] de la línea 24. Esto es lo que permite que el método [IRequest<Integer>.getResponse()] no tenga parámetros. Estos valores se han incrustado dentro del cuerpo del método. Y donde normalmente cambiaríamos el método de parámetros (a, b) -> (x, y), aquí creamos una nueva instancia de [IRequest<Integer>] encapsulando los valores de x e y;

1.17.7. La clase [MainActivity]

La clase [MainActivity], que implementa la interfaz [IDao], evoluciona del siguiente modo:


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

1.17.8. La clase [Vue1Fragment]

La clase [Vue1Fragment] evoluciona del siguiente modo:


  @Click(R.id.btn_Executer)
  protected void doExecute() {
    // clear previous responses
    answers.clear();
    adapterAnswers.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoAnswers.setText(String.format("List of answers (%s)", nbAnswers));
    // Check the validity of the entries
    if (!isPageValid()) {
      return;
    }
    // initialize activity
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // Generate random numbers
    getRandomNumbersInBackground(a, b);
    // start waiting
    beginWaiting();
}
  • línea 18: solicitar números aleatorios al método [getAleasInBackground], llamado así porque los números se solicitarán en un hilo distinto del hilo UI;

  private int nbResponses = 0;
  // subscriptions to observables
  private List<Subscription> subscriptions;
 
// [Background] annotation is unnecessary
  void getRandomNumbersInBackground(int a, int b) {
    // Initially, no responses and no subscriptions
    nbResponses = 0;
    subscriptions.clear();
    // prepare the observable
    Observable<Integer> response = Observable.empty();
    // merge the results of the various HTTP calls
    // they are executed on an I/O thread
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getRandom(a, b).subscribeOn(Schedulers.io()));
    }
    // the merged observable will be observed on the UI thread
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // execute the observable
      subscriptions.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // add the information to the list of responses
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // error message
          showAlert(th);
          // end wait
          doCancel();
        }
      }, new Action0() {
        @Override
        public void call() {
          // end wait
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // display the exception in the UI thread
      showAlert(e);
    }
}
  • línea 3: un observable tiene suscriptores. El vínculo entre un suscriptor y el proceso que observa se denomina suscripción. Aquí, tendremos sólo un proceso observado y un suscriptor. Por lo tanto, sólo tendremos una suscripción. Por el bien del principio, estamos tratando como si pudiéramos tener múltiples procesos observados supervisados por diferentes observadores, lo que daría lugar a múltiples suscripciones;
  • líneas 11-18: configuramos el proceso observado (observable). Es importante comprender que esto es sólo configuración: el proceso no se ejecuta;
  • línea 11: empezamos con un observable vacío, un observable que no emite nada;
  • líneas 14-16: a este observable vacío, añadimos observables [nbAleas], que serán peticiones [nbAleas] HTTP que devuelven números aleatorios [nbAleas];
  • Línea 15: Como antes, se solicita el número aleatorio i a la clase [MainActivity]. Es importante entender que aún no se ha ejecutado ninguna petición HTTP. El método [mainActivity.getRandom(a, b)] se ejecuta y devuelve un [Observable<Integer>]. Se trata de un proceso que será observado una vez sea lanzado;
  • línea 15: el método [subscribeOn(Schedulers.io())] solicita que el proceso se ejecute (cuando lo esté) en un hilo de E/S. La biblioteca RxJava ofrece diferentes tipos de hilos. El hilo de E/S es adecuado para peticiones HTTP;
  • línea 15: el observable #i se fusiona con el observable inicial de la línea 11: a partir de [nbAleas] Observables, cada uno emitiendo un elemento, creamos un observable que emitirá [nbAleas] elementos. Este es el que será observado. Este observable emite la notificación [onCompleted] cuando todos los observables que lo componen han emitido sus propias notificaciones [onCompleted]. Esto nos evitará tener que contar las respuestas, como hacíamos en la versión anterior, para determinar si hemos recibido todos los números esperados;
  • línea 18: en este punto, hemos configurado un observable que es la composición de [nbAleas] observables, cada uno ejecutándose en un hilo de E/S;
  • línea 18: el método [observeOn(AndroidSchedulers.mainThread())] especifica en qué hilo deben observarse los valores emitidos por el observable. Aquí, el hilo [AndroidSchedulers.mainThread())] pertenece a la biblioteca RxAndroid, no a RxJava. Se refiere al hilo UI, también conocido como el bucle de eventos. Este punto es importante: en una aplicación Android, modificar un componente UI sólo puede hacerse en el hilo UI; de lo contrario, se produce una excepción;
  • líneas 19-45: ahora que el proceso a observar ha sido configurado, lo ejecutamos;
  • línea 21: la operación [Observable.subscribe] inicia la ejecución del proceso observado. Esta operación lanzará los procesos asíncronos [nbAleas] configurados anteriormente. Los resultados de estos procesos se pondrán automáticamente a disposición del observador en el hilo UI;
  • Recordemos que el observable emite tres tipos de eventos:
    • [onNext]: cuando emite un elemento;
    • [onError]: cuando encuentra una excepción;
    • [onCompleted]: cuando señala que dejará de emitir;

El método [Observable.subscribe] toma tres objetos como parámetros: [Action1<Integer>, Action1<Throwable>, Action0], cuyos métodos [call] se utilizan para manejar cada uno de estos tres eventos;

  • líneas 21-27: el primer parámetro de tipo [Action1<Integer>] se utiliza para gestionar el evento [onNext]. Su método [call] recibe el elemento emitido por el observable (línea 23);
  • línea 25: reutilizamos el método [showInfo] del ejemplo anterior;
  • líneas 27-35: el segundo parámetro de tipo [Action1<Throwable>] se utiliza para manejar el evento [onError]. Su método [call] recibe la excepción emitida por el observable (línea 29);
  • línea 31: reutilizamos el método [showAlert] del ejemplo anterior;
  • línea 33: iniciamos el procedimiento para cancelar la petición del usuario. Esto implica cancelar todos los observables que se estén ejecutando en ese momento;
  • líneas 35-41: el tercer parámetro de tipo [Action0] se utiliza para manejar el evento [onCompleted]. Su método [call] no recibe parámetros;
  • línea 39: se cancela la espera;

El método [showInfo] evoluciona del siguiente modo:


  // [UiThread] annotation is unnecessary
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // one more piece of info
      nbInfos++;
      infoReponses.setText(String.format("List of responses (%s)", nbInfos));
      // add the information to the list of answers
      answers.add(0, String.valueOf(alea));
      // display the responses
      adapterAnswers.notifyDataSetChanged();
    }
}

El método presenta dos cambios:

  • línea 1: eliminamos la anotación AA [@UiThread];
  • ya no contamos las respuestas para determinar si hay que dejar de esperar o no. Ahora es el evento [onCompleted] del observable el que proporciona esta información;

El método [showAlert] cambia de la siguiente manera:


  // [UiThread] annotation is unnecessary
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception received");
    }
    if (!hasBeenCanceled) {
      // cancel everything
      doCancel();
      // display it
      new AlertDialog.Builder(activity).setTitle("Errors have occurred").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Close", null).show();
    }
}
  • El único cambio está en la línea 1: hemos eliminado la anotación AA [@UiThread];

Por último, el método [doAnnuler] cambia como sigue:


  @Click(R.id.btn_Cancel)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Cancellation requested");
    }
    // memory
    hasBeenCanceled = true;
    // cancel asynchronous tasks
    if (subscriptions != null) {
      for (Subscription subscription : subscriptions) {
        subscription.unsubscribe();
      }
    }
    // end of wait
    cancelWaiting();
}
  • línea 12: cancela una suscripción y, por tanto, la observación del proceso asociado;

1.17.9. Ejecución

Inicie el servicio web (sección 1.16.1.7), inicie el cliente Android y repita las pruebas que realizó con el ejemplo anterior (sección 1.16.2.8).

1.17.10. Gestión de cancelaciones

Repetimos las mismas pruebas que en el ejemplo anterior (sección 1.16.2.9).

Prueba 1

Solicitamos 5 números aunque el servidor no ha sido lanzado. Obtenemos los siguientes registros:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:09.791 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:48:11.789 28272-28272/examples.android D/View1Fragment_: Exception received
06-07 05:48:11.789 28272-28272/examples.android D/View1Fragment_: Cancellation requested

Después de la línea 7, no hay más registros, lo que muestra que el observador (Vue1Fragment) ya no recibe notificaciones del proceso observado.

Prueba 2

Ahora, iniciemos el servidor y solicitemos 5 números con un retardo de 5 segundos, luego hagamos clic en [Cancelar] antes de que termine el retardo. Los registros son los siguientes:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:22.675 28272-28272/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 05:52:23.485 28272-28272/examples.android D/View1Fragment_: Cancellation requested

Después de la línea 6, no hay más registros, lo que muestra que el observador (Vue1Fragment) ya no recibe notificaciones del proceso observado.

Este es el comportamiento esperado de una cancelación. Por lo tanto, podemos eliminar el booleano [hasBeenCanceled] del código [Vue1Fragment] que introdujimos en el ejemplo anterior porque la cancelación no se comportaba como se esperaba.

El hecho de que el observador deje de recibir notificaciones tras la cancelación del observable no significa que se cancelen las propias peticiones HTTP. Para ver esto, modificamos la clase [Dao] como sigue:


  @Override
  public Observable<Integer> getRandom(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getRandom [%s, %s] in progress", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // wait
        waitSomeTime(delay);
        // synchronous HTTP call
        Response<Integer> response = webClient.getAlea(a, b);
        if (isDebugEnabled) {
          try {
            Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
          } catch (JsonProcessingException e) {
            Log.d(String.format("%s", className), "JSON deserialization error");
          }
        }
        return response;
      }
    });
}
  • líneas 15-21: registramos el resultado de la petición HTTP de la línea 14;

Los registros de la prueba nº 2 son los siguientes:

06-07 06:03:20.778 27085-27085/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 06:03:20.784 27085-27085/examples.android D/Dao_: getAlea [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:20.785 27085-27085/examples.android D/Dao_: getRandom [100, 200] in progress
06-07 06:03:21.493 27085-27085/examples.android D/View1Fragment_: Cancellation requested
06-07 06:03:21.636 27085-27440/examples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/examples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/examples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/examples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/examples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • líneas 1-5: se han realizado las 5 solicitudes;
  • línea 6: el usuario cancela;
  • líneas 7-11: recibimos correctamente las respuestas a las cinco peticiones HTTP. Sin embargo, como el observable se canceló, estos elementos no se pasan al observador;

1.17.11. Conclusión

En el resto de este documento, las aplicaciones cliente/servidor se implementarán utilizando la biblioteca RxAndroid en lugar de la biblioteca AA por las siguientes razones:

  1. RxAndroid puede utilizarse en una aplicación Android que no utilice AA;
  2. RxAndroid hace algo más que facilitar las operaciones asíncronas. Ofrece numerosos métodos para crear un nuevo observable a partir de otro. Estos métodos no tienen equivalente en AA;
  3. En cuanto se intenta derivar una clase anotada por AA, como un fragmento, surgen graves problemas. Uno se ve obligado a abandonar AA y utilizar la Solución 1 para la programación asíncrona;

Los lectores interesados en explorar más a fondo las capacidades de la biblioteca RxAndroid pueden consultar el documento [Introducción a RxJava. Aplicación a Entornos Swing y Android]. Utiliza RxAndroid sin la biblioteca AA.

1.18. Ejemplo 17: Componentes de entrada de datos

Crearemos un nuevo proyecto para demostrar algunos componentes comunes utilizados en los formularios de entrada de datos.

1.18.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-13] en [Ejemplo-17]:

El nuevo proyecto sólo tendrá una vista [view1.xml]. Por lo tanto, eliminaremos la vista [view2.xml] y su fragmento asociado [View2Fragment] [2]. Reflejaremos este cambio en el gestor de fragmentos de [MainActivity]:


  // our fragment manager, which must be redefined for each application
  // must define the following methods: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // the fragments
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Vuelva a ejecutar el proyecto. Debería mostrar la vista #1 como antes. Trabajaremos a partir de este proyecto.

1.18.2. La vista XML del formulario

  

La vista generada por el fichero [vue1.xml] es la siguiente:

Image

El texto XML de la vista es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="30dp">
 
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
 
    <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="30dp"
      android:text="@string/view1_title"
      android:textSize="30sp"/>
 
    <Button
      android:id="@+id/formButtonValidate"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/TextViewFormCombo"
      android:layout_below="@+id/TextViewFormCombo"
      android:layout_marginTop="30dp"
      android:text="@string/form_validate"/>
 
    <TextView
      android:id="@+id/textViewFormCheckBox"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormTitle"
      android:layout_below="@+id/textViewFormTitle"
      android:layout_marginTop="30dp"
      android:text="@string/form_checkbox"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormRadioButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormCheckBox"
      android:layout_below="@+id/textViewFormCheckBox"
      android:layout_marginTop="30dp"
      android:text="@string/form_radioButton"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormSeekBar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormRadioButton"
      android:layout_below="@+id/textViewFormRadioButton"
      android:layout_marginTop="30dp"
      android:text="@string/form_seekBar"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireEdtText"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
      android:layout_below="@+id/textViewFormulaireSeekBar"
      android:layout_marginTop="30dp"
      android:text="@string/input_form"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormBool"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormEdtText"
      android:layout_below="@+id/textViewFormEdtText"
      android:layout_marginTop="30dp"
      android:text="@string/boolean_form"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormDate"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:layout_alignLeft="@+id/textViewFormBool"
      android:layout_below="@+id/textViewFormBool"
      android:layout_marginTop="50dp"
      android:gravity="center"
      android:text="@string/form_date"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/multilineFormTextView"
      android:layout_width="150dp"
      android:layout_height="100dp"
      android:gravity="center"
      android:layout_alignBaseline="@+id/textViewFormTitle"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="400dp"
      android:layout_toRightOf="@+id/textViewFormTitle"
      android:text="@string/multiline_form"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormTime"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:gravity="center"
      android:layout_alignLeft="@+id/textViewFormMultiline"
      android:layout_below="@+id/textViewFormMultiline"
      android:layout_marginTop="30dp"
      android:text="@string/time_form"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/TextViewFormCombo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormTime"
      android:layout_below="@+id/textViewFormTime"
      android:layout_marginTop="30dp"
      android:text="@string/comboForm"
      android:textSize="20sp"/>
 
    <CheckBox
      android:id="@+id/formCheckBox1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormCheckBox"
      android:layout_marginLeft="100dp"
      android:layout_toRightOf="@+id/textViewFormCheckBox"
      android:text="@string/form_checkbox1"/>
 
    <RadioGroup
      android:id="@+id/formRadioGroup"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
      android:layout_alignLeft="@+id/formCheckBox1"
      android:orientation="horizontal">
 
      <RadioButton
        android:id="@+id/formRadioButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/form_radiobutton1"/>
 
      <RadioButton
        android:id="@+id/formRadioButton2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/form_radiobutton2"/>
 
      <RadioButton
        android:id="@+id/formRadioButton3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/form_radiobutton3"/>
    </RadioGroup>
 
    <SeekBar
      android:id="@+id/formSeekBar"
      android:layout_width="200dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
      android:layout_alignLeft="@+id/formCheckBox1"/>
 
    <EditText
      android:id="@+id/formEditText1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
      android:layout_alignLeft="@+id/formCheckBox1"
      android:ems="10"
      android:inputType="text">
    </EditText>
 
    <Switch
      android:id="@+id/formSwitch1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormBool"
      android:layout_alignLeft="@+id/formCheckBox1"
      android:text="@string/form_switch"
      android:textOff="No"
      android:textOn="Yes"/>
 
    <TimePicker
      android:id="@+id/formTimePicker1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormTime"
      android:layout_alignLeft="@+id/multilineEditTextForm"
      android:timePickerMode="spinner"
    />
 
    <EditText
      android:id="@+id/multilineEditTextForm"
      android:layout_width="wrap_content"
      android:layout_height="100dp"
      android:layout_alignBaseline="@+id/multilineFormTextView"
      android:layout_alignBottom="@+id/textViewMultiLineForm"
      android:layout_marginLeft="50dp"
      android:layout_toRightOf="@+id/multilineFormTextView"
      android:ems="10"
      android:inputType="textMultiLine">
    </EditText>
 
    <Spinner
      android:id="@+id/formulaireDropDownList"
      android:layout_width="200dp"
      android:layout_height="50dp"
      android:layout_alignBottom="@+id/TextViewComboForm"
      android:layout_alignLeft="@+id/multilineEditTextForm">
    </Spinner>
 
    <DatePicker
      android:id="@+id/DatePickerForm1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormDate"
      android:layout_alignLeft="@+id/formCheckBox1"
      android:datePickerMode="spinner"
      android:calendarViewShown="false">
    </DatePicker>
 
    <TextView
      android:id="@+id/textViewSeekBarValue"
      android:layout_width="30dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormSeekBar"
      android:layout_marginLeft="30dp"
      android:layout_toRightOf="@+id/formSeekBar"
      android:text=""/>
  </RelativeLayout>

Los principales componentes del formulario son los siguientes:

  • Línea 2: un diseño vertical [ScrollView]. Permite
  • mostrar un formulario más grande que la pantalla de la tableta
  • tableta. Puede ver el formulario completo
  • desplazamiento;
 
  • líneas 125-132: una casilla de verificación
  • líneas 134-159: un grupo de tres botones de radio
  • líneas 161-166: una barra de búsqueda
  • líneas 16-176: un campo de entrada de texto
  • líneas 178-186: un interruptor sí/no
  • líneas 188-195: un campo de introducción de la hora
  • líneas 197-207: un cuadro de texto multilínea
  • líneas 209-215: una lista desplegable
  • líneas 217-225: un campo de entrada de fecha
  • Todos los demás componentes son [TextView]s que muestran texto.
 

1.18.3. Las cadenas del formulario

Las cadenas del formulario se definen en el siguiente archivo [res/values/strings.xml]:

  

<resources>
  <string name="app_name">Example-17</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- view 1 -->
  <string name="view1_title">View #1</string>
  <string name="form_checkbox">Checkboxes</string>
  <string name="form_radioButton">Radio Buttons</string>
  <string name="form_seekBar">Seek Bar</string>
  <string name="form_input">Input field</string>
  <string name="form_boolean">Boolean</string>
  <string name="form_date">Date</string>
  <string name="form_time">Time</string>
  <string name="form_multiline">Multiline Input Field</string>
  <string name="form_listview">List</string>
  <string name="form_combo">Drop-down list</string>
  <string name="form_checkbox1">1</string>
  <string name="form_checkbox2">2</string>
  <string name="form_radiobutton1">1</string>
  <string name="form_radiobutton2">2</string>
  <string name="form_radiobutton3">3</string>
  <string name="form_switch"></string>
  <string name="form_submit">Submit</string>
</resources>

1.18.4. El fragmento de formulario

  

La clase [View1Fragment] es la siguiente:


package examples.android.fragments;
 
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import examples.android.R;
import examples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
// A fragment is a view displayed by a fragment container
@EFragment(R.layout.view1)
public class View1Fragment extends AbstractFragment {
 
  // the fields of the view displayed by the fragment
  @ViewById(R.id.formDropDownList)
  Spinner dropDownList;
  @ViewById(R.id.formButtonValidate)
  Button buttonValidate;
  @ViewById(R.id.formCheckBox1)
  CheckBox checkBox1;
  @ViewById(R.id.formRadioGroup)
  RadioGroup radioGroup;
  @ViewById(R.id.formSeekBar)
  SeekBar seekBar;
  @ViewById(R.id.form-EditText1)
  EditText input;
  @ViewById(R.id.formSwitch1)
  Switch switch1;
  @ViewById(R.id.formDatePicker1)
  DatePicker datePicker1;
  @ViewById(R.id.formTimePicker1)
  TimePicker timePicker1;
  @ViewById(R.id.form-multiline-edit-text)
  EditText multiLines;
  @ViewById(R.id.formRadioButton1)
  RadioButton radioButton1;
  @ViewById(R.id.formRadioButton2)
  RadioButton radioButton2;
  @ViewById(R.id.formRadioButton3)
  RadioButton radioButton3;
  @ViewById(R.id.textViewSeekBarValue)
  TextView seekBarValue;
 
  // 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");
  }
 
 
  @SuppressLint("DefaultLocale")
  @Click(R.id.formButtonValidate)
  protected void doValidate() {
    ...
  }
 
@Override
  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);
  }
}
  • líneas 22-49: recuperamos las referencias de todos los componentes del formulario XML [vista1] (línea 18);
  • línea 58: el método [setChecked] permite marcar un botón de radio o una casilla de verificación;
  • línea 60: por defecto, el componente [DatePicker] muestra tanto un campo de entrada de fecha como un calendario. La línea 60 elimina el calendario;
  • línea 62: [SeekBar].setMax() fija el valor máximo del deslizador. El valor mínimo es 0;
  • líneas 63-74: Manejamos los eventos de la barra de búsqueda. Por cada cambio realizado por el usuario, queremos mostrar el valor del deslizador en el [TextView] de la línea 49;
  • línea 71: el parámetro [progress] representa el valor del deslizador;
  • líneas 76-79: una lista de [String]s que se asociarán a la lista desplegable;
  • línea 90: el método [updateFragment] del fragmento. Cuando se ejecuta, se ha inicializado la variable [activity] de la clase padre;
  • línea 92: la fuente de datos [lista] está vinculada al adaptador de lista desplegable;
  • líneas 93-94: el [dataAdapter] está vinculado a la lista desplegable [dropDownList];
  • línea 84: el método [doValider] está asociado a un clic sobre el botón [Valider];

El objetivo del método [doValider] es mostrar los valores introducidos por el usuario. Su código es el siguiente:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    // checkbox
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // radio buttons
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // the SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // the input field
    String text = String.valueOf(input.getText());
    messages.add(String.format("Simple input [value=%s]", text));
    // the switch
    boolean state = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", state));
    // the date
    int year = datePicker1.getYear();
    int month = datePicker1.getMonth() + 1;
    int day = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", day, month, year));
    // multi-line text
    String lines = String.valueOf(multiLines.getText());
    messages.add(String.format("Multi-line input [value=%s]", lines));
    // the time
    int hour = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Time [%d, %d]", hour, minutes));
    // dropdown list
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // display
    doDisplay(messages);
}
  • línea 4: los valores introducidos se añadirán a una lista de mensajes;
  • línea 6: el [CheckBox].isChecked() determina si una casilla de verificación está marcada o no;
  • línea 9: el [RadioGroup].getCheckedButtonId() devuelve el ID del botón de opción seleccionado o -1 si no hay ninguno seleccionado;
  • línea 10: el código [actividad.findViewById(id)] recupera el botón de opción marcado y, por tanto, su etiqueta;
  • línea 13: el [SeekBar].getProgress() devuelve el valor de un deslizador;
  • línea 19: el método [Switch].isChecked() determina si un interruptor En (verdadero) o Fuera de (falso);
  • línea 22: el [DatePicker].getYear() recupera el año seleccionado utilizando un objeto [DatePicker];
  • línea 23: el [DatePicker].getMonth() devuelve el mes seleccionado de un objeto [DatePicker] dentro del rango [0,11];
  • línea 24: el [DatePicker].getDayOfMonth() devuelve el día del mes seleccionado utilizando un objeto [DatePicker] dentro del rango [1,31];
  • línea 30: el [TimePicker].getHour() devuelve la hora seleccionada mediante un objeto [TimePicker];
  • línea 31: el [TimePicker].getMinute() devuelve los minutos seleccionados utilizando un objeto [TimePicker];
  • línea 34: el [Spinner].getSelectedItemPosition() devuelve la posición del elemento seleccionado en una lista desplegable;
  • línea 35: el [Spinner].getSelectedItem() devuelve el elemento seleccionado en una lista desplegable;

El método [doAfficher], que muestra la lista de valores introducidos, es el siguiente:


    private void displayMessages(List<String> messages) {
        // Build the text to be displayed
        StringBuilder text = new StringBuilder();
        for (String message : messages) {
            text.append(String.format("%s\n", message));
        }
        // display it
        new AlertDialog.Builder(activity).setTitle("Entered values").setMessage(text).setNeutralButton("Close", null).show();
}
  • línea 1: el método recibe una lista de mensajes para mostrar;
  • líneas 3-6: se construye un objeto [StringBuilder] a partir de estos mensajes. Para la concatenación de cadenas, el tipo [StringBuilder] es más eficiente que el tipo [String];
  • línea 8: un cuadro de diálogo muestra el texto de la línea 3:

Image

1.18.5. Ejecución del proyecto

Ejecute el proyecto y pruebe los distintos componentes de entrada.

1.19. Ejemplo 18: Uso de un patrón de vista

1.19.1. Creación del proyecto

Creamos un nuevo proyecto [Ejemplo-18] copiando el proyecto [Ejemplo-13].

1.19.2. La plantilla de vista

Queremos reutilizar las dos vistas del proyecto e incluirlas en una plantilla:

  

Image

Cada una de las dos vistas se estructurará de la misma manera:

  • en [1], una cabecera;
  • en [2], una columna izquierda que podría contener enlaces;
  • en [3], un pie de página;
  • en [4], contenido.

Esto se consigue modificando la vista base de la actividad [activity_main.xml];

El código XML para la vista [main] es el 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>
 
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:layout_marginTop="75dp"
                android:orientation="vertical">
 
    <LinearLayout
      android:id="@+id/header"
      android:layout_width="match_parent"
      android:layout_height="100dp"
      android:layout_weight="0.1"
      android:background="@color/lavenderblushh2">
 
      <TextView
        android:id="@+id/textViewHeader"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center_horizontal"
        android:text="@string/txt_header"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@color/red"/>
    </LinearLayout>
 
    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="fill_parent"
      android:layout_weight="0.8"
      android:orientation="horizontal">
 
      <LinearLayout
        android:id="@+id/left"
        android:layout_width="100dp"
        android:layout_height="match_parent"
        android:background="@color/lightcyan2">
 
        <TextView
          android:id="@+id/txt_left"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:gravity="center_vertical|center_horizontal"
          android:text="@string/txt_left"
          android:textAppearance="?android:attr/textAppearanceLarge"
          android:textColor="@color/red"/>
      </LinearLayout>
 
      <examples.android.architecture.MyPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/floral_white"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </LinearLayout>
 
    <LinearLayout
      android:id="@+id/bottom"
      android:layout_width="match_parent"
      android:layout_height="100dp"
      android:layout_weight="0.1"
      android:background="@color/wheat1">
 
      <TextView
        android:id="@+id/textViewBottom"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center_vertical|center_horizontal"
        android:text="@string/txt_bottom"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@color/red"/>
    </LinearLayout>
 
  </LinearLayout>
</android.support.design.widget.CoordinatorLayout>
  • La cabecera [1] es generada por las líneas 38-54;
  • el panel izquierdo [2] está generado por las líneas 56-84;
  • el pie de página [3] se crea con las líneas 86-101;
  • el contenido [4] es generado por las líneas 78-84;

La vista [main] XML utiliza la información que se encuentra en los archivos [res/values/colors.xml] y [res/values/strings.xml]:

  

El archivo [colors.xml] es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>
    <color name="lavenderblushh2">#EEE0E5</color>
    <color name="lightcyan2">#D1EEEE</color>
    <color name="wheat1">#FFE7BA</color>
 
</resources>

y el siguiente archivo [strings.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">example-12</string>
    <string name="action_settings">Settings</string>
    <string name="view1_title">View #1</string>
    <string name="textView_name">What is your name:</string>
    <string name="btn_Validate">Validate</string>
    <string name="btn_view2">View #2</string>
    <string name="view2_title">View #2</string>
    <string name="btn_view1">View #1</string>
    <string name="textView_hello">"Hello "</string>
    <string name="txt_header">Header</string>
    <string name="txt_left">Left</string>
    <string name="txt_bottom">Bottom</string>
 
</resources>

Crea un contexto de ejecución para este proyecto y ejecútalo.

1.20. Ejemplo 19: El componente [ListView]

El componente [ListView] permite repetir una vista específica para cada elemento de una lista. La vista repetida puede ser de cualquier complejidad, desde una simple cadena hasta una vista que permita introducir información para cada elemento de la lista. Crearemos el siguiente [ListView]:

Image

Cada vista de la lista tiene tres componentes:

  • a [TextView] para obtener información;
  • a [CheckBox];
  • un [TextView] clicable;

1.20.1. Creación del proyecto

Creamos un nuevo proyecto [Ejemplo-19] clonando el proyecto [Ejemplo-18].

  

Desarrollaremos el proyecto tal y como se describe en [3].

1.20.2. La sesión

  

La sesión almacena datos compartidos entre la actividad y los fragmentos:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // a list of data
  private List<Data> list = new ArrayList<>();
 
  // getters and setters
...
}
  • línea 11: la lista de datos utilizada por ambas vistas;

La clase [Datos] es la siguiente:


package examples.android.architecture;
 
public class Data {
 
    // data
    private String text;
    private boolean isChecked;
 
    // constructor
    public Data(String text, boolean isChecked) {
        this.text = text;
        this.isChecked = isChecked;
    }
 
    // getters and setters
    ...
}
  • línea 6: el texto que rellenará el primer [TextView] de cada elemento de la lista;
  • línea 7: el booleano que se utilizará para marcar o desmarcar el [checkBox] para cada elemento de la lista;

1.20.3. El [MainActivity]

El código del método [@AfterInject] pasa a ser el siguiente:


  // session injection
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<Data> list = session.getList();
    for (int i = 0; i < 20; i++) {
      list.add(new Data("Text # " + i, false));
    }
}
  • líneas 12-15: inicialización de la lista de datos presentes en la sesión;

1.20.4. La vista inicial [View1]

La vista XML [view1.xml] muestra el área [1] anterior. Su código es el 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:id="@+id/textView_title"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:layout_marginLeft="30dp"
    android:layout_marginTop="20dp"
    android:text="@string/view1_title"
    android:textSize="50sp" />
 
  <Button
    android:id="@+id/button_view2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/textView_title"
    android:layout_below="@+id/textView_title"
    android:layout_marginTop="50dp"
    android:text="@string/btn_vue2" />
 
  <ListView
    android:id="@+id/listView1"
    android:layout_width="600dp"
    android:layout_height="200dp"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/button_vue2"
    android:layout_marginLeft="30dp"
    android:layout_marginTop="50dp" >
  </ListView>
 
</RelativeLayout>
  • líneas 7-16: el componente [TextView] [2];
  • líneas 27-35: el componente [ListView] [4];
  • líneas 18-25: el componente [Botón] [3];

1.20.5. La opinión repetida por el [ListView]

La vista repetida por el [ListView] es la siguiente vista [list_data]:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat" >

    <TextView
        android:id="@+id/txt_Label"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:text="@string/txt_dummy" />
 
    <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/txt_Label"
        android:layout_marginLeft="37dp"
        android:layout_toRightOf="@+id/txt_Label"
        android:text="@string/txt_dummy" />
 
    <TextView
        android:id="@+id/textViewRemove"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Label"
        android:layout_alignBottom="@+id/txt_Label"
        android:layout_marginLeft="68dp"
        android:layout_toRightOf="@+id/checkBox1"
        android:text="@string/txt_remove"
        android:textColor="@color/blue"
        android:textSize="20sp" />
 
</RelativeLayout>
  • líneas 8-14: el componente [TextView] [1];
  • líneas 16-23: el componente [CheckBox] [2];
  • líneas 25-35: el componente [TextView] [3];

1.20.6. El fragmento [Vue1Fragment]

  

El fragmento [Vue1Fragment] gestiona la vista [vue1] XML. Su código es el siguiente:


package exemples.android.fragments;
 
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // fields of the view displayed by the fragment
  @ViewById(R.id.listView1)
  protected ListView listView;
  // the list adapter
  private ListAdapter adapter;
  // initialization complete
  private boolean initDone = false;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone = true;
  }
 
  @Click(R.id.button_vue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  public void remove(int position) {
   ...
  }
 
  @Override
  protected void updateFragment() {
    if (!initDone) {
      // bind data to the [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // In case the fragment has been (re)generated—in this case, the ListView must be re-linked to its adapter
    listView.setAdapter(adapter);
    // if other fragments have changed the data source - in this case, the ListView must be refreshed
    adapter.notifyDataSetChanged();
  }
}
  • línea 15: la vista XML [view1] está asociada al fragmento;
  • líneas 26-30: el método [@AfterViews] no hace nada. Sin embargo, es necesario establecer la variable [afterViewsDone] a verdadero porque lo utiliza la clase padre [AbstractFragment];
  • líneas 42-53: el método [updateFragment], que se llama cada vez que el fragmento se hace visible. El método se ha escrito aquí como si el fragmento pudiera abandonar la adyacencia del fragmento mostrado y, por tanto, reiniciar su ciclo de vida. Este no es el caso aquí, pero lo sería si la aplicación tuviera 3 fragmentos con una adyacencia de 1;
  • línea 44: el adaptador [ListView] sólo necesita inicializarse una vez;
  • línea 46: asociamos un [ListAdapter] a este [ListView]. Construiremos esta clase. Deriva de la clase [ArrayAdapter], que ya hemos utilizado para asociar datos a un [ListView]. Pasamos varios datos al constructor [ListAdapter]:
    • una referencia a la actividad actual,
    • el identificador de la vista que se instanciará para cada elemento de la lista,
    • una fuente de datos para rellenar la lista,
    • una referencia al fragmento. Esto se utilizará para manejar un clic en un enlace [Eliminar] en el [ListView] a través del método [doRemove] en la línea 38;
  • Línea 50: El adaptador está vinculado a [ListView]. Al mismo tiempo, la fuente de datos [lists] se vincula a [ListView]. Esta operación se realiza aquí cada vez que se muestra la vista nº 1. En realidad, sólo es necesario realizarla una vez que se ha ejecutado el método [@AfterViews]. Aquí, la sentencia se ejecuta con demasiada frecuencia. Necesitamos una variable booleana que nos diga que el método [@AfterViews] acaba de ser ejecutado y que por lo tanto el método [ListView] debe ser reasociado con su adaptador;
  • Línea 52: Actualizamos el [ListView]. En este ejemplo, esto no sirve de nada porque sólo la vista nº 1 puede modificar el origen de datos de [ListView]. Consideremos un caso más general en el que la vista nº 2 también podría modificar el origen de datos de [ListView]. Encontraremos ejemplos de este tipo más adelante en este documento. En este caso, al cambiar de la vista nº 2 a la vista nº 1, el [ListView] de la vista nº 1 debe actualizarse;

1.20.7. El [ListAdapter] del [ListView]

La clase [ListAdapter]

  • configura la fuente de datos del [ListView];
  • gestiona la visualización de los distintos elementos del [ListView];
  • gestiona los eventos de estos elementos;

Su código es el siguiente:


package exemples.android.fragments;
 
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
 
    // the runtime context
    private Context context;
    // the ID of the layout for a row in the list
    private int layoutResourceId;
    // the list data
    private List<Data> data;
    // the fragment that displays the [ListView]
    private Vue1Fragment fragment;
    // the adapter
    final ListAdapter adapter = this;
 
    // constructor
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // store the info
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.data = data;
        this.fragment = fragment;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • línea 5: la clase [ListAdapter] extiende la clase [ArrayAdapter];
  • línea 19: el constructor;
  • línea 20: no olvides llamar al constructor de la clase padre [ArrayAdapter] con los tres primeros parámetros;
  • líneas 22-25: almacenamos la información del constructor;
  • línea 29: el método [getView] será llamado repetidamente por el [ListView] para generar la vista para el elemento #[posición]. El resultado [View] devuelto es una referencia a la vista creada.

El código del método [getView] es el siguiente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // create the current row of the ListView
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the text
        TextView textView = (TextView) row.findViewById(R.id.txt_Label);
        textView.setText(data.get(position).getText());
        // the checkbox
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // the [Remove] link
        TextView txtRemove = (TextView) row.findViewById(R.id.textViewRemove);
        txtRemove.setOnClickListener(new OnClickListener() {
 
            public void onClick(View v) {
                fragment.doRemove(position);
            }
        });
        // Handle the click on the checkbox
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // return the row
        return row;
}
  • Línea 2: El método toma tres parámetros. Sólo utilizaremos el primero;
  • línea 4: creamos la vista para el elemento #[posición]. Esta es la vista [list_data] cuyo ID se pasó como segundo parámetro al constructor. A continuación, recuperamos las referencias a los componentes de la vista que acabamos de instanciar;
  • línea 6: recuperamos la referencia a [TextView] #1;
  • línea 7: le asignamos el texto de la fuente de datos que se pasó como tercer parámetro al constructor;
  • línea 9: recuperamos la referencia a [CheckBox] #2;
  • línea 10: lo marcamos o desmarcamos utilizando un valor de la fuente de datos de [ListView];
  • línea 12: recuperar la referencia a [TextView] #3;
  • líneas 13-18: gestionar el clic en el enlace [Eliminar];
  • línea 16: el [Vue1Fragment].doRetirer maneja este clic. Tiene más sentido hacer que el fragmento que muestra el [ListView] maneje este evento. Tiene una visión general que la clase [ListAdapter] no tiene. La referencia al fragmento [Vue1Fragment] se ha pasado como cuarto parámetro al constructor de la clase;
  • Líneas 20-25: Manejan el clic en la casilla de verificación. La acción realizada sobre ella se refleja en los datos que muestra. Esto es así por la siguiente razón: El [ListView] es una lista que muestra sólo una parte de sus elementos. Por lo tanto, un elemento de la lista a veces se oculta y a veces se muestra. Cuando el elemento #i necesita ser mostrado, el método [getView] de la línea 2 anterior es llamado para la posición #i. La línea 10 recalculará el estado de la casilla de verificación basándose en los datos a los que está vinculada. Por lo tanto, debe almacenar el estado de la casilla de verificación a lo largo del tiempo;

1.20.8. Eliminar un elemento de la lista

Hacer clic en el enlace [Eliminar] se gestiona en el fragmento [Vue1Fragment] mediante el siguiente método [doRetirer]:


  public void doRemove(int position) {
    // Remove element #[position] from the list
    List<Data> list = mainActivity.getList();
    list.remove(position);
    // note the scroll position to return to it
    // read
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of the first element, whether fully visible or not
    int firstPosition = listView.getFirstVisiblePosition();
    // Y offset of this element relative to the top of the ListView
    // measures the height of the potentially hidden portion
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // refresh the [ListView]
    adapter.notifyDataSetChanged();
    // move to the correct position in the ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • línea 1: Obtener la posición en el [ListView] del enlace [Remove] que fue pulsado;
  • línea 3: recuperar la lista de datos;
  • línea 4: eliminar el elemento en la posición [posición];
  • línea 15: refrescamos el [ListView]. Sin esto, nada cambia visualmente.
  • Líneas 5-13, 17: un proceso bastante complejo. Sin él, ocurre lo siguiente:
    • el [ListView] muestra las líneas 15-18 de la lista de datos,
    • se suprime la línea 16,
    • la línea 15 anterior lo reinicia completamente, y el [ListView] muestra entonces las líneas 0-3 de la lista de datos;

Con las líneas anteriores, se produce el borrado y el [ListView] permanece posicionado en la línea siguiente a la línea borrada.

1.20.9. La vista XML [Vista2]

El código XML para la vista es el 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:id="@+id/textView_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="20dp"
        android:text="@string/view2_title"
        android:textSize="50sp" />
 
    <Button
        android:id="@+id/button_view1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textViewResultats"
        android:layout_marginTop="25dp"
        android:layout_alignLeft="@+id/textView_title"
        android:text="@string/btn_view1" />
 
    <TextView
        android:id="@+id/textViewResultats"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView_title"
        android:layout_marginTop="50dp"
        android:layout_alignLeft="@+id/textView_title"
        android:text="" />
 
</RelativeLayout>
  • líneas 6-15: [TextView] componente #1;
  • líneas 26-33: [TextView] componente #2;
  • líneas 17-24: [Botón] componente #3;

1.20.10. El fragmento [Vue2Fragment]

123

El fragmento [Vue2Fragment] gestiona la vista [vue2] XML. Su código es el siguiente:


package exemples.android.fragments;
 
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
    // view fields
  @ViewById(R.id.textViewResultats)
  TextView txtResults;
 
    @AfterViews
    void initFragment(){
        // memory
        afterViewsDone = true;
    }
 
  @Click(R.id.button_view1)
    void navigateToView1() {
        // navigate to view 1
        mainActivity.navigateToView(0);
    }
 
    @Override
    protected void updateFragment() {
        // display the items in the list that were selected in View 1
        StringBuilder text = new StringBuilder("Selected items [");
        for (Data data : mainActivity.getList()) {
            if (data.isChecked()) {
                text.append(String.format("(%s)", data.getText()));
            }
        }
        text.append("]");
        txtResults.setText(text);
    }
}

El código importante está en el método [updateFragment] en la línea 32:

  • línea 34: calculamos el texto que se mostrará en [TextView] #2;
  • líneas 35-39: iteramos por la lista de datos que muestra el [ListView]. Se almacena en la actividad;
  • línea 36: si se ha comprobado el elemento de datos i, la etiqueta asociada se añade a un [StringBuilder];
  • línea 41: el [TextView] muestra el texto calculado;

1.20.11. Ejecución

Cree una configuración de ejecución para este proyecto y ejecútela.

1.20.12. Mejora

En el ejemplo anterior, utilizamos una fuente de datos List<Data> donde la clase [Data] era la siguiente:


package examples.android.fragments;
 
public class Data {
 
    // data
    private String text;
    private boolean isChecked;
 
    // constructor
    public Data(String text, boolean isChecked) {
        this.text = text;
        this.isChecked = isChecked;
    }
...
 
}

En la línea 7, utilizamos una variable booleana para gestionar las casillas de verificación de los elementos del [ListView]. A menudo, el [ListView] necesita mostrar datos que pueden seleccionarse marcando una casilla, aunque el elemento del origen de datos no tenga un campo booleano correspondiente a esa casilla. En ese caso, puede proceder como se indica a continuación:

La clase [Datos] se convierte en lo siguiente:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String text;
 
    // constructor
    public Data(String text) {
        this.text = text;
    }
 
    // getters and setters
...
}

Creamos una clase [CheckedData] derivada de la anterior:


package examples.android.fragments;
 
public class CheckedData extends Data {
 
    // checked element
    private boolean isChecked;
 
    // constructor
    public CheckedData(String text, boolean isChecked) {
        // parent
        super(text);
        // local
        this.isChecked = isChecked;
    }

    // getters and setters
...
}

A continuación, basta con sustituir el tipo [Data] por el tipo [CheckedData] en todo el código (MainActivity, ListAdapter, View1Fragment, View2Fragment). Por ejemplo, en [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<CheckedData> list = session.getList();
    for (int i = 0; i < 20; i++) {
      list.add(new CheckedData("Text # " + i, false));
    }
}

El proyecto de esta versión se proporciona con el nombre [Ejemplo-19B].

1.21. Ejemplo 20: Utilización de un menú

1.21.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-19B] en el proyecto [Ejemplo-20]:

3

Eliminaremos los botones de las vistas 1 y 2 y los sustituiremos por opciones de menú [1-2].

1.21.2. La definición XML de los menús

  

El archivo [res/menu/menu_vue1] define el menú para la vista #1:


<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/menuOptions"
    app:showAsAction="ifRoom"
    android:title="@string/menuOptions">
    <menu>
      <item
        android:id="@+id/actionHideShowAll"
        android:title="@string/actionHideShowAll"/>
      <item
        android:id="@+id/actionHideShowActions"
        android:title="@string/actionHideShowActions"/>
      <item
        android:id="@+id/actionHideShowActionsValidate"
        android:title="@string/actionHideShowActionsValidate"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationView2"
        android:title="@string/navigationView2"/>
    </menu>
  </item>
</menu>

Los elementos del menú se definen mediante la siguiente información:

  • android:id: el identificador del elemento;
  • android:título: la etiqueta del artículo;
  • app:showsAsAction: indica si el elemento del menú puede colocarse en la barra de acciones de la actividad. [ifRoom] indica que el elemento debe colocarse en la barra de acciones si hay espacio para él;
  • una opción de menú puede ser a su vez un submenú (la etiqueta <menú>, líneas 25, 29);

El archivo [res / menu / menu_vue2] define el menú de la vista nº 2:


<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/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationView1"
        android:title="@string/navigationView1"/>
    </menu>
  </item>
</menu>

1.21.3. Gestión de menús en la clase abstracta [AbstractFragment]

Factorizaremos la gestión de menús en la clase padre [AbstractFragment] de las dos vistas:


package exemples.android.architecture;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
 
import java.util.ArrayList;
import java.util.List;
 
public abstract class AbstractFragment extends Fragment {
 
  // data accessible to child classes
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
  // session
  protected Session session;
 
  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;
 
  // constructor
  public AbstractFragment() {
    // initialization
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("creating menu"));
    }
    // retrieve the menu options if this hasn't already been done
    if (!initDone) {
      // Retrieve the menu options
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // Transfer the list of options to an array
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memory
      initDone = true;
    }
 
    // we ask the child fragment to update
    updateFragment();
  }
 
 
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }
 
  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }
 
  // update child class
  protected abstract void updateFragment();
}
  • línea 42: los registros muestran que el método [onCreateOptionsMenu] es llamado cada vez que se muestra el fragmento. Se llama muy tarde, concretamente después de que se haya llamado al método [updateFragment]. Esto sugiere que podría utilizarse para actualizar el fragmento. Eso es lo que haremos aquí (línea 63);
  • línea 42: el método tiene dos parámetros:
    • [menú]: que es un menú vacío;
    • [Inflador]: herramienta que nos permite crear el menú a partir de su descripción inicial. Aquí no utilizaremos esta opción porque usaremos una anotación AA que lo hará por nosotros;
  • línea 44: almacenamos el menú. Lo necesitaremos más adelante;
  • líneas 52-53: almacenamos el IDs de todos los elementos del menú en el array de la línea 28;
  • líneas 55-57: los registros muestran que cuando se llama al método [onCreateOptionsMenu], el método [Fragment.getActivity()] devuelve la actividad asociada al fragmento;
  • línea 55: almacenamos la actividad como una instancia de la clase Android [Activity];
  • línea 56: almacenamos la actividad como una instancia de la interfaz [IMainActivity];
  • línea 57: almacenamos la sesión;
  • línea 59: observamos que la clase ya ha sido inicializada, por lo que no tenemos que hacerlo de nuevo (línea 50);
  • línea 63: pedimos al fragmento hijo que se actualice. Esto es posible porque el fragmento es visible y está asociado a su vista y a su menú;

El método [getMenuOptions], que recupera el IDs de los elementos del menú, es el siguiente:


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

El método [setAllMenuOptions] permite ocultar o mostrar todas las opciones del menú;


  protected void setAllMenuOptions(boolean isVisible) {
    // update all menu options
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

El método [setMenuOptions] permite ocultar o mostrar determinadas opciones del menú;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

La clase [MenuItemState] es la siguiente:

  

package exemples.android.architecture;
 
public class MenuItemState {
 
  // menu option 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
...
}

1.21.4. Gestión de menús en el fragmento [View1Fragment]

La clase [Vue1Fragment] pasa a ser la siguiente:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
 
...
 
  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // navigate to View 2
    mainActivity.navigateToView(1);
  }
 
  @OptionsItem(R.id.actionValider)
  void validate() {
    // display a message
    Toast.makeText(activity, "Validate", Toast.LENGTH_SHORT).show();
  }
 
  private boolean hideShowAll = true;
  @OptionsItem(R.id.actionHideShowAll)
  void hideShowAll() {
    // change state
    actionHideShowAll = !actionHideShowAll;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionHideShowAll), new MenuItemState(R.id.menuActions, actionHideShowAll)});
  }
 
  private boolean actionHideShowActions = true;
  @OptionsItem(R.id.actionHideShowActions)
  void actionHideShowActions() {
    // change state
    actionHideShowActions = !actionHideShowActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionHideShowActions)});
  }
 
  private boolean actionHideShowActionsValid = true;
  @OptionsItem(R.id.actionHideShowActionsValidate)
  void actionHideShowActionsValidate() {
    // change state
    actionHideShowActionsValidate = !actionHideShowActionsValidate;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionHideShowActionsValidate)});
  }
...
 
  @Override
  protected void updateFragment() {
    ....
    // update the menu
    //setMenuOptions(...)
  }
}
  • línea 2: el menú [res/menu/menu_vue1.xml] está asociado al fragmento;
  • línea 48: cuando se ejecuta el método [updateFragment], el menú también puede actualizarse para reflejar el nuevo estado del fragmento;
  • línea 7: la anotación [@OptionsItem(R.id.navigationVue2)anota el método que debe ejecutarse cuando se hace clic en la opción de menú [Navegación / Vista 2];
  • líneas 19-25: para ocultar una rama del menú, basta con ocultar su opción raíz;
  • línea 24: se muestran u ocultan las opciones raíz [menuNavigation, menuActions];
  • línea 40: para mostrar una opción en una rama del menú, no sólo debe mostrar esa opción, sino también todas las opciones encontradas al desplazarse desde la opción hoja hasta la raíz del menú;

1.21.5. Gestión de menús en el fragmento [Vue2Fragment]

Se puede encontrar un código similar en el fragmento de la Vista 2:


package exemples.android.fragments;
 
import android.widget.TextView;
import exemples.android.R;
import examples.android.architecture.AbstractFragment;
import examples.android.models.CheckedData;
import org.androidannotations.annotations.*;
 
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class View2Fragment extends AbstractFragment {
 
  // view fields
  @ViewById(R.id.textViewResultats)
  TextView txtResults;
 
  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // navigate to view 1
    mainActivity.navigateToView(0);
  }
 
  @Override
  protected void updateFragment() {
    // display the items in the list that were selected in View 1
    StringBuilder text = new StringBuilder("Selected items [");
    for (CheckedData data : session.getList()) {
      if (data.isChecked()) {
        text.append(String.format("(%s)", data.getText()));
      }
    }
    text.append("]");
    txtResults.setText(text);
    // update the menu
    // setMenuOptions(...)
  }
}
  • línea 35: mostrar la opción [Navegación / Vista 1];
  • líneas 17-20: cuando se hace clic en la opción [Navegación / Vista 1], se llama al método [navigateToView1];

1.21.6. Ejecución

Crea un contexto de ejecución para este proyecto y ejecútalo.

1.22. Ejemplo 21: Refactorización de la clase [AbstractFragment]

El ejemplo anterior nos mostró que cuando el fragmento tiene un menú, su método [onCreateOptionsMenu] es un buen lugar para pedirle al fragmento que se actualice:

  • se llama exactamente una vez cuando el fragmento está a punto de mostrarse;
  • cuando se llama, se establecen las asociaciones del fragmento con su actividad, vista y menú;

Para demostrarlo, volveremos al ejemplo 12, que presenta muchos fragmentos cuya adyacencia puede modificarse. En ese ejemplo, los fragmentos no tenían menú. Les asociaremos un menú vacío.

1.22.1. Creación del proyecto

Duplicamos el proyecto [Ejemplo-12] en el proyecto [Ejemplo-21]:

1.22.2. El menú de fragmentos

  

El menú añadido para los fragmentos estará vacío:


<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">
</menu>

Lo que tienes que entender aquí es que la actividad ya tiene su propio menú [menu_main]:


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

Cuando una actividad ya tiene un menú, el menú asociado a los fragmentos se añade al menú de la actividad: por lo tanto, tendrá las opciones de ambos menús. En este caso, el menú de los fragmentos estará vacío. Por lo tanto, sólo verá el menú de la actividad.

1.22.3. Los fragmentos

  

Reutilizamos la clase abstracta [AbstractFragment] del ejemplo anterior (véase la sección 1.21.3). Asociamos el menú [menu_fragment] a los dos fragmentos:


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

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

En ambos fragmentos [PlaceholderFragment] y [Vue1Fragment], eliminamos cualquier referencia a la antigua clase abstracta [AbstractFragment].

1.22.4. Ejecución

Ejecuta la aplicación y comprueba que funciona. Comprueba en los logs cuándo se ejecuta el método [onCreateOptionsMenu] de la clase [AbstractFragment]. Ahora es este método el que llama al método [updateFragment] de los fragmentos hijos.

1.23. Ejemplo-22: Guardar/Restaurar el Estado de la Actividad y los Fragmentos

1.23.1. El problema

Aquí abordamos la cuestión de la rotación del dispositivo Android (retrato <--> paisaje). Para ilustrarlo, volveremos al anterior Ejemplo 21:

Image

If we rotate the device [1], we get the following new view:

Image

Podemos verlo:

  • en [1], la pestaña [Fragmento #3] ha desaparecido;
  • en [2], el texto mostrado es efectivamente el del Fragmento nº 3, pero el contador de visitas es incorrecto;

Durante esta rotación, los registros son los siguientes:

07-13 04:08:27.188 1677-1677/examples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/examples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor View1Fragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/examples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/examples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/examples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/examples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/examples.android D/View1Fragment: afterViews View1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/examples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/examples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/examples.android D/menu: creating menu
07-13 04:08:27.351 1677-1677/examples.android D/PlaceholderFragment_: creating menu
07-13 04:08:27.351 1677-1677/examples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • línea 1: podemos ver que la actividad está completamente reconstruida;
  • líneas 3-7: lo mismo se aplica a los cinco fragmentos gestionados por la actividad;
  • línea 21: el fragmento #3 está a punto de mostrarse. Vemos que antes de incrementarse, el recuento de visitas es 0;

A continuación, podemos explicar el resultado obtenido tras la rotación de la siguiente manera:

  • la clase [MainActivity] crea inicialmente una barra de pestañas con una única pestaña etiquetada como [Vista 1]. Esta es la pestaña visible;
  • Después de que el dispositivo gire, el administrador de páginas [mViewPager] vuelve a mostrar el mismo fragmento, que en este caso es el fragmento nº 3. Es importante recordar aquí que las pestañas y los fragmentos son conceptos diferentes y tienen ciclos de vida distintos. El método [updateFragment] del fragmento #3 se ejecutará:

  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // increment visit count
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visit %s", text, numVisit));
}
  • Línea 7: La última visita ID se lee de la sesión. Sin embargo, la sesión -como todo lo demás- se ha reiniciado, y la visita ID se ha puesto a cero. Esto explica el resultado mostrado en el fragmento #3;

1.23.2. Métodos para salvar/restaurar la actividad y los fragmentos

1.23.2.1. Solución 1: copia de seguridad manual

Cuando el dispositivo gira, se llama a dos métodos de la actividad:


// Activity save/restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // Save activity state
    // ....
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    // restore the activity
    // ...
  }
  • Líneas 2-8: El método [onSaveInstanceState] es llamado por el sistema durante la rotación. Aquí es donde se puede guardar la actividad. Si no se hace nada, no se guarda nada. El estado de la actividad debe ser guardado en el parámetro [Bundle outState] pasado al método. La clase [Bundle] se parece a un diccionario. Tiene métodos [putString, putInt, putLong, putBoolean, putChar, ...] con dos parámetros: void putT(Clave de cadena, T valor);
  • Líneas 10-16: El método [onCreate] es llamado cuando la actividad es creada. Si el estado de la actividad ha sido guardado, este estado guardado se le pasa en el parámetro [Bundle savedInstanceState]. Para recuperar los valores guardados, se dispone de métodos como [getString, getInt, getLong, getBoolean, getChar, ...] con un único parámetro: T getT(Cadena clave);

Los fragmentos tienen estos dos mismos métodos para guardar su estado.

Utilizaremos esta información para guardar y restaurar el estado del Ejemplo 21. Para ello, duplicamos el proyecto [Ejemplo-21] en [Ejemplo-22].

1.23.2.2. Solución 2: Guardado automático

La documentación de Android indica que cuando se gira el dispositivo, se puede evitar que se destruya un fragmento utilizando la sentencia [Fragmento].setRetainInstance(true). Varios artículos sobre [StackOverflow] recomiendan utilizar esta instrucción sólo para fragmentos sin interfaz visual [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. He probado esta afirmación con dos ejemplos: Ejemplo-17 (Sección 1.18-una aplicación de un solo fragmento que muestra un formulario) y Ejemplo-21 (Sección 1.22-una aplicación de cinco fragmentos). En ambos casos, la aplicación de esta única instrucción a todos los fragmentos de la aplicación resultó insuficiente para restaurar correctamente la vista mostrada al rotar el dispositivo. En lugar de construir dos modelos, uno basado en [setRetainInstance(true)] y otro basado en [setRetainInstance(false)]-que es el valor por defecto- decidí seguir las recomendaciones de [StackOverflow] y mantener el valor por defecto de falso para el método [setRetainInstance(boolean)]. La declaración: [Fragmento].setRetainInstance(true) nunca se ha utilizado en el resto de este documento.

1.23.3. El método de copia de seguridad/restauración del proyecto [Ejemplo-22

El proyecto [Ejemplo-22] evoluciona del siguiente modo:

  

Aparecen dos nuevas clases:

  • [PlaceHolderFragmentState], que almacenará el estado de un fragmento de tipo [PlaceHolderFragment];
  • [Vue1FragmentState], que almacenará el estado de un fragmento de tipo [Vue1Fragment];

Estas clases son las siguientes:


package exemples.android;
 
public class Vue1FragmentState {
  // Vue1Fragment state
  private boolean hasBeenVisited = false;
  // getters and setters
...
}
  • línea 5: el booleano [hasBeenVisited] es verdadero si el fragmento [Vue1Fragment] ha sido visitado (visualizado) al menos una vez. Este campo se creó para el ejemplo porque el fragmento [Vue1Fragment] no tiene nada que guardar;

La clase [PlaceHolderFragmentState] es la siguiente:


package exemples.android;
 
public class PlaceHolderFragmentState {
  // visited or not
  private boolean hasBeenVisited;
  // displayed text
  private String text;
 
  // getters and setters
...
}
  • línea 5: vemos el booleano [hasBeenVisited];
  • línea 7: el texto que muestra el fragmento en el momento en que debe guardarse. Hemos visto que este texto se perdía durante la rotación;

El estado de los fragmentos se almacenará en la sesión, y la actividad se encargará de guardar y restaurar esta sesión. La sesión evoluciona de la siguiente manera:


package exemples.android;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // number of [PlaceholderFragment] fragments displayed in the second tab
  private int numFragment = -1;
  // selected tab number
  private int selectedTab = 0;
  // current view number
  private int currentView;
 
  // fragment saves ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
 
  // constructor
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // getters and setters
...
}
  • línea 18: el estado del fragmento [Vue1Fragment];
  • línea 19: el estado de los fragmentos de tipo [PlaceHolderFragment];
  • líneas 22-27: en el constructor de sesión, se inicializan los campos de las líneas 18 y 19;
  • líneas 12-15: aparecen dos nuevos campos:
    • línea 13: el número de la última pestaña seleccionada;
    • línea 15: el número del último fragmento visualizado;

La actividad guarda/restaura la sesión de la siguiente manera:


  // Activity save/restore management ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session
    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();
      }
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // retrieve 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();
        }
      }
    }
}
  • línea 8: la sesión se guarda como una cadena JSON;
  • línea 29: restaurar la sesión desde su cadena JSON;

Para gestionar el guardado y la restauración de fragmentos, la clase abstracta [AbstractFragment] evoluciona de la siguiente manera:


// Save/restore management -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // Save?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // the fragment is about to be hidden - save it
      saveFragment();
      saveFragmentDone = true;
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the fragment must be restored
    fragmentHasToBeInitialized = true;
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // parent
    super.onSaveInstanceState(outState);
    // Save the fragment only if it is visible
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }
 
  // child classes
  protected abstract void updateFragment();
 
  protected abstract void saveFragment();
  • Decidimos guardar el estado de los fragmentos en la sesión en dos puntos:
    • líneas 2-14: cuando el fragmento cambia de visible a oculto;
    • líneas 29-42: cuando el sistema indica que el fragmento debe guardarse y el fragmento es visible (línea 38);

Este mecanismo evita guardar más veces de las necesarias. En efecto, como guardamos el estado del fragmento i cuando pasó de visible a oculto, cuando se visualiza el fragmento j y se produce una rotación, no es necesario guardar de nuevo el fragmento i. Si no se ha vuelto a mostrar desde la última vez que se guardó, su estado no ha cambiado. Sólo hay que guardar el estado del fragmento j. Este mecanismo también tiene otra ventaja: no sólo es necesario guardar el estado de un fragmento durante la rotación del dispositivo. También existe el caso de la navegación pura entre fragmentos, por ejemplo en un sistema de pestañas. En estos casos, queremos recuperar un fragmento en el estado en que se encontraba la última vez que se visualizó. Este estado puede haber desaparecido parcialmente si el fragmento se retiró en algún momento de la vecindad de los fragmentos visualizados. En ese caso, el fragmento no se reconstruye por completo, pero sí su vista asociada. El guardado realizado cuando el fragmento se ocultó se utilizará para restaurar el último estado de esta vista;

  • líneas 10, 40: para evitar realizar dos guardados sucesivos, se utiliza el booleano [saveFragmentDone] para indicar que se ha realizado un guardado;
  • líneas 9, 39: se pide al fragmento hijo que guarde su estado. El método [saveFragment] es abstracto (línea 47). Por lo tanto, corresponde a las clases hijas implementarlo;
  • líneas 16-26: el método [onActivityCreated] se utiliza para establecer el booleano [fragmentHasToBeInitialized] en true. Esto se debe a que el fragmento hijo necesita saber que debe reinicializar completamente el estado del fragmento a partir de un estado que encontrará en la sesión;

Todavía en la clase [AbstractFragment], el método [onCreateOptionsMenu] cambia como sigue:


// Update the fragment
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("creating menu"));
    }
    ...
    // ask the child fragment to update
    updateFragment();
    // save to be performed
    saveFragmentDone = false;
  }
  • línea 14: vimos que el booleano [saveFragmentDone] estaba puesto a verdadero cuando se realizó un guardado. En algún momento, debe restablecerse a falso. Cuando se ejecuta el método [updateFragment] (línea 12) del fragmento hijo, éste se hace visible. Sin embargo, es cuando un fragmento es visible cuando debe guardarse, concretamente en el momento en que pasa del estado visible al estado oculto. A continuación, establecemos el booleano [saveFragmentDone] a falso para que se pueda guardar;

1.23.4. Guardar el fragmento [Vue1Fragment]

Los fragmentos se guardan en el método [saveFragment] llamado por la clase padre [AbstractFragment]:


 // save fragment state
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // Save the fragment's state to the session
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • líneas 9-11: guardar el estado del fragmento en la sesión. Cuando se llama al método [saveFragment], el fragmento es visible. Por lo tanto, el booleano [hasBeenVisited] debe tener el valor verdadero (línea 10);

1.23.5. Guardar el fragmento [PlaceHolderFragment]

Los fragmentos se guardan en el método [saveFragment] llamado por la clase padre [AbstractFragment]:


  @Override
  public void saveFragment() {
    // Save the fragment's state to the session
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • líneas 4-7: guardar el estado del fragmento en la sesión;
  • línea 5: el texto mostrado actualmente por el [TextView] textViewInfo se guarda;
  • línea 6: el booleano [hasBeenVisited] del fragmento se establece en verdadero;
  • línea 7: el estado del fragmento se guarda en la sesión en la matriz [placeHolderFragmentStates]. El índice del elemento a inicializar es el número de sección del fragmento menos uno;

1.23.6. Restauración del fragmento [Vue1Fragment]

Los fragmentos se restauran en el método [updateFragment]:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // restore?
    if (fragmentHasToBeInitialized) {
      // restore state
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // navigation?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // increment visit count
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // display the visit number
      Toast.makeText(activity, String.format("Visit #%s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // change current view number
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • Líneas 8-12: Restaurando el estado del fragmento. El booleano [fragmentHasToBeInitialized] fue inicializado por la clase padre [AbstractFragment]. Cuando se verdadero, el fragmento acaba de ser reconstruido y debe ser reinicializado. Esto es lo que ocurre. En este ejemplo concreto, no hay nada que hacer. Simplemente mostramos que podíamos recuperar el valor del booleano [hasBeenVisited] del estado guardado del fragmento (línea 10);
  • línea 11: no olvides volver a poner [fragmentHasToBeInitialized] a falso, para que cuando volvamos a este fragmento más tarde sin que el dispositivo haya girado, no realicemos una inicialización innecesaria del fragmento;
  • líneas 18-26: incrementa el contador de visitas. Aquí se plantea un reto: al restaurar el fragmento, no queremos incrementar este contador. Tenemos que distinguir aquí entre:
    • navegación sencilla que devuelve al usuario a la pestaña [Ver 1];
    • una restauración cuando el usuario gira su dispositivo mientras se muestra la pestaña [Ver 1];

Distinguimos entre estos dos casos utilizando el número de vista almacenado en la sesión. Este número es el de la última vista visualizada (línea 28).

  • línea 18: se produce una navegación en lugar de una actualización si el número de la última vista difiere del de la vista actual;
  • líneas 21-25: incremento del contador de visitas y visualización del mismo;

1.23.7. Restaurar el [PlaceHolderFragment]

Los fragmentos se restauran en el método [updateFragment]:


  // data
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...
 
public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // Which fragment is this?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // Does the fragment need to be initialized?
    if (fragmentHasToBeInitialized) {
      // initial text
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // navigation?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // increment visit count
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // modified text
      newText = String.format("%s, visit %s", text, numVisit);
    } else {
      // this is a restore
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // display text
    textViewInfo.setText(newText);
    // current view
    session.setCurrentView(numView);
}
  • líneas 15-16: determinar el número de la vista que se está actualizando;
  • líneas 18-22: caso en el que el fragmento se encuentra en un ciclo de guardar/restaurar tras un cambio de orientación del dispositivo. Aquí debe restaurarse. Por lo general, esto implica restaurar determinados campos del fragmento;
  • línea 20: el campo [text] de la línea 2 debe contener el texto inicial mostrado por el fragmento: [Hola mundo de la sección i]. Debe regenerarse aquí;
  • línea 21: observe que el fragmento ha sido inicializado;
  • líneas 24-36: Al igual que con el fragmento [Vue1Fragment] anterior, el contador de visitas no debe incrementarse durante una restauración. Como antes, debemos distinguir entre navegación y restauración;
  • líneas 32-36: caso de restauración;
  • línea 34: se recupera de la sesión el estado del fragmento antes de la rotación del dispositivo;
  • línea 35: se recupera el texto que se mostraba en ese momento;
  • línea 38: este texto se muestra de nuevo;
  • línea 40: se anota en la sesión el número de la nueva vista visualizada;

1.23.8. Gestión de pestañas

En las secciones anteriores no se abordó la gestión de pestañas. Sin embargo, en el ejemplo 21 nos encontramos con un problema al girar el dispositivo: sólo se conservaba la primera pestaña [Vista 1]. La segunda pestaña se perdió.

Resolvemos este problema en la clase [MainActivity] de la siguiente manera:


@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
 ...
 
    // First 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);
    }
 
    // Which tab should be selected?
    tabLayout.getTabAt(session.getSelectedTab()).select();
 
...
 
  }
  • líneas 14-16: creación de la primera pestaña;
  • líneas 18-23: creación de la segunda pestaña. Para determinar si hay que crearla, comprobamos en la sesión el número del fragmento mostrado en la pestaña 2. Si este número no es -1 (su valor inicial), entonces se crea la segunda pestaña. Si este número no es -1 (su valor inicial), se crea la segunda pestaña. En este punto, tenemos dos pestañas, con la primera seleccionada por defecto;
  • línea 26: recuperamos de la sesión el número de la pestaña que estaba seleccionada antes de guardar/restaurar y vuelva a seleccionarlo. Si el campo [selectedTab] aún no ha sido inicializado por el código, se utiliza su valor inicial de 0;