Skip to content

1. Aprendizaje de la programación para Android

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

Los ejemplos del documento están disponibles |AQUÍ|.

1.1. Introduction

1.1.1. Contenido

Este documento es una reescritura de varios documentos existentes:

  1. Android para desarrolladores J2EE;
  1. Introducción a la programación de tabletas Android con ejemplos;
  2. Cómo controlar un Arduino con una tableta Android;
  3. Introducción a la programación de tabletas Android con ejemplos - versión 2

e introduce las siguientes novedades:

  • el documento 1 presentaba una arquitectura llamada AVAT (Actividad-Vistas-Acciones-Tareas) para facilitar la programación asíncrona en una aplicación de Android. En este documento, se utiliza la biblioteca estándar RxJava para gestionar las acciones asíncronas;
  • el documento 2 utilizaba Eclipse con el complemento IDE para Android. Este documento utiliza Android Studio;
  • el documento 3 se mantiene tal cual;
  • el documento 4 utilizaba la biblioteca [Android Annotations] (AA) con IntelliJ IDE Community Edition. Este documento retoma la totalidad del documento 4 con las siguientes diferencias:
    • IDE ahora es Android Studio;
    • el sistema de compilación es Gradle para todos los proyectos de cliente o servidor (en el documento 4, a veces se utilizaba Maven)
    • la programación asíncrona se realiza con la biblioteca RxJava (en el documento 4 se utilizaba la biblioteca AA);
  • este documento explora temas que no se abordaron, o se abordaron muy poco, en los documentos anteriores:
    • el concepto de adyacencia de fragmentos;
    • el guardado y la restauración de la actividad y sus fragmentos;
    • el ciclo de vida de los fragmentos;

Por último, presenta el esqueleto de un cliente de Android que se comunica con un servicio web / jSON, en el que se factorizan un gran número de elementos que se encuentran con frecuencia 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:

Exemple
Naturaleza
1
Importación de un proyecto de Android existente
2
Un proyecto básico de Android
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
Manejo de fragmentos en una aplicación de Android
13
Navegación entre vistas renovada
14
Arquitectura de dos capas
15
Arquitectura cliente/servidor
16
Manejar la asincronía con RxJava
17, 17B
Componentes de entrada de datos
18
Uso de una plantilla de vistas
19
El componente ListView
20
Usar un menú
21
Uso de una clase padre para los fragmentos
22, 22B
Guardar y restaurar el estado de la actividad y los fragmentos
23
Cliente de clima
Chap 2
Estructura básica de un cliente de Android que se comunica con un servicio web / jSON. En ella se agrupan un gran número de elementos que suelen encontrarse en este tipo de clientes de Android.
Chap 3
Administración de citas de un consultorio médico
Chap 4
Ejercicio práctico: gestión de una nómina básica
Chap 5
Ejercicio práctico: control de placas Arduino

Este documento se utilizó en el último año de la escuela de ingeniería IstiA de la Universidad de Angers [istia.univ-angers.fr]. Esto explica el tono a veces un poco peculiar del texto. Los dos ejercicios de aplicación son textos de TP de los que solo se dan las líneas generales de la solución. El lector debe construirla por sí mismo.

El código fuente de los ejemplos está disponible |ICI|. Para ejecutar estos ejemplos, debe seguir el procedimiento del párrafo 6.12.

Este documento es un material de introducción a la programación para Android. No pretende ser exhaustivo. Está dirigido principalmente a principiantes.

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

1.1.2. Requisitos previos

El requisito previo para aprovechar al máximo este documento es tener un buen dominio del lenguaje Java.

1.1.3. Las herramientas utilizadas

Los siguientes ejemplos se han probado en el siguiente entorno:

  • computadora 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, debes instalar:

  • un JDK (véase el párrafo 6.8);
  • el administrador de emuladores de Android Genymotion (véase el párrafo 6.9);
  • el gestor de dependencias Maven (véase el párrafo 6.10);
  • el IDE [Android Studio] (véase el párrafo 6.11);

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

1.2.1. Creación del proyecto

Creemos con Android Studio un primer proyecto de Android. En primer lugar, creemos una carpeta vacía donde se ubicarán todos nuestros proyectos:

  

luego, creemos un proyecto con Android Studio. Primero importaremos uno de los ejemplos que vienen con IDE [1-5]:

 

Image

La importación del proyecto puede generar errores debido a la incompatibilidad entre el entorno utilizado al crear el proyecto y el que se utiliza aquí para ejecutarlo. Esta es una oportunidad para ver cómo resolver este tipo de errores. En este caso, tenemos el siguiente error:

El proyecto importado está configurado por 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"
}

// La compilación de ejemplo utiliza varios directorios para
// separar el código repetitivo y común del
// el código principal de la muestra.
List<String> dirs = [
    'main',     // código principal de la muestra; aquí encontrarás lo interesante.
    'common',   // componentes que se reutilizan en varias muestras
    'template'] // código repetitivo generado por el proceso de plantillas de ejemplo

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 y 35: no contamos con la versión SDK 21. Reemplazamos esta versión por la versión 23 de la que disponemos.

En el archivo [build.gradle], Android Studio ofrece las siguientes sugerencias:

 

Para aceptar las sugerencias, se selecciona [alt-entrée] en la sugerencia:

 

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

 

Este error se debe a una incompatibilidad entre la versión de Gradle que requiere el archivo [build.gradle] del proyecto (la 2.10, línea 6, a continuación):


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

y la que figura en el archivo [<projet>/gradle/wrapper/gradle-wrapper.properties]:


#Mié 10 de abril 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, arriba, hay que reemplazar 2.8 por 2.10.

Para acceder al archivo [<projet>/gradle/wrapper/gradle-wrapper.properties], hay que utilizar la perspectiva de proyecto del proyecto:

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

 

Image

Detengamos la aplicación:

  

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

  

1.2.2. Algunos puntos sobre el IDE

1.2.2.1. Las perspectivas

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

  • la perspectiva [Android] [1]:
  • la perspectiva [Project] [4];
 
  

La mayor parte del tiempo trabajaremos con la perspectiva [Android]. Cuando dupliquemos un proyecto en otro, necesitaremos la perspectiva [Project].

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 [Rerun] [3] detiene la ejecución del proyecto [2] y luego lo reinicia [1].

1.2.2.3. Administración de la caché

Android Studio mantiene una caché de los proyectos que administra con el fin de que IDE sea lo más ágil posible. Con la versión Android 2.1 (mayo de 2016), a menudo esta caché no tomaba en cuenta los cambios de código que acabábamos de realizar. En ese caso, hay que invalidar esta caché:

Con Android 2.1 (mayo de 2016), la operación anterior debía realizarse varias veces y, en ocasiones, esto no era suficiente para resolver la anomalía detectada. La solución fue desactivar la tecnología [Instant Run]:

  • en [3-4], se desactivó todo;

En todo lo que sigue, se trabajó con esta configuración de la caché y no se presentaron problemas.

1.2.2.4. Manejo de los registros

Al ejecutar un proyecto, aparecen registros 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 se desea ver los registros de una acción específica:

  • se borran los registros;
  • en el dispositivo Android, se realiza la acción de la que se quieren los registros;
  • los registros que aparecen entonces son los relacionados con la acción realizada;

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

Los registros son muy útiles para saber en qué momentos de la ejecución de un proyecto se ejecutan ciertos métodos. Los utilizaremos con frecuencia. Tomemos el código de la clase [MainActivity] del proyecto [Exemple-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);
    }
}

En el ejemplo anterior, los métodos [onCreate, ligne 14] y [onCreateOptionsMenu, ligne 26] pertenecen a la clase padre [Activity] (línea 9). Se invocan en diferentes momentos del ciclo de vida de la aplicación. A veces se ejecutan varias veces. Incluso al leer la documentación, a veces es difícil determinar si un método del ciclo de vida se ejecutará antes o después de un método que hayamos escrito nosotros mismos. Sin embargo, a menudo es importante conocer esta información. Por lo tanto, podemos incluir registros como los que se muestran 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()) {
      ...
  }
}
  • En las líneas 7, 14 y 21 se utiliza la clase [Log]. Esta clase permite escribir registros en la consola de Android [logcat]. Los registros se clasifican en distintos 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 registros. Para poder diferenciarlas, se utiliza este primer argumento. El segundo argumento es el mensaje que se escribirá en la consola de registros;

Si volvemos a ejecutar el proyecto [Exemple-01], obtenemos los siguientes registros:


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

De esta manera, vemos que el método [onCreate], que crea la actividad de 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 agrega el siguiente registro en la consola de registros:


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

En lo sucesivo, añadiremos con frecuencia instrucciones de registro en el código de Android. La mayoría de las veces, no las comentaremos. Están ahí simplemente para invitar al lector a consultar la consola de registros con el fin de comprender gradualmente el ciclo de vida de una aplicación de Android.

1.2.2.5. Manejo del emulador [Genymotion]

A veces, el emulador Genymotion se cuelga y ya no se puede reiniciar. Esto se debe a que algunos procesos de VirtualBox siguen activos en el administrador de tareas. Ábrelo [Ctrl-Alt-Supp] y elimina todas las tareas de VirtualBox que estén presentes:

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

1.2.2.6. Gestión del binario APK creado

La compilación del proyecto genera un binario con la extensión .apk:

Hay dos versiones: una llamada [debug] y la otra llamada [debug-unaligned]. Se debe utilizar la primera, ya que la otra es una versión intermedia. El archivo binario .pak generado en [4] se puede transferir directamente a un emulador o a un dispositivo Android. Para transferirlo a un emulador, simplemente arrastra y suelta el archivo sobre el emulador con el ratón.

1.3. Ejemplo-02: un proyecto básico de Android

Creemos con Android Studio un nuevo proyecto de Android [1-12]:

 

En [13], se ejecuta la aplicación. A continuación, se obtiene la pantalla [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 "exemples.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 archivo fue generado por IDE con los elementos de su configuración. Se trata de un archivo mínimo que iremos ampliando progresivamente.

  • líneas 3-12: las características de la aplicación para Android;
  • líneas 22-25: sus dependencias. Es principalmente aquí donde realizaremos modificaciones según los ejemplos estudiados;

1.3.2. El manifiesto de la aplicación

  

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


<?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 3: el paquete del proyecto de Android;
  • línea 10: el nombre de la actividad;

Esta información proviene de los datos ingresados al crear el proyecto:

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

Volvamos al manifiesto:


<?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 10: la actividad principal de la aplicación. Hace referencia a la clase [1] mencionada anteriormente;
  • línea 6: el ícono [2] de la aplicación. Se puede cambiar;
  • línea 7: el nombre de la aplicación. Se encuentra en el archivo [strings.xml] [3]:

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

El archivo [strings.xml] contiene las cadenas de texto que utiliza la aplicación. En la línea 2, el nombre de la aplicación proviene de la información ingresada al crear el proyecto [4]:

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

1.3.3. La actividad principal

 

Una aplicación de Android se basa en una o varias actividades. Aquí se ha generado una actividad [1]: [MainActivity]. Una actividad puede mostrar una o varias vistas según 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 de Android [AppCompatActivity]. Este será el caso de 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 invoca el método [onCreate] de la clase padre. Esto siempre debe hacerse;
  • línea 11: el archivo [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"
  tools:context="exemples.android.MainActivity">

  <TextView
    android:text="Hello World!"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</RelativeLayout>
  • líneas b-k: el administrador de formato. El elegido por defecto es el tipo [RelativeLayout]. En este tipo de contenedor, los componentes se colocan unos respecto a otros (a la derecha de, a la izquierda de, debajo, encima);
  • líneas m-p: un componente de tipo [TextView] que sirve para mostrar texto;
  • línea n: el texto que se muestra. No se recomienda incluir texto fijo en las vistas. Es preferible trasladar estos textos al archivo [res/values/strings.xml] [3]:

Por lo tanto, el texto que se mostrará será [Hello World!]. ¿Dónde se mostrará? El contenedor [RelativeLayout] ocupará toda la pantalla. El [TextView], que es su único elemento, se mostrará en la parte superior izquierda de este contenedor, es decir, en la parte superior izquierda de la pantalla;

¿Qué significa [R.layout.activity_main] en la línea 11? A cada recurso de Android (vistas, fragmentos, componentes, etc.) se le asigna un identificador. Así, una vista [V.xml] ubicada en la carpeta [res / layout] se identificará 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] es el identificador de la cadena [app_name] en el archivo [res / values / string.xml]:
  • línea 19: el atributo [R.mipmap.ic_launcher] es el identificador de la imagen [res / mipmap / ic_launcher];

Por lo tanto, hay que recordar que cuando se hace referencia a [R.layout.activity_main] en el código, se hace referencia a un atributo de la clase [R]. El IDE nos ayuda a conocer los diferentes elementos de esta clase:

1.3.4. Ejecución de la aplicación

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

  • en [1], selecciona [Edit Configurations];
  • el proyecto se creó con una configuración [app], que vamos a eliminar ([2]) para volver a crearla;
  • en [3], crear una nueva configuración de ejecución;
  
  • en [4], seleccione [Android Application];

Image

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

    <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], seleccione [Show Chooser Dialog], que permite elegir el dispositivo en el que se ejecutará la aplicación (emulador, tableta);
  • en [9], se indica que esta selección debe guardarse;
  • confirmar la configuración;
  
  • en [11], inicie el administrador de emuladores [Genymotion] (véase el párrafo 6.9);
  • en [12], seleccione un emulador de tableta e inicie el [13];
  • en [14], ejecute la configuración de ejecución [app];
  • En [15] aparece el formulario para seleccionar el dispositivo de ejecución. Aquí solo hay uno disponible: el emulador [Genymotion] que se inició anteriormente;

El emulador de software muestra, al cabo de un momento, 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] de las líneas 8 a 12 forma parte de los métodos que pueden invocarse durante el ciclo de vida de una actividad. La documentación de Android proporciona la siguiente lista de estos métodos:

 
  • [1]: el método [onCreate] se invoca al iniciar la actividad. Es en este método donde se asocia la actividad a una vista y se obtienen las referencias de sus componentes;
  • [2-3]: a continuación, se invocan los métodos [onStart, onResume]. Se observa que el método [onResume] es el último método que se ejecuta antes de llegar al estado [4] de la actividad en ejecución;

1.4. Ejemplo-03: reescritura del proyecto [Exemple-02] con la biblioteca [Android Annotations]

Ahora vamos a introducir la biblioteca [Android Annotations], que facilita la creación de aplicaciones para Android. Para ello, duplicamos el ejemplo [Exemple-02] en [Exemple-03] siguiendo el procedimiento [1-16].

  • en [1]; utilice la perspectiva [Project] para ver el proyecto de Android completo;

Nota: entre [14] y [15], se pasó de una perspectiva [Android] a una perspectiva [Project] (véase el párrafo 1.2.2.1).

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

 

El archivo [strings.xml] se modifica de la siguiente manera:


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

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

 

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

Ahora vamos a introducir la biblioteca [Android Annotations], a la que llamaremos, por conveniencia, AA. Esta biblioteca introduce nuevas clases para anotar los archivos fuente de Android. Estas anotaciones serán utilizadas por un procesador que creará nuevas clases de Java en el módulo, clases que participarán en la compilación del mismo al igual que las clases escritas por el desarrollador. De este modo, tenemos la siguiente cadena de compilación:

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


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 y 5 agregan las dos dependencias que conforman la biblioteca AA;

El archivo [build.gradle] se modifica nuevamente para utilizar un complemento llamado [android-apt] que divide el proceso de compilación en dos etapas:

  • procesamiento de las anotaciones de Android, lo que da lugar a nuevas clases;
  • compilación de todas las clases del proyecto;

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    // Desde la versión 0.11 del complemento Gradle para Android, debes usar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • línea 8: versión del complemento [android-apt] que se buscará en el repositorio central de Maven (línea 3);
  • línea 13: activación de este complemento;

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

Ahora vamos a introducir una primera anotación de la biblioteca AA en la clase [MainActivity]:

  

La clase [MainActivity] es, por el momento, 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);
  }
}

Ya hemos explicado este código en el párrafo 1.3.3. Lo modificamos de la siguiente manera:


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á una clase [MainActivity_] derivada de la clase [MainActivity], y será esta clase la que constituya 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.
// Generado con AndroidAnnotations 4.0.0.
// 
// Puede crear una obra más amplia que contenga este archivo y distribuirla bajo los términos que usted elija.
//


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. Estas se encargan de la complejidad que las anotaciones buscan ocultar. Sin embargo, a veces puede ser útil examinarlo cuando se desea comprender cómo se «traducen» las anotaciones que utilizamos.

Ahora podemos volver a ejecutar la configuración [app]. Obtenemos el mismo resultado que antes. A partir de ahora, utilizaremos este proyecto, que duplicaremos para presentar los conceptos importantes de la programación para Android.

1.5. Ejemplo-04: vistas y eventos

1.5.1. Creación del proyecto

Seguiremos el procedimiento descrito para duplicar [Exemple-02] en [Exemple-03] en el párrafo 1.4:

Nosotros:

  • duplicamos el proyecto [Exemple-03] en [Exemple-04] (después de haber eliminado la carpeta [app / build] de [Exemple-03]);
  • cargamos el proyecto [Exemple-04];
  • cambiemos el nombre del proyecto en el archivo [app / res / values / strings.xml] (perspectiva Android);
  • eliminemos el archivo [Exemple-04 / Exemple-04.iml] (perspectiva Project);
  • compilemos y luego ejecutemos el proyecto;
 

1.5.2. Crear una vista

Ahora modificaremos, con el editor gráfico, la vista que muestra el proyecto [Exemple-04]:

  • en [1-4], cree una nueva vista XML;
  • en [5], asigne un nombre a la vista;
  • en [6], indique la etiqueta raíz de la vista. Aquí elegimos un contenedor [RelativeLayout]. En este contenedor de componentes, estos se colocan unos respecto a otros: «a la derecha de», «a la izquierda de», «debajo de», «encima de»;
  

El archivo [vue1.xml] generado a partir de [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á todo el ancho de la tableta (línea 3) y toda su altura (línea 4);
  • en [1], seleccione la pestaña [Design] en la vista [vue1.xml] que se muestra;
  • en [2-4], cambie al modo tableta;
  • en [5], ajusta la escala de la tableta a 1;
  • en [6], elija el modo «paisaje» para la tableta;
  • la captura de pantalla [7] resume las opciones seleccionadas.
  • en [1], toma un [Large Text] y arrástralo a la vista [2];
  • en [3], haz doble clic en el componente;
  • en [4], modifica el texto que se muestra. En lugar de escribirlo directamente en la vista XML, lo vamos a externalizar al archivo [res / values / string.xml]
  • en [5], se agrega un nuevo valor al archivo [strings.xml];
  • en [8], se le asigna un identificador a la cadena;
  • En [9], se proporciona el valor de la cadena;
  • en [10], la nueva vista tras validar el paso anterior;
  • tras hacer doble clic en el componente, se cambia su identificador [11];
  • a [12]; en las propiedades del componente, se cambia el tamaño de la fuente de [50sp];
  • a [13], la nueva 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/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    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 la interfaz gráfica se encuentran en las líneas 10, 11 y 14. Los demás atributos del [TextView] son valores predeterminados o se derivan de la posición del componente en la vista;
  • líneas 7-8: el tamaño del componente es el mismo que el del texto que contiene (wrap_content) tanto en altura como en ancho;
  • línea 13: la parte superior del componente está alineada con la parte superior de la vista (línea 13), 50 píxeles más abajo (línea 13);
  • línea 12: el lado izquierdo del componente está alineado con el lado izquierdo de la vista (línea 13), 213 píxeles a la derecha (línea 12);

Por lo general, 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:

Id
Type
Rôle
1
textViewTitreVue1
TextView
Titre de la vue
2
textView1
TextView
une question
3
editTextNom
EditText
saisie d'un nom
4
buttonValider
Button
pour valider la saisie
5
buttonVue2
Button
pour passer à la vue n° 2

Colocar los componentes unos respecto a otros puede resultar frustrante, ya que las reacciones del editor gráfico a veces son inesperadas. Puede ser preferible utilizar las propiedades de los componentes:

El componente [textView1] debe colocarse a 50 píxeles debajo del título y a 50 píxeles del borde izquierdo del contenedor:

  • en [1], el borde superior (top) del componente está alineado con respecto al borde inferior (bottom) del componente [textViewTitreVue1] a una distancia de 50 píxeles de [3] (top);
  • en [2], el borde izquierdo (left) del componente está alineado con respecto al borde izquierdo del contenedor a una distancia de 50 píxeles de [3] (left);

El componente [editTextNom] debe colocarse a 60 píxeles a la derecha del componente [textView1] y alinearse por la parte inferior con ese mismo componente;

 
  • En [1], el borde izquierdo (left) del componente está alineado con respecto al borde derecho (right) del componente [textView1] a una distancia de 60 píxeles de [2] (left). Está alineado con el borde inferior (bottom:bottom) del componente [textView1] [1];

El componente [buttonValider] debe colocarse a 60 píxeles a la derecha del componente [editTextNom] y alinearse por la parte inferior con ese mismo componente;

 
  • En [1], el borde izquierdo (left) del componente está alineado con respecto al borde derecho (right) del componente [editTextNom] a una distancia de 60 píxeles de [2] (left). Está alineado con el borde inferior del componente (bottom:bottom) [editTextNom] [1];

El componente [buttonVue2] debe colocarse 50 píxeles debajo del componente [textView1] y alinearse a la izquierda con este mismo componente;

 
  • En [1], el borde izquierdo (left) del componente está alineado con respecto al borde izquierdo (left) del componente [textView1] y se coloca debajo (top:bottom) a una distancia de 50 píxeles de [2] (top);

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/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    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_nom"
    android:id="@+id/textView1"
    android:layout_below="@+id/textViewTitreVue1"
    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/editTextNom"
    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_valider"
    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>

En él se encuentra todo lo que se ha hecho de manera gráfica. Otra forma de crear una vista es, entonces, escribir directamente este archivo. Una vez que uno se acostumbra, esto puede ser más rápido que usar el editor gráfico.

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

Todos los textos provienen del siguiente archivo [strings.xml] [2]:


<resources>
  <string name="app_name">Exemple-04</string>
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
</resources>

Ahora, modifiquemos la actividad [MainActivity] para que esta vista se muestre al iniciar 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: ahora la actividad muestra la vista [vue1.xml];

Modifique 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="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_"
      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 evita que aparezca el teclado al mostrar la vista [vue1]. De hecho, esta vista tiene un campo de entrada que recibe el foco al mostrarse. Este foco hace que, por defecto, aparezca el teclado virtual;

Ejecute la aplicación y verifique que se muestre efectivamente la vista [vue1.xml]:

Image

1.5.3. Manejo de eventos

Ahora gestionemos el clic en el botón [Valider] de la vista [Vue1]:

Image

El código de [MainActivity] cambia de la siguiente manera:


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 {

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

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
    super.onCreate(savedInstanceState);
  }

  @AfterViews
  protected void afterViews(){
    Log.d("MainActivity","afterViews");
  }

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

}
  • líneas 17-18: se asocia el campo [protected EditText editTextNom] al componente de identificador [R.id.editTextNom] de la interfaz visual. El campo asociado al componente debe ser accesible en la clase derivada [MainActivity_] y, por esta razón, no puede tener el ámbito [private]. 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/editTextNom"
    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)] designa el método que maneja el evento «Click» en el botón con el identificador [R.id.buttonValider]. Este identificador también proviene 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 ingresado:
    • Toast.makeText(...).show(): muestra un texto en pantalla,
    • el primer parámetro de makeText es la actividad,
    • el segundo parámetro es el texto que se mostrará en el cuadro que aparecerá gracias a makeText,
    • el tercer parámetro es el tiempo de vida del cuadro mostrado: Toast.LENGTH_LONG o Toast.LENGTH_SHORT;
  • en la línea 26, la anotación [@AfterViews] indica el método que se debe ejecutar cuando se hayan inicializado todos los campos anotados por [@ViewById]. Es importante saber cuándo se inicializan estos campos. Por ejemplo, ¿se puede utilizar en el método [onCreate] la referencia de la línea 18? Para responder a esta pregunta, hemos incluido registros;

Ejecute el proyecto [Exemple-04] y verifique que ocurra algo al hacer clic en el botón [Valider]. Obtenemos los siguientes registros:

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

De ello se deduce que, cuando se ejecuta el método [onCreate], los campos anotados por [@ViewById] aún no están inicializados. Una vez más, se recomienda al lector principiante que incluya este tipo de registros 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 [Vue n° 2]. Nos proponemos utilizarlo creando una segunda vista y mostrando cómo navegar de una vista a otra. Hay varias formas de resolver este problema. La que se propone aquí es asociar cada vista a una actividad. Otro método consiste en tener una única actividad de tipo [AppCompatActivity] que muestre vistas de tipo [Fragment]. Este será el método que se utilizará en futuras aplicaciones.

1.6.1. Creación del proyecto

Duplicamos el proyecto [Exemple-04] en [Exemple-05]. Para ello, seguiremos el procedimiento descrito para duplicar [Exemple-02] en [Exemple-03] en el párrafo 1.4 y que se ha reproducido en el párrafo 1.5.

1.6.2. Agregar una segunda actividad

Para gestionar una segunda vista, crearemos una segunda actividad. Esta será la que se encargue de la vista n.º 2. Aquí seguimos el modelo «una vista = una actividad». Existen otros modelos posibles.

123

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 (vue2.xml) asociada a la nueva actividad;
  
  • en [7-8], los archivos 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="exemples.android.SecondActivity">

</RelativeLayout>

Por el momento, es una vista vacía con un administrador de diseño de tipo [RelativeLayout] (línea 2). En la línea 11, se observa que se ha asociado a la nueva actividad.

El manifiesto del módulo de Android [AndroidManifest.xml] ha cambiado 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_"
      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, se ha registrado una segunda actividad.

1.6.3. Navegación de la vista n.º 1 a la vista n.º 2

Volvamos al código de la clase [MainActivity] que muestra la vista n.º 1. Actualmente, el paso a la vista n.º 2 no está implementado:

  

Lo gestionamos de la siguiente manera:


  // navegar a la vista n.º 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // se navega a la vista n.º 2 pasándole el nombre ingresado en la vista n.º 1
    // se crea un Intent
    Intent intent = new Intent();
    // se asocia este Intent a una actividad
    intent.setClass(this, SecondActivity.class);
    // se asocia información a este Intent
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // se inicia la actividad de tipo [SecondActivity] pasándole el Intent
    startActivity(intent);
}
  • líneas 2-3: el método [navigateToView2] gestiona el «clic» en el botón identificado por [R.id.buttonVue2], definido 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 cambiar de vista:

  1. línea 6: crear un objeto de tipo [Intent]. Este objeto permitirá especificar tanto la actividad que se lanzará como la información que se le pasará;
  2. línea 8: asociar el Intent a una actividad, en este caso una actividad de tipo [SecondActivity] que se encargará de mostrar la vista n.º 2. Hay que recordar que la actividad [MainActivity] muestra la vista n.º 1. Por lo tanto, tenemos una vista = una actividad. Tendremos que definir el tipo [SecondActivity];
  3. línea 10: de manera opcional, se puede ingresar información en el objeto [Intent]. Esta información está destinada a la actividad [SecondActivity] que se va a iniciar. Los parámetros de [Intent.putExtra] son (objeto clave, objeto valor). Cabe señalar que el método [EditText.getText()], que devuelve el texto ingresado en el campo de entrada, no devuelve un tipo [String], sino un tipo [Editable]. Se debe utilizar el método [toString] para obtener el texto ingresado;
  4. línea 12: ejecute la actividad definida por el objeto [Intent].

Ejecute el proyecto [Exemple-05] y verifique que se muestre correctamente la vista n.º 2 (vacía por el momento):

1.6.4. Creación de la vista n.º 2

 
  • En [1-2], eliminamos la vista [main.xml], que ya no necesitamos, y luego modificamos la vista [vue2.xml] de la siguiente manera:
 

Los componentes son los siguientes:

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 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="exemples.android.SecondActivity">


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue2"
    android:id="@+id/textViewTitreVue2"
    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/textViewBonjour"
    android:layout_centerVertical="true"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/textViewTitreVue2"
    android:layout_marginTop="50dp"
    android:layout_marginLeft="50dp"
    android:textSize="30sp"
    android:text="Bonjour !"
    android:textColor="#ffffb91b"/>

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

</RelativeLayout>

Ejecute el proyecto [Exemple-05] y verifique que se muestre correctamente la nueva vista al hacer clic en el botón [Vue n° 2].

1.6.5. La actividad [SecondActivity]

En [MainActivity], hemos escrito el siguiente código:


    // navegar a la vista n.º 2
    protected void navigateToView2() {
        // se navega a la vista n.º 2 pasándole el nombre ingresado en la vista n.º 1
        // se crea un Intent
        Intent intent = new Intent();
        // se asocia este Intent a una actividad
        intent.setClass(this, SecondActivity.class);
        // se asocia información a este Intent
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // se inicia la actividad de tipo [SecondActivity] pasándole el Intent
        startActivity(intent);
}

En la línea 9, incluimos información para [SecondActivity] que no se había utilizado. Ahora la utilizamos y esto ocurre en el código de [SecondActivity]:

  

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


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 {

  // componentes de la interfaz visual
  @ViewById
  protected TextView textViewBonjour;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @AfterViews
  protected void afterViews() {
    // se recupera el Intent, si existe
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // se recupera el nombre
        String nom = extras.getString("NOM");
        if (nom != null) {
          // lo mostramos
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }

}
  • línea 11: se utiliza la anotación [@EActivity] para indicar que la clase [SecondActivity] es una actividad asociada a la vista [vue2.xml];
  • líneas 15-16: se obtiene una referencia al componente [TextView] identificado por [R.id.textViewBonjour]. Aquí no se ha escrito [@ViewById(R.id.textViewBonjour)]. En este caso, AA supone que el identificador del componente es idéntico al campo anotado, en este caso el campo [textViewBonjour];
  • línea 23: la anotación [@AfterViews] anota un método que debe ejecutarse después de que se hayan inicializado los campos anotados por [@ViewById]. En el método [OnCreate] (línea 19), no se pueden utilizar estos campos porque aún no se han inicializado. En el proyecto [Exemple-05], se cambia de una actividad a otra y, a primera vista, no estaba claro si el método anotado [@AfterViews] se ejecutaría una sola vez al instanciar la actividad por primera vez 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 tipo [Bundle], que es una especie de diccionario que contiene la información asociada al objeto [Intent] de la actividad;
  • línea 31: se recupera el nombre almacenado en el objeto [Intent] de la actividad;
  • línea 34: se muestra.

Recordatorio: los campos marcados con la anotación [@ViewById] no deben contener caracteres acentuados.

Volvamos a la clase [SecondActivity]. Como hemos escrito:


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

AA generará una clase [SecondActivity_] derivada de [SecondActivity] y será esta clase la que constituya la actividad real. Esto nos lleva a realizar modificaciones en:

[MainActivity]


  // navegar a la vista n.º 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // se asocia este Intent a una actividad
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • En la línea 6, debemos reemplazar [SecondActivity] por [SecondActivity_];

[AndroidManifest.xml]


<?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_"
      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, hay que reemplazar [SecondActivity] por [SecondActivity_];

Prueba esta nueva versión. Escribe un nombre en la vista n.º 1 y verifica que la vista n.º 2 lo muestre correctamente.

1.6.6. Navegación de la vista n.º 2 a la vista n.º 1

Para navegar de la vista n.º 2 a la vista n.º 1, seguiremos el procedimiento visto anteriormente:

  • insertar el código de navegación en la actividad [SecondActivity] que muestra la vista n.º 2;
  • escribir el método [@AfterViews] en la actividad [MainActivity], que muestra la vista n.º 1;

El código de [SecondActivity] queda así:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // se crea un Intent para la actividad [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // se recupera el Intent de la actividad actual [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // se ingresa el nombre en el Intent de [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // se inicia [MainActivity]
      startActivity(intent1);
    }
}
  • líneas 1-2: se asocia el método [navigateToView1] al clic en el botón [btn_vue1];
  • línea 4: se crea un nuevo [Intent];
  • línea 5: se asocia a la actividad [MainActivity_];
  • línea 7: se recupera el Intent asociado a [SecondActivity];
  • línea 9: se recupera la información de este Intent;
  • línea 12: se recupera la clave [NOM] de [intent2] para colocarla en [intent1] con el mismo valor asociado;
  • línea 15: se inicia la actividad [MainActivity_].

En el código de [MainActivity] se agrega el siguiente método [@AfterViews]:


  @AfterViews
  protected void afterViews() {
    // se recupera el Intent, si existe
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // se recupera el nombre
        String nom = extras.getString("NOM");
        if (nom != null) {
          // se muestra
          editTextNom.setText(nom);
        }
      }
    }
}

Realice estos cambios y pruebe su aplicación. Ahora, al regresar de la vista n.º 2 a la vista n.º 1, debe aparecer el nombre ingresado inicialmente, lo cual no ocurría hasta ahora.

1.6.7. Ciclo de vida de las actividades

En el párrafo 1.3.5, presentamos el ciclo de vida de una actividad. Aquí tenemos dos actividades y se alterna entre una y otra durante la ejecución. Estas actividades contienen dos métodos de los que no sabemos muy bien cuándo se invocan uno respecto al otro: [onCreate] y [afterViews]. Es importante saberlo. Para ello, agregamos registros en ambas actividades:

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


  // fabricante
  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 más veces;
  • línea 8: queremos saber si el método [onCreate] se invoca una o más veces;
  • línea 14: queremos saber si el método [afterViews] se invoca una o más veces;

Hacemos exactamente lo mismo en la clase [SecondActivity].

Al iniciar la aplicación, tenemos los siguientes registros:

1
2
3
05-28 09:38:09.429 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/exemples.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 [Vue n° 2], los nuevos registros son los siguientes:

1
2
3
05-28 09:39:26.607 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/exemples.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 [Vue n° 1], los nuevos registros son los siguientes:

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

Por lo tanto, la clase [MainActivity] se instancia nuevamente. Al hacer clic en el botón [Vue n° 2], los nuevos registros son los siguientes:

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

Por lo tanto, la clase [SecondActivity] se instancia nuevamente.

Por lo tanto, ambas actividades se recrean sistemáticamente cada vez que se cambia de actividad.

Ahora veremos una arquitectura con una sola actividad capaz de gestionar varias vistas llamadas fragmentos. La actividad y las vistas solo se instanciarán una 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 presenta todos los elementos que utilizaremos posteriormente: actividad única, administrador de fragmentos (vistas), contenedor de fragmentos y navegación entre fragmentos. El concepto de pestañas es diferente al de los fragmentos y tiene una importancia secundaria en lo que queremos mostrar en este ejemplo.

1.7.1. Creación del proyecto

Creamos un nuevo proyecto:

 
  • en [7], elegimos una actividad con pestañas (Tabbed Activity);
  • en [10-14], se mantienen los valores propuestos por defecto;
  • en [15], se eligen pestañas con una barra de títulos;

El proyecto creado es entonces el siguiente:

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

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

 

Se puede ejecutar. A continuación, aparece una ventana con tres pestañas [3-6]:

Image

1.7.2. Configuración de Gradle

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

 

apply plugin: 'com.android.application'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.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 una novedad respecto a lo que ya se ha visto: la línea 25. Esta biblioteca 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 a la actividad [MainActivity] del proyecto. En el modo [design], la vista es la siguiente:

Image

Contiene los siguientes componentes:

  
  • [main_content] es la vista completa;
  • [appbar] (recuadro rojo, 1) es la barra de aplicación. Contiene dos componentes:
    • [toolbar] (recuadro amarillo 4) es la barra de herramientas;
    • [tabs] (recuadro naranja, 5) es la barra de título de las pestañas;
  • [container] (recuadro verde, 2) puede albergar diversos fragmentos. Un fragmento es una vista. De este modo, una misma actividad podrá mostrar varias vistas (fragmentos) en este contenedor;
  • [fab] (componente 3) se denomina componente flotante;

En el modo [text], 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"
                                                 tools:context="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>

Encontramos los elementos descritos anteriormente:

  • líneas 2-49: la definición del componente [main_content] (línea 5), que constituye la totalidad de la vista. Se observa que se trata de un layout (administrador de disposición de componentes) de tipo [CoordinatorLayout] (línea 2);
  • líneas 11-33: el contenedor [appbar] (línea 12). Se trata de un layout de tipo [AppBarLayout] (línea 11);
  • líneas 18-24: el componente [toolbar] (línea 19) del tipo [Toolbar] (línea 18);
  • líneas 28-31: el contenedor [tabs] (línea 29). Se trata de un layout de tipo [TabLayout] (línea 28). Mostrará los títulos de las pestañas;
  • líneas 35-39: el componente [container] (línea 36). Este contenedor es el que muestra las diferentes vistas de la actividad;
  • líneas 41-47: el componente [fab] (línea 42) de tipo [FloatingActionButton] (línea 41). Es un botón en el que se puede hacer clic. Se coloca por defecto en la esquina inferior derecha de la vista total;

No intentaremos comprender el significado de todos los atributos de estos componentes. Los utilizaremos tal como están. Es con la experiencia y, a menudo, en el modo [design], que se descubre su función. En este modo, se observa que los componentes tienen varias decenas de atributos. Por lo general, solo algunos están inicializados, mientras que los demás conservan un valor por defecto.

No obstante, aclaremos algunos puntos. La mayoría de los valores que configuran las diferentes vistas se encuentran en la carpeta [res / values]:

  

Estos valores se mencionan en las líneas 15-16, 23, 39 y 46 del archivo [activity_main.xml]. Veamos un ejemplo:

  • línea 15:

    android:paddingTop="@dimen/appbar_padding_top"

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


<resources>
  <!-- Márgenes predeterminados de la pantalla, según las pautas de diseño de Android. -->
  <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;

De manera análoga, la anotación:

  • [@string] hace referencia al archivo de recursos [res / values / strings.xml];
  • [@color] hace referencia al archivo de recursos [res / values / colors.xml];
  • [@style] hace referencia al archivo de recursos [res / values / styles.xml];

1.7.4. La actividad

  

El código generado para la actividad está a la altura de la vista descrita anteriormente: es complejo. Lo analizaremos en varias etapas.

1.7.4.1. La gestión de fragmentos y pestañas

El código de [MainActivity] relacionado con los fragmentos y las 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 {

  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // el contenedor de fragmentos 
  private ViewPager mViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // padre
    super.onCreate(savedInstanceState);
    // vista
    setContentView(R.layout.activity_main);
    // barra de herramientas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // el administrador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, que el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // la barra de pestañas también está asociada al contenedor de fragmentos
    // es decir, que la pestaña n.º i muestra el fragmento n.º i del contenedor
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }


  // un fragmento
  public static class PlaceholderFragment extends Fragment {
 ...
  }

  // el administrador de fragmentos
  // es a quien se le solicitan los fragmentos que se mostrarán en la vista principal
  //debe definir los métodos [getItem] y [getCount]; los demás son opcionales
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • línea 28: Android proporciona un contenedor de vistas de tipo [android.support.v4.view.ViewPager] (línea 12). A este contenedor se le debe proporcionar un administrador de vistas o fragmentos. Es el desarrollador quien lo proporciona;
  • línea 25: el administrador de fragmentos utilizado en este ejemplo. Su implementación se encuentra en las líneas 61-63;
  • línea 31: el método que se ejecuta al crear la actividad;
  • línea 35: la vista [activity_main.xml] se asocia a la actividad;
  • línea 37: se obtiene la referencia del 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 (un concepto de Android) de la actividad;
  • línea 40: se instancia el administrador de fragmentos. El parámetro del constructor es la clase de Android [android.support.v4.app.FragmentManager] (línea 10);
  • línea 44: se recupera en la vista [activity_main.xml] la referencia del contenedor de fragmentos mediante su identificador;
  • línea 45: el administrador de fragmentos se vincula al contenedor de fragmentos. Esto significa que cuando se le pida al contenedor de fragmentos que muestre el fragmento n.º i, este se solicitará al administrador de fragmentos;
  • línea 48: se recupera una referencia de la barra de pestañas mediante su identificador;
  • línea 49: el administrador de pestañas está asociado al contenedor de fragmentos. Esto significa que cuando se haga clic en la pestaña n.º i, el contenedor mostrará el fragmento n.º i. La asociación entre el administrador de pestañas y el contenedor de fragmentos nos evita tener que gestionar las pestañas. De este modo, no tenemos que definir un controlador de eventos para el clic en una pestaña. La asociación con el contenedor de fragmentos lo proporciona de forma predeterminada. Veremos un ejemplo en el que habrá más fragmentos que pestañas. En ese caso, no se realiza esta asociación.

El manejador de fragmentos [SectionsPagerAdapter] es el siguiente:


//: el administrador de fragmentos
  //: es a este al que se le solicitan los fragmentos que se mostrarán en la vista principal
  // debe definir los métodos [getItem] y [getCount]; los demás son opcionales
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // fragmento n.º posición
    @Override
    public Fragment getItem(int position) {
      // se instancia un fragmento [PlaceHolder] y se devuelve
      return PlaceholderFragment.newInstance(position + 1);
    }

    // devuelve el número de fragmentos gestionados
    @Override
    public int getCount() {
      return 3;
    }

    // opcional: asigna un título a los fragmentos gestionados
    @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 ella. El administrador de fragmentos lo define el desarrollador;
  • línea 5: el administrador de fragmentos extiende la clase de Android [android.support.v4.app.FragmentPagerAdapter]. El constructor nos viene impuesto. Debemos definir al menos los dos métodos siguientes:
    • int getCount(): devuelve el número de fragmentos que se deben gestionar;
    • Fragment getItem(i): devuelve el fragmento n.º i;

El método CharSequence getPageTitle(i), que devuelve el título del fragmento n.º i, es opcional. Dado que el administrador de pestañas se ha vinculado al administrador de fragmentos, el título de la pestaña n.º i será el título del fragmento n.º i. Por lo tanto, los títulos de las líneas 27 a 33 serán los títulos de las pestañas;

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

1.7.4.2. Los fragmentos mostrados

  

Los fragmentos de la actividad tienen aquí todos el mismo tipo 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="exemples.android.MainActivity$PlaceholderFragment">

  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

</RelativeLayout>
  • líneas 1-16: un layout de tipo [RelativeLayout];
  • líneas 11-14: el único componente de la vista (fragmento): un [TextView] identificado por [section_label];

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


// un fragmento
  public static class PlaceholderFragment extends Fragment {
      // un texto que se muestra en el fragmento
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
    }

    // devuelve un fragmento con una información: el número del fragmento pasado como parámetro
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragmento
      PlaceholderFragment fragment = new PlaceholderFragment();
      // información integrada
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // resultado
      return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // se instancia la vista [fragment_main]
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // se ha encontrado el [TextView]
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // se modifica su contenido
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // se devuelve la vista
      return rootView;
    }
  }
  • línea 2: la clase [PlaceholderFragment] extiende la clase de Android [Fragment]. Por lo general, esto siempre es así;
  • línea 2: la clase [PlaceholderFragment] es 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] de 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] del cual se recupera la referencia a través de su identificador;
  • línea 29: se muestra un texto en el [TextView];
    • [getString] es un método de la clase padre [AppCompatActivity];
    • el primer argumento es un número de componente. [R.string.section_format] designa el número del componente identificado por [section_format] en el archivo [res / values / strings.xml] (línea 4 a continuación):

<resources>
  <string name="app_name">Exemple-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) anterior %1$d indica que el argumento n.º 1 (%1) debe formatearse como un número entero ($d);
    • el segundo argumento de [getString] es el valor que se debe asignar al argumento $1 de la línea (d) anterior;
    • [getArguments] proporciona la referencia del paquete de los argumentos del fragmento. Hay que recordar aquí que cada argumento se creó con el siguiente paquete (líneas f-h):

    // devuelve un fragmento con una información: el número del fragmento pasado como parámetro
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragmento
      PlaceholderFragment fragment = new PlaceholderFragment();
      // información incorporada
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // resultado
      return fragment;
}
  • (continuación)
    • getArguments().getInt(ARG_SECTION_NUMBER) devolverá, por lo tanto, el valor [sectionNumber] de las líneas (g) y (b) anteriores;
  • línea 31: se devuelve la vista así creada;

1.7.4.3. Administración del menú

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

  

El contenido del archivo [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 del menú identificado como [action_settings] (línea 5);
  • línea 6: la etiqueta de la opción del menú. Se encuentra en el archivo [res / values / strings.xml] (línea (c) a continuación:

<resources>
  <string name="app_name">Exemple-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) {
    // Expande el menú; esto agrega elementos a la barra de acciones, si está presente.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Maneja aquí los clics en los elementos de la barra de acciones. La barra de acciones
    // gestionará automáticamente los clics en el botón Inicio/Arriba, siempre
    // se especifique una actividad principal en 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 se invoca cuando el sistema está listo para crear el menú de la aplicación. El parámetro de entrada [Menu menu] es un menú vacío que aún no tiene opciones;
  • 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 archivo;
  • línea 5: se indica que se ha creado el menú;
  • líneas 8-21: el método [onOptionsItemSelected] se ejecuta tan pronto como se hace clic en una opción del menú;
  • línea 13: la referencia de la opción del menú en la que se hizo clic;
  • líneas 16-18: si la opción seleccionada es la que tiene el identificador [action_settings], no se realiza ninguna acción y se indica que el evento ha sido procesado (línea 17);
  • línea 20: se pasa el evento a la clase padre;

Para ver mejor lo que sucede con este menú, agregamos registros de log al código anterior:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "création menu en cours");
    // Expande el menú; esto agrega elementos a la barra de acciones si está presente.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Aquí se gestionan los clics en los elementos de la barra de acciones. La barra de acciones
    // gestionará automáticamente los clics en el botón Inicio/Arriba, siempre
    // se especifique una actividad principal en AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // actividad principal
    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 soporte de Android, la de un sobre.

Este componente se maneja en la clase [MainActivity] de la siguiente manera:


    // botón flotante
    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: se recupera la referencia del botón flotante en la vista asociada a la actividad (activity_main);
  • líneas 3-9: se le asigna un controlador para gestionar el clic 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 principal en la que mostrar el mensaje. En este caso, [view] es la vista del sobre en el que se hizo clic (línea 5). La vista principal que se encontrará será la vista [activity_main]. El segundo argumento es el mensaje que se mostrará. El tercer argumento es la duración de la visualización (SHORT o LONG);
  • línea 7: se puede hacer clic en el mensaje mostrado y así activar una acción. Aquí no hay ninguna acción asociada al clic en el mensaje. Finalmente, el método [show] muestra el mensaje;

Al hacer clic en el botón flotante se obtiene el siguiente resultado visual:

 

1.7.5. Ejecución del proyecto

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

Image

Al hacer clic en la pestaña n.º i, se muestra el fragmento n.º i en el contenedor de vistas. Esto se observa en el texto que aparece en [4]. También se puede notar que es posible pasar de una pestaña a otra deslizando la vista hacia la derecha o hacia la izquierda con el ratón (deslizamiento). Veremos que se puede controlar este comportamiento.

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

 

1.7.6. Ciclo de vida de los fragmentos

  • En [1], se observa que el método [onCreateView] y los siguientes se ejecutan durante la primera visualización del fragmento y cada vez que la actividad debe volver a mostrarlo;

Para dar seguimiento al ciclo de vida de la actividad y de los fragmentos, agregamos los siguientes registros en el código de [MainActivity]:


// constructor
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // padre
    super.onCreate(savedInstanceState);
...
  }

  // un fragmento
  public static class PlaceholderFragment extends Fragment {
    // un texto que se muestra en el fragmento
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }

    // devuelve un fragmento con una información: el número del fragmento pasado como parámetro
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // fragmento
      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/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • 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 n.º 1;
  • líneas 5-6: instanciación del fragmento n.º 2;
  • línea 7: inicialización del fragmento n.º 2;
  • línea 8: inicialización del fragmento n.º 1;
  • línea 9: creación del menú de la actividad;

Es importante recordar aquí el código que rige la creación de los fragmentos:


  // el administrador de fragmentos
  // es a él a quien se le solicitan los fragmentos que se mostrarán en la vista principal
  // debe definir los métodos [getItem] y [getCount]; los demás son opcionales
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // fragmento n.º posición
    @Override
    public Fragment getItem(int position) {
      // se instancia un fragmento [PlaceHolder] y se muestra
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • líneas 11-15: [newInstance] instancia un fragmento cada vez que el contenedor de fragmentos lo solicita;

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/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • líneas 1-3: el fragmento n.º 3 se instancia e inicializa. Recordemos que el que se muestra es el fragmento n.º 2;

Ahora, hagamos clic en la pestaña n.º 3. Aquí no hay ningún registro. Probablemente porque el fragmento n.º 3 que se iba a mostrar ya había sido instanciado. Ahora, regresemos a la pestaña n.º 1. Los registros son entonces los siguientes:

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

El fragmento n.º 1 no se ha instanciado de nuevo, pero su método [onCreateView] se ejecuta nuevamente. Este comportamiento se repite para los otros dos fragmentos.

De estos registros, cabe destacar que:

  • la actividad se instanció y se inicializó una vez;
  • cada fragmento se ha instanciado una vez;
  • que el método [onCreateView] de cada fragmento se ejecutó varias veces;

Lo que hay que saber, y lo que confirman los registros, es que, por defecto, cuando se muestra un fragmento n.º i, los fragmentos i-1 e i+1 se instancian, si es que aún no lo están. Esto explica, por ejemplo, que al inicio, cuando se debe mostrar el fragmento n.º 1, sean los fragmentos 1 y 2 los que se han instanciado e inicializado. Lo que también muestran los registros es que el método [getItem(i)] solo se invoca una vez, aunque el fragmento n.º i se muestre varias veces. Así pues, parece que el contenedor de fragmentos [ViewPager], que debe mostrar el fragmento n.º i, solicita este una vez al administrador de fragmentos [SectionsPagerAdapter]. Después, ya no lo vuelve a solicitar y continúa utilizando el que obtuvo.

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

  • al inicio, se instanciaron los fragmentos 1 y 2 y se ejecutó su método [onCreateView];
  • al pasar 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, es posible que el usuario haya realizado una operación con el fragmento 1 cuyo resultado debería mostrarse en el fragmento 2. Se observa que el método [onCreateView] no podrá utilizarse para actualizar el fragmento 2. Habrá que encontrar otra solución;

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

1.8.1. Creación del proyecto

Vamos a duplicar el proyecto [Exemple-06] en [Exemple-07] para introducir en este último las anotaciones de Android. Para ello, siga el procedimiento del párrafo 1.4. Obtenemos el siguiente resultado:

1.8.2. Configuración de Gradle

 

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


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Desde la versión 0.11 del complemento Gradle de Android, debes usar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.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 agregado la configuración necesaria para utilizar la biblioteca [Android Annotations] (véase el párrafo 1.4).

1.8.3. Agregación de las primeras anotaciones AA

Vamos a crear anotaciones AA en [MainActivity]:

  

La clase [MainActivity] evoluciona de la siguiente manera:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // el contenedor de fragmentos
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // el administrador de pestañas
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // el botón flotante
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;


  // constructor
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // barra de herramientas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // el administrador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // la barra de pestañas también está asociada al contenedor de fragmentos
    // es decir, que la pestaña n.º i muestra el fragmento n.º i del contenedor
    tabLayout.setupWithViewPager(mViewPager);

    // botón flotante
    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 administrada 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]. Este es el administrador de pestañas;
  • líneas 14-15: el componente identificado con [R.id.fab] se inserta en el campo [fab]. Se trata del botón flotante;
  • líneas 23-50: el código que antes se encontraba 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 así anotado, se garantiza que todos los componentes de la interfaz visual anotados con [@ViewById] se hayan inicializado;
  • además, se han agregado registros para observar el ciclo de vida de la actividad;

Recordemos que la anotación [@EActivity] generará una clase [MainActivity_], que será la verdadera actividad del proyecto. Por lo tanto, hay que 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="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_"
      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 nuevamente el proyecto y verifique que siga obteniendo la interfaz con pestañas.

1.8.4. Reescritura de los fragmentos

Vamos a revisar la gestión de los fragmentos del proyecto. Por el momento, la clase [PlaceholderFragment] es una clase interna estática de la actividad [MainActivity]. Volveremos a un caso de uso más común, aquel en el que los fragmentos se definen en clases externas. Además, introducimos las anotaciones AA para los fragmentos.

El proyecto [Exemple-07] evoluciona de la siguiente manera:

  

En la imagen anterior, aparece la clase [PlaceholderFragment], que se ha externalizado fuera de la clase [MainActivity]. Se reescribe de la siguiente manera:


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;

// un fragmento es una vista que muestra un contenedor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

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

  // n.º de fragmento
  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)));
    // padre
    super.onResume();
    // visualización
    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: inyectan en el campo [textViewInfo] la referencia del componente de [fragment_main.xml] identificado por [R.id.section_label], que es de tipo [TextView] (línea (l) a continuación):

<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="exemples.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. Se puede utilizar para actualizar la interfaz visual que se va a mostrar;
  • línea 47: se debe llamar al método del mismo nombre de la clase padre;
  • línea 49: existe la duda de si el método [onResume] puede ejecutarse o no antes de la inicialización del campo de la línea 20. Los registros configurados para seguir el ciclo de vida del fragmento nos lo indicarán. Por el momento y como medida de precaución, se realiza una comprobación de nulo;
  • línea 51: actualizamos la información del campo [textViewInfo] con el argumento entero pasado al fragmento al momento de su creación;

La clase [MainActivity] pierde su clase interna [PlaceholderFragment] y su administrador de fragmentos evoluciona de la siguiente manera:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private Fragment[] fragments;
    // número de fragmentos
    private static final int FRAGMENTS_COUNT = 3;
    // n.º de fragmento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // fabricante
    public SectionsPagerAdapter(FragmentManager fm) {
      // padre
      super(fm);
      // inicialización de la tabla de fragmentos
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // se crea un fragmento
        fragments[i] = new PlaceholderFragment_();
        // se pueden pasar argumentos al fragmento
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }

    // fragmento n.º posición
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }

    // devuelve el número de fragmentos gestionados
    @Override
    public int getCount() {
      return fragments.length;
    }

    // opcional: asigna un título a los fragmentos gestionados
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
    }
  }
  • línea 4: los fragmentos se colocan en un array;
  • líneas 16-23: la inicialización de la matriz de fragmentos se realiza en el constructor. Son de tipo [PlaceholderFragment_] (línea 18) y no [PlaceholderFragment]. De hecho, la clase [PlaceholderFragment] ha sido anotada con una anotación AA y dará lugar a una clase [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: se han cambiado los títulos de los fragmentos. Como estos también son los títulos de las pestañas, deberíamos ver un cambio en la barra de pestañas;

Compilemos este proyecto: [Make] y [1]:

 
  • en [2], se observa que las clases generadas por la biblioteca AA se encuentran en la carpeta [app / build / generated / source / apt / debug] (hay que estar en la perspectiva [Project] para ver [2]);

Ejecute el proyecto [Exemple-07] y verifique que siga funcionando.

1.8.5. Revisión de los registros

Al iniciar la aplicación, los registros son los siguientes:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • línea 1: construcción de la única actividad;
  • línea 2: método [afterViews] de la actividad: se inicializan sus campos anotados por [@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: método [onResume] del fragmento 1;
  • líneas 14-15: método [onResume] del fragmento 2;
  • línea 16: creación del menú de la actividad;

Cabe señalar que aquí se responde a una pregunta planteada 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 se ejecuta el método [onResume], puede utilizar los campos anotados por [@ViewById]. Así pues, ahora podremos escribir el método [onResume] de la siguiente manera:


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

Ahora pasemos 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/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • línea 1: el contenedor de fragmentos [ViewPager] solicita el fragmento n.º 3;
  • líneas 2-3: métodos del fragmento n.º 3. Recordemos que este fragmento se había instanciado al inicio de la aplicación;
  • líneas 4-5: se ejecuta el método [onResume] del fragmento n.º 3. Recordemos que el que se muestra es el fragmento n.º 2;

Ahora pasemos de la pestaña 2 a la pestaña 3. No hay ningún registro. Por lo tanto, no se ejecuta ninguno de los métodos [onCreateView, afterViews, onResume] del fragmento n.º 3. Muestra correctamente el texto [Hello World from section:3] únicamente porque ese texto ya se había creado en el paso anterior, al mostrar el fragmento n.º 2. De hecho, recordemos que en ese paso se había ejecutado el método [onResume] del fragmento n.º 3. Aquí nos damos cuenta de que, al igual que el método [onCreateView], el método [onResume] tampoco puede utilizarse para actualizar el fragmento 3. Si hubiera sido necesario cambiar el texto mostrado por el fragmento, ninguno de estos dos métodos podría haberlo hecho.

Ahora, regresemos de la pestaña n.º 3 a la pestaña n.º 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/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Se observa que se han ejecutado todos los métodos del fragmento 1. Se observa que el método getItem no se ha llamado. Como ya se mencionó, este método solo se llama una vez por cada fragmento;

Ahora, pasemos de la pestaña 1 a la pestaña adyacente 2. Tenemos los siguientes registros:

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

Sorprendente, ¿no? Todos los métodos del fragmento n.º 3 se vuelven a ejecutar.

Para entender estos fenómenos, hay que recordar que, por defecto, cuando el contenedor de fragmentos va a mostrar el fragmento i, inicializa los fragmentos i-1, i e i+1. Volvamos a leer los registros a la luz de esta información.

En primer lugar, los registros al iniciar la aplicación:

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

Dado que 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/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Dado que el contenedor de fragmentos va a mostrar el fragmento 2, se deben inicializar los fragmentos 1, 2 y 3. Los fragmentos 1 y 2 ya están inicializados desde el paso anterior. El fragmento 3 se inicializa en las líneas 2 a 5.

Pasamos de la pestaña 2 a la pestaña 3. No hay registros. Como el contenedor de fragmentos va a mostrar el fragmento 3, los fragmentos 2 y 3 deben inicializarse. Sin embargo, desde el paso anterior, ya lo están. Lo que no se ve aquí es que el fragmento 1, que no es adyacente al fragmento 3, pierde su estado, el cual 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/exemples.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/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Dado que el contenedor de fragmentos mostrará el fragmento 1, el fragmento 2 también debe inicializarse. Ya está inicializado desde el paso anterior. En este mismo paso, se había perdido el estado del fragmento 1. Por lo tanto, se reinicia en las líneas 1-4. Lo que no se ve aquí es que el fragmento 3, que no es adyacente al fragmento 1, pierde su estado, el cual no se conserva en la memoria.

Al pasar de la pestaña 1 a la pestaña adyacente 2, se obtienen los siguientes registros:

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

Dado que el contenedor de fragmentos mostrará el fragmento 2, los fragmentos 1, 2 y 3 deben inicializarse. Los fragmentos 1 y 2 ya lo están desde el paso anterior. El fragmento 3 se inicializa en las líneas 1-4.

¿Qué hemos aprendido?

  • que la gestión predeterminada de los fragmentos es muy particular y que hay que conocerla si no queremos volvernos locos. Podemos cambiar este modo de gestión y lo haremos un poco más adelante;
  • que con esta gestión predeterminada, ninguno de los métodos [onCreateView, onResume] puede utilizarse para actualizar el fragmento que se va a mostrar, ya que no se tiene la certeza de que se vayan a ejecutar;

1.8.6. onDestroyView

El método [onDestroyView] forma parte del ciclo de vida de los fragmentos (véase el párrafo 1.7.6):

Se observa que, en el ciclo de vida de un fragmento:

  • el método [onCreateView] puede ejecutarse varias veces;
  • antes de regresar al método [onCreateView] posteriormente, es obligatorio pasar por el método [onDestroyView] [2];

Vamos a insertar estos métodos en los fragmentos para poder seguir mejor su ciclo de vida. El código del fragmento queda así:


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;

// un fragmento es una vista que muestra un contenedor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

...

  @Override
  public void onDestroyView() {
    // registro
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // padre
    super.onDestroyView();
  }

}

Ejecutemos la aplicación. Los primeros registros son los siguientes:

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • línea 1: construcción de la única actividad;
  • línea 2: método [afterViews] de la actividad: se inicializan sus campos anotados por [@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 del fragmento 2 (no necesariamente se hace visible);
  • líneas 10-11: se crea la vista del fragmento 1 (no necesariamente se hace visible);
  • líneas 12-13: método [onResume] del fragmento 1;
  • líneas 14-15: método [onResume] del fragmento 2;
  • línea 16: creación del menú de la actividad;

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


06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
  • línea 1: el contenedor de fragmentos solicita el tercer fragmento;
  • líneas 2-3: se crea la vista del 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 implica que cuando el usuario regrese al fragmento 1 o a un fragmento adyacente, se volverá a ejecutar el ciclo de vida de ese fragmento;

Se regresa de la pestaña 3 a la pestaña 1:


06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
  • líneas 1-4: se vuelve a ejecutar el ciclo de vida del fragmento 1 porque había sufrido un [onDestroyView];
  • línea 5: ahora es el fragmento 3 el que ve ejecutado su método [onDestroyView]. Una vez más, cuando el usuario regrese 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 la hace necesariamente visible. Eso es lo que veremos ahora. El método [Fragment.setUserVisibleHint] se ejecuta cada vez que cambia la visibilidad del fragmento. Agregamos este método al código del fragmento:


package exemples.android;

....

// un fragmento es una vista mostrada por un contenedor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

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

  ...

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // registro
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

Al inicio, los registros son los siguientes:


06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
  • los registros de las líneas 7, 9 y 10 muestran que solo el fragmento 1 se vuelve visible. También vemos que se vuelve 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/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
  • el fragmento 1 está oculto (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/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
  • el fragmento 2 está oculto (línea 1), el fragmento 3 está visible (línea 2);

Volvamos a la pestaña 1:


06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
  • el fragmento 3 está oculto (línea 2), el fragmento 1 está visible (línea 3);

¿Qué hemos aprendido?

  • el método [setUserVisibleHint] se ejecuta una vez con la propiedad [isVisibleToUser] a true, para el fragmento que se va a mostrar;
  • no se puede 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] se dispone a mostrar el fragmento n.º i, ejecuta —si aún no lo ha hecho— el ciclo de vida de los fragmentos adyacentes i-1 e i+1. Este funcionamiento se puede controlar mediante el método [ViewPager].setOffscreenPageLimit:

// desplazamiento de los fragmentos
    [ViewPager].setOffscreenPageLimit(n);

Con la instrucción anterior,

  1. cuando el contenedor de fragmentos [ViewPager] se dispone a mostrar el fragmento n.º i, ejecuta, si aún no lo ha hecho, el ciclo de vida de los fragmentos adyacentes del intervalo [i-n, i+n];
  2. si luego se muestra el fragmento j:
    • se repite el mismo fenómeno para los fragmentos adyacentes del intervalo [j-n, j+n];
    • los fragmentos inicializados en el paso 1 y que ya no se encuentran en la adyacencia [j-n, j+n] del nuevo fragmento pueden entonces ser sometidos a una operación [onDestroyView]. Sin embargo, he podido observar en otras aplicaciones, en particular en la del capítulo 3, que esto no siempre era así;

Modificamos el método [MainActivity.afterViews] de la siguiente manera:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // barra de herramientas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // el administrador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // se desactiva el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);

    // desplazamiento de los fragmentos
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);

    // la barra de pestañas también está asociada al contenedor de fragmentos
    // es decir, la pestaña n.º i muestra el fragmento n.º i del contenedor
    tabLayout.setupWithViewPager(mViewPager);

    // botón flotante
    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 que se deben inicializar como el número total de fragmentos menos 1. De este modo, al inicio, cuando el contenedor de fragmentos muestre el fragmento n.º 1, inicializará al mismo tiempo los fragmentos 2, 3, ..., n, donde n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Por lo tanto, se inicializarán todos los fragmentos. Cuando la ventana de visualización se desplace a otro fragmento, el contenedor de fragmentos:
    • descubrirá que todos los fragmentos adyacentes al nuevo fragmento ya están inicializados y, por lo tanto, no los inicializará;
    • dado que la adyacencia del nuevo fragmento también abarca la totalidad de los fragmentos, el contenedor de fragmentos no «desinicializará» ninguno;

En total, deberíamos ver todos los fragmentos instanciados e inicializados al iniciar la aplicación y luego nunca más. Esto es lo que verificamos ahora al revisar los registros.

Al inicio, tenemos los siguientes registros:

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • 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/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/exemples.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/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/exemples.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/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

Los registros confirman la teoría. Todos los fragmentos se instanciaron e inicializaron al inicio. Después, los métodos de su ciclo de vida ya no se ejecutan. Aquí tenemos un funcionamiento muy predecible de los fragmentos que facilita enormemente su uso.

Lo que queremos encontrar es una forma de actualizar un fragmento que se va a mostrar, independientemente de la disposición de los fragmentos que elija el desarrollador. Los registros nos han mostrado dos cosas:

  • el método [setUserVisibleHint, true] siempre se ejecuta para el fragmento que se va a mostrar y no para los demás;
  • este evento puede ocurrir antes o después del ciclo de vida del fragmento. Esto depende de la adyacencia de fragmentos elegida por el desarrollador. Esto es un problema, ya que si el ciclo de vida aún no se ha llevado a cabo, significa que el fragmento no puede actualizarse mediante el método [setUserVisibleHint, true];

Los registros al iniciar la aplicación, cuando la adyacencia de los fragmentos era 1, fueron los siguientes:


06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
  • Se observa que, cuando el fragmento 1 se vuelve visible, su vista aún no se ha creado. Por lo tanto, no se puede modificar. Esto se podrá hacer durante el ciclo de vida del fragmento, por ejemplo, en los métodos [onCreateView] (línea 11) o [onResume] (líneas 13-14). Como utilizamos las anotaciones AA, normalmente no tenemos que escribir el método [onCreateView]. Por lo tanto, el método [onResume] parece ser el más adecuado aquí para actualizar el fragmento 1;

Al pasar de la pestaña 1 a la pestaña 2, los registros fueron los siguientes:


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

Esta vez, solo tenemos el método [setUserVisibleHint, true] de la línea 4 para actualizar el fragmento 2;

Al pasar de la pestaña 2 a la pestaña 3, los registros fueron los siguientes:


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

Aquí solo tenemos el método [setUserVisibleHint, true] de la línea 2 para actualizar el fragmento 3;

Al pasar de la pestaña 3 a la pestaña 1, los registros fueron los siguientes:


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

Aquí hay que utilizar el método [onResume] del fragmento 1 (líneas 6-7) para actualizar el fragmento 1.

Así pues, en este ejemplo vemos que para actualizar un fragmento que se va a mostrar, contamos con dos métodos: [setUserVisibleHint] y [onResume].

Vamos a implementar esta solución en un nuevo proyecto en el que cada fragmento deberá mostrar el número de veces que se ha mostrado, lo que llamaremos una visita. Por lo tanto, habrá que actualizar su visualización cada vez que se muestre. Este es precisamente el problema que buscamos resolver.

Antes de eso, analizaremos la última etapa del ciclo de vida de una actividad o un fragmento: aquella en la que se destruye. El sistema puede tomar la iniciativa de eliminar una actividad si otras actividades con mayor prioridad requieren recursos que no están disponibles. Para liberar dichos recursos, el sistema tomará la iniciativa de eliminar ciertas actividades. En ese momento, se llamará al método [onDestroy] de la actividad y los fragmentos.

1.8.9. OnDestroy

Permitiremos que el usuario elimine la actividad mediante una opción de menú [5]. Para ello, agregamos una nueva opción de menú en el archivo [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>

Simplemente copiamos y pegamos la primera opción del menú y adaptamos el resultado (líneas 9 y 10). El texto de esta nueva opción se agrega al archivo [strings.xml] [2]:


<resources>
  <string name="app_name">Exemple-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>

Por último, en la clase [MainActivity], se gestiona el clic en la opción [Terminate]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Aquí se gestionan los clics en los elementos de la barra de acciones. La barra de acciones
    // gestionará automáticamente los clics en el botón Inicio/Arriba, siempre
    // se especifique una actividad principal en 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");
      //al finalizar la actividad
      finish();
      return true;
    }
    // principal
    return super.onOptionsItemSelected(item);
}
  • líneas 14-19: se copia y pega el contenido de las líneas 10-13 y se adapta el código a la nueva opción;
  • línea 17: la actividad se termina mediante una acción de software;

Ahora ejecutemos esta nueva versión y, tan pronto como se muestre la primera vista, hagamos clic en la opción de menú [Terminate]. Los registros son 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/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • líneas 1-2: se hace clic en la opción [Terminate];
  • línea 4: se invoca el método [onDestroy] de la actividad;
  • líneas 4-5: se invoca el método [onDestroyView] del fragmento 1 y, a continuación, su método [onDestroy];
  • líneas 6-9: esta operación se repite para los otros dos fragmentos;

Cabe recordar, pues, que el método [onDestroy] de la actividad y los fragmentos se invoca cuando la actividad va a ser eliminada por el sistema, el desarrollador o el usuario. Este método se puede utilizar para guardar información, por ejemplo, localmente en la tableta, a fin de recuperarla cuando el usuario vuelva a iniciar la aplicación.

1.9. Ejemplo-08: actualización de un fragmento con una adyacencia variable de fragmentos

1.9.1. Creación del proyecto

Se duplica el proyecto [Exemple-07] en [Exemple-08]. Para ello, se seguirá el procedimiento descrito para duplicar [Exemple-02] en [Exemple-03] en el párrafo 1.4.

1.9.2. Reescritura del fragmento [PlaceholderFragment]

El nuevo código del fragmento [PlaceholderFragment] es el siguiente. Funciona independientemente de la adyacencia que se les asigne 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;

// un fragmento es una vista que muestra un contenedor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // componente de la interfaz visual
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // datos
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

  // número de fragmento
  private static final String ARG_SECTION_NUMBER = "section_number";

  // constructor
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }


  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // registro
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // texto inicial
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inicialización completada
      initDone = true;
    }
    // visualización del texto actual
    textViewInfo.setText(text);
  }


  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  @Override
  public void onDestroyView() {
...
  }

  @Override
  public void onResume() {
...
  }

  // actualización de fragmento
  public void update() {
    // el trabajo a realizar depende del número de visita
    if (numVisit > 1) {
      // registro
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // texto modificado
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // información local para registros
  private String getInfos() {
    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. Se utilizaba para inicializar el texto del fragmento (línea 42). Seguimos haciéndolo, pero para que solo se haga una vez, manejamos un valor booleano [initDone] (línea 44) para indicar que la inicialización ya se ha realizado y que no es necesario repetirla;
  • líneas 56-59: introducimos el método [onDestroyView] para indicar que la próxima vez que se vuelva a mostrar el fragmento, se volverá a ejecutar su ciclo de vida;
  • los registros han mostrado que dos métodos pueden ejecutarse después del método [@AfterViews]: los métodos [setUserVisibleHint] y [onResume]. El método [onResume] solo se ejecuta cuando se ejecuta el ciclo de vida del fragmento. Por su parte, el método [setUserVisibleHint] no siempre se ejecuta después del método [@AfterViews]. Los registros han mostrado 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 medida de precaución, se establecerá un valor booleano [updateDone] cuando se haya realizado una actualización;

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


  // datos
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // registro
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // número de visitas
    if (isVisibleToUser) {
      // incremento
      numVisit++;
      // actualización del fragmento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // el fragmento se ocultará
      updateDone = false;
    }
  }

  @Override
  public void onResume() {
    // padre
    super.onResume();
    // registro
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // actualización
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • línea 14: se almacena si el fragmento está visible o no;
  • líneas 22-25: si el fragmento está visible y se ha ejecutado el método [@AfterViews], se ejecuta el método [update] y se pasa el valor booleano [updateDone] a true;
  • líneas 26-28: si el fragmento se va a ocultar, se restablece el valor booleano [updateDone] a false. De hecho, necesitamos un evento para reiniciar a false la variable booleana [updateDone], que se había establecido en true tan pronto como se invocó el método [update], para que se puedan realizar nuevas actualizaciones. Aprovechamos el hecho de que el fragmento ya no es visible para lograrlo. Cuando vuelva a ser visible, la actualización del fragmento deberá realizarse nuevamente;
  • líneas 32-42: los registros muestran que, según la adyacencia elegida para los fragmentos, el método [onResume] puede ejecutarse aunque el fragmento no esté visible. Si no está visible, no se realiza la actualización (línea 39) y, al igual que hicimos con [setMenuVisibility], gestionamos el valor booleano [updateDone].

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


  @Override
  public void onDestroyView() {
    // padre
    super.onDestroyView();
    // actualización del indicador
    afterViewsDone = false;
    // registro
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

El método [onDestroyView] se ejecuta cuando finaliza un ciclo de vida del fragmento. Posteriormente, podrá reiniciarse otro ciclo.

  • línea 6: el método [onDestroyView] elimina cualquier vínculo con la vista asociada al fragmento. Se volverá a crear en el próximo ciclo de vida del fragmento. Por ahora, debemos cambiar el valor del booleano [afterViews] a false, para indicar que el enlace con la vista ya no existe;

Vamos a ejecutar la aplicación con 5 fragmentos que tienen una adyacencia de 2. Los cambios se realizan en [MainActivity]:


    // número de fragmentos
  private final int FRAGMENTS_COUNT = 5;
  // adyacencia de fragmentos
  private final int OFF_SCREEN_PAGE_LIMIT=2;


  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    ....

    // desplazamiento de los fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

...
}

Los registros al inicio son los siguientes:


05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
  • 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] hasta false;
  • línea 14: se ejecuta el método [setUserVisibleHint] del fragmento 1 con los métodos desde [visibleToUser] hasta true;
  • líneas 15-17: se invoca el método [afterViews] de los tres segmentos adyacentes. Por lo tanto, aquí vemos un caso en el que este método se invoca después de que un fragmento se haya vuelto visible (el fragmento 1, línea 14);
  • líneas 18-20: se invoca el método [onResume] de los tres segmentos adyacentes;

Se pasa de la pestaña 1 a la pestaña 2:


05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • debido a que la adyacencia de los fragmentos se desplaza una posición hacia la derecha, el contenedor de fragmentos solicita el fragmento 4;
  • línea 2: se invoca el método [setUserVisibleHint] del fragmento 4 con [visibleToUser] a false;
  • línea 3: se invoca el método [setUserVisibleHint] del fragmento 1 con [visibleToUser] a false. De hecho, el fragmento 1 ahora está oculto;
  • línea 4: se invoca el método [setUserVisibleHint] del fragmento 2 con [visibleToUser] a true. El fragmento 2 ahora está visible;
  • líneas 5-6: el ciclo de vida del fragmento 4 continúa;

Se pasa de la pestaña 2 a la pestaña 3:


05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • debido a que la adyacencia de los fragmentos se desplaza una posición hacia la derecha, el contenedor de fragmentos solicita el fragmento 5;
  • línea 2: se invoca el método [setUserVisibleHint] del fragmento 5 con [visibleToUser] a false;
  • línea 3: se invoca el método [setUserVisibleHint] del fragmento 2 con [visibleToUser] a false. De hecho, el fragmento 2 ahora está oculto;
  • línea 4: se llama al método [setUserVisibleHint] del fragmento 3 con [visibleToUser] a true. El fragmento 3 ya es visible;
  • líneas 5-6: el ciclo de vida del fragmento 5 continúa;

Se pasa de la pestaña 3 a la pestaña 4:


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

Pasamos de la pestaña 4 a la pestaña 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/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • línea 1: el fragmento 4 ahora está oculto;
  • línea 2: el fragmento 5 ahora está visible. Cabe señalar que no se ejecuta el ciclo de vida del fragmento 5. Esto ya se hizo dos pasos antes;
  • línea 3: el fragmento 2 sale de la adyacencia del fragmento 5 que se muestra. Se ejecuta su método [onDestroyView];

Se pasa de la pestaña 5 a la pestaña 1:


05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • líneas 1, 4, 5, 6: se vuelve a ejecutar el ciclo de vida del fragmento 1. De hecho, 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: los fragmentos 4 y 5 salen de la adyacencia del fragmento mostrado;
  • línea 7: se actualiza el fragmento 1;
 

Los registros nunca mostraron que los métodos [setUserVisibleHint] y [onResume] intentaran ambos actualizar el fragmento. Es uno u otro. Se invita al lector a realizar más pruebas y a revisar los registros para comprender bien el concepto de adyacencia y el ciclo de vida de los fragmentos.

Ahora, supongamos una adyacencia total y realicemos las mismas pruebas.

En [MainActivity]:


  // número de fragmentos
  private final int FRAGMENTS_COUNT = 5;
  // adyacencia de los fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Los registros al inicio son los siguientes:


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

Se pasa de la pestaña 1 a la pestaña 2:


05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • línea 1: el fragmento 1 se oculta;
  • línea 2: se muestra el fragmento 2;

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


05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.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: se muestra el fragmento 3;

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


05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.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: se muestra el fragmento 4;

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


05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.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: se muestra el fragmento 5;

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


05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.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: se muestra el fragmento 1;
  • línea 3: se actualiza el fragmento 1;

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


05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.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: se muestra el fragmento 4;
  • línea 3: se actualiza el fragmento 4;

Notamos que, con la adyacencia total, el comportamiento de los fragmentos es mucho más predecible.

Ahora, establezcamos una adyacencia nula y veamos qué sucede. La clase [MainActivity] evoluciona de la siguiente manera:


  // número de fragmentos
  private final int FRAGMENTS_COUNT = 5;
  // adyacencia de los fragmentos
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Los registros al inicio son los siguientes:


06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
  • En las líneas 8 y 10, se observa que el contenedor de fragmentos ha solicitado 2 fragmentos, los n.º 1 y 2. Por lo tanto, todo ocurre como si hubiera una adyacencia de 1. La adyacencia de 0, por lo tanto, 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. Por «estado de la aplicación» entendemos aquí el conjunto de información que esta almacena a lo largo de su vida útil. Entonces surge el siguiente problema:

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

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

Existen diferentes soluciones para este problema. Ya hemos visto una: el fragmento i puede transmitir el estado E2 de la aplicación al fragmento j a través de argumentos. Nos encontramos con este método en la clase [MainActivity] al crear los fragmentos:


      for (int i = 0; i < fragments.length; i++) {
        // se crea un fragmento
        fragments[i] = new PlaceholderFragment_();
        // se pueden pasar argumentos al fragmento
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

Esta solución no se puede aplicar de inmediato aquí. De hecho, cuando el usuario hace clic en la pestaña j, lo que hará que aparezca el fragmento j, nuestro código no se ejecuta. Solo se ejecuta código del sistema. Veremos en un próximo proyecto cómo interceptar el clic en una pestaña, pero por ahora tomaremos otro camino.

Hemos hablado del estado de la aplicación: el conjunto de datos que la aplicación gestiona a lo largo del tiempo. Aquí, la aplicación está compuesta por una actividad y n fragmentos, todos instanciados una sola vez al iniciar la aplicación y cuya vida útil es la misma que la de la aplicación. Por lo tanto, cada uno de estos elementos, o varios de ellos juntos, pueden ser 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 en esta.

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


  // actualizar fragmento
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // el trabajo a realizar depende del número de visita
    if (numVisit > 1) {
      // registro
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // texto modificado
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // información local para los registros
  private String getInfos() {
    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 [getInfos] muestra parte del estado de la aplicación;

Iniciamos la aplicación con una adyacencia de fragmentos de 2. Los registros al iniciar la aplicación:


06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.android D/menu: création menu en cours
  • líneas 9, 10, 13, 14: se observa que en los métodos [setUserVisibleHint], se ejecuta [getActivity()==null] si el fragmento aún no es visible (isVisibleToUser==false);
  • línea 19: se observa que cuando el flujo de ejecución llega al método [update] del fragmento 1, el método [getActivity] devuelve correctamente la actividad;

Cuando se establece la adyacencia de fragmentos en 4 (adyacencia total), 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 -- aceptar y descartar
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.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/exemples.android D/menu: création menu en cours

Se obtienen los mismos resultados. De ello se deduce que, tan pronto como el fragmento es visible, el método [getActivity] devuelve la actividad del fragmento. También se observa que, cuando la ejecución llega al método [update] del fragmento que se va a mostrar, el método [getActivity] devuelve correctamente un valor.

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

1.10. Ejemplo-09: comunicación entre fragmentos, deslizamiento y desplazamiento

1.10.1. Creación del proyecto

Duplicamos el proyecto [Exemple-07] en [Exemple-08]. Para ello, seguiremos el procedimiento descrito para duplicar [Exemple-02] en [Exemple-03] en el párrafo 1.4.

1.10.2. La sesión

En este nuevo proyecto, queremos que los fragmentos muestren el número total de fragmentos visualizados por el usuario. Para ello, es necesario mantener un contador al que puedan acceder todos los fragmentos. Llamaremos «sesión» al objeto que encapsula los datos compartidos por los fragmentos. Esta terminología proviene del desarrollo web, donde se colocan en una sesión los datos que deben compartir las diferentes vistas solicitadas por un mismo usuario. El hecho de encapsular la información compartida por los distintos fragmentos en un mismo objeto hace que todo sea más legible.

La clase [Session] será la siguiente:

  

package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // número de fragmentos visitados
  private int numVisit;

  // getters y 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] designa el alcance (o vida útil) de la clase así anotada. Aquí, el atributo [scope = EBean.Scope.Singleton] hace que la clase [Session] sea un singleton: se instanciará una vez y solo una vez al iniciar la aplicación. La referencia de una clase anotada con [EBean] puede luego inyectarse en otra clase. Este es el concepto de inyección de dependencias;

1.10.3. La actividad [MainActivity]

La actividad [MainActivity] evoluciona de la siguiente manera:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  ...

  // inyección de sesión
  @Bean(Session.class)
  protected Session session;

  // número de fragmentos
  private final int FRAGMENTS_COUNT = 5;
  // adyacencia de fragmentos
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");

    // inicialización de sesión
    session.setNumVisit(0);
  }

...
  • líneas 7-8: inyección de la referencia al singleton de la sesión mediante la anotación [@Bean]. El parámetro de la anotación es la clase del bean que se va a inyectar. El campo así anotado no puede tener el alcance [private];
  • línea 15: la anotación [@AfterInject] sirve para designar un método que se llamará cuando se hayan realizado todas las inyecciones de la clase. Así, al ingresar al método [afterInject] de la línea 16, la referencia de la línea 8 ya se ha inicializado;
  • línea 20: se pone a cero el contador de visitas;

1.10.4. El fragmento [PlaceholderFragment]

El fragmento [PlaceholderFragment] evoluciona de la siguiente manera:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

....

  // sesión
  protected Session session;

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // registro
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // número de visitas
    if (isVisibleToUser) {
      // actualización del fragmento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // el fragmento se va a ocultar
      updateDone = false;
    }
  }

  // actualización del fragmento
  public void update() {
    // registro
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // sesión
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // incremento del número de visitas
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texto modificado
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • línea 7: la sesión;
  • líneas 35-37: sabemos que al llegar al 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, en ese momento, el método [getActivity] devuelve la actividad. Aquí decidimos no asignarle un papel específico a este método y trasladar el código específico de un fragmento al método [update], que está diseñado para eso;
  • línea 43: muestra el número de la visita;

Cuando se ejecuta esta aplicación con 5 fragmentos, con una adyacencia de 2 fragmentos, 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/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
  • líneas 2-3: se observa que el método [afterInject] de la actividad se ejecuta antes que su método [afterViews];

Se invita al lector a probar esta nueva aplicación.

1.10.5. Desactivar el deslizamiento

En la aplicación anterior, al deslizar el ratón hacia la izquierda o hacia la derecha sobre el emulador de Android, la vista actual da paso a la vista de la derecha o de la izquierda, según el caso. Este comportamiento predeterminado no siempre es deseable. Aprenderemos a desactivar el deslizamiento de vistas (swipe).

Volvamos a la vista principal XML:

  

En el código XML de la vista se encuentra el del 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 indica la clase que administra 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 {

  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // el contenedor de fragmentos
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

En la línea 12, el contenedor de fragmentos es de tipo [android.support.v4.view.ViewPager] (línea 1). Para desactivar el barrido, debemos derivar esta clase de la siguiente manera:

  

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 {

  // controla el deslizamiento
  private boolean isSwipeEnabled;

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

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

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

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // ¿Deslizamiento permitido?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }

}
  • línea 8: la clase [MyPager] extiende la clase de Android [ViewPager] (línea 4);
  • al deslizar la mano, se pueden invocar los manejadores de eventos de las líneas 24 y 34. Ambos devuelven un valor booleano. Basta con que devuelvan el valor booleano [false] para desactivar el deslizamiento;
  • línea 11: el valor booleano que sirve para indicar si se acepta o no el deslizamiento con la mano.

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

  

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

En la línea 1, se utiliza la nueva clase. En [MainActivity], el código cambia de la siguiente manera:


package exemples.android;

...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // el contenedor de fragmentos
  @ViewById(R.id.container)
  protected MyPager mViewPager;

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // se desactiva el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);
    // la barra de pestañas también está asociada al contenedor de fragmentos
...
  • línea 12: el administrador de páginas ahora tiene el tipo [MyPager];
  • línea 23: se desactiva o no el desplazamiento de la mano.

Prueba esta nueva versión. Desactiva o no el desplazamiento y observa la diferencia en el comportamiento de las vistas cuando las arrastras hacia la derecha o hacia la izquierda con el ratón. En todas las aplicaciones futuras, el desplazamiento estará desactivado. No lo volveremos a mencionar.

1.10.6. Desactivar el desplazamiento entre fragmentos

Continuemos con una mejora del administrador de pestañas. Al pasar de la pestaña 1 a la pestaña 4, se ven desplazarse las dos pestañas intermedias, la 2 y la 3. En la jerga de Android, esto se conoce como smoothScrolling. Este comportamiento puede resultar molesto si hay muchas pestañas. Se puede desactivar agregando el siguiente código en el administrador de fragmentos [MyPager]:


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

 ...
  // desplazamiento
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }

  // setters
...

  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }

Dado que el administrador de pestañas se ha asociado con el administrador de fragmentos [MyPager], al hacer clic en la pestaña n.º i, el contenedor de fragmentos muestra el fragmento n.º i mediante el método [setCurrentItem] mencionado anteriormente (línea 9). [position] es el número del fragmento que se debe mostrar;

  • línea 10: se invoca el método [setCurrentItem] de la clase padre. El segundo argumento de [false] indica que debe haber una transición inmediata entre el fragmento anterior y el nuevo (sin desplazamiento), mientras que en [true] se indica que debe haber una transición mediante scrolling. Aquí, el segundo argumento es el valor del campo de la línea 4, campo que el desarrollador puede establecer con el método de las líneas 16 a 18;

Si se desea desactivar el desplazamiento, la clase [MainActivity] será la siguiente:


...
    // desplazamiento de los fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // se desactiva el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);

    // sin desplazamiento
    mViewPager.setScrollingEnabled(false);
...

Ejecute nuevamente el proyecto y verifique que ya no aparezca scrolling entre las pestañas 1 y 4, por ejemplo. En lo sucesivo, siempre desactivaremos el desplazamiento. No volveremos a mencionarlo.

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

En primer lugar, copiemos la vista [vue1.xml] del proyecto [Exemple-04] al proyecto [Exemple-09] [1]:

 
  • en [1], la vista [vue1.xml];
  • en [3], la vista presenta errores debidos a textos que faltan en el archivo [res/values/strings.xml];

En [2], se agregan los textos que faltan tomándolos del archivo [res/values/strings.xml] del proyecto [Exemple-04]


<resources>
  <string name="app_name">Exemple-07</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
</resources>
  • anterior, se han agregado las líneas 6 a 9;

Ahora creamos la clase [Vue1Fragment], que será el fragmento encargado 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 {

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

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

  • línea 10: la anotación [@EFragment] hace que el fragmento utilizado por la actividad sea, en realidad, la clase [Vue1Fragment_]. Es importante recordarlo. El fragmento está asociado a la vista [vue1.xml];
  • líneas 14-15: el componente identificado como [R.id.editTextNom] se inyecta en el campo [editTextNom] de 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()] permite obtener la actividad en la que se encuentra el fragmento. Se trata de [MainActivity], ya que en esta arquitectura solo tenemos una actividad que muestra diferentes vistas o fragmentos;

En la clase [MainActivity], el administrador de fragmentos evoluciona de la siguiente manera:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private Fragment[] fragments;
    // N.º de fragmento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // creador
    public SectionsPagerAdapter(FragmentManager fm) {
      // padre
      super(fm);
      // inicialización de la tabla de fragmentos
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // se crea un fragmento
        fragments[i] = new PlaceholderFragment_();
        // se pueden pasar argumentos al fragmento
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // un fragmento de +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

 ...
  }
  • línea 13: hay fragmentos [FRAGMENTS_COUNT]: fragmentos [FRAGMENTS_COUNT-1] de tipo [PlaceholderFragment] (líneas 14-21) y un fragmento de tipo [Vue1Fragment_], línea 23 (cuidado con el guión bajo);

Compila y luego ejecuta el proyecto [Exemple-09]. La pestaña n.º 5 debe ser diferente:

1.10.8. Derivar todos los fragmentos de una misma clase abstracta

El nuevo fragmento [Vue1Fragment] también necesita actualizarse cuando se muestra. Para ello, tendremos que crear un código similar al creado para el fragmento [PlaceholderFragment]. Para evitar repeticiones, vamos a factorizar lo que sea posible 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: hacer que todos los fragmentos deriven de una clase abstracta

1.11.1. Creación del proyecto

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

1.11.2. Gestión del modo de depuración

Agregamos al proyecto la opción de mostrar o no los registros del modo de depuración. Para ello, agregamos una constante estática a la clase [MainActivity]:


  // modo de depuración
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. La clase abstracta padre 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 {

  // datos privados
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;

  // datos  accesibles a las clases hijas
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;

  // actividad
  protected MainActivity activity;

  // sesión
  protected Session session;

  // constructor
  public AbstractFragment() {
    // inicialización
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }

  @Override
  public void onDestroyView() {
    // padre
    super.onDestroyView();
    ...
  }

  @Override
  public void onResume() {
    // padre
    super.onResume();
    ...
  }

  // información local
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }

  // actualizar fragmento
  protected void update() {
    ...
    // se le pide a la clase hija que se actualice
    updateFragment();
  }

  protected abstract void updateFragment();
}
  • línea 7: la clase [AbstractFragment] extiende la clase de Android [Fragment];
  • todo fragmento debe poder actualizarse. Por eso, la clase padre [AbstractFragment] exige que sus clases hijas cuenten con un método [updateFragment] (línea 68) al que ella misma 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 recopilan 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, es decir, el nombre de una clase hija;
  • líneas 15-22: estos campos tienen el atributo [protected] para que las clases hijas puedan acceder a ellos. Cabe señalar que las clases hijas ignoran la existencia de los valores 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 similares a los que había en la clase [PlaceholderFragment] del proyecto anterior:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // caso en el que el fragmento se volverá visible
    if (isVisibleToUser) {
      // actualizar fragmento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // al salir del fragmento
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // padre
    super.onDestroyView();
    // actualización del indicador
    afterViewsDone = false;
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }

  @Override
  public void onResume() {
    // padre
    super.onResume();
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // actualización
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

El método [update] es el siguiente:


  // actualización de fragmento
  protected void update() {
    // se recuperan la actividad y la sesión
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // se le pide a la clase hija que se actualice
    updateFragment();
}

Según el código anterior, cuando se ejecuta el método [update] de un fragmento, este queda 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 el párrafo 1.10.8), lo que a su vez da acceso a la sesión.

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

1.11.4. La clase [PlaceholderFragment]

  

La clase [PlaceholderFragment] evoluciona 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.*;

// un fragmento es una vista mostrada por un contenedor de fragmentos
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {

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

  // datos
  private boolean initDone;

  // datos
  private String text;
  private int numVisit;

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

  // constructor
  public PlaceholderFragment() {
    super();
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }


  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
 ...
  }

  // actualización de fragmento
  public void updateFragment() {
  ...
  }

}
  • línea 10: la clase [PlaceholderFragment] extiende la clase [AbstractFragment]. Con esta arquitectura, la implementación de un fragmento consiste en:
    • escribir el método [@AfterViews], que sirve para inicializar el fragmento durante su primer ciclo de vida o para reinicializarlo si previamente se ha ejecutado un [onDestroyView]. La línea 39 es obligatoria para gestionar correctamente el ciclo de vida del fragmento;
    • en escribir el método [updateFragment], que actualizará el fragmento justo antes de su visualización. Este método puede utilizar la sesión de su clase padre;
    • escribir los controladores de eventos del fragmento. Esto es lo que haremos en proyectos futuros;

Los métodos [@AfterViews] y [updateFragment] siguen siendo similares a lo que eran en el proyecto anterior:


@AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // texto inicial
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inicialización completada
      initDone = true;
    }
    // visualización del texto actual
    textViewInfo.setText(text);
  }

  // actualización de fragmento
  public void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // incremento del número de visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texto modificado
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }

  // información local para registros
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • líneas 7 y 23: en los registros, se muestra la información de la clase padre con el método heredado [getParentInfos];

1.11.5. La clase [Vue1Fragment]

  

La clase [Vue1Fragment] presenta 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 {

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

  // datos
  private int numVisit;

  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }

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

  // información local para registros
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }

  // actualización de fragmento
  @Override
  protected void updateFragment() {
    // incremento del número de visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // se muestra el número de visita
    Toast.makeText(getActivity(), String.format("Visite n° %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. Sin embargo, hay que escribirlo para establecer el valor booleano [afterViewsDone] en true, ya que la clase padre utiliza esta información;
  • líneas 42-49: el método [updateFragment] consiste en mostrar un mensaje breve con el número de la visita (línea 48) e incrementar ese número en la sesión (líneas 44-46);

Se invita al lector a probar este nuevo proyecto.

En todos los proyectos futuros retomaremos esta arquitectura:

  • 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 de pestañas y fragmentos

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


// la barra de pestañas también está vinculada al contenedor de fragmentos
// es decir, la pestaña n.º i muestra el fragmento n.º i del contenedor
tabLayout.setupWithViewPager(mViewPager);

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

Para ilustrar este comportamiento, vamos a agregar las opciones [Fragment 1, Fragment 2, ...] al menú actual. Cuando el usuario haga clic en la opción [Fragment i], se le pedirá al contenedor de fragmentos que muestre el fragmento n.º i. Entonces veremos si la pestaña n.º i ha sido seleccionada 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 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="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment5"
        android:title="@string/fragment5"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>
  • líneas 9-28: las cinco nuevas opciones del menú;
  • los nombres 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">Exemple-10</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
  <!-- menú -->
  <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 del clic en estas opciones de menú se realiza en la clase [MainActivity]:


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

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // se cambia el fragmento mostrado
      mViewPager.setCurrentItem(i);
    }
  }
  • línea 2: se llama al método [onOptionsItemSelected] cuando se hace clic en una de las opciones del menú;
  • línea 8: se recupera el identificador de la opción en la que se hizo clic;
  • líneas 9-36: los distintos casos se procesan mediante un switch;
  • líneas 16-36: al hacer clic en la opción [Fragment i] se remite al método [showFragment(i-1)] de las líneas 41-45;
  • línea 43: se le pide al contenedor de fragmentos que muestre el fragmento solicitado;
  • línea 42: antes se verifica que se pueda hacer (condición 1) y que sea necesario (condición 2);

Se invita al lector a probar esta nueva versión. Se observa que, cuando se solicita la visualización del fragmento n.º i, este se muestra correctamente y la pestaña n.º i queda seleccionada.

Ahora que hemos visto cómo funciona la asociación entre pestañas y fragmentos, nos centraremos en otro caso: aquel en el que la gestión de las pestañas está separada de la de los fragmentos. Este es el caso, por ejemplo, cuando hay menos pestañas que fragmentos. Para ilustrar este nuevo caso de uso, crearemos un nuevo proyecto.

1.12. Ejemplo 11: pestañas desvinculadas de los fragmentos

1.12.1. Creación del proyecto

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

1.12.2. Objetivos

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

  • la primera pestaña siempre mostrará el fragmento [Vue1];
  • la segunda pestaña mostrará un fragmento seleccionado del menú;

Image

  • en [1], el fragmento [Vue1];
  • en [2], el fragmento de tipo [PlaceholderFragment] seleccionado por el usuario;
  • en [3], se sigue contando el número de 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 {
  // número de fragmentos visitados
  private int numVisit;
  // N.º del fragmento de tipo [PlaceholderFragment] mostrado en la segunda pestaña
  private int numFragment;

  // getters y setters
...
}
  • línea 10: gestionaremos nosotros mismos el clic en las pestañas. Al hacer clic en una pestaña, hay que recuperar el fragmento que mostraba la última vez que fue seleccionada. El campo [numFragment] almacenará el número de este para la pestaña n.º 2, un número en [0, Fragments_COUNT-2]. Cuando se haga clic en la pestaña n.º 2, buscaremos en la sesión el número del fragmento que se debe mostrar;

1.12.4. El menú

  

El menú [res / menu / menu_main.xml] evoluciona 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="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

La pestaña n.º 2 mostrará uno de los cuatro fragmentos de las líneas 9 a 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] ahora debe gestionar las pestañas y la navegación entre ellas, algo que hasta ahora no hacía. Su código evoluciona de la siguiente manera:


  // el administrador de pestañas
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...

    // sin desplazamiento
    mViewPager.setScrollingEnabled(false);

    // visualización de Vista1
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

    // al inicio solo hay una pestaña
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);

    // gestor de eventos
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // se ha seleccionado una pestaña: se cambia el fragmento que muestra el contenedor de fragmentos
        ...
      }

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

      }

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

      }
    });

...

}
  • línea 17: el primer fragmento que mostrará el contenedor de fragmentos será el fragmento [Vue1Fragment]. Por diseño, este será el último fragmento del contenedor;
  • líneas 20-22: dado que no se ha establecido ninguna asociación entre las pestañas y el contenedor de fragmentos, debemos gestionar las pestañas nosotros mismos. Inicialmente, la barra de pestañas [tabLayout] de la línea 3 no tiene ninguna pestaña;
  • línea 20: creamos la primera pestaña;
  • línea 21: le asignamos un título. En los ejemplos anteriores, el título de las pestañas era el título de los fragmentos. Eso ya no es así. Por lo tanto, eliminamos el método [getPageTitle] del administrador de fragmentos. Ya no lo necesitamos:

    // opcional: asigna un título a los fragmentos gestionados
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
}
  • línea 22: la pestaña creada se agrega a la barra de pestañas. Nuestra barra de pestañas ahora tiene una pestaña. ¿Qué muestra esta pestaña? Hay que entender que las pestañas y los fragmentos son dos conceptos independientes. El fragmento que se muestra es siempre el elegido por el contenedor de fragmentos. Si cambiamos de pestaña y no le pedimos al contenedor que cambie el fragmento que se muestra, no pasa nada: sigue mostrándose el mismo fragmento, pero la pestaña seleccionada sí ha cambiado. Por lo tanto, aquí el fragmento que se muestra es el elegido en la línea 17: el fragmento [Vue1Fragment];
  • líneas 26-30: el método que hay que escribir para gestionar el cambio de pestaña por parte del usuario;

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


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // se ha seleccionado una pestaña: se cambia el fragmento que muestra el contenedor de fragmentos
        // posición de la pestaña
        int position = tab.getPosition();
        // N.º del fragmento que se mostrará
        int numFragment;
        switch (position) {
          case 0:
            // N.º del fragmento [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // N.º del fragmento [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // visualización del fragmento
        mViewPager.setCurrentItem(numFragment);
}
  • línea 8: se recupera la posición de la pestaña en la que se hizo clic. Aquí se obtendrá un número 0 o 1;
  • líneas 12-15: si se hizo clic en la primera pestaña, nos preparamos para mostrar el fragmento [Vue1Fragment];
  • líneas 16-18: en los demás casos (se hizo clic en la pestaña n.º 2), nos preparamos para volver a mostrar el fragmento que se mostraba la última vez que se seleccionó la pestaña n.º 2. El número de este fragmento se había guardado en la sesión de la aplicación;
  • línea 21: se le solicita al contenedor de fragmentos que muestre el fragmento deseado;

Veamos ahora la gestión de las opciones del menú (siempre en [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // procesamiento de las opciones del menú
    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;
      }
    }
    // elemento procesado
    return true;
}
  • líneas 16-31: gestión de las 4 opciones del menú. Cada controlador llama al método [showFragment] con el número del fragmento que se debe mostrar;

El método [showFragment] es el siguiente:


  // la pestaña n.º 2
  private TabLayout.Tab tab2 = null;

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // si la segunda pestaña aún no existe, se crea
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // se establece el título de la segunda pestaña
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // se cambia el fragmento que se muestra
      mViewPager.setCurrentItem(i);
      // se guarda el n.º del fragmento mostrado en la sesión
      session.setNumFragment(i);
      // se selecciona la pestaña 2; no hace nada si ya está seleccionada
      tab2.select();
    }
}
  • Recordemos que, al iniciar la aplicación, solo hay una pestaña;
  • línea 2: una referencia a la pestaña n.º 2, null al inicio;
  • 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 agrega a la barra de pestañas (línea 9);
  • línea 12: se coloca en el título de la segunda pestaña el número del fragmento que se va a mostrar, con una numeración que comienza en 1;
  • línea 14: se muestra el fragmento deseado;
  • línea 16: se guarda su número en la sesión;
  • línea 18: se selecciona la pestaña n.º 2. Si ya estaba seleccionada, no ocurrirá nada: no se ejecutará el método [onTabSelected]. Si no estaba seleccionada, se activará el método [onTabSelected]. Este método le pide entonces al contenedor de fragmentos que muestre el fragmento que ya se mostraba en la línea 14. Una simple comprobación en el método [onTabSelected] evita este caso:

        // se muestra el fragmento solo si es necesario
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Se invita al lector a probar esta nueva versión.

1.12.6. Mejoras

Ahora contamos con una buena 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. Además, contamos con una arquitectura robusta que acaba de superar 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, vamos a especificar las relaciones entre la actividad y los fragmentos mediante la adición de una interfaz.

1.13. Ejemplo 12: codificar 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 definirá lo que los fragmentos pueden solicitar a la actividad;
  • una clase abstracta [AbstractFragment] que definirá el estado y los métodos que debe tener todo fragmento;

1.13.1. Creación del proyecto

Duplicamos el proyecto [Exemple-11] en [Exemple-12] siguiendo el procedimiento del párrafo 1.4. Obtenemos el siguiente resultado:

1.13.2. La interfaz [IMainActivity]

De los ejemplos anteriores se desprende que los fragmentos necesitan tener acceso a la sesión instanciada por la actividad. Además, aunque no se ve en estos ejemplos, pero es previsible: los controladores de eventos de los fragmentos a veces terminan con un cambio de vista. Se le pedirá a la actividad que realice este cambio. La interfaz [IMainActivity] podría ser entonces la siguiente:

  

package exemples.android;

public interface IMainActivity {

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

  // cambio de vista
  void navigateToView(int position);

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

En la línea 12, cabe destacar la presencia de una constante que antes se encontraba en la clase [MainActivity]. Se busca reducir el acoplamiento entre los fragmentos y la actividad, limitándolo a un acoplamiento entre [AbstractFragment] y [IMainActivity]. De esta manera, la actividad podrá tener un nombre distinto al de [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:


  // datos  accesibles a las clases hijas
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;

  // actividad
  protected IMainActivity mainActivity;
  protected Activity activity;

...
  // actualización de fragmento
  protected void update() {
    // se recuperan la actividad y la sesión
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // se le pide a la clase hija que se actualice
    updateFragment();
}
  • líneas 6 y 7: se mantienen dos tipos de referencia 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 de todas las actividades;

Por supuesto, estas dos referencias apuntan al mismo objeto. Sin embargo, este se ve con dos tipos diferentes. Esto nos evitará conversiones de tipo durante la ejecución;

  • línea 14: se obtiene una referencia a la actividad mediante el método [getActivity];
  • línea 15: si esta referencia no es nula, entonces podemos acceder a la sesión;
  • líneas 16-17: se guardan la actividad, que implementa la interfaz [IMainActivity], y la sesión;

1.13.4. Modificación del administrador de fragmentos

El administrador de fragmentos [SectionsPagerAdapter] en la clase [MainActivity] se modifica en un solo punto: en lugar de administrar fragmentos de tipo [Fragment], ahora administra fragmentos de tipo [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private AbstractFragment[] fragments;
    // N.º de fragmento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // padre
      super(fm);
      // inicialización de la tabla de fragmentos
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // un fragmento de +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

    // n.º de posición del fragmento
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }

    // devuelve el número de fragmentos gestionados
    @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{

...
  // inyección de sesión
  @Bean(Session.class)
  protected Session session;
...
  // getter de sesión
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // se muestra la vista de la posición
    if(mViewPager.getCurrentItem()!=position){
      // visualización del fragmento
      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 n.º [position];
  • línea 17: se verifica si hay algo que hacer;
  • línea 19: se muestra el fragmento n.º [position];

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

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

Actualmente, la clase [MainActivity] muestra un fragmento mediante la instrucción:


    // visualización de Vista1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Dado que el método [navigateToView] hace lo mismo, se reemplaza este tipo de instrucció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, siempre utilizaremos la arquitectura anterior:

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

1.14. Ejemplo-13: Ejemplo-05 con fragmentos

En el proyecto [Exemple-05] hemos introducido la navegación entre vistas. En ese caso, se trataba de una navegación entre actividades: 1 vista = 1 actividad. Aquí nos proponemos tener una sola actividad con varias vistas del tipo [AbstractFragment].

1.14.1. Creación del proyecto

Duplicamos el proyecto anterior [Exemple-12] en [Exemple-13] siguiendo el procedimiento del párrafo 1.4. Obtenemos el siguiente resultado:

1.14.2. Estructuración del proyecto

Comenzaremos a utilizar paquetes para organizar el código. Por el momento, podemos distinguir dos ámbitos distintos:

  • la gestión de la actividad;
  • la gestión de fragmentos;

Para ello, creamos dos paquetes: [exemples.android.activity] y [exemples.android.fragments]:

 

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

En [8], creamos un tercer paquete llamado [architecture], en el que colocaremos las entidades [IMainActivity, AbstractFragment, Session, MyPager], que son los elementos básicos de la arquitectura de nuestra aplicación. Esto es para recordarnos que hemos elegido una arquitectura específica. A continuación, mueva los elementos existentes del proyecto tal como se indica en [9]. Cada movimiento debe validarse haciendo clic en el botón [Refactor].

En esta etapa, compila la aplicación. Aparecen los siguientes errores en [MainActivity:

 

Al mover las clases a los paquetes, Android Studio realizó los cambios necesarios en el código de la aplicación (líneas 18-21, por ejemplo). Las clases a las que se refieren las líneas 15 y 17 no se han movido. Son generadas por la biblioteca Android Annotations. Para estas clases, hay que cambiar los imports manualmente. Por lo tanto, estas líneas quedan así:

 

Una vez hecho esto, ya no hay errores de compilación. Ejecuta la aplicación. Entonces aparece 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="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_"
      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 indican que la actividad designada es [exemples.android.MainActivity_]. Sin embargo, dado que la actividad se ha migrado al paquete [activity], la línea 12 ahora debe ser:


      android:name=".activity.MainActivity_"

Presta atención al punto (.) delante de [activity]. Una vez más, Android Studio no pudo actualizar el manifiesto porque este hace referencia a una clase de Android Annotations que no se ha movido. Por lo tanto, el uso de la biblioteca AA conlleva 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 manejará dos fragmentos: [Vue1Fragment], que ya tenemos, y [Vue2Fragment], que habrá que crear;
  • la sesión ya no es la misma;

1.14.3.1. Limpieza de fragmentos

Elimine 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

La sesión actual es la siguiente:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // número de fragmentos visitados
  private int numVisit;
  // N.º del fragmento de tipo [PlaceholderFragment] mostrado en la segunda pestaña
  private int numFragment;

  // getters y 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 conservamos nada de esta sesión.

Compila el proyecto. Las líneas con errores son aquellas que utilizaban el contenido de la sesión. Elimínalas. En la clase [Vue1Fragment] también se elimina la variable [numVisit] del código, que queda así:


package exemples.android.fragments;

import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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 {

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

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

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


  // actualización del fragmento
  @Override
  protected void updateFragment() {
  }
}

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

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

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

La eliminación del menú también se realiza en dos lugares:

  • en la vista [res / menu / menu-main.xml], que define las opciones del menú;
  • en el código de la 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>

  <exemples.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>
  • se eliminan las líneas [28-31, 41-47];
  • también se elimina la barra de herramientas de las líneas 18 a 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>
  • Se eliminarán las líneas 9 a 24. De esta manera, se deja una opción que no se utilizará. Simplemente para tener un ejemplo de declaración de una opción de menú que se pueda reproducir mediante copiar y pegar;

En la clase [MainActivity] se elimina todo lo que haga referencia a las pestañas, al botón flotante, a la barra de herramientas y al menú. Para encontrar estas referencias, lo más sencillo es eliminar su declaración:


  // el administrador de pestañas
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // el botón flotante
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

y recompilar la aplicación. Las líneas erróneas son aquellas que hacen referencia a los elementos que ya no existen. Elimine todas esas líneas. Además, modifique el administrador de fragmentos para que ya no haga referencia al fragmento [PlaceholderFragment] que hemos eliminado:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private AbstractFragment[] fragments;

    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // padre
      super(fm);
    }

    // fragmento n.º posición
    @Override
    public AbstractFragment getItem(int position) {
      // registro
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // devuelve el número de fragmentos gestionados
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • líneas 7-10: se ha eliminado toda la generación de fragmentos;

En este punto, ya no debería haber ningún error 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 exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;

@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {

  // el contenedor de fragmentos
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// la barra de herramientas
@ViewById(R.id.toolbar)
protected Toolbar toolbar;

  // sesión de inyección
  @Bean(Session.class)
  protected Session session;

  // número de fragmentos
  private final int FRAGMENTS_COUNT = 5;
  // adyacencia de los fragmentos
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

  // modo de depuración
  public static final boolean IS_DEBUG_ENABLED = true;

  // el administrador de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // constructor
  public MainActivity() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }

  @AfterViews
  protected void afterViews() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }

    // barra de herramientas: aquí es donde se muestra el nombre de la aplicación
    setSupportActionBar(toolbar);

    // el administrador de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al administrador de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el administrador de fragmentos
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // desplazamiento de los fragmentos
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // se desactiva el deslizamiento entre fragmentos
    mViewPager.setSwipeEnabled(false);

    // sin desplazamiento
    mViewPager.setScrollingEnabled(false);

    // visualización Vista1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }

  // obtener sesión
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // se muestra la vista «posición»
    if (mViewPager.getCurrentItem() != position) {
      // visualización del fragmento
      mViewPager.setCurrentItem(position);
    }
  }

  // el administrador de fragmentos
  // es a él a quien se le solicitan los fragmentos que se mostrarán en la vista principal
  // debe definir los métodos [getItem] y [getCount]; los demás son opcionales
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private AbstractFragment[] fragments;

    // constructor
    public SectionsPagerAdapter(FragmentManager fm) {
      // padre
      super(fm);
    }

    // n.º de posición del fragmento
    @Override
    public AbstractFragment getItem(int position) {
      // registro
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // devuelve el número de fragmentos gestionados
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Quedan algunas modificaciones por hacer:

  • elimina la línea 31, que ya no es necesaria;
  • línea 33: pon 1 como adyacencia de fragmentos;
  • línea 76: navega a la vista 0. Esta será la que se mostrará primero;
  • línea 108: inicialice la matriz con el fragmento [Vue1Fragment_]:

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

Así que solo tenemos un fragmento. Ejecuta la aplicación. Deberías obtener el siguiente resultado:

Image

El botón [Valider] debe funcionar.

1.14.4. Creación de los fragmentos y las vistas asociadas

La aplicación tendrá dos vistas, las del proyecto [Exemple-05]. Ya contamos con la vista [vue1.xml] en el proyecto actual. Ahora duplicaremos [vue2.xml] de [Exemple-05] a [Exemple-12] (abre ambos proyectos y realiza un copiar y pegar) entre ellos.

 
  • en [1], la nueva vista. Al intentar editarla, aparecen errores en [2]. Debemos modificar el archivo [strings.xml] [3] para agregar las cadenas a las que hace referencia esta nueva vista:

<resources>
  <string name="app_name">Exemple-13</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- vista 2 -->
  <string name="btn_vue2">Vue n° 2</string>
  <string name="titre_vue2">Vue n° 2</string>
  <string name="btn_vue1">Vue n° 1</string>
</resources>

Duplicamos la clase [Vue1Fragment] en [Vue2Fragment]:

  

y modificamos el código copiado de la siguiente manera:


package exemples.android.fragments;

import android.util.Log;
import exemples.android.R;
import exemples.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() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // actualización del fragmento
  @Override
  protected void updateFragment() {
  }
}
  • línea 9: el fragmento está asociado a la vista [res / layout / vue2.xml];
  • línea 10: la clase extiende la clase abstracta [AbstractFragment];
  • líneas 12-20: el método obligatorio [@AfterViews];
  • líneas 23-25: el método [updateFragment] es obligatorio;

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

La actividad gestionará a partir de ahora dos fragmentos. Su clase [SectionsPagerAdapter] se modifica de la siguiente manera:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};

    ...
}

La interfaz [IMainActivity] permite la navegación entre vistas mediante su método [navigateToView]. Vamos a gestionar el clic en el botón [Vue n° 2] del fragmento [Vue1Fragment]:


package exemples.android.fragments;

import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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 {

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

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

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

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }

  // Actualización del fragmento
  @Override
  protected void updateFragment() {
  }
}
  • líneas 37-40: el método [showVue2] maneja el evento «clic» en el botón [Vue n° 2];
  • línea 39: se navega con el método [navigateToView] de la actividad. Cabe recordar aquí que la actividad se ha almacenado en la clase padre de la forma:

  // actividad
protected IMainActivity mainActivity;

y que esta actividad ya se ha inicializado al llegar a cualquier manejador de eventos.

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

protected Activity activity;

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


package exemples.android.fragments;

import android.util.Log;
import exemples.android.R;
import exemples.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() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // gestores de eventos ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // actualización de fragmento
  @Override
  protected void updateFragment() {
  }
}
  • líneas 24-27: el método [showVue1] maneja el evento «clic» en el botón [Vue n° 1];

Ejecute el proyecto y verifique que la navegación entre vistas funcione.

1.14.6. Definición de la sesión

El funcionamiento de la aplicación es el siguiente:

  • ingreso de un nombre en la vista n.º 1;
  • se muestra ese nombre en la vista n.º 2;

Para que la vista n.º 1 pueda transmitir el nombre ingresado a la vista n.º 2, utilizaremos la siguiente sesión;


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // nombre
  private String nom;

  // getters y setters
...
}
  • línea 8: el nombre ingresado;

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


  // inyección de sesión
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // inicialización de sesión
    session.setNom("");
}

1.14.7. Escritura final de los fragmentos

En el fragmento [Vue1Fragment] modificamos el código del controlador del clic en el botón [Valider]:


package exemples.android.fragments;

import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.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 {

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

...
  // manejadores de eventos ----------------------------------

  @Click(R.id.buttonValider)
  protected void doValider() {
    // se guarda el nombre ingresado
    String nom = editTextNom.getText().toString();
    // se muestra
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // se guarda el nombre ingresado en la sesión
    session.setNom(editTextNom.getText().toString());
    // se navega a la vista n.º 2
    mainActivity.navigateToView(1);
  }

  // actualización del fragmento
  @Override
  protected void updateFragment() {

  }
}
  • líneas: 31-37: gestionamos el clic en el botón [Vue n° 2];
  • línea 34: antes de navegar a la vista n.º 2, guardamos el nombre ingresado en la sesión para que la nueva vista tenga acceso a él;

La vista [Vue2Fragment] se modifica de la siguiente manera:


package exemples.android.fragments;

import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.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 {

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

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

  // gestores de eventos ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // Actualización de fragmento
  @Override
  protected void updateFragment() {
    // se recupera el nombre ingresado en la sesión
    String nom = session.getNom();
    // se muestra
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Cuando se muestra la vista n.º 2, hay que mostrar el nombre ingresado en la vista n.º 1. Sabemos que, inmediatamente después de su visualización, se ejecutará su método [updateFragment]. Por lo tanto, es en este método (líneas 36-42) donde se puede colocar el código para mostrar el nombre.

  • líneas 16-17: declaración del único componente visual de la vista;
  • línea 39: se recupera de la sesión el nombre ingresado en la vista n.º 1;
  • línea 41: se modifica la etiqueta [textViewBonjour];

Ejecute el proyecto y verifique que funcione.

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() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Este método está incompleto. De hecho, siempre hay que prever el caso en que el fragmento se recicle después de una operación [onDestroyView]. En este caso, se regenera la vista del fragmento 1 y el nombre que se haya podido ingresar anteriormente desaparecerá de la vista. No queremos que eso suceda. Actualmente, el nombre ingresado permanece visible porque la adyacencia de los fragmentos de 1 hace que el ciclo de vida del fragmento [Vue1Fragment] solo se ejecute una vez. Sin embargo, es preferible prever el caso de la reutilización del fragmento.

Hay varias formas de resolver este problema:

  • se puede aprovechar el hecho de que el método [update] se ejecuta sistemáticamente cada vez que se muestra el fragmento para actualizar el nombre ingresado;
  • se puede realizar esta actualización únicamente cuando se vuelve a ejecutar el método [@AfterViews]. Es esta última opción la que elegimos;

Modificamos el código de [Vue1Fragment] de la siguiente manera:


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

    // datos
    private String nom;

    @AfterViews
    protected void afterViews() {
        // memoria
        afterViewsDone = true;
        // registro
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // se (re)inicializa el texto mostrado
        editTextNom.setText(nom);
    }

    // controladores de eventos ----------------------------------

...

    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // se registra el nombre ingresado para poder recuperarlo si se recicla el fragmento
        nom = editTextNom.getText().toString();
        // se guarda el nombre ingresado en la sesión
        session.setNom(nom);
        // se navega a la vista n.º 2
        activity.navigateToView(1);
}
  • línea 27: justo antes de salir de la vista 1 hacia la vista 2, se guarda el nombre ingresado;
  • línea 17: cada vez que se ejecuta de nuevo el ciclo de vida del fragmento, se vuelve a mostrar el último nombre ingresado;

Para el fragmento [Vue2Fragment], el código existente es suficiente:


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

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

  // actualización del fragmento
  @Override
  protected void updateFragment() {
    // se recupera el nombre ingresado en la sesión
    String nom = session.getNom();
    // se muestra
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • 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 agregar;

1.14.9. Conclusión

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

  • una actividad que implementa la interfaz [IMainActivity];
  • fragmentos que extienden la clase [AbstractFragment], lo que les obliga a implementar el método [updateFragment]. Estos también deben contar con un método [@AfterViews] en el que establecen el valor booleano [afterViewsDone] en true;
  • una sesión que encapsule los datos que se compartirán entre los fragmentos y la actividad;

1.15. Ejemplo 14: una arquitectura de dos capas

Vamos a construir una aplicación de una sola vista con la siguiente arquitectura:

1.15.1. Creación del proyecto

Duplicamos el proyecto anterior [Exemple-12] en [Exemple-13] siguiendo el procedimiento del párrafo 1.4. Obtenemos el siguiente resultado:

1.15.2. La vista [vue1]

La aplicación solo tendrá una vista [vue1.xml]. Por lo tanto, eliminaremos la otra vista [vue2.xml], así como su fragmento asociado:

 

Compila la aplicación. Aparecen errores en [MainActivity]:

 

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


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

La línea 4 anterior queda así:


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

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

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

 

Sus componentes son los siguientes:

Id
Type
Rôle
1
edtNbAleas
EditText
nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]
2
edtA
EditText
valeur de a
2
edtB
EditText
valeur de b
4
btnExécuter
Button
lance la génération des nombres
5
ListView
lstReponses
liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

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_Titre2"
    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_errorIntervalle"
    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_errorIntervalle"
    android:textColor="@color/red" />


  <Button
    android:id="@+id/btn_Executer"
    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_executer" />


  <TextView
    android:id="@+id/txt_Reponses"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/btn_Executer"
    android:layout_marginTop="30dp"
    android:text="@string/list_reponses"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:textColor="@color/blue" />

  <ListView
    android:id="@+id/lst_reponses"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/txt_Reponses"
    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 el archivo [res / values / strings.xml]:


<resources>
  <string name="app_name">Exemple-14</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="list_reponses">Liste des réponses</string>
  <string name="btn_executer">Exécuter</string>
  <string name="aleas">Génération de N nombres aléatoires</string>
  <string name="txt_nbaleas">Valeur de N :</string>
  <string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
  <string name="txt_b">"b : "</string>
  <string name="txt_dummy">Dummy</string>
  <string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
  <string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>

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


<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <!-- colores de la aplicación -->
  <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 en este caso solo hay un fragmento, no es necesario prever 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, compila la aplicación. Aparecerán errores en las líneas que utilizaban elementos de la sesión, que ahora está vacía. Elimina esas líneas y verifica que la compilación ya no genere errores.

1.15.4. El fragmento [Vue1Fragment]

  

Modificamos el fragmento [Vue1Fragment] existente 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 exemples.android.R;
import exemples.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 {

  // los elementos de la interfaz visual
  @ViewById(R.id.lst_reponses)
  protected ListView listReponses;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  protected TextView txtErrorIntervalle;

  // lista de respuestas a un comando
  private List<String> reponses = new ArrayList<>();
  // adaptador de la vista de lista
  private ArrayAdapter<String> adapterReponses;

  // los datos ingresados
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // se ocultan los  mensajes de error
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
    // se ocultan los posibles mensajes de error anteriores
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // se verifica la validez de los datos ingresados
    if (!isPageValid()) {
      return;
    }
  }

  // se verifica la validez de los datos ingresados
  private boolean isPageValid() {
...
  }

  @Override
  protected void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Aquí solo hay un fragmento cuyo ciclo de vida se ejecutará una sola vez, al iniciar la aplicación. Por esta razón, los métodos [@AfterViews] (líneas 46-57) y [udateFragment] (líneas 75-81) solo se ejecutarán una vez al iniciar la aplicación;
  • líneas 55-56: se ocultan los dos mensajes de error de la vista (que se muestran a continuación) [1-2];
 
  • líneas 59-60: el método que se ejecuta al hacer clic en el botón [Exécuter];
  • líneas 71-73: se verifica la validez de los datos ingresados;

El método [isPageValid] es el siguiente:


  // los datos ingresados
  private int nbAleas;
  private int a;
  private int b;

...

// se verifica la validez de los datos ingresados
  private boolean isPageValid() {
    // ingreso del número de nombres aleatorios
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // ¿Error?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // ingreso de a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // ¿Error?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // ingreso de b
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // ¿Error?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // retorno
    return (nbErreurs == 0);
  }

  • líneas 2-4: estos tres campos son inicializados por el método [isPageValid]. Además, este método devuelve true si todas las entradas son válidas, y false en caso contrario. Si hay entradas inválidas, se muestran los mensajes de error correspondientes;

En esta etapa, la aplicación ya está lista para ejecutarse. Verifique el funcionamiento del método [isPageValid] ingresando datos incorrectos.

1.15.5. La capa [métier]

  

La capa [métier] presenta la siguiente interfaz [IMetier]:


package exemples.android.metier;

import java.util.List;

public interface IMetier {

    List<Object> getAleas(int a, int b, int n);
}

El método [getAleas(a,b,n)] normalmente devuelve n números enteros aleatorios en el intervalo [a,b]. También se ha previsto que, una de cada tres veces, devuelva una excepción, la cual también se incluye en las respuestas que genera el método. Al final, este devuelve una lista de objetos de tipo [Exception] o [Integer].

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 Metier implements IMetier {

    public List<Object> getAleas(int a, int b, int n) {
        // la lista de objetos
        List<Object> réponses = new ArrayList<Object>();
        // algunas verificaciones
        if (n < 1) {
            réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
        }
        if (a < 0) {
            réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
        }
        if (b < 0) {
            réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
        }
        if (a >= b) {
            réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
        }
        // ¿error?
        if (réponses.size() != 0) {
            return réponses;
        }
        // se generan los números aleatorios
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // se genera una excepción aleatoria 1 de cada 3 veces
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // de lo contrario, se devuelve un número aleatorio entre dos límites [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // resultado
        return réponses;
    }
}
  • línea 9: se utiliza la anotación AA [@EBean] en la clase [Metier] para poder inyectar referencias de esta última en la capa [Présentation]. El atributo (scope = EBean.Scope.Singleton) garantiza que la clase [Metier] solo se instancie una vez. Por lo tanto, siempre se inyecta la misma referencia si se inyecta varias veces en la capa [Présentation];
  • 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], lo que la convierte en una excepción no controlada: no es necesario manejarla en un try/catch ni incluirla en la firma de los métodos;

1.15.6. La actividad [MainActivity] revisada

  

Capa

[metier]

Actividad

Vista

Usuario

La actividad implementará la interfaz [IMetier] de la capa [métier]. De este modo, el fragmento o la vista solo tendrá a la actividad como interlocutor.

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

  • agregar la interfaz [IMetier] a las interfaces implementadas por la actividad;
  • hacer que la interfaz [IMainActivity] extienda a su vez la interfaz [IMetier]. Esta es la opción que elegimos;

La interfaz [IMainActivity] queda así:

  

package exemples.android.architecture;

import exemples.android.metier.IMetier;

public interface IMainActivity extends IMetier {

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

  // cambio de vista
  void navigateToView(int position);

  // modo de depuración
  public static final boolean IS_DEBUG_ENABLED = true;

}
  • línea 5: la interfaz [IMainActivity] extiende la interfaz [IMetier]

La clase [MainActivity] cambia de la siguiente manera:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {

  ...

  // inyección de sesión
  @Bean(Session.class)
  protected Session session;

  // inyección de lógica de negocio
  @Bean(Metier.class)
  protected IMetier metier;

...
  // implementación IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • líneas 11-12: la capa [métier] se inyecta en la actividad. Para ello se utiliza la anotación AA [@Bean], cuyo parámetro es la clase que lleva la anotación AA [@EBean];
  • línea 2: la actividad implementa la interfaz [IMainActivity] y, por lo tanto, la interfaz [IMetier] de la capa [métier];
  • líneas 16-19: implementación del único método de la interfaz [IMetier]. Simplemente se delega la llamada a la capa [métier];

1.15.7. El fragmento [Vue1Fragment] revisado

  

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 exemples.android.R;
import exemples.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 {

  // elementos de la interfaz visual
  @ViewById(R.id.lst_reponses)
  protected ListView listReponses;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  protected TextView txtErrorIntervalle;

  // lista de respuestas a un comando
  private List<String> reponses = new ArrayList<>();
  // adaptador de la vista de lista
  private ArrayAdapter<String> adapterReponses;

  // los datos ingresados
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
   ...
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }

  // se verifica la validez de los datos ingresados
  private boolean isPageValid() {
   ...
  }

  @Override
  protected void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // solo se ejecutará una vez al iniciar la aplicación
    // se crea el adaptador de ListView; para ello, es necesario que la variable [activity] haya sido inicializada
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • líneas 69-70: se establece el adaptador del componente de tipo [ListView];

El componente [ListView] sirve para mostrar una lista de elementos. Lo hace mediante un adaptador de tipo [ListAdapter], el cual a su vez está conectado a la fuente de datos que debe alimentar al [ListView]. Para definir el adaptador de un [ListView], se dispone del 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 anterior es el siguiente:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] es la actividad que muestra el [ListView];
  • [resource] es el número entero que identifica la vista utilizada para mostrar un elemento de [ListView]. Esta vista puede tener cualquier nivel de complejidad. Es el desarrollador quien la construye según sus necesidades;
  • [textViewResourceId] es el número entero que identifica un componente [TextView] en la vista [resource]. La cadena se mostrará mediante este componente;
  • [objects]: 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] en la vista identificada por [resource].

La tarea del desarrollador es crear la vista [resource] que mostrará cada elemento del [ListView]. Para el caso sencillo en el que solo se desea mostrar una simple cadena de caracteres, como en este ejemplo, Android proporciona la vista identificada como [android.R.layout.simple_list_item_1]. Esta contiene un componente [TextView] identificado como [android.R.id.text1]. Este es el método utilizado en la línea 69 para crear el adaptador de [ListView]. Este adaptador solo necesita definirse una vez. Para permitir su reutilización, se ha definido como variable de instancia de la clase (línea 39). Veamos nuevamente la línea 69:


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

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

El método [doExecuter] procesa el clic en el botón [Exécuter]. Su código es el siguiente:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // se ocultan los posibles mensajes de error anteriores
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // se borran las respuestas anteriores
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // se verifica la validez de los datos ingresados
    if (!isPageValid()) {
      return;
    }
    // se solicitan los números aleatorios a la actividad
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // se crea una lista de cadenas a partir de estos datos
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // actualizar la vista de lista
    adapterReponses.notifyDataSetChanged();
  }
  • líneas 7-8: se desea vaciar el ListView. Para ello, se vacía la fuente de datos [reponses] y se solicita al adaptador asociado al ListView que se actualice;
  • líneas 10-12: antes de ejecutar la acción solicitada, se verifica que los valores ingresados sean correctos;
  • línea 14: se solicita a la actividad la lista de números aleatorios. Se obtiene una lista de objetos en la que 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] que muestra el ListView;
  • línea 24: se solicita al adaptador de ListView que se actualice;

1.15.8. Ejecución

Ejecute el proyecto y verifique que funcione correctamente.

1.16. Ejemplo 15: arquitectura cliente/servidor

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

Se ha agregado a la aplicación de Android una capa [DAO] para comunicarse con el servidor remoto. Esta se comunicará con el servidor que genera los números aleatorios que se muestran en la tableta de Android. Este servidor tendrá la siguiente arquitectura de dos capas:

Los clientes consultan ciertos URL de la capa [web / jSON] y reciben una respuesta de texto en formato jSON (notación de objetos JavaScript). Aquí, nuestro servicio web procesará un único URL de tipo [/a/b] que devolverá un número aleatorio dentro del intervalo [a,b]. Describiremos la aplicación en el siguiente orden:

El servidor

  • su capa [métier];
  • su servicio [web / jSON] implementado con Spring MVC;

El cliente

  • su capa [DAO]. No habrá una capa [métier];

1.16.1. El servidor [web / jSON]

Queremos construir la siguiente arquitectura:

1.16.1.1. Creación del proyecto

Vamos a desarrollar el servicio web con el ecosistema Spring [http://spring.io/]. Accedemos al sitio [http://start.spring.io/] (junio de 2016), que nos permitirá generar un proyecto Gradle con las dependencias necesarias para nuestro proyecto, el cual no es un proyecto de Android y para cuya construcción Android Studio no ofrece ayuda alguna:

  • en [1]: elige un proyecto Gradle;
  • en [2-3]: las características de la dependencia jar generada por el proyecto (ver más abajo);
  • en [4]: elija la dependencia web [5] para que estén disponibles los binarios necesarios para nuestro servicio web;
  • en [6]: genere el proyecto. Se generará entonces el archivo zip de un proyecto Gradle de plantilla y se ofrecerá para su descarga;

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

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Desde la versión 0.11 del complemento Gradle para Android, debes usar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  ...
}
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: una dependencia tiene el formato [groupId:artifactId:version]. Lo que se solicita en el formulario del sitio [http://start.spring.io/]:
    • en [2] es [groupId];
    • en [3] es [artifactId];

Descomprime en la carpeta de otros proyectos el archivo zip mencionado:

Con Android Studio, abre el proyecto Gradle [server-01] [1-2]. El proyecto que se abre es [3] (perspectiva Project).

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 corresponden a IDE en Eclipse. Las eliminamos;
  • las líneas 1 a 11 y 15 sirven para agregar un complemento llamado [spring-boot] a nuestro proyecto de Gradle. Spring Boot es un proyecto del ecosistema Spring [http://projects.spring.io/spring-boot/]. Este complemento define las versiones de las dependencias que se usan con mayor frecuencia con Spring. Esto permite no tener que especificar 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 que se utilizará, en este caso la versión 1.8;
  • líneas 25-27: los repositorios de binarios que se utilizarán para descargar las dependencias;
  • línea 26: indica el repositorio central de Maven. Actualmente es el mayor repositorio de binarios 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 compilar un servicio web de Spring;
  • línea 31: esta dependencia incluye todos los binarios necesarios para las pruebas, en particular para las pruebas JUnit;
  • una dependencia [compile] indica que se necesita la dependencia para compilar el proyecto. Una dependencia [testCompile] indica que se necesita la dependencia únicamente para ejecutar las pruebas. Por lo tanto, no se incluye en el binario del proyecto;

Realizamos una primera limpieza del 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'

// binario del proyecto
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}

// versiones de Java
sourceCompatibility = 1.8
targetCompatibility = 1.8

// repositorios de Maven
repositories {
  mavenLocal()
  mavenCentral()
}

// dependencias
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • línea 30: hemos agregado el repositorio local de Maven de la estación de desarrollo. Este se crea al instalar Maven (véase el párrafo 6.10). Si la dependencia solicitada ya se encuentra en el repositorio local de Maven, no se solicitará al repositorio central de Maven;
  • líneas 19-22: una tarea de Gradle que permite generar el binario del proyecto. La utilizaremos para ver qué se hace;
  • en [1-4], ejecuta la tarea [jar] definida en el archivo [build.gradle] ([1] se encuentra en la esquina superior derecha y al lado de IDE);

La operación anterior crea el archivo JAR del proyecto y lo coloca en la carpeta [build / libs] [5]:

  

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

Todas las dependencias del proyecto se pueden ver de la siguiente manera:

 

En [1] se puede observar que la única dependencia del proyecto [compile('org.springframework.boot:spring-boot-starter-web')] trajo consigo decenas de binarios. Spring Boot para la web ha incluido las dependencias que probablemente necesitará una aplicación web de Spring MVC. Esto significa que algunas pueden ser innecesarias. Spring Boot es ideal para un tutorial:

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

En el sitio web del ecosistema de Spring [http://spring.io/guides] se pueden encontrar numerosos ejemplos que utilizan Spring Boot.

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


// Spring Boot
...
// dependencias
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

// plugin para crear un binario conforme a las normas de Maven en el repositorio local de Maven
apply plugin: 'maven-publish'
publishing {
  publications {
    maven(MavenPublication) {
      groupId 'istia.st.exemples.android'
      artifactId 'server-01'
      version '0.0.1-SNAPSHOT'
      from components.java
    }
  }
  repositories {
    maven {
      // cámbialo para que apunte a tu repositorio, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • línea 10: importamos un complemento de Gradle llamado [maven-publish] que permite publicar el binario del proyecto en un repositorio de Maven siguiendo las normas de Maven;
  • línea 11: una tarea de Gradle llamada [publishing];
  • líneas 14-15: las características del binario de Maven que se va a crear;
  • línea 23: el repositorio de Maven en el que se publicará, en este caso un repositorio de Maven local;

Al agregar el complemento [maven-publish] se crearon nuevas tareas en el proyecto Gradle:

Si en [2] se ejecuta la tarea [publish], se crea el binario del proyecto y se instala en la carpeta indicada en la línea 23 del archivo [build.gradle]:

 

La tarea [jar] permite generar el binario del proyecto. Este binario no incluye sus dependencias, por lo que no es ejecutable. Es posible generar un binario que incluya todas sus dependencias y sea ejecutable. Para ello, agregamos el siguiente código al archivo [build.gradle]:


// crear un binario con todas sus dependencias
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 { it.isDirectory() ? it : zipTree(it) } }
  with jar
}
  • línea 6: hay que poner 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);
    }
}

Actualice el proyecto Gradle y luego ejecute la tarea [fatJar]:

 

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

1.16.1.3. Configuración del proyecto

La configuración de Gradle no es suficiente. También debemos configurar el proyecto. Como no se trata de un proyecto de Android generado por IDE, esta configuración, que hasta ahora no habíamos realizado, debe realizarse aquí.

 
  • en [3-4]: utilice un JDK 1.8;

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

A continuación, se invita al lector a crear el siguiente proyecto. A continuación, comentamos el código final del proyecto [3].

1.16.1.4. La capa [métier]

  

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


package exemples.android.server.metier;

public interface IMetier {
  // número aleatorio en [a,b]
    int getAlea(int a, int b);
}
  • línea 5: el método que genera un 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 Metier implements IMetier {

  @Override
  public int getAlea(int a, int b) {
    // algunas verificaciones
    if (a < 0) {
      throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
    }
    if (b < 0) {
      throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
    }
    if (a >= b) {
      throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
    }
    // generación del resultado
    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 al azar. Cabe destacar, en la línea 8, la anotación de Spring [@Service], que hará que Spring instancie la clase en un único ejemplar (singleton) y ponga su referencia a disposición de otros componentes de Spring. Aquí se podrían haber utilizado otras anotaciones de Spring para lograr el mismo efecto. Los componentes de Spring tienen nombres por defecto que pueden especificarse como atributo de la anotación utilizada. Sin este atributo, como en este caso, el componente de Spring lleva el nombre de la clase con su primer carácter en minúscula. Así, en este caso, el componente de Spring lleva, por defecto, el nombre [metier];

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


package exemples.android.server.metier;

public class AleaException extends RuntimeException {

  // código de error
  private int code;

  // constructores
  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 y setters
....
}
  • línea 3: [AleaException] extiende la clase [RuntimeException]. Por lo tanto, se trata de una excepción no controlada (no es obligatorio manejarla con un try / catch);
  • línea 6: se agrega 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 modelo de arquitectura denominado MVC (Modelo – Vista – Controlador) de la siguiente manera:

El procesamiento de una solicitud de un cliente se lleva a cabo de la siguiente manera:

  1. solicitud: las URL solicitadas tienen el formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] es la clase de Spring que procesa las URL entrantes. Esta «enruta» el URL hacia la acción que debe procesarlo. Estas acciones son métodos de clases específicas llamadas [Contrôleurs]. La C de MVC es aquí la cadena [Dispatcher Servlet, Contrôleur, Action]. Si no se ha configurado ninguna acción para procesar el URL entrante, el servlet [Dispatcher Servlet] responderá que no se encontró el URL solicitado (error 404 NOT FOUND);
  1. procesamiento
  • la acción seleccionada puede utilizar los parámetros parami que le ha transmitido el servlet [Dispatcher Servlet]. Estos pueden provenir de varias fuentes:
    • la ruta [/param1/param2/...] del URL,
    • de los parámetros [p1=v1&p2=v2] del URL,
    • de los parámetros enviados por el navegador junto con su solicitud;
  • Al procesar la solicitud del usuario, la acción puede requerir la capa [metier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
    • una página de error si la solicitud no se pudo procesar correctamente
    • una página de confirmación en caso contrario
  • la acción solicita que se muestre una vista específica: [3]. Esta vista mostrará datos que se denominan el modelo de la vista. Este es el M de MVC. La acción creará este modelo M [2c] y solicitará que se muestre una vista V [3];
  1. respuesta: la vista V seleccionada utiliza la plantilla M generada por la acción para inicializar las partes dinámicas de la respuesta HTML que debe enviar al cliente y, a continuación, envía dicha respuesta.

Para un servicio web / jSON, la arquitectura anterior se modifica ligeramente:

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

En los anexos del párrafo 6.14 se presenta un ejemplo de serialización de un objeto Java en la cadena jSON y de deserialización de una cadena jSON en un objeto Java.

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

En nuestra aplicación, solo hay un controlador:

  

El servicio web /jSON enviará a sus clientes una respuesta del tipo [Response] como se muestra a continuación:


package exemples.android.server.web;

import java.util.List;

public class Response<T> {

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

    // constructores
    public Response() {

    }

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

    // getters y setters
...
}
  • línea 13: el campo [T body] es la respuesta que espera el cliente. Hemos decidido utilizar aquí una respuesta genérica de tipo T, en lugar del tipo Integer del número aleatorio esperado. Queremos poder reutilizar esta clase en otras situaciones. Al procesar la solicitud del cliente, el servidor puede encontrar un problema que luego se resume en los otros dos campos;
    • línea 8: un código de estado (0 si no hay error);
    • línea 9: si el estado es !=0, una lista de mensajes de error, por lo general los de la pila de excepciones si se produjo una excepción; null si no hubo 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 exemples.android.server.metier.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 {

  // capa de negocio
  @Autowired
  private IMetier metier;
  // mapper JSON
  @Autowired
  private ObjectMapper mapper;

  // números aleatorios
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
  public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {

    // la respuesta
    Response<Integer> response = new Response<>();
    // se utiliza la capa de negocio
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // se devuelve la respuesta
    return mapper.writeValueAsString(response);
  }

  private List<String> getMessagesFromException(Throwable e) {
    // lista de mensajes
    List<String> messages = new ArrayList<String>();
    // se recorre la pila de excepciones
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // se devuelve el resultado
    return messages;
  }

}
  • línea 17: la anotación [@Controller] indica que la clase es un controlador MVC cuyos métodos procesan solicitudes para ciertas URL de la aplicación web;
  • líneas 21-22: la anotación [@Autowired] le pide a Spring que inyecte en el campo un componente de tipo [IMetier]. Esta será la clase [Metier] anterior. Es porque le hemos aplicado la anotación [@Service] que se maneja como un componente de Spring;
  • líneas 24-25: hacemos lo mismo con un mapeador jSON que definiremos más adelante. Nuestro servicio web enviará su respuesta en forma de una cadena jSON. Este mapeador será el que realice la serialización de la respuesta en jSON;
  • línea 30: el método que genera el número aleatorio. Su nombre no tiene importancia. Cuando se ejecuta, sus parámetros ya han sido inicializados por Spring MVC. Veremos cómo. Por otra parte, si se ejecuta, es porque el servidor web recibió una solicitud HTTP GET para el URL de la línea 28;
  • línea 28: la anotación [@RequestMapping] define ciertas propiedades del método anotado:
    • [value]: el URL aceptado por el método;
    • [method]: el método HTTP aceptado por el método. Hay principalmente dos: GET y POST. El método [POST] se utiliza cuando el cliente desea adjuntar un documento a su solicitud HTTP;
    • [produces]: establece uno de los encabezados de la respuesta HTTP que se enviará al cliente. En este caso, entre los encabezados HTTP enviados junto con la respuesta al cliente, habrá uno que le indicará que la respuesta se le envía en forma de una cadena jSON. Este encabezado no es obligatorio. Se proporciona a título informativo al cliente si este espera respuestas que pueden tener diversas formas;
    • [consumes]: no aparece aquí. Sirve para indicar los encabezados 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 generado por el método debe enviarse al cliente. Sin esta anotación, la respuesta del método se considera una clave que permite seleccionar la página HTML para enviarla al cliente. En un servicio web /jSON, no hay páginas HTML;
  • línea 28: la URL procesada tiene 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 realiza mediante la anotación @PathVariable("x"). Cabe señalar que {a} y {b} son componentes de un URL y, por lo tanto, son de tipo String. La conversión de String al tipo de los parámetros puede fallar. En ese caso, Spring MVC lanza una excepción. Resumiendo: si desde un navegador solicito el URL /100/200, el método getAlea de la línea 30 se ejecutará con los parámetros enteros a=100, b=200;
  • línea 36: se le pide a la capa [métier] un número aleatorio dentro del intervalo [a,b]. Recordemos que el método [metier].getAlea puede lanzar una excepción;
  • línea 37: no hay error;
  • línea 39: código de error;
  • línea 40: la lista de mensajes de la respuesta es la de la pila de excepciones (líneas 46-57). Aquí sabemos que la pila solo contiene una excepción, pero queríamos mostrar un método más genérico;
  • línea 43: la respuesta de tipo [Response<Integer>] se devuelve en forma de cadena jSON;

1.16.1.6. Configuración del proyecto Spring

  

Existen varias formas de configurar Spring:

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

Decidimos configurar nuestra aplicación web con 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 = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
  // configuración web ------------------------------------
  @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);
  }

  // mapeador jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }

}
  • línea 12: le indicamos a Spring en qué paquetes encontrará los dos componentes que debe gestionar:
    • el componente [Metier], anotado como [@Service], en el paquete [exemples.android.server.metier];
    • el componente [WebController], anotado como [@Controller], en el paquete [exemples.android.server.web];
  • línea 13: la anotación [@EnableWebMvc] permite que Spring Boot realice por sí mismo una serie de configuraciones estándar para una aplicación Spring MVC. Esto alivia la carga de trabajo del desarrollador;
  • líneas 16, 22, 27 y 33: la anotación [@Bean] también define componentes (beans) de Spring, al igual que las dos anotaciones anteriores (@Service, @Controller). Aquí, la anotación [@Bean] anota un método y no una clase, y es el resultado del método el que constituye el componente de Spring. Al no haber un atributo de nomenclatura dentro de la anotación [@Bean], el componente de Spring creado lleva el nombre del método anotado;
  • líneas 16-20: definen el bean [dispatcherServlet]. Se trata de un nombre predefinido de Spring, MVC, que define el controlador principal de la aplicación MVC, un objeto por el que pasan todas las solicitudes de los clientes y que las distribuye (de ahí su nombre) a los distintos [@Controller] 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] sirve para definir qué URL son aceptados por la aplicación. En la línea 24, se aceptan todos los URL;
  • líneas 27-30: el bean [embeddedServletContainerFactory] sirve para definir el servidor integrado en las dependencias del proyecto que debe alojar la aplicación web. La línea 29 indica que se trata de un servidor Tomcat y que este funcionará en el puerto 8080. Por defecto, los binarios de este servidor web se incorporan mediante la dependencia [org.springframework.boot:spring-boot-starter-web] del archivo Gradle;

1.16.1.7. Ejecución del servicio web / jSON

  

El proyecto se ejecuta a partir de 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) {
    // ejecución de la aplicación
    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.run] es un método de [spring Boot] (línea 4) que iniciará la aplicación. Su primer parámetro es la clase Java que configura el proyecto. En este caso, la clase [Config] que acabamos de describir. El segundo parámetro es el array de argumentos que se pasa al método [main] (línea 7);

La aplicación web se puede ejecutar de diversas maneras, entre ellas la siguiente:

 

En la consola aparecen entonces varios registros:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] al manejador de tipo [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 de tipo [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 inicia el servidor Tomcat integrado;
  • líneas 15-19: se carga y configura el servlet [DispatcherServlet] de Spring MVC;
  • línea 20: se detecta el URL [/{a}/{b}] del servidor web;

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

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

En lugar de utilizar un navegador estándar, utilicemos ahora la extensión [Advanced Rest Client] del navegador Chrome (véanse los anexos, párrafo 6.13):

Image

  • en [1], el URL solicitado;
  • en [2], mediante un GET;
  • en [3], se envía la solicitud;

Image

  • en [4], los encabezados HTTP de la respuesta del servidor. Cabe señalar que esta indica que el documento enviado es una cadena jSON;
  • en [5], la cadena jSON recibida;

1.16.1.8. Generación del archivo JAR ejecutable del proyecto

En el apartado 1.16.1.2, mostramos cómo configurar el archivo Gradle para generar un ejecutable de la aplicación con todas sus dependencias. Adaptada a la aplicación actual, esta configuración queda así:


// crear un binario con todas sus dependencias
version = '1.0'
task fatJar(type: Jar) {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    attributes 'Main-Class': 'exemples.android.server.boot.Boot'
  }
  baseName = project.name + '-all'
  from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
  with jar
}

Para generar este ejecutable, se puede proceder de la siguiente manera [1-5]:

Para ejecutarlo, se detendrá el servicio web si está en ejecución [1], y luego se ejecutará el archivo [2-4]:

 

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

1.16.1.9. Gestión de los registros

Al ejecutar el archivo ejecutable, se observa que no se obtienen los mismos registros que al ejecutar el proyecto desde el IDE. Se obtienen 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]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: 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 - Started Boot in 1.984 seconds (JVM running for 2.206)

Se puede gestionar el nivel de los registros agregando un archivo [logback.xml] en la carpeta [resources] del proyecto:

  

Este archivo podría tener el siguiente contenido:


<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!--se les asigna, por defecto, el tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- control del nivel de los registros -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

El nivel de los registros se controla en la línea 12. Si ahora regeneramos el archivo ejecutable y lo ejecutamos, solo tendremos 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
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: 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 - Started Boot in 1.865 seconds (JVM running for 2.203)

1.16.2. El cliente Android del servidor web / jSON

El cliente de Android tendrá la siguiente arquitectura:

El cliente contará con dos componentes:

  1. una capa [Présentation] (vista + actividad) similar a la que analizamos en el ejemplo [Exemple-14];
  2. la capa [DAO], que se comunica con el servicio [web / jSON] que analizamos anteriormente.

1.16.2.1. Creación del proyecto

Duplicamos el proyecto anterior [Exemple-14] en [Exemple-15] siguiendo el procedimiento del párrafo 1.4. Obtenemos el siguiente resultado:

A continuación, se invita al lector a crear el siguiente proyecto.

1.16.2.2. Configuración de Gradle

 

El archivo [build.gradle] es el siguiente:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Desde la versión 0.11 del complemento Gradle de Android, debes usar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  // opciones de empaquetado necesarias para poder generar el APK
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}

Solo comentaremos lo que no se haya mencionado anteriormente:

  • líneas 46-47: inserción de un complemento AA. El complemento [rest-spring-api] permite delegar las comunicaciones entre el cliente y el servidor a la biblioteca AA;
  • línea 50: la biblioteca [spring-android-rest-template] es la que utiliza AA para gestionar las comunicaciones entre el cliente y el 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 lo tanto, hay que especificar, en las líneas 56 a 59, el repositorio que se debe usar (línea 58) para encontrar la biblioteca;
  • línea 51: una biblioteca jSON;
  • líneas 33 a 39: sin esta propiedad, aparecen errores al generar el binario APK del proyecto;

1.16.2.3. El manifiesto de la aplicación para Android

  

El archivo [AndroidManifest.xml] debe modificarse. De hecho, por defecto, el acceso a Internet está desactivado. Es necesario activarlo mediante una directiva especial:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.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: se autoriza el acceso a Internet;

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 {

  // número aleatorio
  int getAlea(int a, int b);

  // URL del servicio web
  void setUrlServiceWebJson(String url);

  // tiempo de espera máximo (ms) de la respuesta del servidor
  void setTimeout(int timeout);

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

}
  • línea 6: el método del servicio web / jSON para obtener un número aleatorio en el intervalo [a,b] de este servicio web;
  • línea 9: el URL del servicio web / jSON para la generación de números aleatorios;
  • línea 12: se establece un tiempo de espera máximo para recibir la respuesta del servidor;
  • línea 15: queremos establecer un tiempo de espera antes de ejecutar la solicitud al servidor, con el fin de darle tiempo al usuario para cancelar su solicitud;

1.16.2.4.2. La interfaz [WebClient]
  

La interfaz [WebClient] se encarga de comunicarse 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 número aleatorio en el intervalo [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • línea 12: [WebClient] es una interfaz que la biblioteca AA implementará por sí misma gracias a las anotaciones que le agregaremos. Esta interfaz debe implementar las llamadas a URL expuestas por el servicio web / jSON:

  // número aleatorio
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
public String getAlea(@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 tabla de convertidores. Aquí, el convertidor [MappingJackson2HttpMessageConverter.class] hace que, cuando el servidor envía una cadena jSON, esta se deserialice automáticamente. Así, en la línea (d) se observa que URL [/{a}/{b}] devuelve un tipo String, 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 recibirá en un tipo [Response<Integer>];
  • línea 15: una anotación AA que indica que URL debe invocarse con un método HTTP GET. El parámetro de la anotación [@Get] es el formato de URL que espera el servicio web. Basta con tomar el parámetro [value] de la anotación [@RequestMapping] (línea b) del método invocado en el controlador [WebController] del servidor. Las llaves {} rodean los parámetros de URL que deben incluirse en los parámetros del método en la línea 16. La sintaxis de [@Path("a") int a] hace que el parámetro [a] del método se asigne al valor {a} de URL. Cuando el parámetro de URL y el del método tienen el mismo nombre, como en este caso, se puede escribir de manera más sencilla como [@Path int a];

En el caso de una consulta 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] es la que designa el valor enviado. Este se serializará automáticamente como jSON. Del lado del servidor, tendremos la siguiente firma:


  // números aleatorios
  @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: se especifica que se espera una solicitud HTTP POST y que el cuerpo de esta solicitud (objeto enviado) debe transmitirse en forma de una cadena jSON (atributo consumes);
  • línea 4: el valor enviado 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 indicar el URL del servicio web al que nos vamos a conectar. Esto se logra extendiendo la interfaz [RestClientRootUrl] proporcionada por AA. Esta interfaz expone un método [setRootUrl(urlServiceWeb] que permite establecer el URL del servicio web al que nos vamos a conectar;
  • además, queremos controlar la llamada al servicio web, ya que queremos limitar el tiempo de espera de la respuesta. Para ello, ampliamos la interfaz [RestClientSupport], que expone el método [setRestTemplate], el cual nos permitirá:
    • crear nosotros mismos el objeto [RestTemplate], que sirve para gestionar las comunicaciones entre el cliente y el servidor;
    • configurar este objeto para establecer el tiempo máximo de espera de la respuesta;

1.16.2.4.3. La clase [Response]

El método [getAlea] de la interfaz [IDao] devuelve una respuesta del tipo [Response] como se indica a continuación:


package exemples.android.dao;

import java.util.List;

public class Response<T> {

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

    // constructores
    public Response() {

    }

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

    // getters y setters
...
}

Se trata de la clase [Response] que ya se utilizó del lado del servidor (párrafo 1.16.1.5). De hecho, desde el punto de vista de la programación, todo ocurre como si la capa [DAO] del cliente se comunicara directamente con el controlador [WebController] del servicio web:

La comunicación de red entre el cliente y el servidor, así como la serialización y deserialización de los objetos Java del 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 exemples.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 {

  // cliente del servicio REST
  @RestService
  protected WebClient webClient;

  // mapeador jSON
  private ObjectMapper mapper = new ObjectMapper();
  // tiempo de espera antes de ejecutar la solicitud
  private int delay;

// interfaz IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    ...
  }

  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
   ...
  }

  @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 podremos inyectar en otro lugar;
  • líneas 19-20: inyectamos la implementación que se realizará de la interfaz [WebClient] que hemos descrito. Es la anotación [@RestService] la que garantiza esta inyección;
  • los demás métodos implementan la interfaz [IDao] (líneas 27-46);

Método [setTimeout]

El método [setTimeout] es el siguiente:


  @Override
  public void setTimeout(int timeout) {
    // se establece el tiempo de espera de las solicitudes del cliente REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // se construye el restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // se configura el convertidor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Se establece el restTemplate del cliente web
    webClient.setRestTemplate(restTemplate);
}
  • La interfaz [WebClient] será implementada por una clase AA que utiliza la dependencia de Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementa la comunicación del cliente con el servidor web / jSON mediante una clase de tipo [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: creamos el objeto de tipo [RestTemplate], que servirá de medio para la comunicación con el servicio web. Le pasamos como parámetro el objeto [factory] que acabamos de crear;
  • línea 10: el diálogo entre el cliente y el servidor puede adoptar diversas formas. Los intercambios se realizan mediante líneas de texto y debemos indicar al objeto de tipo [RestTemplate] qué debe hacer con esa línea de texto. Para ello, le proporcionamos convertidores, es decir, clases capaces de procesar las líneas de texto. La elección del convertidor se realiza generalmente a través de los encabezados HTTP que acompañan a la línea de texto. En este caso, sabemos que solo recibimos líneas de texto en formato jSON. Además, en el párrafo 1.16.1.7 vimos que el servidor enviaba el encabezado HTTP:

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

En la línea 10, el único convertidor de [RestTemplate] será un convertidor jSON implementado con la biblioteca [Jackson]. Hay algo peculiar con respecto a estos convertidores: AA nos obliga a incluirlo también en la anotación del cliente web [WebClient]:


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

En la línea 1, nos vemos obligados a especificar un convertidor, a pesar de que ya lo especificamos mediante programación.

  • Línea 12: el objeto [RestTemplate] así construido se inyecta en la implementación de la interfaz [WebClient] y es este objeto el que llevará a cabo el diálogo entre el cliente y el servidor;

Método [getAlea]

El método [getAlea] es el siguiente:


  @Override
  public int getAlea(int a, int b) {
    // ejecución del servicio
    Response<Integer> info;
    DaoException ex;
    try {
      // en espera
      waitSomeTime(delay);
      // ejecución del servicio
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // se devuelve el resultado
        return info.getBody();
      } else {
        // se registra la excepción
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // se registra la excepción
      ex = new DaoException(e, 100);
    }
    // se lanza la excepción
    throw ex;
  }
...
  // métodos privados -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • línea 8: se espera [delay] milisegundos;
  • línea 10: simplemente se llama al método con la misma firma en la clase que implementa la interfaz [WebClient];
  • línea 11: se analiza la respuesta obtenida del servidor revisando su [status];
  • líneas 12-14: si no hubo error del lado del servidor (status=0), entonces se devuelve el resultado del método;
  • línea 17: si hubo un error del lado del servidor (status!=0), se prepara 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 aquí, necesariamente ha ocurrido una excepción. Entonces la lanzamos;

La excepción [DaoException] utilizada por este código es la siguiente:


package exemples.android.dao;

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

public class DaoException extends RuntimeException {

  // código de error
  private int code;

  // constructores
  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 y setters
...
}
  • línea 6: la excepción [DaoException] es una excepción no controlada;

Método [setUrlServiceWebJson]

El método [setUrlServiceWebJson] es el siguiente:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // se establece el URL del servicio REST
    webClient.setRootUrl(urlServiceWebJson);
}
  • línea 4: se establece el URL del servicio web mediante 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 [architecture]

El paquete [architecture] agrupa 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 {

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

  // cambio de vista
  void navigateToView(int position);

  // espera
  void beginWaiting();

  void cancelWaiting();

  // modo de depuración
  boolean IS_DEBUG_ENABLED = true;
  // tiempo de espera de la respuesta
  int TIMEOUT = 1000;
  // adyacencia de fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

}
  • línea 5: la interfaz [IMainActivity] extiende la interfaz [IDao];
  • líneas 13-16: a los métodos que ya aparecían en los ejemplos anteriores (líneas 7-11), hemos agregado dos métodos para manejar la imagen de espera de la aplicación (líneas 14, 16);
  • línea 21: se establece un tiempo máximo de espera para la respuesta del servidor en 1 segundo;

1.16.2.5.2. La clase [Utils]

En la clase [Utils] se han reunido métodos utilitarios estáticos que pueden invocarse desde diferentes partes de la arquitectura de la aplicación:


package exemples.android.architecture;

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

public class Utils {

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

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

}
  • líneas 9-18: crea la 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 que se mostrará en un mensaje de alerta de Android;
  • líneas 27-28: los mensajes se numeran. El número más bajo (1) corresponde a la excepción inicial y el número más alto, a la excepción más reciente en la pila de excepciones;

1.16.2.5.3. La clase abstracta [AbstractFragment]

La clase [AbstractFragment] tiene dos funciones:

  1. asegurar que el método [updateFragments] de las clases hijas siempre se llame al mostrar el fragmento y que solo se llame una vez;
  2. factorizar el estado y los métodos de las clases hijas que puedan ser factorizados;

Es la función 2 la que nos lleva a incluir en esta clase las operaciones de manejo de la imagen de espera: todos los fragmentos de una aplicación Android asíncrona deben manejar este tipo de problema:


  // gestión de la espera
  protected void beginWaiting() {
    // se muestra el reloj de arena
    mainActivity.beginWaiting();
  }

  protected void cancelWaiting() {
    // se retira el reloj de arena
    mainActivity.cancelWaiting();
}

1.16.2.6. La vista

1.16.2.6.1. La vista [vue1.xml]
  

En comparación con el ejemplo anterior, la vista [vue1.xml] cambia de la siguiente manera:

 
 
  • en [1], el usuario debe especificar el URL del servicio web, así como 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], se muestra un indicador de espera cuando se solicitan los números. Este se borra cuando se han recibido todos o cuando se ha cancelado la operación;

Image

  • en [6], se verifica la validez de los datos ingresados;

Se le pide al usuario que cargue el archivo [vue1.xml] de los ejemplos. A continuación, proporcionamos el identificador de los nuevos componentes:

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

Los botones [10-11] están físicamente uno encima del otro. En un momento dado, solo se mostrará uno de los dos.

1.16.2.6.2. El fragmento [Vue1Fragment]
  

La estructura del fragmento [Vue1Fragment] es la 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 exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.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 {

  // los elementos de la interfaz visual
  @ViewById(R.id.editTextUrlServiceWeb)
  EditText edtUrlServiceRest;
  @ViewById(R.id.textViewErreurUrl)
  TextView txtMsgErreurUrlServiceWeb;
  @ViewById(R.id.editTextDelay)
  EditText edtDelay;
  @ViewById(R.id.textViewErreurDelay)
  TextView textViewErreurDelay;
  @ViewById(R.id.lst_reponses)
  ListView listReponses;
  @ViewById(R.id.txt_Reponses)
  TextView infoReponses;
  @ViewById(R.id.edt_nbaleas)
  EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  EditText edtA;
  @ViewById(R.id.edt_b)
  EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  TextView txtErrorIntervalle;
  @ViewById(R.id.btn_Executer)
  Button btnExecuter;
  @ViewById(R.id.btn_Annuler)
  Button btnAnnuler;
...
  // datos locales
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;

  @AfterViews
  void afterViews() {
    // memoria
    afterViewsDone=true;
    // al inicio no hay mensajes de error
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // botón [Annuler] oculto
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // lista de respuestas
    reponses = new ArrayList<>();
  }
...
  • líneas 24-49: las referencias a los componentes de la vista [vue1.xml] (línea 20);
  • líneas 55-69: el método [@AfterViews] que se ejecuta cuando se han inicializado las referencias de las líneas 24-49;
  • línea 58: no lo olvides; es 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 [Annuler] (línea 65) y se muestra el botón [Exécuter] (línea 66). Cabe recordar que están físicamente uno encima del otro;
  • línea 68: el campo de la línea 52 contendrá la lista de cadenas de caracteres que debe mostrar el ListView de las respuestas;

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


  @Override
  protected void updateFragment() {
    // se crea el adaptador de la lista de respuestas
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • líneas 4-5: se crea el adaptador ListView de las respuestas. Se almacena en una variable de instancia para que esté disponible para los demás métodos de la clase;

Al hacer clic en el botón [Exécuter], se ejecuta el siguiente método:


// las entradas
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;

  // datos locales
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;

  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // se borran las respuestas anteriores
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // Se pone a 0 el contador de respuestas
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // se verifica la validez de los datos ingresados
    if (!isPageValid()) {
      return;
    }
    // inicialización de la actividad
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // se solicitan los números aleatorios
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // se inicia la espera
    beginWaiting();
  }

  @Background(id = "alea")
  void getAlea(int a, int b) {
    // aquí hay que hacer lo menos posible
    // en cualquier caso, no se muestra nada; esto debe hacerse en el UiThead
    try {
      // se muestra el resultado en el UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // se muestra la excepción en el UiThread
      showAlert(e);
    }
  }
  • líneas 17-18: se borra la lista anterior de respuestas del servidor. Para ello, en la línea 17, se vacía la fuente de datos [reponses] asociada al adaptador ListView;
  • línea 19: un valor booleano que nos servirá para saber si el usuario ha cancelado o no su solicitud;
  • líneas 21-22: se muestra un contador con valor cero para el número de respuestas;
  • líneas 24-26: se recuperan los datos ingresados en las líneas [2-6] y se verifica su validez. Si alguno de ellos no es válido, se abandona el método (línea 25) y el usuario regresa a la interfaz visual;
  • líneas 28-29: si todos los datos ingresados son válidos, se transmite a la actividad URL del servicio web (línea 28), así como el tiempo de espera antes de cada llamada al servicio (línea 29). Esta información es necesaria para la capa [DAO] y cabe recordar que es la actividad la que se comunica con ella;
  • líneas 31-33: los números aleatorios se solicitan uno por uno al método [getAlea] de la línea 39;
  • línea 38: el método [getAlea] se anota con la anotación AA [@Background], lo que hace que se ejecute en otro hilo (flujo de ejecución, proceso) distinto al que ejecuta la interfaz visual. De hecho, es obligatorio ejecutar cualquier llamada a Internet en un hilo diferente al de la interfaz visual. Así, en un momento dado, podemos tener varios hilos:
    • el que muestra la interfaz visual UI (User Interface) y gestiona sus eventos,
    • los hilos [nbAleas], cada uno de los cuales solicita un número aleatorio al servicio web. Estos hilos se inician de manera asíncrona: el hilo UI inicia un hilo [getAlea] (línea 32) que solicita un número aleatorio al servicio web y no espera a que este termine. Se le notificará la finalización mediante un evento. De este modo, los hilos [nbAleas] se iniciarán en paralelo. Es posible configurar la aplicación para que solo inicie un hilo a la vez. En ese caso, se crea una fila de espera de hilos por ejecutar;

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

  • línea 44: se invoca el método [getAlea] de la actividad. Por lo tanto, se ejecutará en un hilo distinto al de UI. Este último 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. Es en ese momento cuando, en la línea 44, se llamará al método [showInfo] con la respuesta recibida como parámetro;
  • líneas 45-47: la ejecución de la solicitud web puede generar una excepción. En ese caso, se solicita que los mensajes de error de la excepción se muestren en un mensaje de alerta;
  • línea 35: se espera a que lleguen los resultados:
    • se mostrará un indicador de espera;
    • el botón [Annuler] reemplazará al botón [Exécuter]. Debido a que los subprocesos iniciados son asíncronos, el subproceso de UI no los espera y la línea 35 se ejecuta antes de que estos finalicen. Una vez que el método [beginWaiting] ha finalizado, el UI puede volver a responder a las solicitudes del usuario, como al hacer clic en el botón [Annuler]. Si los subprocesos iniciados hubieran sido sincrónicos, solo se llegaría a la línea 35 una vez que todos los subprocesos hubieran finalizado. La cancelación de estos ya no tendría sentido;

El método [showInfo] es el siguiente:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // más información
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // ¿Ya terminamos?
      if (nbInfos == nbAleas) {
        // terminamos la espera
        cancelWaiting();
      }
      // se agrega la información a la lista de respuestas
      reponses.add(0, String.valueOf(alea));
      // se muestran las respuestas
      adapterReponses.notifyDataSetChanged();
    }
}
  • El método [showInfo] se invoca dentro del hilo [getAlea], anotado por [@Background]. Este método actualizará la interfaz visual UI. Solo puede hacerlo si se ejecuta dentro del hilo de UI. Este es el significado de la anotación [@UiThread] de la línea 1;
  • línea 2: el método recibe un número aleatorio;
  • línea 3: el cuerpo del método solo se ejecuta si el usuario no ha cancelado su solicitud;
  • líneas 5-6: se incrementa el contador de respuestas y se muestra;
  • líneas 8-11: si se han recibido todas las respuestas esperadas, se da por terminada la espera (fin de la señal de espera; el botón [Exécuter] reemplaza al botón [Annuler]);
  • líneas 12-15: se agrega el número aleatorio recibido a la lista de respuestas que muestra el componente [ListView listReponses] y se actualiza este;

El método [showAlert] es el siguiente:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // se cancela todo
      doAnnuler();
      // se muestra
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

Se observa una lógica 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 produjo;
  • línea 3: el método se ejecuta solo si el usuario no ha cancelado su solicitud;
  • línea 5: se cancela la solicitud del usuario como si él mismo hubiera hecho clic en el botón [Annuler];
  • línea 7: se muestra la alerta utilizando la clase de Android [AlertDialog]:
    • [activity]: es la actividad de tipo [Activity] 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];
    • [show]: solicita que se muestre la ventana de alerta;
 

El «clic» en el botón [Annuler] se maneja con el siguiente método:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // memoria
    hasBeenCanceled=true;
    // se cancela la tarea asíncrona
    BackgroundExecutor.cancelAll("alea", true);
    // fin de la espera
    cancelWaiting();
}
  • línea 4: se registra 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 incluso si ya se han iniciado. El identificador [alea] es el que se utiliza para calificar el método [getAlea] del fragmento (línea 1 a continuación):

  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

Nota: se descubrió que la línea 6 del código del método [doAnnuler] no funcionaba correctamente. Por esta razón, se agregó el valor booleano [hasBeenCanceled]. De hecho, en caso de excepción (servidor ausente), se recuperaba n veces la ventana de alerta si se habían 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 agregado una imagen de espera en la vista asociada a la actividad [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">
      <!-- imagen de espera -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>

    </android.support.v7.widget.Toolbar>
    <!-- imagen de espera -->
  </android.support.design.widget.AppBarLayout>
...
  • líneas 17-21: la imagen de espera;

1.16.2.7.2. La actividad [MainActivity]

La actividad [MainActivity] presenta pocos cambios respecto a lo que era en [Exemple-14]. En primer lugar, se le inyecta la capa [DAO]:


  // inyección dao
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // configuración de la capa [DAO]
    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: se establece el timeout de la capa [DAO]

Además, la actividad [MainActivity] debe implementar la interfaz [IMainActivity], la cual a su vez extiende la interfaz [IDao]:


  // implementación IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // se muestra la vista de posición
    if (mViewPager.getCurrentItem() != position) {
      // se muestra el fragmento
      mViewPager.setCurrentItem(position);
    }
  }

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

  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }

  // Implementación IDao --------------------------------------------------------------------

  @Override
  public int getAlea(int a, int b) {
    // ejecución
    return dao.getAlea(a, b);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
  }

  @Override
  public void setUrlServiceWebJson(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 (párrafo 1.16.1.7) y, a continuación, inicie el cliente de Android:

Image

Para saber qué introducir en [1], sigue estos pasos. Abre una ventana de comandos y escribe el siguiente comando:


C:\Program Files\Console2>ipconfig

Configuration IP de Windows

Carte réseau sans fil Connexion au réseau local* 3 :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :

Carte Ethernet VirtualBox Host-Only Network :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte Ethernet VirtualBox Host-Only Network #2 :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :

Carte Ethernet Ethernet :

   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254

Carte réseau sans fil Wi-Fi :

   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr

Si ha instalado [GenyMotion], la máquina virtual VirtualBox ha agregado direcciones IP a su equipo (líneas 10 y 18). Estas direcciones son particularmente prácticas, ya que no son bloqueadas por el firewall de Windows. La línea 30 muestra la dirección IP de tu equipo en una red local. Para usar esta dirección, por lo general es necesario desactivar el firewall de Windows. Si estás conectado a una red wifi, usa la dirección wifi y, en ese caso también, desactiva el firewall si tienes uno.

Prueba la aplicación en los siguientes casos:

  • 100 números aleatorios en el intervalo [1000, 2000] sin tiempo de espera;
  • 2000 números aleatorios en el intervalo [10000, 20000] sin tiempo de espera y cancela la espera antes de que termine la generación;
  • 5 números aleatorios en el intervalo [100, 200] con un tiempo de espera de 5000 ms y cancele la espera antes de que finalice la generación;

1.16.2.9. Manejo de la cancelación

Para dar seguimiento a lo que sucede cuando el usuario solicita la cancelación o cuando esta se solicita debido a que se ha producido una excepción, agregamos el siguiente método a la interfaz [IDao] (véase el párrafo 1.16.2.4.1):


package exemples.android.dao;

public interface IDao {

  ...

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

En la clase [Dao], agregamos el siguiente código:


  // modo de depuración
  private boolean isDebugEnabled;
  // nombre de la clase
  private String className;
..
  // constructor
  public Dao() {
    // nombre de la clase
    className = getClass().getSimpleName();
  }
...
  // interfaz IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // ejecución del servicio
    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 registro cada vez que se llama al método [getAlea];

Además, en el fragmento [Vue1Fragment], agregamos los siguientes registros:


  @UiThread
  protected void showInfo(int alea) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }

  @UiThread
  protected void showAlert(Throwable th) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Cada vez que el fragmento [Vue1Fragment] recibe información de la capa [DAO], se genera un registro. Además, cuando se invoca el método [doAnnuler], se registra el evento.

Prueba 1

Se solicitan 5 números aunque el servidor no se ha iniciado. Se obtienen los siguientes registros:

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • líneas 1-5: el método [getAlea] de la clase [Dao] se invoca cinco veces. Recordemos que se trata de llamadas asincrónicas realizadas por el fragmento [VueFragment] y que este no espera el resultado de su llamada;
  • línea 7: se ha realizado la primera solicitud HTTP y el fragmento [VueFragment] ha recibido su primera excepción;
  • línea 8: a continuación, solicita la cancelación de todas las solicitudes;
  • líneas 9-12: sin embargo, se observa que recibe las siguientes cuatro excepciones. Por lo tanto, todas las solicitudes asincrónicas que estaban en espera se han ejecutado;

Prueba 2

Ahora, iniciemos el servidor y solicitemos 5 números con un retraso de 5 segundos, y hagamos clic en [Annuler] antes de que termine ese retraso. Los registros son los siguientes:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • líneas 1-5: se invoca cinco veces el método [getAlea] de la clase [Dao];
  • línea 7: el usuario solicitó la cancelación de las solicitudes;
  • línea 8: se observa que [Vue1_Fragment] recibe 5 valores. Una vez más, todas las solicitudes asincrónicas que estaban en espera se han ejecutado;

Por eso tuvimos que manejar un valor booleano [hasBeenCanceled] para evitar mostrar algo cuando se había solicitado una cancelación. En el código de la cancelación:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memoria
    hasBeenCanceled = true;
    // se cancela la tarea asíncrona
    BackgroundExecutor.cancelAll("alea",true);
    // fin de la espera
    cancelWaiting();
}

el código de la línea 10 no hace lo que se espera. Es posible que esto se deba a que las tareas asincrónicas comparten el mismo método anotado [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Ejemplo 16: gestionar la asincronía con RxAndroid

Ahora nos proponemos gestionar el asincronismo necesario para las aplicaciones de Android con una biblioteca llamada RxJava [http://reactivex.io/] y su versión derivada para el entorno de Android, [RxAndroid]. Para ello, utilizaremos el curso [Introduction à RxJava. Application aux environnements Swing et Android].

1.17.1. Creación del proyecto

Duplicamos el proyecto [Exemple-1] en [Exemple-16]:

1.17.2. Configuración de Gradle

  

En [build.gradle], agregamos 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] queda así:


package exemples.android.dao;

import rx.Observable;

public interface IDao {

  // número aleatorio
  Observable<Integer> getAlea(int a, int b);

  // URL del servicio web
  void setUrlServiceWebJson(String url);

  // tiempo máximo de espera (ms) para la respuesta del servidor
  void setTimeout(int timeout);

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

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);
}
  • línea 8: el método [getAlea] ahora devuelve 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 (suscriptores, observadores, consumidores) de tipo Subscriber<T>. La biblioteca RxJava permite que el flujo Observable<T> se ejecute en un hilo T1 y su observador Subscriber<T> en un hilo T2 sin que el desarrolladortenga que preocuparse por gestionar el ciclo de vida de estos hilos ni por problemas naturalmente complejos, como el intercambio de datos entre hilos y su sincronización 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 exemples.android.dao;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;

public abstract class AbstractDao {

  // mapeador jSON
  private ObjectMapper mapper = new ObjectMapper();

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

  // solicitud genérica
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // ejecución de servicio
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // ejecución del servicio
        try {
          // se realiza la solicitud de forma sincrónica y se reenvía la respuesta al suscriptor
          Response<T> response = request.getResponse();
          // ¿Error?
          int status = response.getStatus();
          if (status != 0) {
            // se registra la excepción
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // se envía la respuesta
            subscriber.onNext(response.getBody());
            // se notifica el fin del observable
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // se registra la excepción
          ex = new DaoException(e, 100);
        }
        // ¿excepción?
        if (ex != null) {
          // se genera la excepción
          subscriber.onError(ex);
        }
      }
    });
  }

}
  • La clase [AbstractDao] tiene como elemento principal un método genérico [getResponse] que sirve para obtener del servidor un tipo [Response<T>], donde T es el tipo del resultado deseado por el cliente HTTP (en este caso, Integer);
  • línea 20: el único parámetro del método genérico [getResponse] es una instancia de la interfaz genérica [IRequest<T>] de las líneas 15 a 17. Esta interfaz solo tiene un método, [getResponse], y es este método el que proporciona la respuesta deseada [Response<T>];
  • gracias a los dos elementos anteriores, la clase [AbstractDao] puede servir como clase padre para cualquier capa [Dao] 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 que realmente espera el cliente HTTP (en este caso, 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 cuenta con los siguientes métodos:
    • [onNext(T element)]: permite enviar a un observador un elemento de tipo T;
    • [onError(Throwable th)]: permite emitir una excepción a un observador;
    • [onCompleted]: permite indicar a un observador el fin de las emisiones;

Un tipo [Observable<T>] cumple con ciertas restricciones:

  • envía sus elementos mediante el método [onNext(T element)];
  • el método [onCompleted] debe llamarse una sola vez tan pronto como ya no haya elementos que enviar al observador;
  • el método [onCompleted] no se invoca si ya se ha invocado el método [onError(Throwable th)];

En nuestro ejemplo:

  • el observador será el fragmento [Vue1Fragment]. Es este el que procesa los elementos emitidos por el [Observable<T>] (elemento o excepción);
  • el tipo [Observable<T>] creado solo emitirá un único elemento (línea 37);
  • línea 29: realiza una consulta HTTP sincrónica al servidor y obtiene el tipo [Response<T>]. Esta consulta HTTP es gestionada por el tipo [IRequest], que se pasa como parámetro al método genérico [getResponse];
  • línea 31: se recupera el status de la respuesta;
  • líneas 32-34: si este status corresponde a un error, se prepara una excepción;
  • líneas 36-39: si este status no corresponde a un error, entonces se envía la respuesta que el cliente realmente espera (línea 37) y se le indica al observador que no habrá más envíos (línea 39);
  • líneas 41-44: si la solicitud HTTP termina en una excepción, se registra esta;
  • líneas 46-49: si la excepción [ex] es diferente de null, entonces se envía al observador. No es necesario llamar al método [onCompleted] para indicar al observador que ya no habrá más emisiones de elementos. Esto queda implícito;

De estas explicaciones se desprende que:

  • el método genérico [<T> Observable<T> getResponse(final IRequest<T> request)] devuelve un tipo [Observable<T>] que solo emite un elemento de tipo T o bien una excepción;
  • que este método admite como único parámetro un tipo [IRequest<T>], cuyo único método [getResponse()] realiza el acceso 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 {

  // cliente del servicio REST
  @RestService
  protected WebClient webClient;

  // tiempo de espera antes de ejecutar la solicitud
  private int delay;
  // modo de depuración
  private boolean isDebugEnabled;
  // nombre de la clase
  private String className;

  // constructor
  public Dao() {
    // nombre de la clase
    className = getClass().getSimpleName();
  }


  // interfaz IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // ejecución del cliente web
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // espera
        waitSomeTime(delay);
        // llamada síncrona a HTTP
        return webClient.getAlea(a, b);
      }
    });
}
...
  • línea 2: la clase [Dao] extiende la clase [AbstractDao];
  • línea 24: el método [getAlea] ahora devuelve 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: se realiza la consulta HTTP a través de la interfaz AA [webClient], tal como se había hecho anteriormente. Sabemos que obtendremos un tipo [Response<Integer>], que es precisamente el tipo que debe devolver el método [IRequest<Integer>.getReponse()];
  • línea 36: aquí se utiliza una propiedad llamada closure: la capacidad de encapsular en una instancia valores externos a ella en el momento de su creación, en este caso los valores de [a, b] de la línea 24. Esto es lo que permite que el método [IRequest<Integer>.getReponse()] no tenga parámetros. Estos se han incorporado en el cuerpo del método. Y donde normalmente se cambiarían los parámetros del método (a, b) → (x, y), aquí se crea una nueva instancia de [IRequest<Integer>] que encapsula los valores de x e y;

1.17.7. La clase [MainActivity]

La clase [MainActivity], que implementa la interfaz [IDao], evoluciona de la siguiente manera:


  // implementación de IDao --------------------------------------------------------------------

  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // ejecución
    return dao.getAlea(a, b);
}

1.17.8. La clase [Vue1Fragment]

La clase [Vue1Fragment] evoluciona de la siguiente manera:


  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // se borran las respuestas anteriores
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // se pone a 0 el contador de respuestas
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // se verifica la validez de los datos ingresados
    if (!isPageValid()) {
      return;
    }
    // inicialización de la actividad
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // se solicitan los números aleatorios
    getAleasInBackground(a, b);
    // se inicia la espera
    beginWaiting();
}
  • línea 18: se solicitan los números aleatorios al método [getAleasInBackground], llamado así porque los números se solicitarán en un hilo diferente al de la interfaz de usuario;

  private int nbReponses = 0;
  // suscripciones a los observables
  private List<Subscription> abonnements;

// anotación [Background] innecesaria
  void getAleasInBackground(int a, int b) {
    // al principio no hay respuestas ni suscripciones
    nbReponses = 0;
    abonnements.clear();
    // se prepara el observable
    Observable<Integer> response = Observable.empty();
    // se fusionan los resultados de las diferentes llamadas HTTP
    // se ejecutan en un hilo de E/S
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // el observable acumulado se observará en el hilo del UI
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // se ejecuta el observable
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // se agrega la información a la lista de respuestas
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // mensaje de error
          showAlert(th);
          // fin de la espera
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // fin de la espera
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // se muestra la excepción en el UiThread
      showAlert(e);
    }
}
  • línea 3: un observable tiene suscriptores. El vínculo entre un suscriptor y el proceso que observa se denomina suscripción (Subscription). Aquí solo tendremos un proceso observado y un suscriptor. Por lo tanto, solo tendremos una suscripción. A modo de ejemplo, supongamos que pudiéramos tener varios procesos observados por diferentes observadores, lo que daría lugar a varias suscripciones;
  • líneas 11-18: se configura el proceso observado (observable). Hay que tener en cuenta que se trata únicamente de una configuración: el proceso no se ejecuta;
  • línea 11: se parte de un observable vacío, un observable que no emite nada;
  • líneas 14-16: a este observable vacío se le agregan los observables [nbAleas], que serán las consultas [nbAleas] que devolverán números aleatorios [nbAleas];
  • línea 15: al igual que antes, se solicita el número aleatorio n.º i a la clase [MainActivity]. Es importante entender que, en este punto, aún no se ha ejecutado ninguna consulta HTTP. Se ejecuta el método [mainActivity.getAlea(a, b)] y devuelve un tipo [Observable<Integer>]. Este es un proceso que se observará cuando se inicie;
  • línea 15: el método [subscribeOn(Schedulers.io())] solicita que el proceso se ejecute (cuando lo haga) en un hilo de E/S. La biblioteca RxJava ofrece diferentes tipos de hilos. El de E/S es adecuado para las llamadas HTTP;
  • línea 15: el observable n.º i se fusiona con el observable inicial de la línea 11: a partir de los observables [nbAleas], cada uno de los cuales emite un elemento, se crea un observable que emitirá [nbAleas] elementos. Este será el que se observará. Este observable emite la notificación [onCompleted] cuando todos los observables que lo componen hayan emitido su propia notificación [onCompleted]. Esto nos evitará tener que contar las respuestas, como lo habíamos hecho en la versión anterior, para saber si hemos recibido todos los números esperados;
  • línea 18: al llegar aquí, hemos configurado un observable que es la composición de [nbAleas] observables, cada uno de los cuales se ejecuta en un hilo de E/S;
  • línea 18: el método [observeOn(AndroidSchedulers.mainThread())] sirve para indicar en qué hilo debe realizarse la observación de los valores emitidos por el observable. Aquí, el hilo [AndroidSchedulers.mainThread())] pertenece a la biblioteca RxAndroid y no a RxJava. Se refiere al hilo de la interfaz de usuario (UI), también conocido como «event loop». Este punto es importante: en una aplicación de Android, la modificación de un componente de la interfaz de usuario solo puede realizarse en el hilo de la interfaz de usuario; de lo contrario, se produce una excepción;
  • líneas 19-45: ahora que se ha configurado el proceso a observar, se ejecuta;
  • línea 21: es la operación [Observable.subscribe] la que inicia la ejecución del proceso observado. Esta operación iniciará los procesos asíncronos [nbAleas] configurados previamente. Los resultados de estos se pondrán automáticamente a disposición del observador en el hilo de la interfaz de usuario;
  • Recordemos que el observable emite tres tipos de eventos:
    • [onNext]: cuando emite un elemento;
    • [onError]: cuando se ha encontrado con una excepción;
    • [onCompleted]: cuando indica que ya no emitirá más;

El método [Observable.subscribe] tiene como parámetros tres objetos [Action1<Integer>, Action1<Throwable>, Action0], cuyos métodos [call] sirven para procesar cada uno de estos tres eventos;

  • líneas 21-27: el primer parámetro de tipo [Action1<Integer>] sirve para procesar el evento [onNext]. Su método [call] recibe el elemento que fue emitido por el observable (línea 23);
  • línea 25: se reutiliza el método [showInfo] del ejemplo anterior;
  • líneas 27-35: el segundo parámetro de tipo [Action1<Throwable>] sirve para procesar el evento [onError]. Su método [call] recibe la excepción emitida por el observable (línea 29);
  • línea 31: se reutiliza el método [showAlert] del ejemplo anterior;
  • línea 33: se inicia el procedimiento para cancelar la solicitud del usuario. Esto consistirá en cancelar todos los observables que se estén ejecutando;
  • líneas 35-41: el tercer parámetro de tipo [Action0] sirve para procesar el evento [onCompleted]. Su método [call] no recibe ningún parámetro;
  • línea 39: se cancela la espera;

El método [showInfo] evoluciona de la siguiente manera:


  // anotación [UiThread] innecesaria
  protected void showInfo(int alea) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // información adicional
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // se agrega la información a la lista de respuestas
      reponses.add(0, String.valueOf(alea));
      // se muestran las respuestas
      adapterReponses.notifyDataSetChanged();
    }
}

El método presenta dos cambios:

  • línea 1: se ha eliminado la anotación AA [@UiThread];
  • ya no se cuentan las respuestas para determinar si se debe detener o no la espera. Ahora es el evento [onCompleted] del observable el que nos brinda esta información;

El método [showAlert] cambia de la siguiente manera:


  // nota: [UiThread] innecesario
  protected void showAlert(Throwable th) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // se cancela todo
      doAnnuler();
      // se publica
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • el único cambio está en la línea 1: se ha eliminado la anotación AA [@UiThread];

Por último, el método [doAnnuler] cambia de la siguiente manera:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memoria
    hasBeenCanceled = true;
    // se cancelan las tareas asincrónicas
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // fin de la espera
    cancelWaiting();
}
  • línea 12: cancela una suscripción y, por lo tanto, la observación del proceso asociado;

1.17.9. Ejecución

Inicie el servicio web (párrafo 1.16.1.7), inicie el cliente de Android y repita las pruebas que realizó con el ejemplo anterior (párrafo 1.16.2.8).

1.17.10. Manejo de la cancelación

Repetimos las mismas pruebas que en el ejemplo anterior (párrafo 1.16.2.9).

Prueba 1

Se solicitan 5 números cuando el servidor no se ha iniciado. Se obtienen los siguientes registros:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Después de la línea 7, ya no hay registros, lo que indica que el observador (Vue1Fragment) ya no recibe notificaciones del proceso observado.

Prueba 2

Ahora, iniciemos el servidor y solicitemos 5 números con un intervalo de 5 segundos, y hagamos clic en [Annuler] antes de que termine ese intervalo. Los registros son los siguientes:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Después de la línea 6, ya no hay registros, lo que demuestra que el observador (Vue1Fragment) ya no recibe notificaciones del proceso observado.

Este es el comportamiento esperado de una cancelación. Por lo tanto, en el código de [Vue1Fragment] podemos eliminar la variable booleana [hasBeenCanceled] que habíamos introducido en el ejemplo anterior, ya que la cancelación no funcionaba como esperábamos.

El hecho de que el observador ya no reciba notificaciones tras la cancelación del observable no significa que las solicitudes HTTP se cancelen también. Para comprobarlo, modificamos la clase [Dao] de la siguiente manera:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // ejecución del cliente web
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // espera
        waitSomeTime(delay);
        // Llamada síncrona a HTTP
        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),"erreur désérialisation jSON");
          }
        }
        return response;
      }
    });
}
  • líneas 15-21: registramos el resultado de la consulta HTTP de la línea 14;

Los registros para la prueba n.º 2 son entonces los siguientes:

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • líneas 1-5: se realizaron las 5 solicitudes;
  • línea 6: el usuario canceló;
  • líneas 7-11: se reciben correctamente las respuestas de las cinco consultas HTTP. Sin embargo, debido a la cancelación del observable, estos elementos no se transmiten al observador;

1.17.11. Conclusión

En el resto de este documento, las aplicaciones cliente/servidor se desarrollarán con la biblioteca RxAndroid en lugar de con la biblioteca AA por las siguientes razones:

  1. RxAndroid se puede utilizar en una aplicación de Android que no utilice AA;
  2. RxAndroid no solo facilita las operaciones asincrónicas, sino que ofrece numerosos métodos para crear un nuevo observable a partir de otro. Estos métodos no tienen equivalente en AA;
  3. tan pronto como se desea derivar una clase anotada con AA, como un fragmento, surgen serios problemas. Esto lleva a abandonar AA y a utilizar la solución 1 para la programación asíncrona;

El lector interesado en profundizar en las posibilidades de la biblioteca RxAndroid puede consultar el documento [Introduction à RxJava. Application aux environnements Swing et Android]. En él se utiliza RxAndroid sin la biblioteca AA.

1.18. Ejemplo 17: componentes de entrada de datos

Vamos a escribir un nuevo proyecto para presentar algunos componentes comunes en los formularios de entrada de datos.

1.18.1. Creación del proyecto

Duplicamos el proyecto [Exemple-13] en [Exemple-17]:

El nuevo proyecto solo tendrá una vista: [vue1.xml]. Por lo tanto, eliminamos la vista [vue2.xml] y sus fragmentos asociados [Vue2Fragment] y [2]. Tomamos en cuenta esta modificación en el administrador de fragmentos de [Mainactivity]:


  // nuestro administrador de fragmentos debe redefinirse para cada aplicación
  // debe definir los siguientes métodos: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // los fragmentos
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Vuelva a ejecutar el proyecto. Debería mostrar la vista n.º 1 como antes. Trabajaremos a partir de este proyecto.

1.18.2. La vista XML del formulario

  

La vista generada por el archivo [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/textViewFormulaireTitre"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="10dp"
      android:layout_marginTop="30dp"
      android:text="@string/titre_vue1"
      android:textSize="30sp"/>

    <Button
      android:id="@+id/formulaireButtonValider"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/TextViewFormulaireCombo"
      android:layout_below="@+id/TextViewFormulaireCombo"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_valider"/>

    <TextView
      android:id="@+id/textViewFormulaireCheckBox"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireTitre"
      android:layout_below="@+id/textViewFormulaireTitre"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_checkbox"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireRadioButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
      android:layout_below="@+id/textViewFormulaireCheckBox"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_radioButton"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireSeekBar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
      android:layout_below="@+id/textViewFormulaireRadioButton"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_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/formulaire_saisie"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireBool"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireEdtText"
      android:layout_below="@+id/textViewFormulaireEdtText"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_bool"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireDate"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:layout_alignLeft="@+id/textViewFormulaireBool"
      android:layout_below="@+id/textViewFormulaireBool"
      android:layout_marginTop="50dp"
      android:gravity="center"
      android:text="@string/formulaire_date"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireMultilignes"
      android:layout_width="150dp"
      android:layout_height="100dp"
      android:gravity="center"
      android:layout_alignBaseline="@+id/textViewFormulaireTitre"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="400dp"
      android:layout_toRightOf="@+id/textViewFormulaireTitre"
      android:text="@string/formulaire_multilignes"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/textViewFormulaireTime"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:gravity="center"
      android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
      android:layout_below="@+id/textViewFormulaireMultilignes"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_time"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/TextViewFormulaireCombo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireTime"
      android:layout_below="@+id/textViewFormulaireTime"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_combo"
      android:textSize="20sp"/>

    <CheckBox
      android:id="@+id/formulaireCheckBox1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
      android:layout_marginLeft="100dp"
      android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
      android:text="@string/formulaire_checkbox1"/>

    <RadioGroup
      android:id="@+id/formulaireRadioGroup"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:orientation="horizontal">

      <RadioButton
        android:id="@+id/formulaireRadioButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radiobutton1"/>

      <RadioButton
        android:id="@+id/formulaireRadioButton2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radionbutton2"/>

      <RadioButton
        android:id="@+id/formulaireRadionButton3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radiobutton3"/>
    </RadioGroup>

    <SeekBar
      android:id="@+id/formulaireSeekBar"
      android:layout_width="200dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
      android:layout_alignLeft="@+id/formulaireCheckBox1"/>

    <EditText
      android:id="@+id/formulaireEditText1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:ems="10"
      android:inputType="text">
    </EditText>

    <Switch
      android:id="@+id/formulaireSwitch1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireBool"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:text="@string/formulaire_switch"
      android:textOff="Non"
      android:textOn="Oui"/>

    <TimePicker
      android:id="@+id/formulaireTimePicker1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormulaireTime"
      android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
      android:timePickerMode="spinner"
    />

    <EditText
      android:id="@+id/formulaireEditTextMultiLignes"
      android:layout_width="wrap_content"
      android:layout_height="100dp"
      android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
      android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
      android:layout_marginLeft="50dp"
      android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
      android:ems="10"
      android:inputType="textMultiLine">
    </EditText>

    <Spinner
      android:id="@+id/formulaireDropDownList"
      android:layout_width="200dp"
      android:layout_height="50dp"
      android:layout_alignBottom="@+id/TextViewFormulaireCombo"
      android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
    </Spinner>

    <DatePicker
      android:id="@+id/formulaireDatePicker1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormulaireDate"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      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/textViewFormulaireSeekBar"
      android:layout_marginLeft="30dp"
      android:layout_toRightOf="@+id/formulaireSeekBar"
      android:text=""/>
  </RelativeLayout>

Los componentes principales del formulario son los siguientes:

  • línea 2: un diseño [ScrollView] vertical. Permite
  • presentar un formulario más grande que la pantalla de la
  • tableta. Se puede ver el formulario completo
  • desplazarse;
 
  • líneas 125-132: una casilla de selección
  • líneas 134-159: un grupo de tres botones de opción
  • líneas 161-166: una barra de búsqueda
  • líneas 16-176: un campo de entrada
  • líneas 178-186: un conmutador sí/no
  • líneas 188-195: un campo de entrada de hora
  • líneas 197-207: un campo de entrada de varias líneas
  • líneas 209-215: una lista desplegable
  • líneas 217-225: un campo de entrada de fecha
  • Todos los demás componentes son [TextView] que muestran textos.
 

1.18.3. Las cadenas de texto del formulario

Las cadenas de texto del formulario se definen en el siguiente archivo [res / values / strings.xml]:

  

<resources>
  <string name="app_name">Exemple-17</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="formulaire_checkbox">Cases à cocher</string>
  <string name="formulaire_radioButton">Boutons Radio</string>
  <string name="formulaire_seekBar">Seek Bar</string>
  <string name="formulaire_saisie">Champ de saisie</string>
  <string name="formulaire_bool">Booléen</string>
  <string name="formulaire_date">Date</string>
  <string name="formulaire_time">Heure</string>
  <string name="formulaire_multilignes">Champ de saisie multilignes</string>
  <string name="formulaire_listview">Liste</string>
  <string name="formulaire_combo">Liste déroulante</string>
  <string name="formulaire_checkbox1">1</string>
  <string name="formulaire_checkbox2">2</string>
  <string name="formulaire_radiobutton1">1</string>
  <string name="formulaire_radionbutton2">2</string>
  <string name="formulaire_radiobutton3">3</string>
  <string name="formulaire_switch"></string>
  <string name="formulaire_valider">Valider</string>
</resources>

1.18.4. El fragmento del formulario

  

La clase [Vue1Fragment] es la siguiente:


package exemples.android.fragments;

import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.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;

// un fragmento es una vista que muestra un contenedor de fragmentos
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // los campos de la vista mostrada por el fragmento
  @ViewById(R.id.formulaireDropDownList)
  Spinner dropDownList;
  @ViewById(R.id.formulaireButtonValider)
  Button buttonValider;
  @ViewById(R.id.formulaireCheckBox1)
  CheckBox checkBox1;
  @ViewById(R.id.formulaireRadioGroup)
  RadioGroup radioGroup;
  @ViewById(R.id.formulaireSeekBar)
  SeekBar seekBar;
  @ViewById(R.id.formulaireEditText1)
  EditText saisie;
  @ViewById(R.id.formulaireSwitch1)
  Switch switch1;
  @ViewById(R.id.formulaireDatePicker1)
  DatePicker datePicker1;
  @ViewById(R.id.formulaireTimePicker1)
  TimePicker timePicker1;
  @ViewById(R.id.formulaireEditTextMultiLignes)
  EditText multiLignes;
  @ViewById(R.id.formulaireRadioButton1)
  RadioButton radioButton1;
  @ViewById(R.id.formulaireRadioButton2)
  RadioButton radioButton2;
  @ViewById(R.id.formulaireRadionButton3)
  RadioButton radioButton3;
  @ViewById(R.id.textViewSeekBarValue)
  TextView seekBarValue;

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

  @AfterViews
  void afterViews() {
    // se marca el primer botón
    radioButton1.setChecked(true);
    // el calendario
    datePicker1.setCalendarViewShown(false);
    // el seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // la lista desplegable
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }


  @SuppressLint("DefaultLocale")
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    ...
  }
 
@Override
  protected void updateFragment() {
    // inicialización del adaptador de la lista desplegable
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }
}
  • líneas 22-49: se recuperan las referencias de todos los componentes del formulario XML [vue1] (línea 18);
  • línea 58: el método [setChecked] permite marcar un botón de opción o una casilla de selecció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() permite establecer el valor máximo de la barra de ajuste. El valor mínimo es 0;
  • líneas 63-74: se gestionan los eventos de la barra de ajuste. Se busca que, cada vez que el usuario realice un cambio, se muestre el valor de la regla en el [TextView] de la línea 49;
  • línea 71: el parámetro [progress] representa el valor de la regla;
  • líneas 76-79: una lista de [String] que se asociará a la lista desplegable;
  • línea 90: el método [updateFragment] del fragmento. Al ejecutarse, se ha inicializado la variable [activity] de la clase padre;
  • línea 92: la fuente de datos [list] se asocia al adaptador de la lista desplegable;
  • líneas 93-94: el adaptador [dataAdapter] se asocia a la lista desplegable [dropDownList];
  • línea 84: se asocia el método [doValider] al clic en el botón [Valider];

El método [doValider] tiene como objetivo mostrar los valores ingresados por el usuario. Su código es el siguiente:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lista de mensajes para mostrar
    List<String> messages = new ArrayList<>();
    // casilla de selección
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // los botones de opción
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // el SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // el campo de entrada
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // el conmutador
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // la fecha
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // el texto de varias líneas
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // la hora
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // lista desplegable
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // visualización
    doAfficher(messages);
}
  • línea 4: los valores ingresados se acumularán en una lista de mensajes;
  • línea 6: el método [CheckBox].isCkecked() permite saber si una casilla está marcada o no;
  • línea 9: el método [RadioGroup].getCheckedButtonId() permite obtener el identificador del botón de opción que se ha marcado o -1 si no se ha marcado ninguno;
  • línea 10: el código [activity.findViewById(id)] permite identificar el botón de opción seleccionado y obtener así su texto;
  • línea 13: el método [SeekBar].getProgress() permite obtener el valor de una barra deslizante;
  • línea 19: el método [Switch].isChecked() permite determinar si un conmutador es On (verdadero) o Off (falso);
  • línea 22: el método [DatePicker].getYear() permite obtener el año seleccionado mediante un objeto [DatePicker];
  • línea 23: el método [DatePicker].getMonth() permite obtener el mes seleccionado con un objeto [DatePicker] dentro del intervalo [0,11];
  • línea 24: el método [DatePicker].getDayOfMonh() permite obtener el día del mes seleccionado con un objeto [DatePicker] dentro del intervalo [1,31];
  • línea 30: el método [TimePicker].getHour() permite obtener la hora seleccionada con un objeto [TimePicker];
  • línea 31: el método [TimePicker].getMinute() permite obtener los minutos seleccionados con un objeto [TimePicker];
  • línea 34: el método [Spinner].getSelectedItemPosition() permite obtener la posición del elemento seleccionado en una lista desplegable;
  • línea 35: el método [Spinner].getSelectedItem() permite obtener el objeto seleccionado en una lista desplegable;

El método [doAfficher], que muestra la lista de valores ingresados, es el siguiente:


    private void doAfficher(List<String> messages) {
        // se construye el texto que se mostrará
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // se muestra
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • línea 1: el método recibe una lista de mensajes para mostrar;
  • líneas 3-6: se crea un objeto [StringBuilder] a partir de estos mensajes. Para concatenar 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 una plantilla de vistas

1.19.1. Creación del proyecto

Creamos un nuevo proyecto [Exemple-18] copiando el proyecto [Exemple-13].

1.19.2. La plantilla de vistas

Queremos tomar 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], un encabezado;
  • en [2], una columna izquierda que podría contener enlaces;
  • en [3], un pie de página;
  • en [4], un contenido.

Esto se logra modificando la vista básica [activity_main.xml] de la actividad;

El código XML de 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>

      <exemples.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>
  • el encabezado [1] se obtiene con las líneas 38-54;
  • la banda izquierda [2] se obtiene con las líneas 56 a 84;
  • el pie de página [3] se obtiene con las líneas 86 a 101;
  • el contenido [4] se obtiene con las líneas 78 a 84;

La vista XML [main] utiliza 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 archivo [strings.xml] es el siguiente:


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">exemple-12</string>
    <string name="action_settings">Settings</string>
    <string name="titre_vue1">Vue n° 1</string>
    <string name="textView_nom">Quel est votre nom :</string>
    <string name="btn_Valider">Validez</string>
    <string name="btn_vue2">Vue n° 2</string>
    <string name="titre_vue2">Vue n° 2</string>
    <string name="btn_vue1">Vue n° 1</string>
    <string name="textView_bonjour">"Bonjour "</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 tener cualquier nivel de complejidad, desde una simple cadena de caracteres hasta una vista que permita ingresar información para cada elemento de la lista. Vamos a crear el siguiente [ListView]:

Image

Cada vista de la lista tiene tres componentes:

  • un [TextView] de información;
  • un [CheckBox];
  • un [TextView] en el que se puede hacer clic;

1.20.1. Creación del proyecto

Creamos un nuevo proyecto [Exemple-19] copiando el proyecto [Exemple-18].

  

Vamos a desarrollar el proyecto tal como se indica en [3].

1.20.2. La sesión

  

La sesión almacena los 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 {
  // una lista de datos
  private List<Data> liste=new ArrayList<>();

  // métodos getter y setter
...
}
  • línea 11: la lista de datos utilizada por ambas vistas;

La clase [Data] es la siguiente:


package exemples.android.architecture;

public class Data {

    // datos
    private String texte;
    private boolean isChecked;

    // constructor
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }

    // métodos getter y setter
    ...
}
  • línea 6: el texto que alimentará el primer [TextView] de cada elemento de la lista;
  • línea 7: el valor booleano que servirá para marcar o no el [checkBox] de cada elemento de la lista;

1.20.3. La actividad [MainActivity]

El código del método [@AfterInject] queda así:


  // inyección de sesión
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // se crea una lista de datos
    List<Data> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new Data("Texte n° " + i, false));
    }
}
  • líneas 12-15: inicialización de la lista de datos presentes en la sesión;

1.20.4. La vista inicial [Vue1]

La vista XML [vue1.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_titre"
    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/titre_vue1"
    android:textSize="50sp" />

  <Button
    android:id="@+id/button_vue2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/textView_titre"
    android:layout_below="@+id/textView_titre"
    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 [Button] [3];

1.20.5. La vista 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_Libellé"
        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_Libellé"
        android:layout_marginLeft="37dp"
        android:layout_toRightOf="@+id/txt_Libellé"
        android:text="@string/txt_dummy" />

    <TextView
        android:id="@+id/textViewRetirer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Libellé"
        android:layout_alignBottom="@+id/txt_Libellé"
        android:layout_marginLeft="68dp"
        android:layout_toRightOf="@+id/checkBox1"
        android:text="@string/txt_retirer"
        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] administra la vista XML [vue1]. Su código es el siguiente:


package exemples.android.fragments;

import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.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;

import java.util.List;

@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // los campos de la vista mostrada por el fragmento
  @ViewById(R.id.listView1)
  protected ListView listView;
  // el adaptador de lista
  private ListAdapter adapter;
  // inicialización completada
  private boolean initDone = false;

  @AfterViews
  void afterViews() {
    // memoria
    afterViewsDone = true;
  }

  @Click(R.id.button_vue2)
  void navigateToView2() {
    // se navega a la vista 2
    mainActivity.navigateToView(1);
  }

  public void doRetirer(int position) {
   ...
  }

  @Override
  protected void updateFragment() {
    if (!initDone) {
      // se asocian datos al [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // caso en el que el fragmento se ha (re)generado; en este caso, hay que volver a vincular el ListView a su adaptador
    listView.setAdapter(adapter);
    // caso en el que otros fragmentos han cambiado la fuente de datos; en este caso, hay que actualizar el ListView
    adapter.notifyDataSetChanged();
  }
}
  • línea 15: la vista XML [vue1] está asociada al fragmento;
  • líneas 26-30: el método [@AfterViews] no hace nada. Sin embargo, es necesario para establecer la variable [afterViewsDone] en true, ya que esta es utilizada por la clase padre [AbstractFragment];
  • líneas 42-53: el método [updateFragment], que se invoca cada vez que el fragmento va a estar visible. El método se ha escrito aquí como si el fragmento pudiera salir de la adyacencia del fragmento mostrado y, por lo tanto, reiniciar su ciclo de vida. Este no es el caso aquí, pero lo sería si la aplicación llegara a tener 3 fragmentos con una adyacencia de 1;
  • línea 44: el adaptador de [ListView] solo necesita inicializarse una vez;
  • línea 46: a este [ListView] se le asocia un adaptador de tipo [ListAdapter]. Vamos a crear esta clase. Deriva de la clase [ArrayAdapter], que ya hemos utilizado anteriormente para asociar datos a un [ListView]. Pasamos diversa información al constructor de [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 alimentar la lista,
    • una referencia al fragmento. Esta se utilizará para gestionar el clic en un enlace [Retirer] del [ListView] mediante el método [doRetirer] de la línea 38;
  • línea 50: el adaptador se asocia con [ListView]. Al mismo tiempo, la fuente de datos [listes] se asocia con [ListView]. Esta operación se realizará aquí cada vez que se muestre la vista n.º 1. En realidad, solo sería necesario realizarla cuando se haya ejecutado el método [@AfterViews]. Aquí la instrucción se ejecuta con demasiada frecuencia. Se nota la necesidad de una variable booleana que nos indique que el método [@AfterViews] acaba de ejecutarse y que, por lo tanto, el [ListView] debe volver a asociarse a su adaptador;
  • línea 52: se actualiza el [ListView]. En este ejemplo, esto no sirve de nada, ya que solo la vista n.º 1 puede modificar la fuente de datos del [ListView]. Consideremos un caso más general en el que la vista n.º 2 también pudiera cambiar la fuente de datos del [ListView]. Encontraremos ejemplos de este tipo más adelante en este documento. En este caso, al pasar de la vista n.º 2 a la vista n.º 1, el [ListView] de la vista n.º 1 debe actualizarse;

1.20.7. El adaptador [ListAdapter] del [ListView]

La clase [ListAdapter]

  • configura la fuente de datos del [ListView];
  • administra la visualización de los distintos elementos de [ListView];
  • administra 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> {

    // el contexto de ejecución
    private Context context;
    // el ID del diseño de visualización de una fila de la lista
    private int layoutResourceId;
    // los datos de la lista
    private List<Data> data;
    // el fragmento que muestra el [ListView]
    private Vue1Fragment fragment;
    // el adaptador
    final ListAdapter adapter = this;

    // fabricante
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // se almacena la información
        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: se guardan los datos del constructor;
  • línea 29: el método [getView] será llamado repetidamente por [ListView] para generar la vista del elemento n.º [position]. El resultado devuelto por [View] 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) {
        // se crea la línea actual del ListView
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // el texto
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // la casilla de selección
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // el enlace [Retirer]
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // se gestiona el clic en la casilla de selección
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // se muestra la línea
        return row;
}
  • línea 2: el método recibe tres parámetros. Solo utilizaremos el primero;
  • línea 4: se crea la vista del elemento n.º [position]. Se trata de la vista [list_data], cuyo identificador se pasó como segundo parámetro al constructor. A continuación, se recuperan las referencias de los componentes de la vista que acabamos de instanciar;
  • línea 6: se recupera la referencia del [TextView] n.º 1;
  • línea 7: se le asigna un texto proveniente de la fuente de datos que se pasó como tercer parámetro al constructor;
  • línea 9: se recupera la referencia del [CheckBox] n.º 2;
  • línea 10: se marca o no con un valor proveniente de la fuente de datos del [ListView];
  • línea 12: se recupera la referencia del [TextView] n.º 3;
  • líneas 13-18: se gestiona el clic en el enlace [Retirer];
  • línea 16: el método [Vue1Fragment].doRetirer es el que gestionará este clic. De hecho, parece más lógico que este evento sea gestionado por el fragmento que muestra el [ListView]. Este cuenta con una visión general de la que carece la clase [ListAdapter]. La referencia del fragmento [Vue1Fragment] se había pasado como cuarto parámetro al constructor de la clase;
  • líneas 20-25: se gestiona el clic en la casilla de selección. La acción realizada sobre ella se refleja en el dato que muestra. Esto se debe a lo siguiente: [ListView] es una lista que solo muestra una parte de sus elementos. Por lo tanto, un elemento de la lista a veces está oculto y otras veces visible. Cuando se debe mostrar el elemento n.º i, se llama al método [getView] de la línea 2 anterior para la posición n.º i. La línea 10 recalculará el estado de la casilla de selección a partir del dato al que está vinculada. Por lo tanto, esta debe memorizar el estado de la casilla de selección a lo largo del tiempo;

1.20.8. Eliminar un elemento de la lista

Al hacer clic en el enlace [Retirer], el fragmento [Vue1Fragment] gestiona la acción mediante el siguiente método [doRetirer]:


  public void doRetirer(int position) {
    // se elimina el elemento n.º [position] de la lista
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // se guarda la posición del desplazamiento para volver a ella
    // leer
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posición del primer elemento, ya sea que se vea por completo o no
    int firstPosition = listView.getFirstVisiblePosition();
    // desplazamiento en el eje Y de este elemento respecto a la parte superior de ListView
    // mide la altura de la parte que podría estar oculta
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // se actualiza el [ListView]
    adapter.notifyDataSetChanged();
    // nos posicionamos en el lugar correcto del ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • línea 1: se recibe la posición en el [ListView] del enlace [Retirer] en el que se hizo clic;
  • línea 3: se recupera la lista de datos;
  • línea 4: se elimina el elemento con el n.º [position];
  • línea 15: se actualiza el [ListView]. Sin esto, visualmente no cambia nada.
  • líneas 5-13, 17: un proceso bastante complejo. Sin él, ocurre lo siguiente:
    • el [ListView] muestra las líneas 15 a 18 de la lista de datos,
    • se elimina la línea 16,
    • la línea 15 anterior lo reinicia por completo y el [ListView] muestra entonces las líneas 0 a 3 de la lista de datos;

Con las líneas anteriores, se realiza la eliminación y el [ListView] permanece posicionado en la línea que sigue a la línea eliminada.

1.20.9. La vista XML [Vue2]

El código XML de 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_titre"
        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/titre_vue2"
        android:textSize="50sp" />

    <Button
        android:id="@+id/button_vue1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textViewResultats"
        android:layout_marginTop="25dp"
        android:layout_alignLeft="@+id/textView_titre"
        android:text="@string/btn_vue1" />

    <TextView
        android:id="@+id/textViewResultats"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView_titre"
        android:layout_marginTop="50dp"
        android:layout_alignLeft="@+id/textView_titre"
        android:text="" />

</RelativeLayout>
  • líneas 6-15: el componente [TextView] n.º 1;
  • líneas 26-33: el componente [TextView] n.º 2;
  • líneas 17-24: el componente [Button] n.º 3;

1.20.10. El fragmento [Vue2Fragment]

123

El fragmento [Vue2Fragment] administra la vista XML [vue2]. Su código es el siguiente:


package exemples.android.fragments;

import android.widget.TextView;
import exemples.android.R;
import exemples.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 {

    // los campos de la vista
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

    @AfterViews
    void initFragment(){
        // memoria
        afterViewsDone=true;
    }

  @Click(R.id.button_vue1)
    void navigateToView1() {
        // navegamos hacia la vista 1
        mainActivity.navigateToView(0);
    }

    @Override
    protected void updateFragment() {
        // se muestran los elementos de la lista que se han seleccionado en la vista 1
        StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
        for (Data data : mainActivity.getListe()) {
            if (data.isChecked()) {
                texte.append(String.format("(%s)", data.getTexte()));
            }
        }
        texte.append("]");
        txtResultats.setText(texte);
    }
}

El código importante se encuentra en el método [updateFragment], en la línea 32:

  • línea 34: se calcula el texto que se mostrará en el [TextView] n.º 2;
  • líneas 35-39: se recorre la lista de datos mostrada por el [ListView]. Esta se almacena en la actividad;
  • línea 36: si se ha marcado el dato n.º i, se agrega la etiqueta asociada en un tipo [StringBuilder];
  • línea 41: el [TextView] muestra el texto calculado;

1.20.11. Ejecución

Crea una configuración de ejecución para este proyecto y ejecútala.

1.20.12. Mejora

En el ejemplo anterior utilizamos una fuente de datos List<Data> donde la clase [Data] era la siguiente:


package exemples.android.fragments;

public class Data {

    // datos
    private String texte;
    private boolean isChecked;

    // fabricante
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...

}

En la línea 7, se había utilizado un valor booleano para gestionar la casilla de selección de los elementos de [ListView]. A menudo, el [ListView] debe mostrar datos que se pueden seleccionar marcando una casilla, sin que por ello el elemento de la fuente de datos tenga un campo booleano correspondiente a esa casilla. En ese caso, se puede proceder de la siguiente manera:

La clase [Data] queda así:


package exemples.android.fragments;

public class Data {

    // datos
    private String texte;

    // constructor
    public Data(String texte) {
        this.texte = texte;
    }

    // getters y setters
...
}

Se crea una clase [CheckedData] derivada de la anterior:


package exemples.android.fragments;

public class CheckedData extends Data {

    // elemento marcado
    private boolean isChecked;

    // constructor
    public CheckedData(String text, boolean isChecked) {
        // padre
        super(text);
        // local
        this.isChecked = isChecked;
    }

    // getters y setters
...
}

Luego, basta con reemplazar en todo el código (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment), el tipo [Data] por el tipo [CheckedData]. Por ejemplo, en [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // se crea una lista de datos
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

El proyecto de esta versión se le proporciona con el nombre [Exemple-19B].

1.21. Ejemplo 20: usar un menú

1.21.1. Creación del proyecto

Duplicamos el proyecto [Exemple-19B] en el proyecto [Exemple-20]:

3

Vamos a eliminar los botones de las vistas 1 y 2 para reemplazarlos 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ú de la vista n.º 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/actionCacherMontrerTout"
        android:title="@string/actionCacherMontrerTout"/>
      <item
        android:id="@+id/actionCacherMontrerActions"
        android:title="@string/actionCacherMontrerActions"/>
      <item
        android:id="@+id/actionCacherMontrerActionsValider"
        android:title="@string/actionCacherMontrerActionsValider"/>
    </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/navigationVue2"
        android:title="@string/navigationVue2"/>
    </menu>
  </item>
</menu>

Los elementos del menú se definen mediante la siguiente información:

  • android:id: el identificador del elemento;
  • android:title: el texto del elemento;
  • 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ú (etiqueta <menu>, 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/navigationVue1"
        android:title="@string/navigationVue1"/>
    </menu>
  </item>
</menu>

1.21.3. La gestión del menú en la clase abstracta [AbstractFragment]

Vamos a factorizar la gestión del menú en la clase padre [AbstractFragment] de ambas 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 {

  // datos  accesibles a las clases hijas
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;

  // actividad
  protected IMainActivity mainActivity;
  protected Activity activity;

  // sesión
  protected Session session;

  // menú
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;

  // constructor
  public AbstractFragment() {
    // inicialización
    className = getClass().getSimpleName();
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memoria
    this.menu = menu;
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // se recuperan las # opciones del menú si aún no se ha hecho
    if (!initDone) {
      // se recuperan las # opciones del menú
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // se transfiere la lista de opciones a un array
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // actividad
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memoria
      initDone = true;
    }

    // se le pide al fragmento hijo que se active
    updateFragment();
  }


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }

  // visualización de las opciones del menú -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }

  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }

  // actualizar clase hija
  protected abstract void updateFragment();
}
  • línea 42: los registros muestran que el método [onCreateOptionsMenu] se invoca cada vez que se muestra el fragmento. Se invoca muy tarde, concretamente después de que se haya invocado el 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:
    • [menu]: que es un menú vacío;
    • [inflater]: una herramienta que permite crear el menú a partir de su descripción inicial. No utilizaremos esta opción aquí, ya que usaremos una anotación AA que lo hará por nosotros;
  • línea 44: guardamos el menú. Lo necesitaremos más adelante;
  • líneas 52-53: almacenamos en el array de la línea 28 los identificadores de todos los elementos del menú;
  • líneas 55-57: los registros muestran que, cuando se invoca el 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 inicialización de la clase ya se realizó para no tener que volver a hacerla (línea 50);
  • línea 63: le pedimos al fragmento hijo que se actualice. Esto es posible porque el fragmento está visible y, al mismo tiempo, asociado a su vista y a su menú;

El método [getMenuOptions], que permite obtener los identificadores de los elementos de un menú, es el siguiente:


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

El método [setAllMenuOptions] permite ocultar o mostrar todas las opciones del menú;


  protected void setAllMenuOptions(boolean isVisible) {
    // se actualizan todas las opciones del menú
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

El método [setMenuOptions] permite ocultar o mostrar algunas de las opciones del menú;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // se actualizan ciertas opciones del menú
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

La clase [MenuItemState] es la siguiente:

  

package exemples.android.architecture;

public class MenuItemState {

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

  // constructores
  public MenuItemState() {

  }

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

  // getters y setters
...
}

1.21.4. La gestión del menú en el fragmento [Vue1Fragment]

La clase [Vue1Fragment] queda así:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {

...

  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // se navega a la vista 2
    mainActivity.navigateToView(1);
  }

  @OptionsItem(R.id.actionValider)
  void valider() {
    // se muestra un mensaje
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }

  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // se cambia de estado
    actionCacherMontrerTout = !actionCacherMontrerTout;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
  }

  private boolean actionCacherMontrerActions = true;
  @OptionsItem(R.id.actionCacherMontrerActions)
  void actionCacherMontrerActions() {
    // se cambia de estado
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }

  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // se cambia de estado
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...

  @Override
  protected void updateFragment() {
    ....
    // se actualiza el menú
    //setMenuOptions(...)
  }
}
  • línea 2: el menú [res / menu / menu_vue1.xml] se asocia 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)] indica el método que debe ejecutarse al hacer clic en la opción de menú [Navigation / Vue 2];
  • líneas 19-25: para ocultar una rama del menú, basta con ocultar su opción raíz;
  • línea 24: se muestran o se ocultan las opciones raíz [menuNavigation, menuActions];
  • línea 40: para mostrar una opción de una rama del menú, no solo hay que mostrar esa opción, sino también todas las opciones que se encuentran al remontar desde la opción hoja hasta la raíz del menú;

1.21.5. La gestión del menú en el fragmento [Vue2Fragment]

Encontramos un código similar en el fragmento de la vista n.º 2:


package exemples.android.fragments;

import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;

@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {

  // los campos de la vista
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // se navega a la vista 1
    mainActivity.navigateToView(0);
  }

  @Override
  protected void updateFragment() {
    // se muestran los elementos de la lista que se seleccionaron en la vista 1
    StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
    for (CheckedData data : session.getListe()) {
      if (data.isChecked()) {
        texte.append(String.format("(%s)", data.getTexte()));
      }
    }
    texte.append("]");
    txtResultats.setText(texte);
    // se actualiza el menú
    // setMenuOptions(...)
  }
}
  • línea 35: se muestra la opción [Navigation / Vue 1];
  • líneas 17-20: al hacer clic en la opción [Navigation / Vue1], se invoca el 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 abstracta [AbstractFragment]

El ejemplo anterior nos mostró que, cuando el fragmento tiene un menú, su método [onCreateOptionsMenu] es un buen lugar para solicitar que el fragmento se actualice:

  • se invoca exactamente una vez cuando el fragmento está a punto de mostrarse;
  • cuando se invoca, se establecen las asociaciones del fragmento con su actividad, su vista y su menú;

Para demostrarlo, retomamos el ejemplo 12, que tiene la particularidad de contar con muchos fragmentos cuya adyacencia se puede modificar. En este ejemplo, los fragmentos no tenían menú. Les asociaremos un menú vacío.

1.22.1. Creación del proyecto

Duplicamos el proyecto [Exemple-12] en el proyecto [Exemple-21]:

1.22.2. El menú de los fragmentos

  

El menú agregado 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="exemples.android.MainActivity">
</menu>

Lo que hay 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="exemples.android.MainActivity">
  <item android:id="@+id/action_settings"
        android:title="@string/action_settings"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment1"
        android:title="@string/fragment1"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment2"
        android:title="@string/fragment2"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment3"
        android:title="@string/fragment3"
        android:orderInCategory="100"
        app:showAsAction="never"/>
  <item android:id="@+id/fragment4"
        android:title="@string/fragment4"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

Cuando una actividad ya tiene un menú, el menú asociado a los fragmentos se suma al de la actividad: por lo tanto, se tienen las opciones de ambos menús. En este caso, el menú de los fragmentos estará vacío. Así que solo se verá el menú de la actividad.

1.22.3. Los fragmentos

  

Retomamos la clase abstracta [AbstractFragment] del ejemplo anterior (véase el párrafo 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 Vue1Fragment extends AbstractFragment {

En los dos fragmentos [PlaceholderFragment] y [Vue1Fragment], eliminamos las referencias a la antigua clase abstracta [AbstractFragment].

1.22.4. Ejecución

Ejecute la aplicación y compruebe que funciona. Revise los registros para ver 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: guardado y restauración del estado de la actividad y de los fragmentos

1.23.1. El problema

Aquí abordamos el problema de la rotación del dispositivo Android (vertical <--> horizontal). Para ilustrarlo, retomamos el ejemplo 21 anterior:

Image

Si giramos el dispositivo [1], obtenemos la siguiente vista:

Image

Se observa que:

  • en [1], la pestaña [Fragment n° 3] ha desaparecido;
  • en [2], el texto que se muestra 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/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • línea 1: se observa que la actividad se ha reconstruido por completo;
  • líneas 3-7: lo mismo ocurre con los cinco fragmentos gestionados por la actividad;
  • línea 21: se va a mostrar el fragmento n.º 3. Se observa que, antes del incremento, el número de visita es 0;

Entonces, el resultado obtenido tras la rotación se puede explicar de la siguiente manera:

  • la clase [MainActivity] crea inicialmente una barra de pestañas con una sola pestaña, denominada [Vue 1]. Esa es la pestaña que se ve;
  • tras la rotación del dispositivo, el administrador de páginas [mViewPager] vuelve a mostrar el mismo fragmento, es decir, en este caso el fragmento n.º 3. Hay que recordar aquí que las pestañas y los fragmentos son conceptos diferentes y tienen un ciclo de vida distinto. Se ejecutará el método [updateFragment] del fragmento n.º 3:

  public void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // incrementar número de visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // texto modificado
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • línea 7: se lee el último número de visita de la sesión. Sin embargo, esta, al igual que todo lo demás, se ha reconstruido y el número de visita se ha reiniciado a cero. Esto explica el resultado que se muestra en el fragmento n.º 3;

1.23.2. Métodos para guardar y restaurar la actividad y los fragmentos

1.23.2.1. Solución 1: guardado manual

Al girar el dispositivo, se invocan dos métodos de la actividad:


// gestión de copia de seguridad / restauración de la actividad ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // padre
    super.onSaveInstanceState(outState);
    // copia de seguridad del estado de la actividad
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // superior
    super.onCreate(savedInstanceState);
     // restauración de la actividad
    // ...
  }
  • líneas 2-8: el sistema invoca el método [onSaveInstanceState] durante la rotación. Aquí es donde se puede realizar la copia de seguridad de la actividad. Si no se hace nada, no se guarda nada. El estado de la actividad debe guardarse en el parámetro [Bundle outState] que se pasa al método. La clase [Bundle] se asemeja a un diccionario. Cuenta con el método [putString, putInt, putLong, putBoolean, putChar, ...] con dos parámetros: void putT(String key, T value);
  • líneas 10-16: el método [onCreate] se invoca al crear la actividad. Si se ha guardado el estado de esta, dicha copia de seguridad se le pasa en el parámetro [Bundle savedInstanceState]. Para recuperar los valores guardados, se dispone de métodos como [getString, getInt, getLong, geBoolean, getChar, ...] con un parámetro: T getT(String key);

Los fragmentos cuentan con estos mismos dos métodos para guardar su estado.

Vamos a utilizar esta información para guardar y restaurar el estado del ejemplo 21. Para ello, duplicamos el proyecto [Exemple-21] en [Exemple-22].

1.23.2.2. Solución 2: guardado automático

La documentación de Android indica que, al girar el dispositivo, se puede evitar la destrucción de un fragmento utilizando la instrucción: [Fragment].setRetainInstance(true). Varios artículos de [StackOverflow] recomiendan utilizar esta instrucción únicamente 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 instrucción en dos ejemplos: Ejemplo-17 (párrafo 1.18: una aplicación con un fragmento que muestra un formulario) y Ejemplo-21 (párrafo 1.22: una aplicación con cinco fragmentos). En ambos casos, esta única instrucción aplicada a todos los fragmentos de la aplicación resultó insuficiente para restaurar correctamente la vista que se mostraba al girar el dispositivo. En lugar de crear 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 predeterminado false del método [setRetainInstance(boolean )]. La instrucción: [Fragment].setRetainInstance(true) nunca se utilizó en el resto de este documento.

1.23.3. El método de respaldo y restauración del proyecto [Exemple-22]

El proyecto [Exemple-22] evoluciona de la siguiente manera:

  

Aquí aparecen dos nuevas clases:

  • [PlaceHolderFragmentState], que almacenará el estado de un fragmento de tipo [PlaceHolderFragment];
  • [Vue1FragmentState], que almacenará el estado del fragmento de tipo [Vue1Fragment];

Estas clases son las siguientes:


package exemples.android;

public class Vue1FragmentState {
  // estado Vue1Fragment
  private boolean hasBeenVisited=false;
  // getters y setters
...
}
  • línea 5: el valor booleano [hasBeenVisited] es verdadero si el fragmento [Vue1Fragment] ha sido visitado (visualizado) al menos una vez. Este campo se creó para el ejemplo, ya que el fragmento [Vue1Fragment] no tiene nada que guardar;

La clase [PlaceHolderFragmentState] es la siguiente:


package exemples.android;

public class PlaceHolderFragmentState {
  // estado visitado o no
  private boolean hasBeenVisited;
  // texto mostrado
  private String text;

  // getters y setters
...
}
  • línea 5: aparece el valor booleano [hasBeenVisited];
  • línea 7: el texto que muestra el fragmento en el momento en que debe guardarse. Ya vimos que este texto se perdió durante la rotación;

El estado de los fragmentos se almacenará en la sesión y será la actividad la encargada de guardar y restaurar dicha 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 {
  // número de fragmentos visitados
  private int numVisit;
  // N.º del fragmento de tipo [PlaceholderFragment] que se muestra en la segunda pestaña
  private int numFragment = -1;
  // N.º de la pestaña seleccionada
  private int selectedTab = 0;
  // N.º de vista actual
  private int currentView;

  // copias de seguridad de fragmentos ---------------
  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 y 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 la 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 mostrado;

La actividad guarda y restaura la sesión de la siguiente manera:


  // gestión de guardado/restauración de la actividad ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // padre
    super.onSaveInstanceState(outState);
    // guardado de sesión
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // registro
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // padre
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // recuperación de sesión
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // registro
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    }
}
  • línea 8: se guarda la sesión en forma de su cadena jSON;
  • línea 29: se restaura la sesión a partir de su cadena jSON;

Para gestionar el guardado y la restauración de los fragmentos, la clase abstracta [AbstractFragment] se modifica de la siguiente manera:


// gestión de copia de seguridad / restauración -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // superior
    super.setUserVisibleHint(isVisibleToUser);
    // ¿copia de seguridad?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // el fragmento se va a ocultar; lo guardamos
      saveFragment();
      saveFragmentDone = true;
    }
    // memoria
    this.isVisibleToUser = isVisibleToUser;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // padre
    super.onActivityCreated(savedInstanceState);
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // el fragmento debe restaurarse
    fragmentHasToBeInitialized = true;
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // elemento principal
    super.onSaveInstanceState(outState);
    // guardar el fragmento solo si está visible
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // clases hijas
  protected abstract void updateFragment();

  protected abstract void saveFragment();
  • se decide guardar el estado de los fragmentos en la sesión en dos momentos:
    • líneas 2-14: cuando el fragmento pasa de visible a oculto;
    • líneas 29-42: cuando el sistema indica que se debe realizar una copia de seguridad del fragmento y este está visible (línea 38);

Este mecanismo evita realizar guardados con más frecuencia de lo necesario. De hecho, como ya se guardó el estado del fragmento i cuando pasó de visible a oculto, cuando se muestra el fragmento j y se realiza una rotación, no es necesario volver a guardar el fragmento i. Si no se ha vuelto a mostrar desde su último guardado, entonces su estado no ha cambiado. Solo se debe guardar el estado del fragmento j. Este mecanismo también tiene otra ventaja: no solo es necesario guardar el estado de un fragmento cuando se gira el dispositivo. También existe el caso de la navegación pura entre fragmentos, por ejemplo, en un sistema de pestañas. En ese caso, queremos recuperar un fragmento en el estado en que lo dejamos la última vez que se mostró. Este estado puede haber desaparecido en parte si ese fragmento salió, en algún momento, de la adyacencia de los fragmentos mostrados. En ese caso, el fragmento no se reconstruye en su totalidad, pero sí se reconstruye su vista asociada. La copia de seguridad que se realizó cuando el fragmento quedó oculto servirá para recuperar el último estado de esa vista;

  • líneas 10, 40: para evitar realizar dos guardados sucesivos, se utiliza el valor booleano [saveFragmentDone] para indicar que ya se ha realizado un guardado;
  • líneas 9, 39: se le 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: se utiliza el método [onActivityCreated] para establecer el valor booleano [fragmentHasToBeInitialized] en verdadero. De hecho, el fragmento hijo debe saber que debe reiniciar por completo el estado del fragmento a partir de un estado que encontrará en la sesión;

También en la clase [AbstractFragment], el método [onCreateOptionsMenu] cambia de la siguiente manera:


// actualización del fragmento
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memoria
    this.menu = menu;
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // se le pide al fragmento hijo que se actualice
    updateFragment();
    // se debe realizar una copia de seguridad
    saveFragmentDone = false;
  }
  • línea 14: hemos visto que el valor booleano [saveFragmentDone] pasó a vrai cuando se realizó una guardada. En algún momento debe volver a faux. Cuando se ejecuta el método [updateFragment] (línea 12) del fragmento hijo, este se vuelve visible. Ahora bien, es precisamente cuando está visible que se debe guardar un fragmento, en el momento específico en que pase del estado visible al estado oculto. Entonces se cambia el valor del booleano [saveFragmentDone] a false para que se pueda realizar el guardado;

1.23.4. Guardado del fragmento [Vue1Fragment]

El guardado de los fragmentos se realiza en el método [saveFragment], llamado por la clase padre [AbstractFragment]:


// copia de seguridad del estado del fragmento
  @Override
  public void saveFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // se guarda el estado del fragmento en la sesión
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • líneas 9-11: guardado del estado del fragmento en la sesión. Cuando se invoca el método [saveFragment], el fragmento está visible. Por lo tanto, hay que establecer el valor booleano [hasBeenVisited] en vrai (línea 10);

1.23.5. Guardar el fragmento [PlaceHolderFragment]

El guardado de los fragmentos se realiza en el método [saveFragment], llamado por la clase padre [AbstractFragment]:


  @Override
  public void saveFragment() {
    // se guarda el estado del fragmento en la sesión
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • líneas 4-7: guardado en sesión del estado del fragmento;
  • línea 5: se guarda el texto que muestra actualmente el [TextView] textViewInfo;
  • línea 6: el valor booleano [hasBeenVisited] del fragmento se cambia a vrai;
  • línea 7: el estado del fragmento se registra en la tabla [placeHolderFragmentStates]. El número del elemento que se debe inicializar es el número de sección del fragmento menos uno;

1.23.6. Restauración del fragmento [Vue1Fragment]

La restauración de los fragmentos se realiza en el método [updateFragment]:


@Override
  protected void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // ¿restauración?
    if (fragmentHasToBeInitialized) {
      // restauración del estado
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // ¿navegación?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // incremento del número de visita
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // se muestra el número de visita
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // cambio del número de la vista actual
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • líneas 8-12: restauración del estado del fragmento. El valor booleano [fragmentHasToBeInitialized] fue inicializado por la clase padre [AbstractFragment]. Cuando su valor es vrai, el fragmento acaba de ser reconstruido y es necesario reinicializarlo. Aquí es donde ocurre esto. En este ejemplo concreto, no hay nada que hacer. Simplemente hemos mostrado que se puede recuperar el valor de la variable booleana [hasBeenVisited] en el estado guardado del fragmento (línea 10);
  • línea 11: no hay que olvidar volver a establecer [fragmentHasToBeInitialized] en faux, para que, cuando regresemos posteriormente a este fragmento sin que haya habido una rotación del dispositivo, no se vuelva a realizar una inicialización innecesaria del fragmento;
  • líneas 18-26: incremento del contador de visitas. Aquí hay una dificultad: cuando se restaura el fragmento, no queremos incrementar este contador. Debemos distinguir aquí entre:
    • una simple navegación que lleva al usuario de vuelta a la pestaña [Vue 1];
    • una restauración cuando el usuario gira su dispositivo mientras se muestra la pestaña [Vue 1];

Distinguimos estos dos casos gracias al número de vista almacenado en la sesión. Este número es el de la última vista mostrada (línea 28).

  • línea 18: se produce una navegación y no una restauración si el número de la última vista es diferente al de la vista actual;
  • líneas 21-25: incremento del contador de visitas y su visualización;

1.23.7. Recuperación del fragmento [PlaceHolderFragment]

La restauración de los fragmentos se realiza en el método [updateFragment]:


  // datos
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...

public void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // ¿De qué fragmento se trata?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // ¿Se debe inicializar el fragmento?
    if (fragmentHasToBeInitialized) {
      // texto inicial
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // ¿navegación?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // incrementar número de visitas
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // texto modificado
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // se trata de una restauración
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // visualización de texto
    textViewInfo.setText(newText);
    // vista actual
    session.setCurrentView(numView);
}
  • líneas 15-16: se determina 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 guardado/restauración tras un cambio de orientación del dispositivo. Aquí hay que restaurarlo. Por lo general, se trata de restaurar ciertos campos del fragmento;
  • línea 20: el campo [text] de la línea 2 debe contener el texto inicial que muestra el fragmento: [Hello world from section i]. Aquí debe regenerarse;
  • línea 21: se observa que se ha realizado la inicialización del fragmento;
  • líneas 24-36: al igual que anteriormente con el fragmento [Vue1Fragment], 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 la restauración;
  • línea 34: el estado del fragmento antes de la rotación del dispositivo se recupera de la sesión;
  • línea 35: allí se recupera el texto que se mostraba en ese momento;
  • línea 38: este texto se vuelve a mostrar;
  • línea 40: se registra en la sesión el número de la nueva vista mostrada;

1.23.8. Manejo de las pestañas

En los párrafos anteriores no se abordó la gestión de las pestañas. Sin embargo, en el ejemplo 21 observamos un problema al girar el dispositivo: solo se conservó la primera pestaña, [Vue 1]. La segunda pestaña se perdió.

Resolvemos este problema en la clase [MainActivity] de la siguiente manera:


@AfterViews
  protected void afterViews() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // barra de herramientas
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // primera pestaña
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // ¿Segunda pestaña?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }

    // ¿qué pestaña seleccionar?
    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 saber si hay que crearla, se revisa en la sesión el número del fragmento que se mostraba en la pestaña 2. Si ese número es distinto de -1, su valor inicial, entonces se crea la segunda pestaña. En este punto, tenemos dos pestañas, de las cuales, por defecto, la primera está seleccionada;
  • línea 26: buscamos en la sesión el número de la pestaña que estaba seleccionada antes del guardado o la restauración y la seleccionamos nuevamente. Si el campo [selectedTab] aún no ha sido inicializado por el código, se utiliza su valor inicial 0;