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. Control de 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 denominada 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 IDE con un complemento de 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 recoge la totalidad del documento 4 con las siguientes diferencias:
    • IDE es ahora 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 lleva a cabo con la biblioteca RxJava (en el documento 4 se utilizaba la biblioteca AA);
  • Este documento explora ámbitos que no se han tratado, o se han tratado 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 Android que se comunica con un servicio web / jSON, en el que se factorizan un gran número de elementos que suelen encontrarse en este tipo de clientes. Este esqueleto se retoma 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 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
Gestión de fragmentos en una aplicación de Android
13
Navegación entre vistas revisada
14
Arquitectura de dos capas
15
Arquitectura cliente/servidor
16
Gestión de la asincronía con RxJava
17, 17B
Componentes de introducción de datos
18
Uso de una plantilla de vistas
19
El componente ListView
20
Utilizar un menú
21
Uso de una clase principal para los fragmentos
22, 22B
Guardar y restaurar el estado de la actividad y de los fragmentos
23
Cliente de información meteorológica
Chap 2
Estructura básica de un cliente Android que se comunica con un servicio web / jSON. En ella se agrupan numerosos elementos que suelen aparecer en este tipo de clientes Android.
Chap 3
Gestión de citas de una consulta médica
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 curso de la Escuela de Ingeniería IstiA de la Universidad de Angers [istia.univ-angers.fr]. Esto explica el tono, en ocasiones un poco peculiar, del texto. Los dos ejercicios prácticos son textos de TP de los que solo se ofrecen las líneas generales de la solución. El lector debe elaborarla por sí mismo.

El código fuente de los ejemplos está disponible |ICI|. Para ejecutar estos ejemplos, debe seguir el procedimiento descrito en el apartado 6.12.

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

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

1.1.2. Requisitos previos

El requisito previo para sacar el máximo partido a este documento es un buen dominio del lenguaje Java.

1.1.3. Herramientas utilizadas

Los ejemplos que siguen se han probado en el siguiente entorno:

  • ordenador 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 apartado 6.8);
  • el gestor de emuladores Android Genymotion (véase el apartado 6.9);
  • el gestor de dependencias Maven (véase el apartado 6.10);
  • el IDE [Android Studio] (véase el apartado 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 [exemples] vacía donde se almacenarán todos nuestros proyectos:

  

y, a continuación, creemos un proyecto con Android Studio. En primer lugar, importaremos uno de los ejemplos que se incluyen con el IDE [1-5]:

 

Image

La importación del proyecto puede dar lugar a errores debido a la incompatibilidad entre el entorno utilizado al crear el proyecto y el que se utiliza aquí para su ejecución. 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 mediante el siguiente archivo [build.gradle] [2]:


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

apply plugin: 'com.android.application'

repositories {
    jcenter()
}

dependencies {
    compile "com.android.support:support-v4:23.3.0"
    compile "com.android.support:support-v13:23.3.0"
    compile "com.android.support:cardview-v7:23.3.0"
}

// La compilación de ejemplo utiliza varios directorios para
// separar el código repetitivo y común del
// el código principal del ejemplo.
List<String> dirs = [
    'main',     // código principal de la muestra; aquí encontrarás lo más interesante.
    'common',   // componentes que se reutilizan en varios ejemplos
    '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 señalado se debe a las líneas 31, 34 y 35: no disponemos de la versión 21 de SDK. Sustituimos esta versión por la versión 23, de la que sí disponemos.

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

 

Para aceptar las sugerencias, se introduce «[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 discrepancia entre la versión de Gradle que requiere el archivo [build.gradle] del proyecto (la 2.10, línea 6, que se muestra 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 sustituir 2.8 por 2.10.

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

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

 

Image

Detengamos la aplicación:

  

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

  

1.2.2. Algunas observaciones sobre el IDE

1.2.2.1. Las perspectivas

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

  • 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, a continuación, lo vuelve a iniciar [1].

1.2.2.3. Gestión de la caché

Android Studio mantiene una caché de los proyectos que gestiona 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 tenía en cuenta los cambios de código que se acababan de realizar. En ese caso, hay que invalidar dicha caché:

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

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

En todo lo que viene a continuación, se ha trabajado con esta configuración de la caché y no se han encontrado problemas.

1.2.2.4. Gestión 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 resulta útil cuando se desea ver los registros de una acción concreta:

  • 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 concreto.

Los registros son muy útiles para saber en qué momentos de la ejecución de un proyecto se muestran determinados métodos. Recurriremos a ellos con frecuencia. Tomemos como ejemplo 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 resulta difícil determinar si un método del ciclo de vida de este tipo 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, hay varias fuentes que pueden enviar mensajes a la consola de registros. Para poder diferenciarlas, se utiliza este primer argumento. El segundo argumento es el mensaje que se va a 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 este modo, 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 bien, si hacemos clic en la opción del menú en el emulador de Android [1]:

  

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


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

A partir de ahora, 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 progresivamente el ciclo de vida de una aplicación de Android.

1.2.2.5. Gestión del emulador [Genymotion]

A veces, el emulador Genymotion se cuelga y no se puede volver a iniciar. 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 haya:

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 denominada [debug] y otra denominada [debug-unaligned]. Hay que 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, basta con arrastrarlo y soltarlo 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, aparece 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 ha sido 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 sobre todo aquí donde introduciremos 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;

Estos dos datos proceden de la información introducida al crear el proyecto:

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

Volvamos al manifiesto:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="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 icono [2] de la aplicación. Se puede modificar;
  • 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 caracteres utilizadas por la aplicación. En la línea 2, el nombre de la aplicación procede de la entrada realizada durante la creación del 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. En este caso, 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] hereda de la clase de Android [AppCompatActivity]. Así ocurrirá con 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 gestor 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 mostrado. 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 dicho 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] que se encuentre 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 tener en cuenta 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], mantener 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 gestor de emuladores [Genymotion] (véase el apartado 6.9);
  • en [12], seleccione un emulador de tableta e inicie [13];
  • en [14], ejecute la configuración de ejecución [app];
  • En [15] aparece el formulario de selección del dispositivo de ejecución. Aquí solo hay uno disponible: el emulador [Genymotion] iniciado anteriormente;

Al cabo de unos instantes, el emulador de software muestra la siguiente vista:

Image

1.3.5. El ciclo de vida de una actividad

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


package exemples.android;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

El método [onCreate] de las líneas 8-12 forma parte de los métodos que pueden invocarse durante el ciclo de vida de una actividad. La documentación de Android ofrece una lista de los mismos:

 
  • [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 recuperan 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 que se ejecuta antes de llegar al estado [4] de la actividad que se está ejecutando;

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 vista [Project] para ver el proyecto de Android al completo;

Nota: entre [14] y [15], se ha pasado de una perspectiva [Android] a una perspectiva [Project] (véase el apartado 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, por comodidad, llamaremos 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 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 añaden las dos dependencias que conforman la biblioteca AA;

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

  • 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, es necesario utilizar 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, comprueba que la configuración de ejecución [app] sigue funcionando.

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

  

La clase [MainActivity] tiene, por el momento, el siguiente aspecto:


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 apartado 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 propiamente dicha. 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_] hereda de la clase [MainActivity];

No vamos a intentar explicar el código de las clases generadas por AA. Estas se encargan de gestionar la complejidad que las anotaciones pretenden ocultar. Sin embargo, a veces puede resultar útil examinarlo cuando se quiere 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 apartado 1.4:

Nosotros:

  • duplicamos el proyecto [Exemple-03] en [Exemple-04] (tras 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, a continuación, ejecutemos el proyecto;
 

1.5.2. Crear una vista

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

  • en [1-4], crea una nueva vista XML;
  • en [5], asigne un nombre a la vista;
  • en [6], indique la etiqueta raíz de la vista. En este caso, 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], selecciona la pestaña [Design] en la vista [vue1.xml] que se muestra;
  • en [2-4], pasa al modo tableta;
  • en [5], ajusta la escala de la tableta a 1;
  • en [6], selecciona el modo «apaisado» para la tableta;
  • la captura de pantalla [7] resume las opciones seleccionadas.
  • en [1], seleccione un [Large Text] y arrástrelo a la vista [2];
  • en [3], haz doble clic en el componente;
  • en [4], modifica el texto que se muestra. En lugar de introducirlo directamente en la vista XML, lo externalizaremos al archivo [res / values / string.xml]
  • en [5], se añade un nuevo valor al archivo [strings.xml];
  • en [8], se asigna un identificador a la cadena;
  • en [9], se asigna el valor a la cadena;
  • en [10], la nueva vista tras la validación del 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 los caracteres [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 de [TextView] son valores por defecto o se derivan de la posición del componente en la vista;
  • líneas 7-8: el tamaño del componente es el del texto que contiene (wrap_content) tanto en altura como en anchura;
  • línea 13: la parte superior del componente está alineada con la parte superior de la vista (línea 13), 50 píxeles por debajo (línea 13);
  • línea 12: el lado izquierdo del componente está alineado con el margen 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 vista siguiente [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 sorprendentes. Puede ser preferible utilizar las propiedades de los componentes:

El componente [textView1] debe colocarse 50 píxeles por 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 60 píxeles a la derecha del componente [textView1] y alinearse por la parte inferior con este 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 este 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 por debajo del componente [textView1] y alinearse a la izquierda con este mismo componente;

 
  • En [1], el borde izquierdo (left) del componente está alineado con el borde izquierdo (left) del componente [textView1] y se sitúa 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 recoge todo lo que se ha hecho de forma gráfica. Otra forma de crear una vista es, por tanto, escribir directamente este archivo. Cuando uno se acostumbra, puede resultar más rápido que utilizar 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 proceden 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];

Modifica 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 impide que aparezca el teclado en cuanto se muestra la vista [vue1]. De hecho, esta vista tiene un campo de entrada que tiene el foco al mostrarse la vista. Este foco hace que, por defecto, aparezca el teclado virtual;

Ejecuta la aplicación y comprueba que se muestra efectivamente la vista [vue1.xml]:

Image

1.5.3. Gestión 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 introducido
    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 este motivo, no puede tener el ámbito [private]. El campo identificado por [R.id.editTextNom] procede 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 utilices caracteres acentuados en los identificadores [id]. AA no los gestiona correctamente.

  • Línea 32: la anotación [@Click(R.id.buttonValider)] designa el método que gestiona el evento «Click» en el botón con el identificador [R.id.buttonValider]. Este identificador también procede de la vista [vue1.xml]:

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_valider"
    android:id="@+id/buttonValider"
    android:layout_alignBottom="@+id/editTextNom"
    android:layout_toRightOf="@+id/editTextNom"
    android:textSize="30sp"
    android:layout_marginLeft="60dp"/>
  • línea 35: muestra el nombre introducido:
    • Toast.makeText(...).show(): muestra 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 va a mostrar makeText,
    • el tercer parámetro es la duración 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;

Ejecuta el proyecto [Exemple-04] y comprueba que ocurre 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í consiste en 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, se seguirá el procedimiento descrito para duplicar [Exemple-02] en [Exemple-03] en el apartado 1.4 y que se ha reproducido en el apartado 1.5.

1.6.2. Añadir una segunda actividad

Para gestionar una segunda vista, vamos a crear una segunda actividad. Será esta la que gestione la vista n.º 2. En este caso 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>

Se trata de una vista que, por el momento, está vacía y cuenta con un gestor de diseño de tipo [RelativeLayout] (línea 2). En la línea 11, vemos 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á gestionado:

  

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 introducido 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 que hay que seguir para cambiar de vista:

  1. línea 6: crear un objeto de tipo [Intent]. Este objeto permitirá especificar tanto la actividad que se va a iniciar como la información que se le va a 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 forma opcional, introducir 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 introducido en el campo de entrada, no devuelve un tipo [String], sino un tipo [Editable]. Hay que utilizar el método [toString] para obtener el texto introducido;
  4. línea 12: ejecuta la actividad definida por el objeto [Intent].

Ejecute el proyecto [Exemple-05] y compruebe que se muestra 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 nos sirve, y a continuación 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>

Ejecuta el proyecto [Exemple-05] y comprueba que se muestra 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 introducido 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, hemos incluido información para [SecondActivity] que no se ha utilizado. Ahora la utilizamos y esto se lleva a cabo 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) {
          // se muestra
          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 pasa de una actividad a otra y, a priori, no estaba claro si el método anotado [@AfterViews] se ejecutaría una vez durante la instanciación inicial de la actividad o cada vez que se iniciara la actividad. Las pruebas demostraron que se cumplía la segunda hipótesis;
  • 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 dicho nombre.

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, hay que sustituir [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 sustituir [SecondActivity] por [SecondActivity_];

Prueba esta nueva versión. Escribe un nombre en la vista n.º 1 y comprueba que la vista n.º 2 lo muestra 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:

  • introducir 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 de la siguiente manera:


  @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 introduce 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 asignarla a [intent1] con el mismo valor asociado;
  • línea 15: se inicia la actividad [MainActivity_].

En el código de [MainActivity] se añade 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 volver de la vista n.º 2 a la vista n.º 1, debería aparecer el nombre introducido inicialmente, lo que hasta ahora no ocurría.

1.6.7. Ciclo de vida de las actividades

En el apartado 1.3.5 hemos presentado 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, añadimos 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 varias veces;
  • línea 8: queremos saber si el método [onCreate] se invoca una o varias veces;
  • línea 14: queremos saber si el método [afterViews] se invoca una o varias veces;

Hacemos exactamente lo mismo en la clase [SecondActivity].

Al iniciar la aplicación, obtenemos 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 han ejecutado 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 han ejecutado 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 vuelve a instanciar. 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, se vuelve a instanciar la clase [SecondActivity].

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

Ahora vamos a descubrir una arquitectura con una única actividad capaz de gestionar varias vistas denominadas «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 mediante pestañas

Aquí vamos a explorar las interfaces con pestañas. El ejemplo es complejo, pero introduce todos los elementos que vamos a utilizar posteriormente: actividad única, gestor 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], seleccionamos una actividad con pestañas (Tabbed Activity);
  • en [10-14], se mantienen los valores propuestos por defecto;
  • en [15], se eligen pestañas con barra de título;

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 ha generado 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 con 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 aplicaciones. 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. Así, 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 (gestor 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) de 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). Se trata de un botón en el que se puede hacer clic. Por defecto, se sitúa en la parte inferior derecha de la vista global;

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

No obstante, precisemos 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 de pantalla predeterminados, según las directrices 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 forma 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] relativo a 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 gestor 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 gestor de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al gestor de fragmentos
    // es decir, que el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor 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 gestor de fragmentos
  // es al que se le solicitan los fragmentos que se deben mostrar 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). Hay que proporcionar a este contenedor un gestor de vistas o fragmentos. Es el desarrollador quien lo proporciona;
  • línea 25: el gestor 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 gestor 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 gestor 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 gestor de fragmentos;
  • línea 48: se recupera una referencia a la barra de pestañas mediante su identificador;
  • línea 49: el gestor de pestañas está asociado al contenedor de fragmentos. Esto significa que, cuando se haga clic en la pestaña n.º i, el contenedor mostrará el fragmento n.º i. La asociación establecida entre el gestor de pestañas y el contenedor de fragmentos nos evita tener que gestionar las pestañas. De este modo, no tenemos que definir un gestor de eventos para el clic en una pestaña. La asociación con el contenedor de fragmentos lo proporciona por defecto. Veremos un ejemplo en el que habrá más fragmentos que pestañas. En ese caso, no se realiza esta asociación.

El gestor de fragmentos [SectionsPagerAdapter] es el siguiente:


// el gestor de fragmentos
  //: es 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);
    }

    // indica 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 esta. El gestor de fragmentos lo define el desarrollador;
  • línea 5: el gestor de fragmentos hereda de 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 hay que 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 gestor de pestañas se ha asociado al gestor de fragmentos, el título de la pestaña n.º i será el título del fragmento n.º i. Así, los títulos de las líneas 27-33 serán los títulos de las pestañas;

  • líneas 18-21: getCount indica el número de fragmentos gestionados, en este caso tres;
  • líneas 11-15: getItem(i) devuelve el fragmento n.º i. En este caso, 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 tipo [PlaceholderFragment], como se indica a continuación:


// 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 ha instanciado 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] hereda de la clase de Android [Fragment]. Por lo general, 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 que 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 más abajo):

<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 debe asignarse al argumento $1 de la línea (d) anterior;
    • [getArguments] proporciona la referencia del paquete de argumentos del fragmento. Hay que recordar aquí que cada argumento se ha creado 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 integrada
      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 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. Gestió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 gestiona de la siguiente manera en la actividad [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Expande el menú; esto añade elementos a la barra de acciones, si está presente.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Gestiona 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 procesa el archivo [res / menu / menu_main.xml]. Al objeto [Menu menu], pasado como parámetro, se le asignan las opciones de menú definidas en dicho archivo;
  • línea 5: se indica que se ha creado el menú;
  • líneas 8-21: el método [onOptionsItemSelected] se ejecuta en cuanto se hace clic en una opción del menú;
  • línea 13: la referencia de la opción del menú en la que se ha hecho 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 se ha procesado (línea 17);
  • línea 20: se pasa el evento a la clase principal;

Para ver mejor lo que ocurre con este menú, añadimos 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 añade 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");
    // Gestiona 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) {
      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 gestiona 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 efímeros 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 sobre el que se ha hecho clic (línea 5). La vista principal que se encontrará será la vista [activity_main]. El segundo argumento es el mensaje que se va a 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, de este modo, activar una acción. En este caso, no hay ninguna acción asociada al clic en el mensaje. Por último, 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 aprecia en el texto que aparece en [4]. También se puede observar 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 es posible 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 la primera vez que se muestra el fragmento y cada vez que la actividad debe volver a mostrarlo;

Para seguir el ciclo de vida de la actividad y de los fragmentos, añadimos 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;

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


  // el gestor de fragmentos
  // es al que se le solicitan los fragmentos que se deben mostrar 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] crea una instancia de 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 se había instanciado. Ahora, volvamos 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 ha vuelto a ejecutar. Este comportamiento se repite con los otros dos fragmentos.

De estos registros, cabe destacar que:

  • la actividad se ha instanciado y, a continuación, se ha inicializado una vez;
  • cada fragmento se ha instanciado una vez;
  • que el método [onCreateView] de cada fragmento se ha ejecutado 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 aún no lo están. Esto explica, por ejemplo, que al inicio, cuando hay que mostrar el fragmento n.º 1, sean los fragmentos 1 y 2 los que se hayan 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, lo solicita una vez al gestor de fragmentos [SectionsPagerAdapter]. A continuación, ya no lo vuelve a solicitar y sigue utilizando el que ha obtenido.

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, con el fragmento 1, una operación 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, sigue el procedimiento del apartado 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 para Android, hay que utilizar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

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

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    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 añadido la configuración necesaria para utilizar la biblioteca [Android Annotations] (véase el apartado 1.4).

1.8.3. Añadido 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 gestor de fragmentos
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // el contenedor de fragmentos
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // el gestor 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 gestor de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al gestor de fragmentos
    // es decir, que el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor 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 gestionada por AA. Su parámetro [R.layout.activity_main] es el identificador de la vista [activity_main.xml] asociada a la actividad;
  • líneas 11-12: el componente identificado por [R.id.tabs] se inyecta en el campo [tabLayout]. Se trata del gestor de pestañas;
  • líneas 14-15: el componente identificado con [R.id.fab] se inyecta 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 han inicializado;
  • además, se han añadido registros para visualizar 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.

Llegados a este punto, vuelve a ejecutar el proyecto y comprueba que sigues 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]. Vamos a volver a un caso de uso más habitual, 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 el ejemplo 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 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;

  // 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 está anotado 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) más abajo):

<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 mostrar la vista asociada al fragmento. Se puede utilizar para actualizar la interfaz visual que se va a mostrar;
  • línea 47: hay que 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 establecidos para seguir el ciclo de vida del fragmento nos lo dirán. Por el momento y por precaución, se realiza una comprobación de nulidad;
  • línea 51: actualizamos la información del campo [textViewInfo] con el argumento entero pasado al fragmento durante su creación;

La clase [MainActivity] pierde su clase interna [PlaceholderFragment] y su gestor 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 de tipo [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 son también 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]);

Ejecuta el proyecto [Exemple-07] y comprueba que sigue 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, a partir de 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 dicho texto ya se había creado en el paso anterior, al mostrar el fragmento n.º 2. Recordemos, en efecto, 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 habría podido hacerlo.

Ahora, volvamos 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 no se ha llamado al método getItem. Como ya se ha dicho, 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, verdad? Se vuelven a ejecutar todos los métodos del fragmento n.º 3.

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 va a 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, deben inicializarse 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-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, que no se conserva en memoria.

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

1
2
3
4
05-28 14:11:18.353 8809-8809/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 va a mostrar el fragmento 1, el fragmento 2 también debe inicializarse. Ya lo está 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, que por lo tanto no se conserva en 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 va a mostrar el fragmento 2, hay que 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 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 por defecto, no se puede utilizar ninguno de los métodos [onCreateView, onResume] para actualizar el fragmento que se va a mostrar, ya que no hay garantía 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 apartado 1.7.6):

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

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

Vamos a insertar estos métodos en los fragmentos para 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 mostrada por 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 visible);
  • líneas 10-11: se crea la vista del fragmento 1 (no necesariamente 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 vuelva al fragmento 1 o a un fragmento adyacente, se volverá a ejecutar el ciclo de vida de dicho fragmento;

Volvemos 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 vuelva al fragmento 3 o a un fragmento adyacente, se volverá a ejecutar el ciclo de vida de este fragmento;

1.8.7. setUserVisibleHint

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


package exemples.android;

....

// 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 se observa 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 dicho 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 a continuación 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 someterse a una operación [onDestroyView]. No obstante, he podido observar en otras aplicaciones, en particular en la del capítulo 3, que esto no ocurría sistemáticamente;

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 gestor de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al gestor de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor de fragmentos
    mViewPager.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, 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 20: establecemos el número de fragmentos adyacentes que se deben inicializar igual al número total de fragmentos menos uno. 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 de ellos;

En total, deberíamos ver todos los fragmentos instanciados e inicializados al iniciar la aplicación y, a partir de ahí, nunca más. Esto es lo que vamos a comprobar ahora examinando 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 y 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;

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

A continuación, 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 han instanciado e inicializado al inicio. A partir de ahí, los métodos de su ciclo de vida ya no se ejecutan. Se trata de 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 adyacencia de fragmentos elegida por 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 producirse antes o después del ciclo de vida del fragmento. Depende de la adyacencia de fragmentos elegida por el desarrollador. Esto supone un problema, ya que si el ciclo de vida aún no se ha producido, 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 hace 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). Dado que utilizamos las anotaciones AA, normalmente no tenemos que escribir el método [onCreateView]. Por lo tanto, el método [onResume] parece 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

En esta ocasión, solo aparece 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, disponemos de 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 denominaremos una visita. Por lo tanto, habrá que actualizar su visualización cada vez que se muestre. Este es precisamente el problema que queremos resolver.

Antes de nada, 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 solicitan recursos que no están disponibles. Para liberar dichos recursos, el sistema tomará la iniciativa de eliminar ciertas actividades. En ese momento se invocará el método [onDestroy] de la actividad y los fragmentos.

1.8.9. OnDestroy

Vamos a permitir que el usuario elimine la actividad mediante una opción de menú [5]. Para ello, añadimos 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 añade 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 sobre la opción [Terminate]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Gestiona 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) {
      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 finaliza mediante una acción de software;

Ahora ejecutemos esta nueva versión y, en cuanto se muestre la primera vista, hagamos clic en la opción de menú [Terminate]. Los registros son entonces los siguientes:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/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, por tanto, que el método [onDestroy] de la actividad y de 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, con el 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 apartado 1.4.

1.9.2. Reescritura del fragmento [PlaceholderFragment]

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


package exemples.android;

import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;

// 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;
  // 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.º 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: es posible que el método [@AfterViews] se ejecute varias veces. Se utilizaba para inicializar el texto del fragmento (línea 42). Seguimos haciéndolo, pero para que solo se haga una vez, gestionamos una variable booleana [initDone] (línea 44) que indica que la inicialización ya se ha realizado y que no es necesario volver a hacerla;
  • líneas 56-59: introducimos el método [onDestroyView] para señalar 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 pueden ejecutarse dos métodos tras el 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 tras el método [@AfterViews]. Los registros han mostrado que al menos uno de los dos se ejecuta tras el método [@AfterViews]. Los registros nunca han mostrado que ambos puedan ejecutarse juntos tras el 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 va a 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 el estado de visibilidad del fragmento;
  • 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 va a ocultarse, se restablece el valor booleano [updateDone] a false. De hecho, necesitamos un evento para restablecer a false la variable booleana [updateDone], que se ha establecido en true tan pronto como se llama al método [update], con el fin de que se puedan realizar nuevas actualizaciones. Para ello, aprovechamos el hecho de que el fragmento ya no es visible. Cuando vuelva a ser visible, habrá que volver a actualizar el fragmento;
  • 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 siguiente ciclo de vida del fragmento. Por ahora, debemos establecer el valor booleano [afterViews] en 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 gestor 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 y 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 queda ahora oculto;
  • línea 4: se invoca el método [setUserVisibleHint] del fragmento 2 con [visibleToUser] a true. El fragmento 2 ya es 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 invoca el 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 queda ahora oculto;
  • línea 2: el fragmento 4 ya es visible. Cabe señalar que no se ejecuta el ciclo de vida del fragmento 4, ya que este ya se ha llevado a cabo dos pasos antes;
  • línea 3: el fragmento 1 sale de la adyacencia del fragmento 4 mostrado. 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 queda ahora oculto;
  • línea 2: el fragmento 5 ya está visible. Cabe señalar que no se ejecuta el ciclo de vida del fragmento 5. Esto ya se ha hecho dos pasos antes;
  • línea 3: el fragmento 2 sale de la adyacencia del fragmento 5 mostrado. 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 y 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 han mostrado 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 seguir 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;

Se pasa 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;

Se pasa 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;

Se pasa 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;

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

Ahora, establezcamos una adyacencia nula y veamos qué ocurre. 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, vemos 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. Denominamos aquí «estado de la aplicación» al 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 se encuentra el estado E2 de la aplicación para actualizarlo?

Hay 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 hemos encontrado 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 directamente 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 vamos a optar por otra vía.

Hemos hablado del estado de la aplicación: el conjunto de datos gestionados por la aplicación a lo largo del tiempo. En este caso, la aplicación está formada por una actividad y n fragmentos, todos ellos instanciados una única 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 ha creado. Dado que todos los fragmentos tienen acceso a la actividad, parece lógico 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 añadiendo 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 igual a 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 y 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 -- accepting and dropping
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, en cuanto 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

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 apartado 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 guardan 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 el código sea más legible.

La clase [Session] tendrá el siguiente aspecto:

  

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 ámbito (o tiempo de vida) de la clase así anotada. En este caso, 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. A continuación, la referencia de una clase anotada con [EBean] puede 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 ámbito [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 entrar en el 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. Habríamos podido incluir 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í se decide no asignar ninguna función específica a este método y trasladar el código específico de un fragmento al método [update], que está diseñado para ello;
  • línea 43: muestra el número de la visita;

Cuando se ejecuta esta aplicación con 5 fragmentos y 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 «swipe» o 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 por defecto no siempre es deseable. Vamos a aprender a desactivar el deslizamiento de vistas (swipe).

Volvamos a la vista principal XML:

  

En el código XML de la vista encontramos 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 designa la clase que gestiona las páginas de la actividad. Esta clase se encuentra en la actividad [MainActivity]:


import android.support.v4.view.ViewPager;
...

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

  // el gestor 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, hay que 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 hay que 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 autorizado?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

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

}
  • línea 8: la clase [MyPager] hereda de la clase de Android [ViewPager] (línea 4);
  • al deslizar la mano, se pueden invocar los controladores 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, hay que utilizar a partir de ahora nuestro nuevo gestor de páginas. Esto se hace en la vista XML [activity_main.xml] y en la actividad principal [MainActivity]. En [activity_main.xml] 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 gestor 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 gestor de fragmentos
    // es decir, que el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor 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 gestor de páginas tiene ahora el tipo [MyPager];
  • línea 23: se desactiva o no el desplazamiento con el ratón.

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

1.10.6. Desactivar el desplazamiento entre fragmentos

Continuemos con una mejora del gestor 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 denomina smoothScrolling. Este comportamiento puede resultar molesto si hay muchas pestañas. Se puede desactivar añadiendo el siguiente código en el gestor 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 gestor de pestañas se ha asociado al gestor 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] anterior (línea 9). [position] es el número del fragmento que se va a 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. En este caso, el segundo argumento es el valor del campo de la línea 4, campo que el desarrollador puede establecer mediante el método de las líneas 16-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);
...

Vuelve a ejecutar el proyecto y comprueba que ya no aparece scrolling entre las pestañas 1 y 4, por ejemplo. A partir de ahora, 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 vamos a aprender 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 añaden 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 añadido las líneas 6-9;

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

  

La clase [Vue1Fragment] tendrá el siguiente aspecto:


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 introducido
    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_]. Hay que tenerlo en cuenta. El fragmento está asociado a la vista [vue1.xml];
  • líneas 14-15: el componente identificado como [R.id.editTextNom] se inserta en el campo [editTextNom] de la línea 15;
  • líneas 18-20: el método [doValider] gestiona 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 gestor 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: aparece [FRAGMENTS_COUNT] fragmentos: [FRAGMENTS_COUNT-1] fragmentos de tipo [PlaceholderFragment] (líneas 14-21) y un fragmento de tipo [Vue1Fragment_], línea 23 (atención al guión bajo);

Compila y, a continuación, 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 se pueda 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

Añadimos al proyecto la opción de mostrar o no los registros del modo de depuración. Para ello, añadimos 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);
  }

  // actualización de fragmento
  protected void update() {
    ...
    // se solicita a la clase hija que se actualice
    updateFragment();
  }

  protected abstract void updateFragment();
}
  • línea 7: la clase [AbstractFragment] hereda de la clase de Android [Fragment];
  • cualquier 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 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 en la que 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 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 hará visible
    if (isVisibleToUser) {
      // actualizar fragmento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // salida del fragmento
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // elemento principal
    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 solicita 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 apartado 1.10.8), lo que a su vez permite acceder 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] hereda de 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 futuros proyectos;

Los métodos [@AfterViews] y [updateFragment] siguen siendo similares a los del 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()));
    }
    // incrementar 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 introducido
    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] hereda de la clase [AbstractFragment];
  • líneas 18-26: el método [@AfterViews] no tiene nada interesante que hacer. No obstante, 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 visita (línea 48) e incrementar dicho 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 heredan de la clase [AbstractFragment];
  • los datos que se van a compartir 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 gestiona las pestañas, se indica lo siguiente:


// 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);

La línea 3 asocia el gestor 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 añadir 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] 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"/>
  <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 y 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:

  

La gestión 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");
    }
    // gestión 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 invoca el 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 ha hecho clic;
  • líneas 9-36: los distintos casos se gestionan mediante un switch;
  • líneas 16-36: al hacer clic en la opción [Fragment i] se redirige al método [showFragment(i-1)] de las líneas 41-45;
  • línea 43: se solicita al contenedor de fragmentos que muestre el fragmento solicitado;
  • línea 42: antes se comprueba que se puede hacer (condición 1) y que es 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, vamos a centrarnos en otro caso: aquel en el que la gestión de las pestañas está disociada 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 mostrará siempre el fragmento [Vue1];
  • la segunda pestaña mostrará un fragmento seleccionado en el menú;

Image

  • en [1], el fragmento [Vue1];
  • en [2], el fragmento de tipo [PlaceholderFragment] elegido 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: vamos a gestionar 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 se seleccionó. El campo [numFragment] almacenará el número de dicho fragmento para la pestaña n.º 2, un número que se encuentra 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 hay que 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-24. El quinto fragmento es el fragmento [Vue1Fragment], que siempre se mostrará en la pestaña n.º 1.

1.12.5. La clase [MainActivity]

La clase [MainActivity] debe gestionar ahora 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 gestor 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 principio 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 mostrado por 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 definición, será el último fragmento del contenedor;
  • líneas 20-22: como 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. Ahora ya no es así. Por lo tanto, eliminamos el método [getPageTitle] del gestor 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 añade a la barra de pestañas. Nuestra barra de pestañas tiene ahora 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 mostrado, no ocurre nada: sigue mostrándose el mismo fragmento, pero la pestaña seleccionada sí ha cambiado. Por lo tanto, en este caso, el fragmento mostrado 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 en cuanto se produce un cambio de pestaña (si el usuario hace clic en una pestaña ya seleccionada, no ocurre 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 mostrado por el contenedor de fragmentos
        // posición de la pestaña
        int position = tab.getPosition();
        // n.º del fragmento que se va a mostrar
        int numFragment;
        switch (position) {
          case 0:
            // N.º del fragmento [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // N.º de 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 ha hecho clic. Aquí se obtendrá un número 0 o 1;
  • líneas 12-15: si se ha pulsado la primera pestaña, nos preparamos para mostrar el fragmento [Vue1Fragment];
  • líneas 16-18: en los demás casos (se ha pulsado 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 entonces en la sesión de la aplicación;
  • línea 21: se 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 gestor llama al método [showFragment] con el n.º del fragmento que se va a 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, la creamos
      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 mostrado
      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 añade a la barra de pestañas (línea 9);
  • línea 12: se introduce 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 por 1;
  • línea 14: se muestra el fragmento deseado;
  • línea 16: se introduce su n.º 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 aún no estaba seleccionada, se activará el método [onTabSelected]. A continuación, este método solicita 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 comprendemos bien 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 heredan de la clase [AbstractFragment];
  • los datos que se deben compartir 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 añadiendo 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 debería tener todo fragmento;

1.13.1. Creación del proyecto

Duplicamos el proyecto [Exemple-11] en [Exemple-12] siguiendo el procedimiento del apartado 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. Por otra parte, aunque no se aprecia en estos ejemplos, es previsible que los gestores de eventos de los fragmentos terminen en ocasiones con un cambio de vista. Se solicitará a la actividad que realice dicho 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 pretende reducir el acoplamiento entre los fragmentos y la actividad, limitándolo a un acoplamiento entre [AbstractFragment] y [IMainActivity]. De este modo, la actividad podrá tener un nombre distinto 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 solicita 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. Pero este se ve con dos tipos diferentes. Esto nos evitará conversiones de tipos en tiempo de 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 almacenan la actividad, como implementadora de la interfaz [IMainActivity], y la sesión;

1.13.4. Modificación del gestor de fragmentos

El gestor de fragmentos [SectionsPagerAdapter] de la clase [MainActivity] se modifica en un único punto: en lugar de gestionar fragmentos de tipo [Fragment], ahora gestiona 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 matriz 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 comprueba 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 la vista 1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

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

navigateToView(...);

A continuación, ejecuta 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 van a compartir entre 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 aquel caso se trataba de una navegación entre actividades: 1 vista = 1 actividad. Aquí nos proponemos tener una única actividad con varias vistas de tipo [AbstractFragment].

1.14.1. Creación del proyecto

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

1.14.2. Estructuración del proyecto

Vamos a empezar 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;

Creamos para ellos 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 nos sirve para recordar que hemos optado por una arquitectura concreta. A continuación, desplaza los elementos existentes del proyecto tal y como se indica en [9]. Cada desplazamiento debe validarse haciendo clic en el botón [Refactor].

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

 

Al mover las clases a los paquetes, Android Studio ha realizado 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 trasladado. Son generadas por la biblioteca Android Annotations. Para estas clases, hay que modificar manualmente los imports. 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 debe quedar ahora así:


      android:name=".activity.MainActivity_"

Presta atención al punto «.» delante de [activity]. Una vez más, Android Studio no ha podido actualizar el manifiesto porque este hace referencia a una clase de Android Annotations que no se ha trasladado. 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 gestionará dos fragmentos: el [Vue1Fragment], que ya tenemos, y el [Vue2Fragment], que habrá que crear;
  • la sesión ya no es la misma;

1.14.3.1. Limpieza de fragmentos

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

  // 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 introducido
    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-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-24. De este modo, 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/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 gestor de pestañas
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // el botón flotante
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

y volver a compilar la aplicación. Las líneas erróneas son aquellas que hacen referencia a los elementos que han desaparecido. Elimina, por tanto, todas esas líneas. Además, modifica el gestor de fragmentos para que ya no haga referencia al fragmento [PlaceholderFragment] que hemos eliminado:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // 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 obtenido el 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 gestor 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 gestor de fragmentos
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // el contenedor de fragmentos está asociado al gestor de fragmentos
    // es decir, el fragmento n.º i del contenedor de fragmentos es el fragmento n.º i proporcionado por el gestor de fragmentos
    mViewPager.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 de la Vista 1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

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

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

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

  // el gestor de fragmentos
  // es a él a quien se le solicitan los fragmentos que se deben mostrar 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 tiene sentido;
  • línea 33: introduce 1 como adyacencia de fragmentos;
  • línea 76: navega a la vista 0. Será esta la que se muestre en primer lugar;
  • línea 108: inicializa la matriz con el fragmento [Vue1Fragment_]:

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

Así pues, solo hay un fragmento. Ejecuta la aplicación. Deberías obtener el siguiente resultado:

Image

El botón [Valider] debería 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 tenemos la vista [vue1.xml] en el proyecto actual. Ahora duplicamos [vue2.xml] desde [Exemple-05] a [Exemple-12] (abre los dos proyectos y realiza un copiar y pegar) entre ellos.

 
  • en [1], la nueva vista. Al intentar editarla, aparecen errores en [2]. Tenemos que modificar el archivo [strings.xml] [3] para añadir 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 hereda de 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. Configuración de los fragmentos y de la navegación entre ellos

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


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

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

    ...
}

La interfaz [IMainActivity] garantiza 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 {

  // 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()));
    }
  }

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

  @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] gestiona el evento «clic» en el botón [Vue n° 2];
  • línea 39: se navega con el método [navigateToView] de la actividad. Recordemos 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 gestor 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] gestiona el evento «clic» en el botón [Vue n° 1];

Ejecuta el proyecto y comprueba que la navegación entre vistas funciona.

1.14.6. Definición de la sesión

El funcionamiento de la aplicación es el siguiente:

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

Para que la vista n.º 1 pueda comunicar el nombre introducido 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 introducido;

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

...
  // gestores de eventos ----------------------------------

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

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // se introduce el nombre introducido 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 pasar a la vista n.º 2, se introduce el nombre introducido en la sesión para que la nueva vista pueda acceder a él;

La vista [Vue2Fragment] evoluciona 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 introducido 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 introducido en la vista n.º 1. Sabemos que, justo 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 podemos incluir 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 introducido en la vista n.º 1;
  • línea 41: se modifica la etiqueta [textViewBonjour];

Ejecuta el proyecto y comprueba que funciona.

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

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


  @AfterViews
  protected void afterViews() {
    // 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 el que el fragmento se recicle tras una operación [onDestroyView]. En este caso, se regenera la vista del fragmento 1 y el nombre que se haya podido introducir anteriormente desaparecerá de la vista. Y eso no nos interesa. Actualmente, el nombre introducido 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 introducido;
  • 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:


    // elementos de la interfaz gráfica
    @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);
    }

    // gestores de eventos ----------------------------------

...

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

Para el fragmento [Vue2Fragment], basta con el código existente:


  // 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 introducido 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 añadir;

1.14.9. Conclusión

Llegados a este punto, hemos vuelto a demostrar 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 tener un método [@AfterViews] en el que establezcan el valor booleano [afterViewsDone] en true;
  • una sesión que encapsule los datos que se van a compartir entre fragmentos y la actividad;

1.15. Ejemplo 14: una arquitectura de dos capas

Vamos a crear 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 apartado 1.4. Obtenemos el siguiente resultado:

1.15.2. La vista [vue1]

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

 

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

 

Corrija la línea 4 que se muestra a continuación en el gestor 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_()};

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

Vamos a crear 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 comprueba 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 introducidos
  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 comprueba la validez de los datos introducidos
    if (!isPageValid()) {
      return;
    }
  }

  // se comprueba la validez de los datos introducidos
  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 única vez, al iniciar la aplicación. Por este motivo, 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 comprueba la validez de los datos introducidos;

El método [isPageValid] es el siguiente:


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

...

// se comprueba la validez de los datos introducidos
  private boolean isPageValid() {
    // introducción 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);
    }
    // introducción 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);
    }
    // introducción 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);
    }
    // volver
    return (nbErreurs == 0);
  }

  • líneas 2-4: estos tres campos se inicializan mediante 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 no válidas, se muestran los mensajes de error correspondientes;

En este punto, la aplicación ya es ejecutable. Comprueba el funcionamiento del método [isPageValid] introduciendo 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)] devuelve normalmente 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 proporcionadas por el método. Finalmente, 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 comprobaciones
        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 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 {
                // si no, 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 en la capa [Présentation]. El atributo (scope = EBean.Scope.Singleton) hace que la clase [Metier] solo se instancie en un único ejemplar. 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 el habitual;

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] hereda de la clase del sistema [RuntimeException], lo que la convierte en una excepción no controlada: no es obligatorio gestionarla 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 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:

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

La interfaz [IMainActivity] queda de la siguiente manera:

  

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] amplía la interfaz [IMetier]

La clase [MainActivity] evoluciona 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 datos 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 introducidos
  private int nbAleas;
  private int a;
  private int b;

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

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

  // se comprueba la validez de los datos introducidos
  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], que 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 [ListView];
  • [resource] es el número entero que identifica la vista utilizada para mostrar un elemento de [ListView]. Esta vista puede tener cualquier grado de complejidad. Es el desarrollador quien la crea en función de sus necesidades;
  • [textViewResourceId] es el número entero que identifica un componente [TextView] en la vista [resource]. La cadena se mostrará a través de este componente;
  • [objects]: la lista de objetos mostrados por el [ListView]. El método [toString] de los objetos se utiliza para mostrar el objeto del [TextView] identificado por [textViewResourceId] en la vista identificada por [resource].

La tarea del desarrollador consiste en 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 caso, 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 es necesario definirlo una vez. Para permitir su reutilización, se ha definido como variable de instancia de la clase (línea 39). Volvamos a fijarnos en 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 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 tiene [activity!=null]. El adaptador está asociado aquí a la fuente de datos [reponses], definida en la línea 37;

El método [doExecuter] gestiona 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 comprueba la validez de los datos introducidos
    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 quiere 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 comprueba que los valores introducidos 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

Ejecuta el proyecto y comprueba que funciona correctamente.

1.16. Ejemplo 15: arquitectura cliente/servidor

Abordamos una arquitectura habitual 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 añadido 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 muestra la tableta Android. Este servidor tendrá la siguiente arquitectura de dos capas:

Los clientes consultan determinados URL de la capa [web / jSON] y reciben una respuesta de texto en formato jSON (JavaScript Object Notation). En este caso, nuestro servicio web procesará una única 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á 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 a la página web [http://start.spring.io/] (junio de 2016), que nos permitirá generar un proyecto Gradle con las dependencias necesarias para nuestro proyecto, que no es un proyecto de Android y para cuya creación Android Studio no ofrece ninguna ayuda:

  • en [1]: elige un proyecto Gradle;
  • en [2-3]: las características de la dependencia jar generada por el proyecto (véase más abajo);
  • en [4]: elija la dependencia web [5] para que estén disponibles los binarios necesarios para nuestro servicio web;
  • en [6]: genera el proyecto. A continuación, se genera el archivo zip de un proyecto Gradle de plantilla y se ofrece para su descarga;

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

 

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

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

android {
  compileSdkVersion 23
  ...
}
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 de la web [http://start.spring.io/]:
    • en [2] es [groupId];
    • en [3] es [artifactId];

Descomprime en la carpeta de los demás proyectos el archivo zip obtenido:

Con Android Studio, abre el proyecto Gradle [server-01] [1-2]. El proyecto abierto 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 Eclipse. Las eliminamos;
  • las líneas 1-11 y 15 sirven para añadir un plugin llamado [spring-boot] a nuestro proyecto Gradle. Spring Boot es un proyecto del ecosistema Spring [http://projects.spring.io/spring-boot/]. Este plugin define las versiones de las dependencias más utilizadas con Spring. Esto permite no tener que especificar sus versiones (líneas 30 y 31). La versión es, por tanto, la definida por la versión de Spring Boot utilizada (línea 3);
  • líneas 22-23: la versión de Java que se va a 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 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}")
  }
}

// complementos
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 añadido el repositorio local de Maven del equipo de desarrollo. Este se crea al instalar Maven (véase el apartado 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 parte superior derecha y junto a 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')] ha traído 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:

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

En la página 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')
}

// complemento 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 plugin de Gradle llamado [maven-publish] que permite publicar el binario del proyecto en un repositorio de Maven respetando 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;

La incorporación del complemento [maven-publish] ha creado 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, añadimos al archivo [build.gradle] el siguiente código:


// 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 introducir el nombre completo de la clase ejecutable del proyecto:
  

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


package istia.st.exemples.android;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Server01Application {

    public static void main(String[] args) {
        System.out.println("Server01Application running");
        //SpringApplication.run(Server01Application.class, args);
    }
}

Actualiza el proyecto Gradle y, a continuación, ejecuta 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. Dado que no se trata de un proyecto de Android generado por IDE, esta configuración, que hasta ahora no habíamos realizado, debe llevarse a cabo aquí.

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

Para compilar el proyecto, el botón disponible para los proyectos de Android ya no aparece. Utilizaremos 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 patrón 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 comprobaciones
    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 análoga a la del ejemplo anterior, salvo que no lanza excepciones de forma aleatoria. Cabe destacar, simplemente, la anotación de Spring [@Service] en la línea 8, que hará que Spring instancie la clase en un único ejemplar (singleton) y ponga su referencia a disposición de otros componentes de Spring. Se podrían haber utilizado otras anotaciones de Spring aquí para conseguir 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 la primera letra 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 gestionarla con un try/catch);
  • línea 6: se añade un código de error a la clase [RuntimeException];

1.16.1.5. El servicio web / jSON

 
  

El servicio web / jSON está implementado por Spring MVC. Spring MVC implementa el 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 denominadas [Contrôleurs]. La C de MVC es, en este caso, 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 ha encontrado 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 proceder 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 necesitar 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 ha podido procesar correctamente
    • una página de confirmación en caso contrario
  • la acción solicita que se muestre una vista concreta: [3]. Esta vista mostrará unos datos que se denominan «modelo de la vista». Es la 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 creada 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], la plantilla, que es una clase Java, se transforma en una cadena jSON mediante una biblioteca jSON;
  • en [4b], esta cadena jSON se envía al navegador;

En los anexos del apartado 6.14 se presenta un ejemplo de serialización de un objeto Java en la cadena jSON y de deserialización de la 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 de tipo [Response] como se indica 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, a continuación, 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, normalmente los de la pila de excepciones si se ha producido alguna; null si no hay errores;

El controlador [WebController] es el siguiente:


package exemples.android.server.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import 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 gestionan solicitudes para determinadas URL de la aplicación web;
  • líneas 21-22: la anotación [@Autowired] indica a Spring que inyecte en el campo un componente de tipo [IMetier]. Será la clase [Metier] anterior. Como le hemos aplicado la anotación [@Service], se gestiona 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 cadena jSON. Este mapeador es el que llevará a cabo 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 han sido inicializados por Spring MVC. Veremos cómo. Por otra parte, si se ejecuta, es porque el servidor web ha recibido 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 cadena jSON. Este encabezado no es obligatorio. Se proporciona a título informativo al cliente si este espera respuestas que pueden adoptar 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 que debe enviarse 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 solicita 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). En este caso, sabemos que la pila solo contiene una excepción, pero hemos querido 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;

Hemos optado por 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: se indica a Spring en qué paquetes va a 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 a Spring Boot realizar por sí mismo una serie de configuraciones estándar para una aplicación Spring MVC. Esto supone un alivio para el 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). En este caso, 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 ningún atributo de nomenclatura en la anotación [@Bean], el componente de Spring creado recibe 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 incluyen a través de 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 la matriz de argumentos pasada al método [main] (línea 7);

La aplicación web se puede iniciar de diversas formas, entre ellas la siguiente:

 

A continuación, aparecen en la consola varios mensajes de registro:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 controlador 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:

En cada caso, 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, apartado 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 destacar que este 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, hemos mostrado 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 marcha ([1]) y, a continuación, 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 añadiendo 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">
    <!-- a los codificadores 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 obtendremos registros de nivel [info]:


...
09:36:52.433 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
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 estructura:

El cliente tendrá dos componentes:

  1. una capa [Présentation] (vista + actividad) similar a la que hemos estudiado en el ejemplo [Exemple-14];
  2. la capa [DAO], que se comunica con el servicio [web / jSON] que hemos estudiado anteriormente.

1.16.2.1. Creación del proyecto

Duplicamos el proyecto anterior [Exemple-14] en [Exemple-15] siguiendo el procedimiento del apartado 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 para Android, hay que utilizar android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

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

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    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 en la biblioteca AA las comunicaciones entre el cliente y el servidor;
  • 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-59, el repositorio que se va a utilizar (línea 58) para encontrar la biblioteca;
  • línea 51: una biblioteca jSON;
  • líneas 33-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 del cliente en milisegundos 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 de 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: se establece un tiempo de espera antes de enviar la solicitud al servidor, con el fin de dar tiempo al usuario a 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 añadiremos. 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 matriz de convertidores. En este caso, 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 va a 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 {} enmarcan los parámetros de URL que deben incorporarse a los parámetros del método de 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 forma más sencilla [@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. En el 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 dicha solicitud (objeto enviado) debe transmitirse en forma de 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 {
  • Tenemos que poder indicar el URL del servicio web al que hay que dirigirse. Esto se consigue 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 hay que conectarse;
  • 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], lo que 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;
    }

    // métodos getter y setter
...
}

Se trata de la clase [Response] ya utilizada en el lado del servidor (apartado 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 en el lado del cliente, son transparentes para el programador.

1.16.2.4.4. Implementación de la capa [DAO]
  

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


package exemples.android.dao;

import com.fasterxml.jackson.databind.ObjectMapper;
import 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 crea 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] se implementará mediante 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] la proporciona 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 soporte 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, por lo general, 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. Por otra parte, en el apartado 1.16.1.7 hemos visto 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 una peculiaridad 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 gestionará la comunicación 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 comprobando su [status];
  • líneas 12-14: si no se ha producido ningún error en el servidor (status=0), se devuelve el resultado del método;
  • línea 17: si se ha producido un error en el 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 se llega hasta aquí, es inevitable que se haya producido una excepción. Por lo tanto, 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 dicha interfaz amplía 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 añadido dos métodos para gestionar la imagen de espera de la aplicación (líneas 14 y 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 a los que se puede acceder desde diferentes puntos 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 va a mostrar
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // resultado
    return texte.toString();
  }

}
  • líneas 9-18: crea la lista de mensajes de error contenidos en un Throwable;
  • líneas 21-32: se basa en 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 pequeño (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. asegurarse de 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 factorizarse;

Es la función n.º 2 la que nos lleva a incluir en esta clase las operaciones de gestión de la imagen de espera: todos los fragmentos de una aplicación Android asíncrona deben gestionar 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 contabilizan 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 desaparece cuando se han recibido todos o cuando se ha cancelado la operación;

Image

  • en [6], se comprueba la validez de los datos introducidos;

Se pide al lector que cargue el archivo [vue1.xml] de los ejemplos. A continuación, indicamos 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 principio 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 hay que olvidarlo; 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). Recordemos 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;

Justo 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 de las respuestas ListView. 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 comprueba la validez de los datos introducidos
    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 mínimo 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 introducidos en las líneas [2-6] y se comprueba su validez. Si alguno de ellos no es válido, se abandona el método (línea 25) y el usuario vuelve a la interfaz gráfica;
  • líneas 28-29: si todos los datos introducidos son válidos, se transmite a la actividad el 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 a 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 de aquel en el que se 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, podremos tener varios hilos:
    • el que muestra la interfaz visual UI (User Interface) y gestiona sus eventos,
    • los subprocesos [nbAleas], cada uno de los cuales solicita un número aleatorio al servicio web. Estos subprocesos se inician de forma asíncrona: el subproceso UI inicia un subproceso [getAlea] (línea 32) que solicita un número aleatorio al servicio web y no espera a que finalice. Se le notificará la finalización mediante un evento. De este modo, los subprocesos [nbAleas] se iniciarán en paralelo. Es posible configurar la aplicación para que solo inicie un subproceso a la vez. En ese caso, se crea una cola de subprocesos pendientes de ejecución;

En la línea 38, el parámetro [id] asigna un nombre al hilo generado. En este caso, todos los hilos [nbAleas] tienen el mismo nombre: [alea]. Esto nos permitirá cancelarlos todos a la vez. 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 independiente del 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] sustituirá al botón [Exécuter]. Dado que los subprocesos iniciados son asíncronos, el subproceso de UI no los espera y la línea 35 se ejecuta antes de que finalicen. Una vez finalizado el método [beginWaiting], 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 finalizados todos los subprocesos. La cancelación de estos ya no tendría entonces 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 hemos terminado?
      if (nbInfos == nbAleas) {
        // se pone fin a la espera
        cancelWaiting();
      }
      // se añade 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 finalizada la espera (fin de la señal de espera; el botón [Exécuter] sustituye al botón [Annuler]);
  • líneas 12-15: se añade el número aleatorio recibido a la lista de respuestas mostrada por el componente [ListView listReponses] y se actualiza esta;

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();
    }
}

La lógica es similar a la del método [showInfo]:

  • línea 1: la anotación [@UiThread] es obligatoria;
  • línea 2: el método recibe la excepción que se ha producido;
  • línea 3: el método solo se ejecuta si el usuario no ha cancelado su solicitud;
  • línea 5: se cancela la solicitud del usuario como si él mismo hubiera pulsado 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 principal [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 gestiona 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: se cancelan todas las tareas identificadas por la cadena [alea]. El segundo parámetro, [true], significa que deben cancelarse aunque ya se hayan 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 ha comprobado que la línea 6 del código del método [doAnnuler] no funcionaba correctamente. Por este motivo, se ha añadido 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 añadido 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] apenas ha cambiado con respecto a cómo 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 tras esta inyección;
  • línea 12: se establece el timeout de la capa [DAO]

Por otra parte, la actividad [MainActivity] debe implementar la interfaz [IMainActivity], que 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) {
      // Visualización del 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 (apartado 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 has instalado [GenyMotion], la máquina virtual VirtualBox ha añadido las direcciones IP a tu equipo (líneas 10 y 18). Estas direcciones resultan especialmente prácticas, ya que no son bloqueadas por el cortafuegos de Windows. La línea 30 indica la dirección IP de tu ordenador en una red local. Para utilizar esta dirección, normalmente es necesario desactivar el cortafuegos de Windows. Si estás conectado a una red wifi, utiliza la dirección wifi y, en este caso también, desactiva el cortafuegos si lo tienes.

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 finalice 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. Gestión de la cancelación

Para hacer un seguimiento de lo que ocurre cuando el usuario solicita la cancelación o cuando esta se solicita debido a que se ha producido una excepción, añadimos el siguiente método a la interfaz [IDao] (véase el apartado 1.16.2.4.1):


package exemples.android.dao;

public interface IDao {

  ...

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);
}

En la clase [Dao], añadimos 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], añadimos 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 haya 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 asíncronas realizadas por el fragmento [VueFragment] y que este no espera el resultado de su llamada;
  • línea 7: se ha producido 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 cuatro excepciones siguientes. Por lo tanto, todas las solicitudes asíncronas 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 finalice dicho 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 llama cinco veces al método [getAlea] de la clase [Dao];
  • línea 7: el usuario ha solicitado la cancelación de las solicitudes;
  • línea 8: se observa que [Vue1_Fragment] recibe 5 valores. De nuevo, todas las solicitudes asíncronas que estaban en espera se han ejecutado;

Por este motivo, hemos tenido que gestionar un valor booleano [hasBeenCanceled] para evitar mostrar nada 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 asíncronas 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 la asincronía necesaria para las aplicaciones de Android con una biblioteca llamada RxJava [http://reactivex.io/] y su versión derivada para el entorno 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], añadimos la dependencia de la biblioteca [RxAndroid]:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. La capa [DAO]

  

1.17.4. La interfaz [IDao]

La interfaz [IDao] 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) de la respuesta del servidor
  void setTimeout(int timeout);

  // tiempo de espera del cliente en milisegundos antes de la solicitud
  void setDelay(int delay);

  // modo de depuración
  void setDebugMode(boolean isDebugEnabled);
}
  • línea 8: el método [getAlea] devuelve ahora un tipo [Observable] de la biblioteca RxJava (línea 3). El principio es el siguiente:

Un flujo de elementos de tipo Observable<T> es observado por uno o varios 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 del 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-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 realmente esperado por 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 posee los siguientes métodos:
    • [onNext(T element)]: permite enviar a un observador un elemento de tipo T;
    • [onError(Throwable th)]: permite enviar una excepción a un observador;
    • [onCompleted]: permite indicar a un observador el fin de las emisiones;

Un tipo [Observable<T>] cumple ciertas restricciones:

  • envía sus elementos mediante el método [onNext(T element)];
  • el método [onCompleted] debe invocarse una sola vez en cuanto no queden más 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 consume 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 síncrona al servidor y obtiene el tipo [Response<T>]. Esta solicitud HTTP la gestiona 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, se envía la respuesta que realmente espera el cliente (línea 37) y se 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, se envía al observador. En este caso no es necesario llamar al método [onCompleted] para indicar al observador que ya no se enviarán más 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] hereda de la clase [AbstractDao];
  • línea 24: el método [getAlea] devuelve ahora un tipo [Observable<Integer>];
  • línea 30: llamada al método genérico [getResponse] de la clase padre. Se le pasa un parámetro de tipo [IRequest<Integer>];
  • líneas 32-37: implementación de la interfaz [IRequest<Integer>];
  • línea 36: se realiza la consulta HTTP a través de la interfaz AA [webClient], tal y como se había hecho anteriormente. Sabemos que vamos a obtener 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 denominada closure: la capacidad de encapsular en una instancia valores externos a la misma 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 grabado 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 comprueba la validez de los datos introducidos
    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], denominado 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 añade 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). En este caso, solo tendremos un proceso observado y un suscriptor. Por lo tanto, solo tendremos una suscripción. A efectos del ejemplo, actuamos como si 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: partimos de un observable vacío, un observable que no emite nada;
  • líneas 14-16: a este observable vacío se le añaden 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 comprender 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 subprocesos. 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 observe. 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 hacíamos 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. En este caso, el hilo [AndroidSchedulers.mainThread())] pertenece a la biblioteca RxAndroid y no a RxJava. Se refiere al hilo de la interfaz de usuario, también denominado «bucle de eventos». 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 que se va 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 anteriormente. 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 producido una excepción;
    • [onCompleted]: cuando indica que ya no va a 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 gestionar cada uno de estos tres eventos;

  • líneas 21-27: el primer parámetro de tipo [Action1<Integer>] sirve para gestionar el evento [onNext]. Su método [call] recibe el elemento que ha sido 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 gestionar 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 de cancelación de 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 gestionar 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) {
      // más información
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // se añade 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 proporciona esta información;

El método [showAlert] evoluciona de la siguiente manera:


  // anotación [UiThread] innecesaria
  protected void showAlert(Throwable th) {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    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();
    }
}
  • el único cambio se produce 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 asíncronas
    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 (apartado 1.16.1.7), inicie el cliente de Android y repita las pruebas que realizó con el ejemplo anterior (apartado 1.16.2.8).

1.17.10. Gestión de la cancelación

Repetimos las mismas pruebas que en el ejemplo anterior (apartado 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

A partir 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 finalice dicho 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 tras 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 de la prueba n.º 2 son, por tanto, 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 han realizado las 5 solicitudes;
  • línea 6: el usuario ha cancelado;
  • 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 asíncronas, sino que ofrece numerosos métodos para crear un nuevo observable a partir de otro. Estos métodos no tienen equivalente en AA;
  3. en cuanto se quiere derivar una clase anotada con AA, como un fragmento, surgen graves problemas. Esto nos 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 introducción de datos

Vamos a crear un nuevo proyecto para presentar algunos componentes habituales en los formularios de introducción 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]. Tendremos en cuenta este cambio en el gestor de fragmentos de [Mainactivity]:


  // nuestro gestor 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_()};
....
}

Vuelve a ejecutar el proyecto. Debería mostrarse la vista n.º 1, igual que antes. Vamos a trabajar 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 principales componentes 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
  • desplazándose;
 
  • 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 la 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
  • el resto de componentes son [TextView] que muestran textos.
 

1.18.3. Las cadenas de caracteres del formulario

Las cadenas de caracteres 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 mostrada por 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 introducción 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 quiere 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 vamos a asociar al menú 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 introducidos por el usuario. Su código es el siguiente:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // lista de mensajes que se van a 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 selector
    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 introducidos 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 localizar el botón de opción marcado y obtener así su texto;
  • línea 13: el método [SeekBar].getProgress() permite obtener el valor de una barra de ajuste;
  • línea 19: el método [Switch].isChecked() permite saber 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 introducidos, es el siguiente:


    private void doAfficher(List<String> messages) {
        // se construye el texto que se va a 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 que se deben mostrar;
  • líneas 3-6: a partir de estos mensajes se crea un objeto [StringBuilder]. Para concatenar cadenas, el tipo [StringBuilder] es más eficaz 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

Ejecuta el proyecto y prueba 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 recuperar las dos vistas del proyecto e incluirlas en una plantilla:

  

Image

Cada una de las dos vistas tendrá la misma estructura:

  • 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 consigue 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 franja izquierda [2] se obtiene con las líneas 56-84;
  • el pie de página [3] se obtiene con las líneas 86-101;
  • el contenido [4] se obtiene con las líneas 78-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 concreta para cada elemento de una lista. La vista repetida puede tener cualquier grado de complejidad, desde una simple cadena de caracteres hasta una vista que permita introducir 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 y 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 campo [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] gestiona 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 a [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 modificado 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 última 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 hacerse 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 tres 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 al [ListView]. Al mismo tiempo, la fuente de datos [listes] se asocia al [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]. En este caso, la instrucción se ejecuta con demasiada frecuencia. Se echa en falta 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];
  • gestiona la visualización de los distintos elementos de [ListView];
  • gestiona los eventos de estos elementos;

Su código es el siguiente:


package exemples.android.fragments;

import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {

    // el contexto de ejecución
    private Context context;
    // el ID del diseño de visualización de una línea 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] hereda de 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 almacenan los datos del constructor;
  • línea 29: el método [getView] será invocado repetidamente por [ListView] para generar la vista del elemento n.º [position]. El resultado [View] devuelto es una referencia a la vista creada.

El código del método [getView] es el siguiente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // 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 vamos a utilizar el primero;
  • línea 4: se crea la vista del elemento n.º [position]. Se trata de la vista [list_data], cuyo ID se ha pasado 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 procedente de la fuente de datos que se ha pasado 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 procedente 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: es el método [Vue1Fragment].doRetirer el que gestionará este clic. De hecho, parece más lógico que este evento lo gestione el fragmento que muestra el [ListView]. Este tiene 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 los datos 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 el elemento n.º i debe mostrarse, 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 verificación a partir del dato al que está vinculada. Por lo tanto, es necesario que esta memorice 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 visible 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 pueda estar oculta
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // se actualiza el [ListView]
    adapter.notifyDataSetChanged();
    // nos situamos 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 ha hecho 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]. Si no se hace esto, visualmente no cambia nada.
  • líneas 5-13, 17: una operación bastante compleja. Sin ella, ocurre lo siguiente:
    • el [ListView] muestra las líneas 15-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-3 de la lista de datos;

Con las líneas anteriores, se lleva a cabo la eliminación y el [ListView] permanece situado en la línea siguiente a la 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] gestiona 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() {
        // se navega a 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 va a mostrar en el [TextView] n.º 2;
  • líneas 35-39: se recorre la lista de datos mostrada por el [ListView]. Está almacenada en la actividad;
  • línea 36: si se ha marcado el dato n.º i, se añade 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 hemos utilizado una fuente de datos List<Data> en la que 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 dicha 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 {

    // casilla marcada
    private boolean isChecked;

    // constructor
    public CheckedData(String text, boolean isChecked) {
        // padre
        super(text);
        // local
        this.isChecked = isChecked;
    }

    // getters y setters
...
}

A continuación, basta con sustituir 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: utilizar 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 sustituirlos 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 y 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 secundario 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) {
    ...
  }

  // actualización de la 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 vamos a hacer 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 emplearemos una anotación AA que lo hará por nosotros;
  • línea 44: almacenamos el menú. Lo necesitaremos más adelante;
  • líneas 52-53: almacenamos en la matriz 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 ha realizado para no tener que volver a hacerla (línea 50);
  • línea 63: se solicita al fragmento secundario 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ú, 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 algunas 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 dicha 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 han seleccionado 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 llama al método [navigateToView1];

1.21.6. Ejecución

Crea un contexto de ejecución para este proyecto y ejecútalo.

1.22. Ejemplo 21: refactorización de la clase abstracta [AbstractFragment]

El ejemplo anterior nos ha mostrado que, cuando el fragmento tiene un menú, su método [onCreateOptionsMenu] es un buen lugar para solicitar al fragmento que se actualice:

  • se invoca exactamente una vez cuando el fragmento va a 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ú. Vamos a asociarles 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ú añadido para los fragmentos estará vacío:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context="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 añade al de la actividad: por lo tanto, se dispone de las opciones de ambos menús. En este caso, el menú de los fragmentos estará vacío. Así pues, solo se verá el menú de la actividad.

1.22.3. Los fragmentos

  

Retomamos la clase abstracta [AbstractFragment] del ejemplo anterior (véase el apartado 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 todo lo que hace referencia a la antigua clase abstracta [AbstractFragment].

1.22.4. Ejecución

Ejecuta la aplicación y comprueba que funciona. Sigue 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 secundarios.

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;

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 única pestaña, denominada [Vue 1]. Esta es la pestaña que se ve;
  • tras la rotación del dispositivo, el gestor 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 el 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 puesto a cero. Esto explica el resultado que se muestra en el fragmento n.º 3;

1.23.2. Métodos de copia de seguridad y restauración de la actividad y los fragmentos

1.23.2.1. Solución 1: copia de seguridad 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) {
    // padre
    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. Posee métodos [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 disponen de 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 (apartado 1.18: una aplicación con un fragmento que muestra un formulario) y Ejemplo-21 (apartado 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 mostrada al girar el dispositivo. En lugar de crear dos plantillas, una basada en [setRetainInstance(true)] y otra basada 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 ha utilizado en el resto de este documento.

1.23.3. El método de copia de seguridad y restauración del proyecto [Exemple-22]

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

  

En él 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 ha creado a modo de 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;

  // métodos getter y setter
...
}
  • 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 hemos visto 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] mostrado 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] evoluciona de la siguiente manera:


// gestión de copia de seguridad/restauración -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // padre
    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");
    }
    // padre
    super.onSaveInstanceState(outState);
    // copia de seguridad del 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 hay que realizar una copia de seguridad del fragmento y este está visible (línea 38);

Este mecanismo evita tener que guardar el estado con más frecuencia de la necesaria. De hecho, como ya se ha guardado 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 última guardada, su estado no ha cambiado. Solo hay que guardar el estado del fragmento j. Este mecanismo tiene además 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 el que lo dejamos la última vez que se mostró. Este estado puede haber desaparecido en parte si dicho 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 dicha vista;

  • líneas 10, 40: para evitar realizar dos copias de seguridad sucesivas, se utiliza el valor booleano [saveFragmentDone] para indicar que ya se ha realizado una copia de seguridad;
  • líneas 9 y 39: se solicita 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 tiene que 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 solicita al fragmento secundario que se actualice
    updateFragment();
    // copia de seguridad pendiente
    saveFragmentDone = false;
  }
  • línea 14: hemos visto que el valor booleano [saveFragmentDone] pasaba a vrai cuando se realizaba una copia de seguridad. En algún momento debe volver a faux. Cuando se ejecuta el método [updateFragment] (línea 12) del fragmento hijo, este pasa a estar visible. Ahora bien, es precisamente cuando está visible cuando hay que guardar un fragmento, en el momento concreto en que pase del estado visible al estado oculto. A continuación, se establece el valor booleano [saveFragmentDone] en false para que se pueda realizar la copia de seguridad;

1.23.4. Guardado del fragmento [Vue1Fragment]

El almacenamiento de los fragmentos se realiza en el método [saveFragment], invocado 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()));
    }
    // copia de seguridad del 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 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. Guardado del fragmento [PlaceHolderFragment]

El almacenamiento de los fragmentos se realiza en el método [saveFragment], invocado 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 va a 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.º 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.º de vista actual
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • líneas 8-12: restauración del estado del fragmento. La variable booleana [fragmentHasToBeInitialized] ha sido inicializada por la clase padre [AbstractFragment]. Cuando su valor es vrai, el fragmento acaba de ser reconstruido y hay que 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 volvamos más adelante a este fragmento sin que se haya producido ninguna 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í surge una dificultad: cuando se restaura el fragmento, no queremos incrementar este contador. Debemos distinguir aquí entre:
    • una simple navegación que devuelve al usuario 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;
    // ¿Hay que 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 el 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 del 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. En este caso, hay que restaurarlo. Por lo general, se trata de restaurar determinados campos del fragmento;
  • línea 20: el campo [text] de la línea 2 debe contener el texto inicial mostrado por el fragmento: [Hello world from section i]. Aquí debe regenerarse;
  • línea 21: se observa que se ha llevado a cabo 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: se recupera el texto que se mostraba en ese momento;
  • línea 38: este texto se vuelve a mostrar;
  • línea 40: se anota en la sesión el número de la nueva vista mostrada;

1.23.8. Gestión de pestañas

En los párrafos anteriores no se ha abordado la gestión de las pestañas. Sin embargo, en el ejemplo 21 observamos un problema al girar el dispositivo: solo se conservaba la primera pestaña, [Vue 1]. La segunda pestaña se perdía.

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 comprueba en la sesión el número del fragmento que se muestra 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: se busca en la sesión el número de la pestaña que estaba seleccionada antes del guardado o la restauración y se vuelve a seleccionar esa misma pestaña. Si el campo [selectedTab] aún no ha sido inicializado por el código, se utiliza su valor inicial, que es 0;