Skip to content

1. Imparare la programmazione Android

Il PDF del documento è disponibile |QUI|.

Gli esempi tratti dal documento sono disponibili |QUI|.

1.1. Introduzione

1.1.1. Indice

Il presente documento è una rielaborazione di diversi documenti esistenti:

  1. Android per sviluppatori J2EE;
  1. Introduzione alla programmazione per tablet Android attraverso esempi;
  2. Controllo di un Arduino con un tablet Android;
  3. Introduzione alla programmazione su tablet Android attraverso esempi - Versione 2

e introduce le seguenti nuove funzionalità:

  • Il documento 1 presentava un'architettura denominata AVAT (Activity-Views-Actions-Tasks) per facilitare la programmazione asincrona in un'applicazione Android. In questo documento, viene utilizzata la libreria standard RxJava per gestire le azioni asincrone;
  • Il documento 2 utilizzava l'IDE Eclipse con un plugin Android. Questo documento utilizza Android Studio;
  • Il documento 3 è incluso così com'è;
  • Il documento 4 utilizzava la libreria [Android Annotations] (AA) con l'IDE IntelliJ IDEA Community Edition. Questo documento riproduce integralmente il documento 4 con le seguenti differenze:
    • l'IDE è ora Android Studio;
    • il sistema di compilazione è Gradle per tutti i progetti client o server (nel documento 4, a volte veniva utilizzato Maven)
    • la programmazione asincrona è implementata utilizzando la libreria RxJava (nel documento 4 veniva utilizzata la libreria AA);
  • il presente documento esplora aree non trattate, o trattate solo brevemente, nei documenti precedenti:
    • il concetto di adiacenza dei frammenti;
    • il salvataggio/ripristino dell'attività e dei suoi frammenti;
    • il ciclo di vita dei frammenti;

Infine, presenta lo scheletro di un client Android che comunica con un servizio web/JSON, in cui isoliamo un gran numero di elementi comunemente presenti in questo tipo di client. Questo scheletro viene utilizzato in tutti gli esempi a partire dal Capitolo 2. Questa è la parte veramente innovativa del documento.

Vengono presentati i seguenti esempi:

Esempio
Natura
1
Importazione di un progetto Android esistente
2
Un progetto Android di base
3
Un progetto [Android Annotations] di base
4
Viste ed eventi
5
Navigazione tra le schermate
6
Navigazione tramite schede
7
Utilizzo della libreria [Android Annotations] con Gradle
8–12
Gestione dei frammenti in un'app Android
13
Navigazione tra le viste rivisitata
14
Architettura a due livelli
15
Architettura client/server
16
Gestione dell'asincronia con RxJava
17, 17B
Componenti per l'inserimento dati
18
Utilizzo di un modello di vista
19
Il componente ListView
20
Utilizzo di un menu
21
Utilizzo di una classe padre per i frammenti
22, 22B
Salvataggio e ripristino dello stato dell'attività e dei frammenti
23
Client meteo
Cap. 2
Struttura di base di un client Android che comunica con un servizio web / JSON. Tiene conto di un gran numero di elementi comunemente presenti in questo tipo di client Android.
Cap. 3
Gestione degli appuntamenti per uno studio medico
Capitolo 4
Esercizio pratico - Gestione di base delle buste paga
Capitolo 5
Esercizio pratico - Ordinare schede Arduino

Questo documento è stato utilizzato nell'ultimo anno della scuola di ingegneria IstiA dell'Università di Angers [istia.univ-angers.fr]. Ciò spiega il tono a volte un po' insolito del testo. Le due esercitazioni pratiche sono compiti di laboratorio per i quali vengono fornite solo le linee generali della soluzione. La soluzione deve essere elaborata dal lettore.

Il codice sorgente degli esempi è disponibile |QUI|. Per eseguire questi esempi, è necessario seguire la procedura descritta nella sezione 6.12.

Questo documento è una guida introduttiva alla programmazione Android. Non pretende di essere esaustivo. È rivolto principalmente ai principianti.

Il sito di riferimento per la programmazione Android si trova all'URL [http://developer.android.com/guide/components/index.html]. È lì che dovresti andare per avere una panoramica della programmazione Android.

1.1.2. Prerequisiti

Per trarre il massimo da questo documento, è necessario avere una solida conoscenza del linguaggio di programmazione Java.

1.1.3. Strumenti utilizzati

Gli esempi seguenti sono stati testati nel seguente ambiente:

  • Computer con Windows 10 Pro a 64 bit;
  • JDK 1.8;
  • Android SDK API 23;
  • Android Studio, versione 2.1;
  • Emulatore Genymotion, versione 2.6.0;

Per seguire questo documento, è necessario installare:

  • un JDK (vedere la sezione 6.8);
  • il gestore dell'emulatore Android Genymotion (vedere la sezione 6.9);
  • il gestore delle dipendenze Maven (vedere la sezione 6.10);
  • l'IDE [Android Studio] (vedere la sezione 6.11);

1.2. Esempio-01: Importazione di un esempio Android

1.2.1. Creazione del progetto

Creiamo il nostro primo progetto Android utilizzando Android Studio. Per prima cosa, creiamo una cartella [examples] vuota in cui verranno archiviati tutti i nostri progetti:

  

quindi creare un progetto con Android Studio. Per prima cosa importeremo uno degli esempi inclusi nell'IDE [1-5]:

 

Image

L'importazione del progetto potrebbe causare errori a causa di una discrepanza tra l'ambiente utilizzato al momento della creazione del progetto e quello utilizzato qui per eseguirlo. Questa è un'occasione per vedere come risolvere questo tipo di errore. In questo caso, abbiamo il seguente errore:

Il progetto importato è configurato dal seguente file [build.gradle] [2]:


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
 
apply plugin: 'com.android.application'
 
repositories {
    jcenter()
}
 
dependencies {
    compile "com.android.support:support-v4:23.3.0"
    compile "com.android.support:support-v13:23.3.0"
    compile "com.android.support:cardview-v7:23.3.0"
}
 
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
    'main',     // main sample code; look here for the interesting stuff.
    'common',   // components that are reused by multiple samples
    'template'] // boilerplate code that is generated by the sample template process
 
android {
    compileSdkVersion 21
    buildToolsVersion "23.0.3"
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    sourceSets {
        main {
            dirs.each { dir ->
                java.srcDirs "src/${dir}/java"
                res.srcDirs "src/${dir}/res"
            }
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
 
    aaptOptions {
        noCompress "pdf"
    }
}
  • L'errore segnalato è dovuto alle righe 31, 34–35: non disponiamo dell'SDK 21. Sostituiamo questa versione con la versione 23, di cui disponiamo.

Nel file [build.gradle], Android Studio fornisce i suggerimenti riportati di seguito:

 

Per accettare i suggerimenti, premere [Alt-Invio] sul suggerimento:

 

Potresti anche riscontrare un errore relativo alla versione di Gradle:

 

Questo errore deriva da una discrepanza tra la versione di Gradle richiesta dal file [build.gradle] del progetto (versione 2.10 alla riga 6 qui sotto):


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

e quello elencato nel file [<project>/gradle/wrapper/gradle-wrapper.properties]:


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

Nella riga 6 sopra, sostituisci 2.8 con 2.10.

Per accedere al file [<project>/gradle/wrapper/gradle-wrapper.properties], utilizzare la vista del progetto:

Una volta apportata la correzione, è possibile compilare l'applicazione [1], avviare l'emulatore Genymotion [2] ed eseguire il progetto [3]:

 

Image

Fermiamo l'applicazione:

  

Ora puoi chiudere il progetto. Ne creeremo uno nuovo.

  

1.2.2. Alcune note sull'IDE

1.2.2.1. Viste

L'IDE Android Studio (AS) offre diverse viste per lavorare su un progetto. Ne useremo principalmente due:

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

Nella maggior parte dei casi, lavoreremo con la vista [Android]. Quando cloniamo un progetto in un altro, avremo bisogno della vista [Progetto].

1.2.2.2. Gestione dell'esecuzione

Esistono diversi modi per eseguire, arrestare o rieseguire un progetto AS. Innanzitutto, ci sono i pulsanti sulla barra degli strumenti:

Il pulsante [Riesegui] [3] arresta il progetto [2] e poi lo riavvia [1].

1.2.2.3. Gestione della cache

Android Studio mantiene una cache dei progetti che gestisce per rendere l'IDE il più reattivo possibile. Con la versione 2.1 di Android Studio (maggio 2016), questa cache spesso non rifletteva le modifiche al codice appena apportate. In questo caso, è necessario invalidare la cache:

Con Android 2.1 (maggio 2016), il passaggio precedente doveva essere eseguito più volte e, a volte, ciò non era sufficiente per risolvere il problema rilevato. La soluzione era disabilitare [Instant Run]:

  • in [3-4], tutto era disabilitato;

In tutti i casi seguenti, abbiamo utilizzato questa configurazione della cache e non abbiamo riscontrato alcun problema.

1.2.2.4. Gestione dei log

Durante l'esecuzione di un progetto, i log vengono visualizzati in Android Monitor:

Nella scheda [Android Monitor] [1], i log vengono visualizzati nella scheda [logcat] [2]. Il pulsante [3] consente di cancellare i log. Questo pulsante è utile quando si desidera visualizzare i log relativi a un'azione specifica:

  • cancella i log;
  • sul dispositivo Android, eseguire l'azione per la quale si desiderano i log;
  • i log che compaiono sono quelli relativi all'azione eseguita;

Esistono diversi livelli di log [4]. Per impostazione predefinita, è selezionata la modalità [Verbose]. Ciò significa che vengono visualizzati i log di tutti i livelli. È possibile utilizzare [4] per selezionare un livello specifico.

I log sono molto utili per determinare in quali punti durante l'esecuzione di un progetto vengono eseguiti determinati metodi. Li useremo frequentemente. Diamo un'occhiata al codice della classe [MainActivity] nel progetto [Example-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);
    }
}

Sopra, i metodi [onCreate, riga 14] e [onCreateOptionsMenu, riga 26] sono metodi della classe padre [Activity] (riga 9). Vengono chiamati in momenti diversi del ciclo di vita dell'applicazione. A volte vengono eseguiti più volte. Anche leggendo la documentazione, può essere difficile capire se un particolare metodo del ciclo di vita verrà eseguito prima o dopo un metodo che abbiamo scritto noi stessi. Tuttavia, spesso è importante conoscere questa informazione. Possiamo quindi aggiungere dei log come mostrato di seguito:


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()) {
      ...
  }
}
  • Le righe 7, 14 e 21 utilizzano la classe [Log]. Questa classe consente di scrivere i log nella console Android [logcat]. I log sono classificati in vari livelli (info, warning, debug, verbose, error). [Log.d] visualizza i log di livello [debug]. Il suo primo argomento è la fonte del messaggio di log. Infatti, diverse fonti possono inviare messaggi alla console di log. Per distinguerle, utilizziamo questo primo argomento. Il secondo argomento è il messaggio da scrivere nella console di log;

Se eseguiamo nuovamente il progetto [Example-01], otteniamo i seguenti log:


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

Possiamo vedere che il metodo [onCreate], che crea l'attività Android, viene eseguito prima del metodo [onCreateOptionsMenu], che crea il menu dell'app.

Ora, se clicchiamo sull'opzione di menu nell'emulatore Android [1]:

  

il seguente messaggio viene aggiunto alla console di log:


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

D'ora in poi, aggiungeremo spesso istruzioni di log al codice Android. Il più delle volte non le commenteremo. Sono lì semplicemente per incoraggiare il lettore a guardare la console di log al fine di comprendere gradualmente il ciclo di vita di un'applicazione Android.

1.2.2.5. Gestione dell'emulatore [Genymotion]

A volte, l'emulatore Genymotion si blocca e non può essere riavviato. Questo perché i processi di VirtualBox sono ancora in esecuzione nel Task Manager. Apri il Task Manager [Ctrl-Alt-Canc] ed elimina tutti i processi di VirtualBox:

Una volta fatto ciò, riavvia l'emulatore Genymotion da Android Studio.

1.2.2.6. Gestione del file binario APK creato

La compilazione del progetto produce un file binario con estensione .apk:

Esistono due versioni: una denominata [debug] e l'altra denominata [debug-unaligned]. È consigliabile utilizzare la prima; l'altra è una versione intermedia. Il file binario .apk generato al punto [4] può essere trasferito direttamente su un emulatore o su un dispositivo Android. Per trasferirlo su un emulatore, è sufficiente trascinarlo con il mouse sull'emulatore.

1.3. Esempio-02: Un progetto Android di base

Creiamo un nuovo progetto Android utilizzando Android Studio [1-12]:

 

In [13], eseguiamo l'app. Successivamente vediamo la schermata mostrata in [14] sull'emulatore Genymotion.

1.3.1. Configurazione Gradle

Il progetto creato è configurato dal seguente file [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'
}

Questo file è stato generato dall'IDE utilizzando le sue impostazioni di configurazione. Si tratta di un file minimale che amplieremo gradualmente.

  • righe 3–12: le caratteristiche dell'applicazione Android;
  • righe 22–25: le sue dipendenze. È qui che apporteremo principalmente le modifiche sulla base degli esempi studiati;

1.3.2. Il manifesto dell'applicazione

  

Il file [AndroidManifest.xml] [1] definisce le caratteristiche del file binario dell'applicazione Android. Il suo contenuto è il seguente:


<?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>
  • riga 3: il pacchetto del progetto Android;
  • riga 10: il nome dell'attività;

Queste due informazioni provengono dai dati inseriti al momento della creazione del progetto:

  • La riga 3 del manifesto (pacchetto) deriva dalla voce [4] sopra riportata. In questo pacchetto vengono generate automaticamente diverse classi;
  • la riga 10 del manifesto (nome dell'attività) proviene dalla voce [1] sopra;

Torniamo al manifesto:


<?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>
  • Riga 10: l'attività principale dell'applicazione. Fa riferimento alla classe [1] sopra;
  • riga 6: l'icona dell'applicazione [2]. Può essere modificata;
  • riga 7: l'etichetta dell'applicazione. Si trova nel file [strings.xml] [3]:

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

Il file [strings.xml] contiene le stringhe utilizzate dall'applicazione. Riga 2: il nome dell'applicazione deriva dalla voce inserita durante la compilazione del progetto [4]:

 
  • riga 10: un tag di attività. Un'applicazione Android può avere più attività;
  • riga 12: l'attività è designata come attività principale;
  • riga 13: e deve apparire nell'elenco delle app che possono essere avviate sul dispositivo Android.

1.3.3. L'attività principale

 

Un'app Android si basa su una o più attività. In questo caso, è stata generata un'attività [1]: [MainActivity]. A seconda del tipo, un'attività può visualizzare una o più viste. La classe [MainActivity] generata è la seguente:


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);
  }
}
  • riga 6: la classe [MyActivity] estende la classe Android [AppCompatActivity]. Questo vale per tutte le attività future;
  • riga 9: il metodo [onCreate] viene eseguito quando viene creata l'attività. Ciò avviene prima che venga visualizzata la vista associata all'attività;
  • riga 10: viene chiamato il metodo [onCreate] della classe padre. Questo deve essere sempre fatto;
  • riga 11: il file [activity_main.xml] [2] è la vista associata all'attività. La definizione XML di questa vista è la seguente:

<?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>
  • righe b-k: il gestore di layout. L'impostazione predefinita è il tipo [RelativeLayout]. In questo tipo di contenitore, i componenti sono posizionati l'uno rispetto all'altro (a destra, a sinistra, sotto, sopra);
  • righe m-p: un componente [TextView] utilizzato per visualizzare il testo;
  • riga n: il testo visualizzato. Non è consigliabile inserire il testo direttamente nelle viste. È preferibile spostare questo testo nel file [res/values/strings.xml] [3]:

Il testo visualizzato sarà quindi [Hello World!]. Dove verrà visualizzato? Il contenitore [RelativeLayout] riempirà lo schermo. Il [TextView], che è il suo unico elemento, verrà visualizzato in alto a sinistra di questo contenitore, e quindi in alto a sinistra dello schermo;

Cosa significa [R.layout.activity_main] alla riga 11? A ogni risorsa Android (viste, frammenti, componenti, ecc.) viene assegnato un identificatore. Pertanto, una vista [V.xml] situata nella cartella [res/layout] sarà identificata come [R.layout.V]. R è una classe generata nella cartella [app/build/generated] [1-3]:

 

La classe [R] è la seguente:


...............
    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;
}
  • riga 14: l'attributo [R.layout.activity_main] è l'identificatore della vista [res/layout/activity_main.xml];
  • Riga 7: l'attributo [R.string.app_name] corrisponde all'ID stringa [app_name] nel file [res/values/string.xml]:
  • Riga 19: l'attributo [R.mipmap.ic_launcher] è l'identificatore dell'immagine [res/mipmap/ic_launcher];

Quindi, ricorda che quando fai riferimento a [R.layout.activity_main] nel codice, stai facendo riferimento a un attributo della classe [R]. L'IDE ti aiuta a identificare i diversi elementi di questa classe:

1.3.4. Esecuzione dell'applicazione

Per eseguire un'applicazione Android, è necessario creare una configurazione di esecuzione:

  • In [1], selezionare [Modifica configurazioni];
  • Il progetto è stato creato con una configurazione [app], che elimineremo [2] per ricrearlo;
  • in [3], creare una nuova configurazione di esecuzione;
  
  • in [4], selezionare [Applicazione Android];

Image

  • in [5], selezionare il modulo [app] dall'elenco a discesa;
  • In [6-8], mantenere i valori predefiniti;
  • In [7], l'attività predefinita è quella definita nel file [AndroidManifest.xml] (riga 1 di seguito):

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
</activity>
  • In [8], selezionare [Mostra finestra di dialogo di selezione] per scegliere il dispositivo su cui verrà eseguita l'app (emulatore, tablet);
  • In [9], specificare che questa scelta deve essere salvata;
  • Confermare la configurazione;
  
  • In [11], avviare il gestore dell'emulatore [Genymotion] (vedere la sezione 6.9);
  • In [12], selezionare un emulatore di tablet e avviarlo [13];
  • in [14], eseguire la configurazione di esecuzione [app];
  • in [15], viene visualizzato il modulo di selezione del dispositivo di runtime. Qui è disponibile una sola opzione: l'emulatore [Genymotion] avviato in precedenza;

Dopo qualche istante, l'emulatore software visualizza la seguente schermata:

Image

1.3.5. Il ciclo di vita di un'attività

Torniamo al codice dell'attività [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);
  }
}

Il metodo [onCreate] alle righe 8-12 è uno dei metodi che possono essere chiamati durante il ciclo di vita di un'attività. La documentazione Android elenca questi metodi:

 
  • [1]: Il metodo [onCreate] viene chiamato all'avvio dell'attività. È in questo metodo che l'attività viene associata a una vista e vengono recuperati i riferimenti ai suoi componenti;
  • [2-3]: Vengono quindi chiamati i metodi [onStart] e [onResume]. Si noti che il metodo [onResume] è l'ultimo metodo ad essere eseguito prima che l'attività attualmente in esecuzione raggiunga lo stato [4];

1.4. Esempio-03: Riscrittura del progetto [Esempio-02] utilizzando la libreria [Android Annotations]

Introdurremo ora la libreria [Android Annotations], che semplifica la scrittura di applicazioni Android. A tal fine, duplicare l'esempio [Esempio-02] in [Esempio-03] seguendo i passaggi [1-16].

  • Al punto [1], selezionare la vista [Progetto] per visualizzare l'intero progetto Android;

Nota: tra [14] e [15] siamo passati dalla vista [Android] alla vista [Progetto] (vedere la sezione 1.2.2.1).

Modifichiamo quindi il file [res/values/strings.xml] [17]:

 

Il file [strings.xml] viene modificato come segue:


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

Ora eseguiamo la nuova applicazione, che ha mantenuto tutte le impostazioni di [Esempio-02]:

 

In [19], otteniamo lo stesso risultato dell'[Esempio-02], ma con un nuovo nome.

Introdurremo ora la libreria [Android Annotations], che chiameremo AA in breve. Questa libreria introduce nuove classi per l'annotazione del codice sorgente Android. Queste annotazioni saranno utilizzate da un processore che creerà nuove classi Java nel modulo; queste classi parteciperanno alla compilazione del modulo proprio come le classi scritte dallo sviluppatore. Abbiamo quindi la seguente catena di build:

Per prima cosa, aggiungeremo le dipendenze per il compilatore di annotazioni AA (il processore menzionato sopra) al file [build.gradle]:


def AAVersion = '4.0.0'
 
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
}
  • Le righe 4–5 aggiungono le due dipendenze che compongono la libreria AA;

Il file [build.gradle] viene modificato nuovamente per utilizzare un plugin chiamato [android-apt], che suddivide il processo di compilazione in due fasi:

  • elaborazione delle annotazioni Android, che genera nuove classi;
  • compilazione di tutte le classi del progetto;

buildscript {
  repositories {
    mavenCentral()
  }
 
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • riga 8: versione del plugin [android-apt] che verrà cercata nel repository centrale di Maven (riga 3);
  • riga 13: attivazione di questo plugin;

A questo punto, verificare che la configurazione di esecuzione [app] funzioni ancora.

Ora introdurremo la prima annotazione della libreria AA nella classe [MainActivity]:

  

La classe [MainActivity] attualmente ha questo aspetto:


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

Abbiamo già spiegato questo codice nella sezione 1.3.3. Lo modifichiamo come segue:


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);
  }
}
  • Riga 7: L'annotazione [@EActivity] è un'annotazione AA (riga 3). Il suo parametro è la vista associata all'attività;

Questa annotazione genererà una classe [MainActivity_] derivata dalla classe [MainActivity], e questa classe sarà l'attività effettiva. Dobbiamo quindi modificare il manifesto del progetto [AndroidManifest.xml] come segue:


<?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>
  • riga 11: la nuova attività;

Una volta fatto questo, possiamo compilare il progetto [1]:

 
  • In [2], vediamo la classe [MainActivity_] generata nella cartella [app/build/generated/source/apt/debug];

La classe [MainActivity_] generata è la seguente:


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

Non tenteremo di spiegare il codice delle classi generate da AA. Esse gestiscono la complessità che le annotazioni mirano a nascondere. Tuttavia, a volte può essere utile esaminarlo quando si desidera comprendere come vengono “tradotte” le annotazioni che si utilizzano.

Ora possiamo eseguire nuovamente la configurazione [app]. Otteniamo lo stesso risultato di prima. Useremo ora questo progetto come punto di partenza e lo duplicheremo per introdurre i concetti chiave della programmazione Android.

1.5. Esempio-04: Viste ed eventi

1.5.1. Creazione del progetto

Seguiremo la procedura descritta per duplicare [Esempio-02] in [Esempio-03] nella sezione 1.4:

Noi:

  • duplichiamo il progetto [Esempio-03] in [Esempio-04] (dopo aver eliminato la cartella [app/build] da [Esempio-03]);
  • carichiamo il progetto [Esempio-04];
  • modifichiamo il nome del progetto nel file [app / res / values / strings.xml] (prospettiva Android);
  • Eliminiamo il file [Example-04 / Example-04.iml] (vista Progetto);
  • compiliamo e poi eseguiamo il progetto;
 

1.5.2. Creazione di una vista

Ora useremo l'editor grafico per modificare la vista visualizzata dal progetto [Example-04]:

  • In [1-4], creare una nuova vista XML;
  • In [5], assegnare un nome alla vista;
  • In [6], specificare il tag radice della vista. Qui, scegliamo un contenitore [RelativeLayout]. All'interno di questo contenitore di componenti, i componenti sono posizionati l'uno rispetto all'altro: "a destra di", "a sinistra di", "sotto", "sopra";
  

Il file [vue1.xml] generato [7] è il seguente:


<?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>
  • Riga 2: un contenitore [RelativeLayout] vuoto che occuperà l'intera larghezza del tablet (riga 3) e la sua intera altezza (riga 4);
  • In [1], selezionare la scheda [Design] nella vista [vue1.xml] visualizzata;
  • in [2-4], passare alla modalità tablet;
  • in [5], imposta la scala su 1 per il tablet;
  • In [6], selezionare la modalità "orizzontale" per il tablet;
  • La schermata [7] riassume le scelte effettuate.
  • In [1], selezionare un [Testo grande] e trascinarlo sulla vista [2];
  • In [3], fare doppio clic sul componente;
  • In [4], modificare il testo visualizzato. Anziché codificarlo direttamente nella vista XML, lo esternalizzeremo nel file [res/values/string.xml]
  • In [5], aggiungi un nuovo valore al file [strings.xml];
  • in [8], assegnare un identificatore alla stringa;
  • in [9], assegnare il valore della stringa;
  • in [10], la nuova vista dopo la convalida del passaggio precedente;
  • dopo aver fatto doppio clic sul componente, ne modifichiamo l'ID [11];
  • in [12], nelle proprietà del componente, modificare la dimensione del carattere [50pt];
  • in [13], la nuova vista;

Il file [vue1.xml] è stato modificato come segue:


<?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>
  • Le modifiche apportate nella GUI si trovano alle righe 10, 11 e 14. Gli altri attributi del [TextView] sono valori predefiniti o derivano dal posizionamento del componente all'interno della vista;
  • righe 7–8: le dimensioni del componente corrispondono a quelle del testo che contiene (wrap_content) sia in altezza che in larghezza;
  • riga 13: la parte superiore del componente è allineata con la parte superiore della vista (riga 13), 50 pixel più in basso (riga 13);
  • riga 12: il lato sinistro del componente è allineato con il lato sinistro della vista (riga 13), 213 pixel a destra (riga 12);

In genere, le dimensioni esatte dei margini sinistro, destro, superiore e inferiore vengono impostate direttamente nel codice XML.

Seguendo la stessa procedura, crea la seguente vista [1]:

 

I componenti sono i seguenti:

N.
ID
Tipo
Ruolo
1
titoloTextView1
TextView
Titolo della vista
2
textView1
TextView
una domanda
3
modificaNomeTesto
Modifica testo
inserisci un nome
4
pulsanteConvalida
Pulsante
per confermare l'inserimento
5
pulsanteView2
Pulsante
per passare alla vista n. 2

Posizionare i componenti l'uno rispetto all'altro può essere frustrante, poiché il comportamento dell'editor grafico è talvolta imprevedibile. Potrebbe essere preferibile utilizzare le proprietà dei componenti:

Il componente [textView1] deve essere posizionato 50 pixel sotto il titolo e a 50 pixel dal bordo sinistro del contenitore:

  • in [1], il bordo superiore del componente è allineato con il bordo inferiore del componente [textViewTitreVue1] a una distanza di 50 pixel [3] (in alto);
  • in [2], il bordo sinistro (left) del componente è allineato con il bordo sinistro del contenitore a una distanza di 50 pixel [3] (left);

Il componente [editTextNom] deve essere posizionato 60 pixel a destra del componente [textView1] e allineato in basso con lo stesso componente;

 
  • in [1], il bordo sinistro del componente è allineato con il bordo destro del componente [textView1] a una distanza di 60 pixel [2] (sinistra). È allineato con il bordo inferiore (bottom:bottom) del componente [textView1] [1];

Il componente [buttonValider] deve essere posizionato 60 pixel a destra del componente [editTextNom] e allineato in basso con lo stesso componente;

 
  • In [1], il bordo sinistro del componente è allineato con il bordo destro del componente [editTextNom] a una distanza di 60 pixel [2] (sinistra). È allineato con il bordo inferiore del componente [editTextNom] (bottom:bottom) [1];

Il componente [buttonVue2] deve essere posizionato 50 pixel sotto il componente [textView1] e allineato a sinistra di tale componente;

 
  • in [1], il bordo sinistro del componente è allineato con il bordo sinistro del componente [textView1] ed è posizionato sotto di esso (top:bottom) a una distanza di 50 pixel [2] (top);

Il file XML generato è il seguente:


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

Questo contiene tutti gli elementi grafici. Un altro modo per creare una vista è modificare direttamente questo file. Una volta che ci si abitua, questo metodo può risultare più veloce rispetto all'uso dell'editor grafico.

  • Alla riga 38 sono presenti informazioni che non abbiamo mostrato. Sono fornite tramite le proprietà del componente [editTextNom] [1]:
 

Tutto il testo proviene dal seguente file [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>

Ora modifichiamo [MainActivity] in modo che questa vista venga visualizzata all'avvio dell'app:


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);
  }
}
  • Riga 7: La vista [vue1.xml] viene ora visualizzata dall'attività;

Modifica il file [AndroidManifest.xml] come segue:


<?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>
  • Riga 12: Questa riga di configurazione impedisce alla tastiera di apparire non appena viene visualizzata la vista [vue1]. Questo perché la vista contiene un campo di immissione che ha il focus quando la vista viene visualizzata. Per impostazione predefinita, questo focus fa apparire la tastiera virtuale;

Esegui l'applicazione e verifica che la vista [view1.xml] venga effettivamente visualizzata:

Image

1.5.3. Gestione degli eventi

Ora gestiamo il clic sul pulsante [Validate] nella vista [View1]:

Image

Il codice per [MainActivity] cambia come segue:


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 {
 
  // visual interface elements
  @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");
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
}
  • Righe 17–18: Associamo il campo [protected EditText editTextNom] al componente identificato da [R.id.editTextNom] nell'interfaccia visiva. Il campo associato al componente deve essere accessibile nella classe derivata [MainActivity_] e per questo motivo non può avere un ambito [private]. Il campo identificato da [R.id.editTextNom] proviene dalla 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: non utilizzare caratteri accentati negli identificatori [id]. AA non li gestisce correttamente.

  • Riga 32: L'annotazione [@Click(R.id.buttonValider)] specifica il metodo che gestisce l'evento 'Click' sul pulsante con ID [R.id.buttonValider]. Anche questo ID proviene dalla 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"/>
  • Riga 35: visualizza il nome inserito:
    • Toast.makeText(...).show(): visualizza il testo sullo schermo,
    • il primo parametro di makeText è l'attività,
    • il secondo parametro è il testo da visualizzare nella finestra di dialogo che verrà visualizzata da makeText,
    • il terzo parametro è la durata della finestra visualizzata: Toast.LENGTH_LONG o Toast.LENGTH_SHORT;
  • riga 26, l'annotazione [@AfterViews] indica che il metodo deve essere eseguito una volta che tutti i campi annotati con [@ViewById] sono stati inizializzati. È importante sapere quando questi campi vengono inizializzati. Ad esempio, possiamo usare il riferimento della riga 18 nel metodo [onCreate]? Per rispondere a questa domanda, abbiamo aggiunto dei log;

Esegui il progetto [Example-04] e verifica che accada qualcosa quando clicchi sul pulsante [Validate]. Otteniamo i seguenti log:

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

Concludiamo che, quando viene eseguito il metodo [onCreate], i campi contrassegnati con [@ViewById] non sono ancora inizializzati. Anche in questo caso, invitiamo i lettori alle prime armi a inserire questo tipo di log nei metodi che gestiscono il ciclo di vita dell'applicazione.

1.6. Esempio-05: Navigazione tra le viste

Nel progetto precedente, il pulsante [View 2] non è stato utilizzato. Proponiamo di utilizzarlo creando una seconda vista e dimostrando come navigare tra le viste. Esistono diversi modi per risolvere questo problema. L'approccio qui proposto consiste nell'associare ogni vista a un'attività. Un altro metodo consiste nell'avere una singola [AppCompatActivity] che visualizza le viste [Fragment]. Questo sarà il metodo utilizzato nelle applicazioni future.

1.6.1. Creazione del progetto

Duplichiamo il progetto [Esempio-04] in [Esempio-05]. Per farlo, seguiremo la procedura descritta per duplicare [Esempio-02] in [Esempio-03] nella Sezione 1.4, che è stata riprodotta nella Sezione 1.5.

1.6.2. Aggiunta di una seconda attività

Per gestire una seconda vista, creeremo una seconda attività. Questa attività gestirà la vista n. 2. Qui stiamo seguendo un modello "una vista per attività". Sono possibili altri modelli.

Image

  • In [1-4], creiamo una nuova attività;

Image

  • in [5], il nome della classe che verrà generata;
  • in [6], il nome della vista (view2.xml) associata alla nuova attività;
  
  • in [7-8], i file interessati dalla configurazione precedente;

L'attività [SecondActivity] è la seguente:


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);
  }
}
  • Riga 11: L'attività è associata alla vista [vue2.xml];

La vista [vue2.xml] è la seguente:


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

Attualmente si tratta di una vista vuota con un gestore di layout [RelativeLayout] (riga 2). Alla riga 11, possiamo vedere che è stata associata alla nuova attività.

Il manifesto del modulo Android [AndroidManifest.xml] è stato modificato come segue:


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

Riga 20: è stata registrata una seconda attività.

1.6.3. Passaggio dalla Vista 1 alla Vista 2

Torniamo al codice della classe [MainActivity], che visualizza la Vista 1. La transizione alla Vista 2 non è attualmente gestita:

  

Procediamo come segue:


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // navigate to view no. 2 by passing it the name entered in view no. 1
    // create an Intent
    Intent intent = new Intent();
    // we associate this Intent with an activity
    intent.setClass(this, SecondActivity.class);
    // we associate information with this Intent
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // launch the [SecondActivity] activity by passing it the Intent
    startActivity(intent);
}
  • righe 2-3: il metodo [navigateToView2] gestisce il 'clic' sul pulsante identificato da [R.id.buttonVue2] definito nella 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"/>

I commenti descrivono i passaggi da seguire per la modifica della vista:

  1. riga 6: crea un oggetto di tipo [Intent]. Questo oggetto specificherà sia l'attività da avviare sia le informazioni da passarle;
  2. riga 8: associa l'Intent a un'attività, in questo caso un'attività di tipo [SecondActivity] che sarà responsabile della visualizzazione della vista n. 2. Ricorda che [MainActivity] visualizza la vista n. 1. Quindi abbiamo una vista = un'attività. Dovremo definire il tipo [SecondActivity];
  3. Riga 10: facoltativamente, aggiungi informazioni all'oggetto [Intent]. Queste informazioni sono destinate alla [SecondActivity] che verrà avviata. I parametri per [Intent.putExtra] sono (Object key, Object value). Nota che il metodo [EditText.getText()], che restituisce il testo inserito nel campo di testo, non restituisce un tipo [String] ma un tipo [Editable]. È necessario utilizzare il metodo [toString] per ottenere il testo inserito;
  4. Riga 12: Avvia l'attività definita dall'oggetto [Intent].

Esegui il progetto [Example-05] e verifica che venga visualizzata la Vista n. 2 (per ora vuota):

1.6.4. Creazione della Vista n. 2

 
  • In [1-2], rimuoviamo la vista [main.xml], che non ci serve più, quindi modifichiamo la vista [vue2.xml] come segue:
 

I componenti sono i seguenti:

N.
ID
Tipo
Ruolo
1
titoloTextView2
TextView
Titolo della vista
2
textViewCiao
TextView
un po' di testo
5
btn_view1
Pulsante
per andare alla vista n. 1

Il file XML [vue2.xml] è il seguente:


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

Esegui il progetto [Example-05] e verifica che venga visualizzata la nuova vista quando fai clic sul pulsante [View #2].

1.6.5. L'attività [SecondActivity]

In [MainActivity], abbiamo scritto il codice seguente:


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

Alla riga 9, abbiamo passato a [SecondActivity] delle informazioni che non sono state utilizzate. Ora le stiamo utilizzando, e questo avviene nel codice di [SecondActivity]:

  

Il codice per [SecondActivity] cambia come segue:


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 {
 
  // visual interface components
  @ViewById
  protected TextView textViewBonjour;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews() {
    // recover intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // we retrieve the name
        String nom = extras.getString("NOM");
        if (nom != null) {
          // we display it
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }
 
}
  • riga 11: utilizziamo l'annotazione [@EActivity] per indicare che la classe [SecondActivity] è un'attività associata alla vista [vue2.xml];
  • righe 15–16: recuperiamo un riferimento al componente [TextView] identificato da [R.id.textViewBonjour]. Qui non abbiamo scritto [@ViewById(R.id.textViewBonjour)]. In questo caso, AA presume che l'identificatore del componente sia identico al campo annotato, in questo caso il campo [textViewBonjour];
  • riga 23: l'annotazione [@AfterViews] contrassegna un metodo che deve essere eseguito dopo che i campi annotati con [@ViewById] sono stati inizializzati. Nel metodo [OnCreate] (riga 19), questi campi non possono essere utilizzati perché non sono ancora stati inizializzati. Nel progetto [Example-05], passiamo da un'attività all'altra e inizialmente non era chiaro se il metodo annotato con [@AfterViews] sarebbe stato eseguito una volta durante l'istanziazione iniziale dell'attività o ogni volta che l'attività viene avviata. I test hanno dimostrato che la seconda ipotesi era corretta;
  • riga 26: la classe [AppCompatActivity] dispone di un metodo [getIntent] che restituisce l'oggetto [Intent] associato all'attività;
  • riga 28: il metodo [Intent.getExtras] restituisce un oggetto [Bundle], che è una sorta di dizionario contenente informazioni associate all'oggetto [Intent] dell'attività;
  • riga 31: recuperiamo il nome memorizzato nell'oggetto [Intent] dell'attività;
  • riga 34: lo visualizziamo.

Promemoria: i campi annotati con l'annotazione [@ViewById] non devono contenere caratteri accentati.

Torniamo alla classe [SecondActivity]. Poiché abbiamo scritto:


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

AA genererà una classe [SecondActivity_] derivata da [SecondActivity], e questa classe sarà l'attività effettiva. Questo ci porta ad apportare modifiche in:

[MainActivity]


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // we associate this Intent with an activity
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • Alla riga 6, dobbiamo sostituire [SecondActivity] con [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>
  • Alla riga 20, sostituisci [SecondActivity] con [SecondActivity_];

Prova questa nuova versione. Digita un nome nella vista n. 1 e verifica che la vista n. 2 lo visualizzi correttamente.

1.6.6. Passaggio dalla vista n. 2 alla vista n. 1

Per passare dalla vista n. 2 alla vista n. 1, seguiremo la procedura vista in precedenza:

  • Inserire il codice di navigazione nell'attività [SecondActivity] che visualizza la Vista 2;
  • scrivere il metodo [@AfterViews] nell'attività [MainActivity] che visualizza la Vista 1;

Il codice per [SecondActivity] cambia come segue:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // we create an Intent for activity [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // retrieve the Intent of the current activity [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // we put the name in the Intent of [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // launch [MainActivity]
      startActivity(intent1);
    }
}
  • righe 1-2: associamo il metodo [navigateToView1] al clic sul pulsante [btn_vue1];
  • riga 4: creiamo un nuovo [Intent];
  • riga 5: associato all'attività [MainActivity_];
  • riga 7: recuperiamo l'Intent associato a [SecondActivity];
  • riga 9: recuperiamo le informazioni da questo Intent;
  • riga 12: la chiave [NAME] viene recuperata da [intent2] e inserita in [intent1] con lo stesso valore associato;
  • riga 15: viene avviata l'attività [MainActivity_].

Nel codice di [MainActivity], aggiungiamo il seguente metodo [@AfterViews]:


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

Apporta queste modifiche e prova la tua app. Ora, quando torni dalla Vista 2 alla Vista 1, dovrebbe essere visualizzato il nome che hai inserito inizialmente, cosa che fino ad ora non avveniva.

1.6.7. Ciclo di vita dell'attività

Nella Sezione 1.3.5 abbiamo introdotto il ciclo di vita di un'attività. Qui abbiamo due attività e passiamo da una all'altra durante l'esecuzione. Queste attività contengono due metodi — [onCreate] e [afterViews] — e non è immediatamente chiaro quando uno viene chiamato rispetto all'altro. È importante saperlo. Per scoprirlo, aggiungeremo dei log a entrambe le attività:

Quindi, nella classe [MainActivity], scriviamo:


  // manufacturer
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    ...
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
    ...
    }
}
  • righe 2–4: vogliamo sapere se la classe [MainActivity] viene istanziata una o più volte;
  • riga 8: vogliamo sapere se il metodo [onCreate] viene chiamato una o più volte;
  • riga 14: vogliamo sapere se il metodo [afterViews] viene chiamato una o più volte;

Facciamo esattamente la stessa cosa nella classe [SecondActivity].

All'avvio dell'app, vediamo i seguenti log:

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

I metodi [onCreate, afterViews] della prima attività sono stati eseguiti in questo ordine. Quando si fa clic sul pulsante [View #2], i nuovi log sono i seguenti:

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

I metodi [onCreate, afterViews] della seconda attività sono stati eseguiti in questo ordine. Quando si fa clic sul pulsante [View #1], i nuovi log sono i seguenti:

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

La classe [MainActivity] viene quindi istanziata nuovamente. Quando si fa clic sul pulsante [View #2], i nuovi log sono i seguenti:

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

La classe [SecondActivity] viene quindi istanziata nuovamente.

Entrambe le attività vengono quindi ricreate sistematicamente ogni volta che si cambia attività.

Esploreremo ora un'architettura con una singola attività in grado di gestire più viste chiamate frammenti. L'attività e le viste verranno istanziate una sola volta, a differenza del metodo precedente in cui un'attività poteva essere istanziata più volte.

1.7. Esempio-06: Navigazione a schede

Qui esploreremo le interfacce a schede. L'esempio è complesso ma introduce tutti gli elementi che useremo in seguito: singola attività, gestore di frammenti (viste), contenitore di frammenti, navigazione tra i frammenti. Il concetto di schede differisce da quello dei frammenti ed è secondario rispetto a ciò che vogliamo dimostrare in questo esempio.

1.7.1. Creazione del progetto

Creiamo un nuovo progetto:

 
  • in [7], selezionare un'attività a schede (Tabbed Activity);
  • in [10-14], mantenere i valori predefiniti;
  • in [15], selezionare le schede con una barra del titolo;

Il progetto risultante è il seguente:

 
  • in [1], l'attività;
  • in [2], le viste;

È stata creata automaticamente una configurazione di runtime [app], che prende il nome dal modulo [2b]:

 

È possibile eseguirla. Viene quindi visualizzata una finestra con tre schede [3-6]:

Image

1.7.2. Configurazione Gradle

Il progetto [Example-06] è stato generato con il seguente file [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'
}

C'è un nuovo elemento rispetto a quanto visto in precedenza: la riga 25. Questa libreria è necessaria per i nuovi componenti utilizzati dall'applicazione generata.

1.7.3. La vista [activity_main]

  

La vista [activity_main] è la vista associata alla [MainActivity] del progetto. In modalità [design], la vista appare così:

Image

Contiene i seguenti componenti:

  
  • [main_content] è l'intera vista;
  • [appbar] (riquadro rosso, 1) è la barra delle applicazioni. Contiene due componenti:
    • [toolbar] (riquadro giallo 4) è la barra degli strumenti;
    • [tabs] (riquadro arancione, 5) è la barra dei titoli delle schede;
  • [container] (riquadro verde, 2) può contenere vari frammenti. Un frammento è una vista. Pertanto, la stessa attività può visualizzare più viste (frammenti) in questo contenitore;
  • [fab] (componente 3) è chiamato componente fluttuante;

In modalità [testo], il codice è il seguente:


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

Vediamo gli elementi descritti in precedenza:

  • righe 2–49: la definizione del componente [main_content] (riga 5), che costituisce l'intera vista. Possiamo vedere che si tratta di un layout [CoordinatorLayout] (riga 2);
  • righe 11–33: il contenitore [appbar] (riga 12). Si tratta di un [AppBarLayout] (riga 11);
  • righe 18–24: il componente [toolbar] (riga 19) di tipo [Toolbar] (riga 18);
  • righe 28–31: il contenitore [tabs] (riga 29). Si tratta di un layout di tipo [TabLayout] (riga 28). Visualizzerà i titoli delle schede;
  • righe 35–39: il componente [container] (riga 36). Questo contenitore visualizza le diverse viste dell'attività;
  • righe 41–47: il componente [fab] (riga 42) di tipo [FloatingActionButton] (riga 41). Si tratta di un pulsante su cui è possibile cliccare. Per impostazione predefinita, è posizionato in basso a destra dell'intera vista;

Non cercheremo di comprendere il significato di tutti gli attributi di questi componenti. Li useremo così come sono. È attraverso l’esperienza — e spesso in modalità [design] — che scopriamo i loro ruoli. In questa modalità, scopriamo che i componenti hanno decine di attributi. Generalmente, solo alcuni vengono inizializzati, mentre gli altri mantengono i loro valori predefiniti.

Chiariamo tuttavia alcuni punti. La maggior parte dei valori che configurano le diverse viste sono raccolti nella cartella [res/values]:

  

Questi valori sono citati alle righe 15–16, 23, 39 e 46 del file [activity_main.xml]. Facciamo un esempio:

  • riga 15:

    android:paddingTop="@dimen/appbar_padding_top"

L'annotazione [@dimen] fa riferimento al file [res/values/dimens.xml]:


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

La riga 15 del file [activity_main.xml] fa riferimento alla riga (f) sopra;

Analogamente, l'annotazione:

  • [@string] fa riferimento al file di risorse [res/values/strings.xml];
  • [@color] fa riferimento al file di risorse [res/values/colors.xml];
  • [@style] si riferisce al file di risorse [res/values/styles.xml];

1.7.4. L'attività

  

Il codice generato per l'attività rispecchia la complessità della vista descritta sopra: è complesso. Lo analizzeremo in diverse fasi.

1.7.4.1. Gestione di frammenti e schede

Il codice in [MainActivity] relativo ai frammenti e alle schede è il seguente:


package exemples.android;
 
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
 
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
 
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container 
  private ViewPager mViewPager;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // parent
    super.onCreate(savedInstanceState);
    // view
    setContentView(R.layout.activity_main);
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }
 
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • riga 28: Android fornisce un contenitore di viste di tipo [android.support.v4.view.ViewPager] (riga 12). Questo contenitore deve essere dotato di un gestore di viste o frammenti. È responsabilità dello sviluppatore fornirlo;
  • riga 25: il gestore di frammenti utilizzato in questo esempio. La sua implementazione si trova alle righe 61–63;
  • riga 31: il metodo eseguito quando viene creata l'attività;
  • riga 35: la vista [activity_main.xml] è associata all'attività;
  • riga 37: recuperiamo il riferimento al componente [toolbar] della vista tramite il suo identificatore;
  • riga 38: questa barra degli strumenti diventa la barra delle azioni dell'attività (un concetto Android);
  • riga 40: il gestore di frammenti viene istanziato. Il parametro del costruttore è la classe Android [android.support.v4.app.FragmentManager] (riga 10);
  • riga 44: recuperiamo il riferimento al contenitore dei frammenti dalla vista [activity_main.xml] tramite il suo ID;
  • riga 45: il gestore dei frammenti viene collegato al contenitore dei frammenti. Ciò significa che quando al contenitore dei frammenti viene richiesto di visualizzare il frammento #i, lo richiederà al gestore dei frammenti;
  • riga 48: recuperiamo un riferimento alla barra delle schede tramite il suo identificatore;
  • riga 49: il gestore delle schede viene associato al contenitore dei frammenti. Ciò significa che quando si fa clic sulla scheda #i, il contenitore visualizzerà il frammento #i. L'associazione tra il gestore delle schede e il contenitore dei frammenti elimina la necessità di una gestione delle schede. Pertanto, non è necessario definire un gestore di eventi per il clic su una scheda. L'associazione con il contenitore dei frammenti lo fornisce di default. Vedremo un esempio in cui ci sono più frammenti che schede. In questo caso, non creiamo questa associazione.

Il gestore di frammenti [SectionsPagerAdapter] è il seguente:


// the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return 3;
    }
 
    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      switch (position) {
        case 0:
          return "SECTION 1";
        case 1:
          return "SECTION 2";
        case 2:
          return "SECTION 3";
      }
      return null;
    }
  }
}
  • I frammenti visualizzati da un'app dipendono dall'app stessa. Il gestore dei frammenti è definito dallo sviluppatore;
  • riga 5: il gestore di frammenti estende la classe Android [android.support.v4.app.FragmentPagerAdapter]. Il costruttore ci viene fornito. Dobbiamo definire almeno i seguenti due metodi:
    • int getCount(): restituisce il numero di frammenti da gestire;
    • Fragment getItem(i): restituisce il frammento n. i;

Il metodo CharSequence getPageTitle(i), che restituisce il titolo del frammento n. i, è facoltativo. Poiché il gestore delle schede è stato associato al gestore dei frammenti, il titolo della scheda n. i sarà il titolo del frammento n. i. Pertanto, i titoli nelle righe 27-33 saranno i titoli delle schede;

  • Righe 18-21: getCount restituisce il numero di frammenti gestiti, in questo caso tre;
  • righe 11–15: getItem(i) restituisce il frammento n. i. In questo caso, tutti i frammenti saranno dello stesso tipo, [PlaceholderFragment];
  • righe 24–35: getPageTitle(int i) restituisce il titolo del frammento #i;

1.7.4.2. I frammenti visualizzati

  

I frammenti dell'attività sono tutti dello stesso tipo e sono tutti associati alla seguente 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>
  • righe 1–16: un layout [RelativeLayout];
  • righe 11-14: l'unico componente della vista (frammento): un [TextView] identificato da [section_label];

In [MainActivity], i frammenti gestiti sono del seguente tipo [PlaceholderFragment]:


// a fragment
  public static class PlaceholderFragment extends Fragment {
      // a text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
    }
 
    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // view [fragment_main] is instantiated
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // the [TextView] is found
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // its content is modified
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // we return the view
      return rootView;
    }
  }
  • riga 2: la classe [PlaceholderFragment] estende la classe [Fragment] di Android. Questo è generalmente sempre il caso;
  • riga 2: la classe [PlaceholderFragment] è statica. Il suo metodo [newInstance] (riga 10) consente di ottenere istanze di tipo [PlaceholderFragment];
  • righe 10–19: il metodo [newInstance] crea e restituisce un oggetto di tipo [PlaceholderFragment];
  • righe 14–16: il frammento viene creato con un argomento;

Un frammento deve definire il metodo [onCreateView] alla riga 22. Questo metodo deve restituire la vista associata al frammento.

  • Riga 25: la vista [fragment_main.xml] è associata al frammento;
  • Riga 27: questa vista contiene un componente [TextView], il cui riferimento viene recuperato tramite il suo ID;
  • riga 29: il testo viene visualizzato nel [TextView];
    • [getString] è un metodo della classe padre [AppCompatActivity];
    • il primo argomento è un ID del componente. [R.string.section_format] si riferisce all'ID del componente identificato da [section_format] nel file [res/values/strings.xml] (riga 4 sotto):

<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>
  • (continua)
    • riga (d) sopra %1$d indica che l'argomento n. 1 (%1) deve essere formattato come un numero intero ($d);
    • il secondo argomento di [getString] è il valore da assegnare all'argomento $1 nella riga (d) sopra;
    • [getArguments] restituisce il riferimento al bundle degli argomenti del frammento. È importante notare qui che ogni argomento è stato creato con il seguente bundle (righe f-h):

    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
}
  • (continua)
    • getArguments().getInt(ARG_SECTION_NUMBER) restituirà quindi il valore [sectionNumber] delle righe (g) e (b) sopra riportate;
  • riga 31: restituiamo la vista così creata;

1.7.4.3. Gestione del menu

Nell'applicazione generata è presente un menu:

  

Il contenuto del file [menu_main.xml] è il seguente:


<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>
  • righe 1-9: il menu;
  • righe 5-8: una voce di menu identificata da [action_settings] (riga 5);
  • riga 6: l'etichetta dell'opzione di menu. Si trova nel file [res/values/strings.xml] (riga (c) qui sotto:

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

Il codice sopra riportato corrisponde alla seguente schermata (il menu si trova in alto a destra nella finestra di esecuzione di Android):

 

Questo menu viene gestito come segue nell'attività [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      return true;
    }
 
    return super.onOptionsItemSelected(item);
}
  • Righe 1–6: questo metodo viene chiamato quando il sistema è pronto a creare il menu dell'applicazione. Il parametro di input [Menu menu] è un menu vuoto che non contiene ancora alcuna opzione;
  • riga 4: viene utilizzato il file [res/menu/menu_main.xml]. All'oggetto [Menu menu] passato come parametro vengono assegnate le opzioni di menu definite in questo file;
  • riga 5: viene indicato che il menu è stato creato;
  • righe 8–21: il metodo [onOptionsItemSelected] viene eseguito ogni volta che si fa clic su un'opzione del menu;
  • riga 13: il riferimento dell'opzione di menu cliccata;
  • righe 16–18: se l'opzione cliccata è quella con l'identificatore [action_settings], non viene eseguita alcuna operazione e viene indicato che l'evento è stato gestito (riga 17);
  • riga 20: l'evento viene passato alla classe padre;

Per vedere meglio cosa succede con questo menu, aggiungiamo dei log al codice precedente:


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

1.7.4.4. Il pulsante fluttuante

La vista generata presenta un pulsante fluttuante:

  

Questo componente è definito nella vista principale [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 riga 7 fa riferimento a un'immagine fornita dal framework Android, in particolare una busta.

Questo componente viene gestito nella classe [MainActivity] come segue:


    // floating button
    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
});
  • riga 2: recuperiamo il riferimento del pulsante fluttuante nella vista associata all'attività (activity_main);
  • righe 3–9: gli assegniamo un gestore per gestire i clic su di esso;
  • riga 6: la classe [Snackbar] consente di visualizzare messaggi temporanei sulla vista utilizzando il suo metodo [Snackbar.make]. Il primo argomento è una vista dalla quale [Snackbar] cercherà una vista padre in cui visualizzare il messaggio. Qui, [view] è la vista della busta cliccata (riga 5). La vista padre che verrà trovata è la vista [activity_main]. Il secondo argomento è il messaggio da visualizzare. Il terzo argomento è la durata della visualizzazione (SHORT o LONG);
  • riga 7: è possibile cliccare sul messaggio visualizzato per attivare un'azione. Qui, nessuna azione è associata al clic sul messaggio. Infine, il metodo [show] visualizza il messaggio;

Cliccando sul pulsante fluttuante si ottiene il seguente risultato visivo:

 

1.7.5. Esecuzione del progetto

Ora che abbiamo spiegato i dettagli del codice generato, possiamo comprenderne meglio l'esecuzione:

Image

Quando si fa clic sulla scheda #i, il frammento #i viene visualizzato nel contenitore della vista. Ciò è evidente dal testo visualizzato in [4]. Si può anche notare che è possibile passare da una scheda all'altra scorrendo la vista verso destra o sinistra con il mouse. Vedremo che questo comportamento può essere controllato.

Quando si fa clic sull'opzione di menu in [6], si ottengono i seguenti log:

 

1.7.6. Ciclo di vita dei frammenti

  • In [1], vediamo che il metodo [onCreateView] e i metodi successivi vengono eseguiti quando il frammento viene visualizzato per la prima volta e ogni volta che l'attività deve ridisegnarlo;

Per monitorare il ciclo di vita dell'attività e dei frammenti, aggiungiamo i seguenti log al codice di [MainActivity]:


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

Eseguiamo nuovamente il progetto. I primi log sono i seguenti:

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
  • riga 1: creazione dell'attività;
  • riga 2: esecuzione del suo metodo [onCreate];
  • righe 3-4: istanziazione del frammento n. 1;
  • righe 5-6: istanziamento del frammento n. 2;
  • riga 7: inizializzazione del frammento n. 2;
  • riga 8: inizializzazione del frammento n. 1;
  • riga 9: creazione del menu dell'attività;

A questo punto, dobbiamo richiamare il codice responsabile della creazione dei frammenti:


  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • righe 11–15: un frammento viene istanziato da [newInstance] ogni volta che il contenitore dei frammenti ne richiede uno;

I log sopra riportati mostrano che i primi due frammenti sono stati istanziati e inizializzati.

Ora, clicchiamo sulla scheda n. 2. I nuovi log sono i seguenti:

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
  • Righe 1–3: il frammento n. 3 viene istanziato e inizializzato. Ricordate che il frammento n. 2 è quello visualizzato;

Ora clicchiamo sulla scheda n. 3. Qui non ci sono registrazioni. Probabilmente ciò è dovuto al fatto che il frammento n. 3, che deve essere visualizzato, era già stato istanziato. Ora torniamo alla scheda n. 1. Le registrazioni sono le seguenti:

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

Il frammento n. 1 non viene istanziato nuovamente, ma il suo metodo [onCreateView] viene eseguito di nuovo. Questo comportamento si verifica anche per gli altri due frammenti.

Da questi log, possiamo concludere che:

  • l'attività è stata istanziata e inizializzata una volta;
  • ogni frammento è stato istanziato una volta;
  • il metodo [onCreateView] di ciascun frammento è stato eseguito più volte;

Quello che devi sapere — e che i log confermano — è che, per impostazione predefinita, quando viene visualizzato il frammento n. i, i frammenti i-1 e i+1 vengono istanziati, se non lo sono già. Questo spiega, ad esempio, perché all'avvio, anche se dovrebbe essere visualizzato il frammento n. 1, sono stati istanziati e inizializzati i frammenti 1 e 2. I log mostrano anche che il metodo [getItem(i)] viene chiamato solo una volta, anche se il frammento #i viene visualizzato più volte. Pertanto, sembra che il contenitore di frammenti [ViewPager], che dovrebbe visualizzare il frammento #i di [SectionsPagerAdapter], lo richieda una sola volta al gestore di frammenti [ ]. Dopodiché, non lo richiede più e continua a utilizzare quello ottenuto.

Infine, i log forniscono informazioni sul metodo [onCreateView] dei frammenti:

  • all'avvio, i frammenti 1 e 2 sono stati istanziati e il loro metodo [onCreateView] è stato eseguito;
  • Quando si passa dal frammento 1 al frammento 2, il metodo [onCreateView] del frammento 2 non viene rieseguito. Pertanto, non può essere utilizzato per aggiornare il frammento 2. Tuttavia, l'utente potrebbe aver eseguito un'operazione nel frammento 1 il cui risultato dovrebbe essere visualizzato dal frammento 2. Vediamo che il metodo [onCreateView] non può essere utilizzato per aggiornare il frammento 2. Dovremo trovare un'altra soluzione;

1.8. Esempio-07: Esempio-06 riscritto utilizzando la libreria [AA]

1.8.1. Creazione del progetto

Duplicheremo il progetto [Esempio-06] in [Esempio-07] per introdurre le annotazioni Android in quest'ultimo. Per farlo, seguire la procedura descritta nella sezione 1.4. Otteniamo il seguente risultato:

1.8.2. Configurazione Gradle

 

Aggiorniamo il file [build.gradle] come segue:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "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'
}

Abbiamo aggiunto la configurazione necessaria per utilizzare la libreria [Android Annotations] (vedere la sezione 1.4).

1.8.3. Aggiunta delle prime annotazioni AA

Creeremo le annotazioni AA in [MainActivity]:

  

La classe [MainActivity] viene modificata come segue:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;
 
 
  // manufacturer
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
  }
  • riga 1: l'annotazione [@EActivity] rende [MainActivity] una classe gestita da AA. Il suo parametro [R.layout.activity_main] è l'identificatore della vista [activity_main.xml] associata all'attività;
  • righe 11-12: il componente identificato da [R.id.tabs] viene iniettato nel campo [tabLayout]. Si tratta del gestore delle schede;
  • righe 14–15: il componente identificato da [R.id.fab] viene iniettato nel campo [fab]. Si tratta del pulsante fluttuante;
  • righe 23–50: il codice che prima si trovava nel metodo [onCreate] viene spostato in un metodo con un nome qualsiasi ma annotato con [@AfterViews] (riga 23). Nel metodo annotato in questo modo, possiamo essere certi che tutti i componenti dell'interfaccia visiva annotati con [@ViewById] siano stati inizializzati;
  • Abbiamo anche aggiunto dei log per visualizzare il ciclo di vita dell'attività;

Ricordate che l'annotazione [@EActivity] genererà una classe [MainActivity_], che sarà l'attività effettiva del progetto. Pertanto, dovete modificare il file [AndroidManifest.xml] come segue:


<?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>
  • Riga 12: la nuova attività.

A questo punto, esegui nuovamente il progetto e verifica che venga ancora visualizzata l'interfaccia con le schede.

1.8.4. Riscrittura dei frammenti

Esamineremo come vengono gestiti i frammenti nel progetto. Per ora, la classe [PlaceholderFragment] è una classe interna statica dell'attività [MainActivity]. Torneremo a un caso d'uso più comune, in cui i frammenti sono definiti in classi esterne. Inoltre, introdurremo le annotazioni AA per i frammenti.

Il progetto [Example-07] si evolve come segue:

  

Sopra vediamo la classe [PlaceholderFragment], che è stata spostata al di fuori della classe [MainActivity]. È stata riscritta come segue:


package exemples.android;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
  }
 
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    return super.onCreateView(inflater, container, savedInstanceState);
  }
 
  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    if (textViewInfo != null) {
      Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
    }
  }
}
  • riga 15: il frammento è annotato con l'annotazione [@EFragment], il cui parametro è l'identificatore della vista XML associata al frammento, in questo caso la vista [fragment_main.xml];
  • righe 19-20: inserisci nel campo [textViewInfo] il riferimento al componente in [fragment_main.xml] identificato da [R.id.section_label], che è di tipo [TextView] (riga (l) sotto):

<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>
  • righe 42–52: il metodo [onResume] viene eseguito prima che venga visualizzata la vista associata al frammento. Può essere utilizzato per aggiornare l'interfaccia utente che verrà visualizzata;
  • riga 47: è necessario chiamare il metodo con lo stesso nome nella classe padre;
  • riga 49: non è chiaro se il metodo [onResume] possa essere eseguito prima che il campo alla riga 20 sia inizializzato. Ce lo diranno i log impostati per tracciare il ciclo di vita del frammento. Per ora, per precauzione, eseguiamo un controllo di null;
  • Riga 51: aggiorniamo le informazioni nel campo [textViewInfo] con l'argomento intero passato al frammento durante la sua creazione;

La classe [MainActivity] perde la sua classe interna [PlaceholderFragment] e vede il suo gestore di frammenti evolversi come segue:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // number of fragments
    private static final int FRAGMENTS_COUNT = 3;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // you can pass arguments to the
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
    }
  }
  • riga 4: i frammenti vengono inseriti in un array;
  • righe 16–23: l'array dei frammenti viene inizializzato nel costruttore. Sono di tipo [PlaceholderFragment_] (riga 18) e non [PlaceholderFragment]. La classe [PlaceholderFragment] è stata infatti annotata con un'annotazione AA e genererà una classe [PlaceholderFragment_] derivata da [PlaceholderFragment], ed è questa la classe che l'attività deve utilizzare. A ogni frammento creato viene passato un argomento intero che verrà visualizzato dal frammento;
  • righe 42–45: abbiamo modificato i titoli dei frammenti. Poiché questi sono anche i titoli delle schede, dovremmo vedere un cambiamento nella barra delle schede;

Compiliamo [Make] [1] questo progetto:

 
  • in [2], possiamo vedere che le classi generate dalla libreria AA si trovano nella cartella [app / build / generated / source / apt / debug] (è necessario trovarsi nella prospettiva [Project] per visualizzare [2]);

Esegui il progetto [Example-07] e verifica che funzioni ancora.

1.8.5. Esame dei log

All'avvio dell'applicazione, i log sono i seguenti:

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
  • riga 1: creazione dell'attività singola;
  • riga 2: il metodo [afterViews] dell'attività: i suoi campi annotati con [@ViewById] vengono inizializzati;
  • righe 3-5: costruzione dei tre frammenti;
  • righe 6-7: il contenitore di frammenti [ViewPager] richiede i primi due frammenti;
  • righe 8-9: metodi del frammento 2;
  • righe 10–11: metodi del frammento 1;
  • righe 12–13: metodo [onResume] del frammento 1;
  • righe 14–15: metodo [onResume] del frammento 2;
  • riga 16: creazione del menu dell'attività;

Si noti che questo risponde a una domanda posta in precedenza: il metodo [onResume] del frammento 1, ad esempio (riga 12), viene eseguito dopo il metodo [afterViews] del frammento (riga 11). Pertanto, quando il metodo [onResume] viene eseguito, può utilizzare i campi annotati con [@ViewById]. Ora possiamo scrivere il metodo [onResume] come segue:


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

Ora passiamo dalla scheda 1 alla scheda 2. I nuovi log sono i seguenti:

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
  • riga 1: il contenitore di frammenti [ViewPager] richiede il frammento n. 3;
  • righe 2-3: metodi del frammento n. 3. Si noti che questo frammento è stato istanziato all'avvio dell'applicazione;
  • righe 4-5: viene eseguito il metodo [onResume] del frammento n. 3. Si noti che il frammento n. 2 è attualmente visualizzato;

Ora passiamo dalla scheda 2 alla scheda 3. Non ci sono log. Pertanto, nessuno dei metodi [onCreateView, afterViews, onResume] del frammento n. 3 viene eseguito. Visualizza correttamente il testo [Hello World from section:3] esclusivamente perché questo testo era già stato creato nel passaggio precedente quando era visualizzato il frammento n. 2. Ricordiamo che in quel passaggio era stato eseguito il metodo [onResume] del frammento n. 3. Possiamo vedere qui che, proprio come il metodo [onCreateView], il metodo [onResume] non può essere utilizzato per aggiornare il frammento n. 3. Se avessimo avuto bisogno di modificare il testo visualizzato dal frammento, nessuno di questi due metodi avrebbe potuto farlo.

Ora torniamo dalla scheda n. 3 alla scheda n. 1. I log sono quindi i seguenti:

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

Possiamo vedere che tutti i metodi nel Fragment 1 sono stati eseguiti. Possiamo vedere che il metodo getItem non è stato chiamato. Come accennato, questo metodo viene chiamato solo una volta per ogni frammento;

Ora, passiamo dalla scheda 1 alla scheda adiacente 2. Otteniamo i seguenti log:

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, vero? Tutti i metodi del frammento n. 3 vengono rieseguiti.

Per comprendere questi fenomeni, ricordate che, per impostazione predefinita, quando il contenitore del frammento visualizza il frammento i, inizializza i frammenti i-1, i e i+1. Rivediamo i log alla luce di queste informazioni.

Innanzitutto, i log all'avvio dell'app:

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

Poiché il contenitore dei frammenti visualizzerà il frammento 1, i frammenti 1 e 2 vengono inizializzati (righe 8–15).

Ora passiamo dalla scheda 1 alla scheda 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

Poiché il contenitore dei frammenti visualizzerà il frammento 2, i frammenti 1, 2 e 3 devono essere inizializzati. I frammenti 1 e 2 sono già stati inizializzati nel passaggio precedente. Il frammento 3 viene inizializzato nelle righe 2–5.

Passiamo dalla scheda 2 alla scheda 3. Non ci sono log. Poiché il contenitore dei frammenti visualizzerà il frammento 3, i frammenti 2 e 3 devono essere inizializzati. Tuttavia, poiché il passaggio precedente, lo sono già. Ciò che non vediamo qui è che il frammento 1, che non è adiacente al frammento 3, perde il suo stato, che non viene conservato in memoria.

Passiamo dalla scheda 3 alla scheda 1. I log sono i seguenti:

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

Poiché il contenitore dei frammenti visualizzerà il frammento 1, anche il frammento 2 deve essere inizializzato. È stato inizializzato nel passaggio precedente. In quello stesso passaggio, lo stato del frammento 1 è andato perso. Viene quindi reimpostato nelle righe 1–4. Ciò che qui non vediamo è che il frammento 3, che non è adiacente al frammento 1, perde il proprio stato, che quindi non viene conservato in memoria.

Quando si passa dalla scheda 1 alla scheda adiacente 2, si ottengono i seguenti log:

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

Poiché il contenitore dei frammenti visualizzerà il frammento 2, i frammenti 1, 2 e 3 devono essere inizializzati. I frammenti 1 e 2 sono già stati inizializzati nel passaggio precedente. Il frammento 3 viene inizializzato nelle righe 1–4.

Cosa abbiamo imparato?

  • che la gestione predefinita dei frammenti è molto specifica e che è necessario comprenderla se non si vuole strapparsi i capelli. Possiamo modificare questa modalità di gestione, e lo faremo più avanti;
  • che con questa gestione predefinita, nessuno dei metodi [onCreateView, onResume] può essere utilizzato per aggiornare il frammento che verrà visualizzato perché non possiamo essere sicuri che vengano eseguiti;

1.8.6. onDestroyView

Il metodo [onDestroyView] fa parte del ciclo di vita del frammento (vedi sezione 1.7.6):

Notiamo che nel ciclo di vita di un frammento:

  • il metodo [onCreateView] può essere eseguito più volte;
  • prima di tornare al metodo [onCreateView] in un secondo momento, è necessaria una chiamata al metodo [onDestroyView] [2];

Inseriremo questi metodi nei frammenti per monitorarne meglio il ciclo di vita. Il codice del frammento diventa il seguente:


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

Eseguiamo l'app. I primi log sono i seguenti:

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
  • riga 1: costruzione dell'attività singola;
  • riga 2: il metodo [afterViews] dell'attività: i suoi campi annotati con [@ViewById] vengono inizializzati;
  • righe 3-5: costruzione dei tre frammenti;
  • righe 6-7: il contenitore dei frammenti [ViewPager] richiede i primi due frammenti;
  • righe 8-9: viene creata la vista per il frammento 2 (non necessariamente resa visibile);
  • righe 10–11: viene creata la vista per il frammento 1 (non necessariamente resa visibile);
  • righe 12–13: metodo [onResume] del frammento 1;
  • righe 14-15: metodo [onResume] del frammento 2;
  • riga 16: viene creato il menu dell'attività;

Passaggio dalla scheda 1 alla scheda 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
  • riga 1: il contenitore dei frammenti richiede il terzo frammento;
  • righe 2-3: viene creata la vista per il frammento 3 (non necessariamente visualizzata);
  • righe 4-5: viene eseguito il metodo [onResume] del frammento 3;
  • riga 6: viene eseguito il metodo [onDestroyView] del frammento 1. Ciò significa che quando l'utente torna al frammento 1 o a un frammento adiacente, il ciclo di vita di questo frammento verrà rieseguito;

Ritorno dalla scheda 3 alla scheda 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
  • Righe 1–4: il ciclo di vita del Fragment 1 viene rieseguito perché ha subito un [onDestroyView];
  • riga 5: viene ora eseguito il metodo [onDestroyView] del frammento 3. Anche in questo caso, quando l'utente torna al frammento 3 o a un frammento adiacente, il ciclo di vita di questo frammento verrà rieseguito;

1.8.7. setUserVisibleHint

Il metodo [onCreateView] del ciclo di vita istanzia la vista associata al frammento, ma non la rende necessariamente visibile. È proprio quello che vedremo ora. Il metodo [Fragment.setUserVisibleHint] viene eseguito ogni volta che cambia la visibilità del frammento. Aggiungiamo questo metodo al codice del frammento:


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

All'avvio, i log sono i seguenti:


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
  • I log delle righe 7, 9–10 mostrano che solo il Fragment 1 diventa visibile. Possiamo anche vedere che diventa visibile prima che venga eseguito il suo metodo [onCreateView];

Passiamo dalla scheda 1 alla scheda 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
  • Il frammento 1 è nascosto (riga 3), il frammento 2 è visualizzato (riga 4);

Passiamo dalla scheda 2 alla scheda 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
  • Il frammento 2 è nascosto (riga 1), il frammento 3 è visualizzato (riga 2);

Torniamo alla scheda 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
  • Il frammento 3 è nascosto (riga 2), il frammento 1 è visualizzato (riga 3);

Cosa abbiamo imparato?

  • Il metodo [setUserVisibleHint] viene eseguito una volta con la proprietà [isVisibleToUser] impostata su true per il frammento che sta per essere visualizzato;
  • Non possiamo determinare quando questo metodo verrà eseguito rispetto al ciclo di vita del frammento. Pertanto, per il frammento 1, il metodo [setUserVisibleHint, true] è stato eseguito prima del metodo [onCreateView] all'inizio del ciclo di vita di questo frammento, mentre per i frammenti 2 e 3 si è verificato il contrario;

1.8.8. setOffscreenPageLimit

I log precedenti mostrano che quando il contenitore di frammenti [ViewPager] sta per visualizzare il frammento #i, esegue, se non è già stato fatto, il ciclo di vita dei frammenti adiacenti i-1 e i+1. Questo comportamento può essere controllato dal metodo [ViewPager].setOffscreenPageLimit:

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

Con l'istruzione sopra riportata,

  1. quando il contenitore di frammenti [ViewPager] sta per visualizzare il frammento n, esegue, se non l'ha già fatto, il ciclo di vita dei frammenti adiacenti nell'intervallo [i-n, i+n];
  2. se viene quindi visualizzato il frammento j:
    • lo stesso fenomeno si verifica per i frammenti adiacenti nell'intervallo [j-n, j+n];
    • i frammenti inizializzati nel passaggio 1 che non sono più adiacenti al nuovo frammento nell'intervallo [j-n, j+n] possono quindi subire un'operazione [onDestroyView]. Tuttavia, ho osservato in altre applicazioni, in particolare quella del Capitolo 3, che non era sempre così;

Modifichiamo il metodo [MainActivity.afterViews] come segue:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
}
  • Riga 20: Impostiamo il numero di frammenti adiacenti da inizializzare al numero totale di frammenti meno 1. Pertanto, all'avvio, quando il contenitore dei frammenti visualizza il frammento n. 1, inizializzerà contemporaneamente i frammenti 2, 3, ..., n, dove n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount(). Ciò significa che tutti i frammenti saranno inizializzati. Quando la finestra di visualizzazione si sposta su un altro frammento, il contenitore dei frammenti:
    • rileverà che tutti i frammenti adiacenti al nuovo frammento sono già inizializzati e quindi non li inizializzerà;
    • poiché l'adiacenza del nuovo frammento copre anche tutti i frammenti, nessuno verrà "deinizializzato" dal contenitore dei frammenti;

In totale, dovremmo vedere tutti i frammenti istanziati e inizializzati all'avvio dell'applicazione e poi mai più. Questo è ciò che ora verificheremo esaminando i log.

All'avvio, abbiamo i seguenti log:

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
  • righe 4–6: creazione dei tre frammenti;
  • righe 7, 9, 11: il contenitore dei frammenti richiede i tre frammenti. Nella versione precedente ne richiedeva due;
  • righe 14-25: il ciclo di vita dei tre frammenti viene eseguito;

Ora passiamo dalla scheda 1 alla scheda 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

Passiamo dalla scheda 2 alla scheda 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

Poi dalla scheda 3 alla scheda 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

I log confermano la teoria. Tutti i frammenti sono stati istanziati e inizializzati all'avvio. Successivamente, i loro metodi del ciclo di vita non vengono più eseguiti. Si tratta di un comportamento molto prevedibile dei frammenti, che li rende molto più facili da usare.

Quello che vogliamo trovare è un modo per aggiornare un frammento che sta per essere visualizzato, indipendentemente dall'adiacenza dei frammenti scelta dallo sviluppatore. I log ci hanno mostrato due cose:

  • il metodo [setUserVisibleHint, true] viene sempre eseguito per il frammento che sta per essere visualizzato, ma non per gli altri;
  • questo evento può verificarsi prima o dopo il ciclo di vita del frammento. Ciò dipende dall'adiacenza dei frammenti scelta dallo sviluppatore. Questo è un problema perché se il ciclo di vita non si è ancora verificato, significa che il frammento non può essere aggiornato dal metodo [setUserVisibleHint, true];

I log all'avvio dell'applicazione, quando l'adiacenza del frammento era 1, erano i seguenti:


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
  • Possiamo notare che quando il Fragment 1 diventa visibile, la sua vista non è ancora stata creata. Pertanto, non possiamo interagire con essa. Ciò può essere fatto durante il ciclo di vita del fragment, ad esempio nel metodo [onCreateView] (riga 11) o nel metodo [onResume] (righe 13–14). Poiché stiamo utilizzando le annotazioni AA, normalmente non è necessario scrivere il metodo [onCreateView]. Pertanto, il metodo [onResume] sembra essere il più appropriato in questo caso per aggiornare il Fragment 1;

Quando siamo passati dalla scheda 1 alla scheda 2, i log erano i seguenti:


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

Questa volta, abbiamo solo il metodo [setUserVisibleHint, true] alla riga 4 per aggiornare il frammento 2;

Quando siamo passati dalla scheda 2 alla scheda 3, i log erano i seguenti:


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

Qui, abbiamo solo il metodo [setUserVisibleHint, true] alla riga 2 per aggiornare il frammento 3;

Quando siamo passati dalla scheda 3 alla scheda 1, i log erano i seguenti:


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

Qui, è necessario utilizzare il metodo [onResume] del Fragment 1 (righe 6–7) per aggiornare il Fragment 1.

Quindi, in questo esempio, vediamo che per aggiornare un frammento che sta per essere visualizzato, abbiamo due metodi: [setUserVisibleHint] e [onResume].

Implementeremo questa soluzione in un nuovo progetto in cui ogni frammento dovrà visualizzare il numero di volte in cui è stato visualizzato, che chiameremo "visita". Dovremo quindi aggiornare la sua visualizzazione ogni volta che viene visualizzato. Questo è infatti il problema che stiamo cercando di risolvere.

Prima di ciò, esaminiamo la fase finale del ciclo di vita di un'attività o di un frammento: quando viene distrutta. Il sistema può decidere di distruggere un'attività se altre attività con priorità più alta richiedono risorse che al momento non sono disponibili. Per liberare queste risorse, il sistema prenderà l'iniziativa di distruggere determinate attività. Verrà quindi chiamato il metodo [onDestroy] dell'attività e dei frammenti.

1.8.9. OnDestroy

Permetteremo all'utente di eliminare l'attività utilizzando un'opzione di menu [5]. Per farlo, aggiungiamo una nuova opzione di menu al file [menu_main.xml] [1]:


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

Basta copiare e incollare la prima opzione del menu e adattare il risultato (righe 9 e 10). L'etichetta per questa nuova opzione viene aggiunta al file [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>

Infine, nella classe [MainActivity], gestiamo il clic sull'opzione [Terminate]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    if (id == R.id.action_terminate) {
      Log.d("menu", "action_terminate selected");
      //we finish the activity
      finish();
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}
  • righe 14–19: copia e incolla le righe 10–13 e adatta il codice alla nuova opzione;
  • riga 17: l'attività viene terminata da un'azione software;

Ora eseguiamo questa nuova versione e, non appena viene visualizzata la prima vista, facciamo clic sull'opzione di menu [Terminate]. I log sono quindi i seguenti:

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
  • righe 1-2: clicca sull'opzione [Terminate];
  • riga 4: viene chiamato il metodo [onDestroy] dell'attività;
  • righe 4-5: viene chiamato il metodo [onDestroyView] del frammento 1, seguito dal suo metodo [onDestroy];
  • righe 6-9: questo processo si ripete per gli altri due frammenti;

È importante ricordare che il metodo [onDestroy] dell'attività e dei frammenti viene chiamato quando l'attività sta per essere distrutta dal sistema, dallo sviluppatore o dall'utente. Questo metodo può essere utilizzato per salvare informazioni, ad esempio localmente sul tablet, in modo che possano essere recuperate quando l'utente riavvia l'applicazione.

1.9. Esempio-08: Aggiornamento di un frammento con adiacenza variabile

1.9.1. Creazione del progetto

Duplicare il progetto [Esempio-07] in [Esempio-08]. A tal fine, seguire la procedura descritta per duplicare [Esempio-02] in [Esempio-03] nella sezione 1.4.

1.9.2. Riscrittura del frammento [PlaceholderFragment]

Il nuovo codice per il frammento [PlaceholderFragment] è il seguente. Funziona indipendentemente dall'adiacenza assegnata ai frammenti (1, parziale, totale):


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  @Override
  public void onDestroyView() {
...
  }
 
  @Override
  public void onResume() {
...
  }
 
  // update fragment
  public void update() {
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  private String getInfos() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
  }
}
  • righe 34-48: il metodo [@AfterViews] può essere eseguito più volte. Lo usavamo per inizializzare il testo del frammento (riga 42). Lo facciamo ancora, ma per assicurarci che avvenga solo una volta, gestiamo un booleano [initDone] (riga 44) per indicare che l'inizializzazione è stata completata e non deve essere ripetuta;
  • righe 56–59: introduciamo il metodo [onDestroyView] per tenere conto del fatto che la prossima volta che il frammento verrà visualizzato nuovamente, il suo ciclo di vita verrà rieseguito;
  • I log hanno mostrato che due metodi possono essere eseguiti dopo il metodo [@AfterViews]: i metodi [setUserVisibleHint] e [onResume]. Il metodo [onResume] viene eseguito solo quando viene eseguito il ciclo di vita del frammento. Il metodo [setUserVisibleHint], tuttavia, non viene sempre eseguito dopo il metodo [@AfterViews]. I log hanno mostrato che almeno uno dei due viene eseguito dopo il metodo [@AfterViews]. I log non hanno mai mostrato che entrambi potessero essere eseguiti insieme dopo il metodo [@AfterViews]. È o l'uno o l'altro. Per precauzione, imposteremo un valore booleano [updateDone] quando viene effettuato un aggiornamento;

I metodi [setUserVisibleHint] e [onResume] sono i seguenti:


  // data
  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) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // increment
      numVisit++;
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // update
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • riga 14: viene memorizzato lo stato di visibilità del frammento;
  • righe 22–25: se il frammento è visibile e il metodo [@AfterViews] è stato eseguito, viene eseguito il metodo [update] e il valore booleano [updateDone] viene impostato su true;
  • righe 26–28: se il frammento sta per essere nascosto, il valore booleano [updateDone] viene reimpostato su false. Abbiamo bisogno di un evento per reimpostare il valore booleano [updateDone] — che viene impostato su true non appena viene chiamato il metodo [update] — su false, in modo che possano essere effettuati nuovi aggiornamenti. Per farlo, sfruttiamo il fatto che il frammento non è più visibile. Quando torna a essere visibile, il frammento deve essere aggiornato nuovamente;
  • righe 32–42: i log mostrano che, a seconda dell'adiacenza scelta per i frammenti, il metodo [onResume] potrebbe essere eseguito anche se il frammento non è visibile. Se non è visibile, non eseguiamo l'aggiornamento (riga 39) e, come abbiamo fatto per [setMenuVisibility], gestiamo il booleano [updateDone].

Infine, il metodo [onDestroyView] è il seguente:


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

Il metodo [onDestroyView] viene eseguito quando il ciclo di vita di un frammento termina. Un altro ciclo di vita potrebbe riprendere in seguito.

  • Riga 6: Il metodo [onDestroyView] rimuove qualsiasi connessione alla vista associata al frammento. Verrà ricreata durante il ciclo di vita successivo del frammento. Per ora, dobbiamo impostare il valore booleano [afterViews] su false per indicare che la connessione alla vista non esiste più;

Eseguiremo l'applicazione con 5 frammenti aventi un'adiacenza pari a 2. Le modifiche vengono apportate in [MainActivity]:


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

I log di avvio sono i seguenti:


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
  • righe 8, 10, 12: il contenitore dei frammenti richiede tutti i frammenti adiacenti al frammento 1;
  • righe 9, 11, 13: il metodo [setUserVisibleHint] di questi frammenti viene eseguito con [visibleToUser] impostato su false;
  • Riga 14: viene chiamato il metodo [setUserVisibleHint] del frammento 1 con [visibleToUser] impostato su true;
  • righe 15–17: viene chiamato il metodo [afterViews] dei 3 segmenti adiacenti. Qui vediamo un caso in cui questo metodo viene chiamato dopo che un frammento è diventato visibile (Frammento 1, riga 14);
  • righe 18–20: viene chiamato il metodo [onResume] dei 3 segmenti adiacenti;

Passaggio dalla scheda 1 alla scheda 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
  • poiché il layout del frammento è spostato di una posizione a destra, il frammento 4 viene occupato dal contenitore dei frammenti;
  • riga 2: il metodo [setUserVisibleHint] del frammento 4 viene chiamato con [visibleToUser] impostato su false;
  • riga 3: il metodo [setUserVisibleHint] del frammento 1 viene chiamato con [visibleToUser] impostato su false. Di conseguenza, il frammento 1 è ora nascosto;
  • riga 4: viene chiamato il metodo [setUserVisibleHint] del frammento 2 con [visibleToUser] impostato su true. Il frammento 2 è ora visibile;
  • righe 5-6: il ciclo di vita del frammento 4 continua;

Passiamo dalla scheda 2 alla scheda 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
  • poiché il layout del frammento è spostato di una posizione a destra, il frammento 5 viene occupato dal contenitore dei frammenti;
  • riga 2: il metodo [setUserVisibleHint] del frammento 5 viene chiamato con [visibleToUser] impostato su false;
  • riga 3: il metodo [setUserVisibleHint] del frammento 2 viene chiamato con [visibleToUser] impostato su false. Di conseguenza, il frammento 2 è ora nascosto;
  • riga 4: viene chiamato il metodo [setUserVisibleHint] del frammento 3 con [visibleToUser] impostato su true. Il frammento 3 è ora visibile;
  • righe 5-6: il ciclo di vita del frammento 5 continua;

Passiamo dalla scheda 3 alla scheda 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
  • Riga 1: il frammento 3 è ora nascosto;
  • riga 2: il frammento 4 è ora visibile. Si noti che il ciclo di vita del frammento 4 non viene eseguito. Ciò è già stato fatto due passaggi prima;
  • riga 3: il frammento 1 esce dall'area del frammento 4 visualizzato. Viene eseguito il suo metodo [onDestroyView]. La prossima volta che verrà visualizzato, il ciclo di vita della sua vista [onCreateView, afterViews, onResume] verrà rieseguito;

Passiamo dalla scheda 4 alla scheda 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
  • Riga 1: il frammento 4 è ora nascosto;
  • riga 2: il frammento 5 è ora visibile. Si noti che il ciclo di vita del frammento 5 non viene eseguito. Ciò è già stato fatto due passaggi prima;
  • riga 3: il frammento 2 esce dall'area del frammento 5 visualizzato. Viene eseguito il suo metodo [onDestroyView];

Passiamo dalla scheda 5 alla scheda 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
  • righe 1, 4, 5, 6: il ciclo di vita del frammento 1 viene rieseguito. Questo perché aveva perso la connessione con la propria vista;
  • righe 2, 5, 8, 9: per lo stesso motivo, il ciclo di vita del frammento 2 viene rieseguito;
  • righe 10–11: i frammenti 4 e 5 vengono rimossi dalla vicinanza del frammento visualizzato;
  • riga 7: il frammento 1 viene aggiornato;
 

I log non hanno mai mostrato che i metodi [setUserVisibleHint] e [onResume] abbiano entrambi tentato di aggiornare il frammento. Si tratta dell'uno o dell'altro. Il lettore è invitato a eseguire ulteriori test e a monitorare i log per comprendere appieno i concetti di adiacenza e ciclo di vita dei frammenti.

Ora, impostiamo l'adiacenza totale ed eseguiamo gli stessi test.

In [MainActivity]:


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

I log di avvio sono i seguenti:


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
  • I log mostrano che il ciclo di vita dei 5 frammenti è in esecuzione;
  • Il frammento 1 viene visualizzato alla riga 18;

Passaggio dalla scheda 1 alla scheda 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
  • riga 1: il frammento 1 è nascosto;
  • riga 2: il frammento 2 è visualizzato;

Passaggio dalla scheda 2 alla scheda 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
  • riga 1: il frammento 2 è nascosto;
  • riga 2: il frammento 3 è visualizzato;

Passaggio dalla scheda 3 alla scheda 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
  • riga 1: il frammento 3 è nascosto;
  • riga 2: il frammento 4 è visualizzato;

Passaggio dalla scheda 4 alla scheda 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
  • riga 1: il frammento 4 è nascosto;
  • riga 2: il frammento 5 è visualizzato;

Passiamo dalla scheda 5 alla scheda 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
  • riga 1: il frammento 5 è nascosto;
  • riga 2: il frammento 1 è visualizzato;
  • riga 3: il frammento 1 è aggiornato;

Passaggio dalla scheda 1 alla scheda 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
  • riga 1: il frammento 1 è nascosto;
  • riga 2: il frammento 4 è visualizzato;
  • riga 3: il frammento 4 è aggiornato;

Possiamo notare che, con l'adiacenza completa, il comportamento dei frammenti è molto più prevedibile.

Ora impostiamo l'adiacenza a zero e vediamo cosa succede. La classe [MainActivity] si evolve come segue:


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

I log di avvio sono i seguenti:


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
  • Nelle righe 8 e 10, vediamo che il contenitore dei frammenti ha richiesto 2 frammenti, i numeri 1 e 2. Tutto procede quindi come se ci fosse un'adiacenza pari a 1. L'adiacenza pari a 0 è stata quindi ignorata.

1.9.3. Comunicazione tra frammenti

Nell'architettura precedente, abbiamo un'attività e n frammenti. L'utente interagisce con i vari frammenti. Queste interazioni modificano lo stato dell'applicazione. In questo caso, lo stato dell'applicazione si riferisce all'insieme di informazioni che essa memorizza durante il suo ciclo di vita. Si presenta quindi il seguente problema:

  • quando l'utente interagisce con il frammento i, l'applicazione passa dallo stato E1 allo stato E2;
  • un'azione dell'utente sul frammento i fa sì che venga visualizzato il frammento j;
  • come aggiorniamo il frammento j con lo stato attuale E2 dell'applicazione?

Dagli esempi precedenti, sappiamo come aggiornare il frammento j. Ma dove troviamo lo stato E2 dell'applicazione per aggiornarlo?

Esistono diverse soluzioni a questo problema. Ne abbiamo vista una: il frammento i può passare lo stato E2 dell'applicazione al frammento j tramite argomenti. Abbiamo incontrato questo metodo nella classe [MainActivity] durante la creazione dei frammenti:


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

Questa soluzione non è immediatamente utilizzabile in questo contesto. Infatti, quando l'utente clicca sulla scheda j, che visualizzerà il frammento j, il nostro codice non viene chiamato. Viene eseguito solo il codice di sistema. Vedremo in un progetto futuro come intercettare il clic su una scheda, ma per ora adotteremo un approccio diverso.

Abbiamo discusso dello stato dell'applicazione: l'insieme di dati gestiti dall'applicazione nel tempo. Qui, l'applicazione è costituita da un'attività e da n frammenti, tutti istanziati una volta all'avvio dell'applicazione e la cui durata corrisponde a quella dell'applicazione. Pertanto, uno qualsiasi di questi elementi, o più di essi insieme, può fungere da candidato per la memorizzazione dello stato dell'applicazione. Ogni frammento ha accesso, tramite il metodo [Fragment.getActivity()], all'attività che lo ha creato. Poiché tutti i frammenti hanno accesso all'attività, sembra naturale memorizzare lo stato dell'applicazione al suo interno.

Tuttavia, il risultato del metodo [Fragment.getActivity()] dipende dal momento in cui viene chiamato nel ciclo di vita. Illustriamo questo punto aggiungendo alcuni log alla classe [PlaceholderFragment]:


  // update fragment
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  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);
}
  • righe 14-16: il metodo [getInfo] visualizza parte dello stato dell'app;

Avviamo l'app con un'adiacenza dei frammenti pari a 2. I log all'avvio dell'app:


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
  • righe 9, 10, 13, 14: vediamo che nei metodi [setUserVisibleHint], abbiamo [getActivity()==null] se il frammento non è ancora visibile (isVisibleToUser==false);
  • riga 19: vediamo che quando il flusso di esecuzione raggiunge il metodo [update] del frammento 1, il metodo [getActivity] restituisce correttamente l'attività;

Quando l'adiacenza dei frammenti è impostata su 4 (adiacenza completa), i log sono i seguenti:


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

Otteniamo gli stessi risultati. Possiamo concludere che non appena il frammento è visibile, il metodo [getActivity] restituisce l'attività del frammento. Notiamo inoltre che quando l'esecuzione raggiunge il metodo [update] del frammento che sta per essere visualizzato, il metodo [getActivity] restituisce effettivamente un valore.

Per illustrare la comunicazione tra frammenti, stiamo creando un nuovo progetto.

1.10. Esempio-09: Comunicazione tra frammenti, scorrimento e scorrimento

1.10.1. Creazione del progetto

Duplichiamo il progetto [Esempio-07] in [Esempio-08]. A tal fine, seguiremo la procedura descritta per la duplicazione di [Esempio-02] in [Esempio-03] nella sezione 1.4.

1.10.2. La sessione

In questo nuovo progetto, vogliamo che i frammenti visualizzino il numero totale di frammenti visualizzati dall'utente. Qui, dobbiamo mantenere un contatore accessibile a tutti i frammenti. Chiameremo "sessione" l'oggetto che incapsula i dati condivisi dai frammenti. Questa terminologia deriva dallo sviluppo web, dove i dati da condividere tra diverse viste richieste dallo stesso utente vengono inseriti in una sessione. Incorporare le informazioni condivise dai diversi frammenti in un unico oggetto rende il codice più leggibile.

La classe [Session] sarà la seguente:

  

package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
 
  // getters and setters
 
  public int getNumVisit() {
    return numVisit;
  }
 
  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
}
  • riga 8: la sessione incapsulerà il numero di frammenti visitati;
  • riga 5: l'annotazione [EBean] è un'annotazione AA. L'attributo [scope] specifica l'ambito (o la durata) della classe annotata. Qui, l'attributo [scope = EBean.Scope.Singleton] rende la classe [Session] un singleton: verrà istanziata una sola volta all'avvio dell'applicazione. Un riferimento a una classe annotata con [EBean] può quindi essere iniettato in un'altra classe. Questo è il concetto di iniezione di dipendenze;

1.10.3. L'attività [MainActivity]

L'attività [MainActivity] si evolve come segue:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");
 
    // session initialization
    session.setNumVisit(0);
  }
 
...
  • righe 7-8: iniezione del riferimento al singleton della sessione utilizzando l'annotazione [@Bean]. Il parametro dell'annotazione è la classe del bean da iniettare. Il campo annotato in questo modo non può avere ambito [private];
  • riga 15: l'annotazione [@AfterInject] viene utilizzata per designare un metodo da chiamare una volta completate tutte le iniezioni per la classe. Pertanto, quando si entra nel metodo [afterInject] alla riga 16, il riferimento della riga 8 è già stato inizializzato;
  • riga 20: il contatore delle visite viene azzerato;

1.10.4. Il frammento [PlaceholderFragment]

Il frammento [PlaceholderFragment] si evolve come segue:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
....
 
  // session
  protected Session session;
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  // update fragment
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // session
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • riga 7: la sessione;
  • righe 35-37: sappiamo che quando entriamo nel metodo [update], il metodo [getActivity] restituisce correttamente l'attività. Cogliamo l'occasione per recuperare la sessione e memorizzarla localmente (riga 36);
  • righe 39–41: per incrementare il numero di visite, lo recuperiamo dalla sessione. Avremmo potuto inserire questo codice nel metodo [setUserVisibleHint] a partire dalla riga 19, poiché sappiamo che il metodo [getActivity] restituisce l'attività in quel punto. Qui, decidiamo di non assegnare un ruolo specifico a questo metodo e di spostare il codice specifico del frammento nel metodo [update] del frammento, progettato proprio a tale scopo;
  • riga 43: visualizza il numero di visite;

Quando si esegue questa applicazione con 5 frammenti, di cui 2 adiacenti, i primi log sono i seguenti:


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
...
  • Righe 2–3: Possiamo vedere che il metodo [afterInject] dell'attività viene eseguito prima del suo metodo [afterViews];

I lettori sono invitati a provare questa nuova applicazione.

1.10.5. Disabilitare lo scorrimento

Nell'app precedente, quando si scorre l'emulatore Android con il mouse verso sinistra o destra, la vista corrente viene sostituita dalla vista a destra o a sinistra, a seconda dei casi. Questo comportamento predefinito non è sempre auspicabile. Impareremo come disabilitare lo scorrimento delle viste.

Torniamo alla vista XML principale [activity_main]:

  

Nel codice XML della vista, troviamo il contenitore del frammento:


  <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 riga 1 specifica la classe che gestisce le pagine dell'attività. Questa classe si trova nell'attività [MainActivity]:


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

Riga 12: Il contenitore del frammento è di tipo [android.support.v4.view.ViewPager] (riga 1). Per disabilitare lo scorrimento, dobbiamo estendere questa classe come segue:

  

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 {
 
  // swipe control
  private boolean isSwipeEnabled;
 
  // manufacturers
  public MyPager(Context context) {
    super(context);
  }
 
  public MyPager(Context context, AttributeSet attrs) {
    super(context, attrs);
  }
 
  // methods to be redefined to manage swiping
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }
 
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // swipe allowed?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • riga 8: la classe [MyPager] estende la classe Android [ViewPager] (riga 4);
  • quando si scorre con il dito, possono essere chiamati i gestori di eventi alle righe 24 e 34. Entrambi restituiscono un valore booleano. Devono semplicemente restituire il valore booleano [false] per disabilitare lo scorrimento;
  • riga 11: il valore booleano utilizzato per indicare se consentire o meno il gesto di scorrimento.

Una volta fatto questo, dobbiamo ora utilizzare il nostro nuovo gestore di pagine. Ciò avviene nella vista XML [activity_main.xml] e nell'attività principale [MainActivity]. In [activity_main.xml] scriviamo:

  

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

Riga 1: Utilizziamo la nuova classe. In [MainActivity], il codice cambia come segue:


package exemples.android;
 
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // the tab bar is also associated with the fragment container
...
  • riga 12: il gestore di pagine è ora di tipo [MyPager];
  • riga 23: abilitiamo o disabilitiamo lo scorrimento.

Prova questa nuova versione. Abilita o disabilita lo scorrimento e osserva la differenza nel comportamento delle viste quando le trascini a destra o a sinistra con il mouse. In tutte le applicazioni future, lo scorrimento sarà disabilitato. Non ne parleremo più.

1.10.6. Disabilita lo scorrimento tra i frammenti

Continuiamo con un miglioramento al gestore delle schede. Quando si passa dalla scheda 1 alla scheda 4, si vedono scorrere le due schede intermedie, la 2 e la 3. Nel gergo Android, questo si chiama smoothScrolling. Questo comportamento può diventare fastidioso se ci sono molte schede. Può essere disabilitato aggiungendo il seguente codice al gestore dei frammenti [MyPager]:


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

Poiché il gestore delle schede è stato associato al gestore dei frammenti [MyPager], quando si clicca sulla scheda n. i, il frammento n. i viene visualizzato dal contenitore dei frammenti utilizzando il metodo [setCurrentItem] sopra riportato (riga 9). [position] è il numero del frammento da visualizzare;

  • riga 10: viene chiamato il metodo [setCurrentItem] della classe padre. Il secondo argomento impostato su [false] richiede una transizione immediata tra il vecchio e il nuovo frammento (senza scorrimento); impostato su [true] richiede una transizione tramite scorrimento. Qui, il secondo argomento è il valore del campo alla riga 4, un campo che lo sviluppatore può impostare utilizzando il metodo alle righe 16–18;

Se si desidera disabilitare lo scorrimento, la classe [MainActivity] avrà questo aspetto:


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

Esegui nuovamente il progetto e verifica che non vi sia più alcuno scorrimento tra le schede 1 e 4, ad esempio. Da questo punto in poi, disabiliteremo sempre lo scorrimento. Non torneremo più su questo argomento.

1.10.7. Un nuovo frammento

Nel nostro esempio, tutti i frammenti sono dello stesso tipo [PlaceHolderFragment]. Ora impareremo come creare un nuovo frammento e visualizzarlo.

Per prima cosa, copia la vista [vue1.xml] dal progetto [Example-04] nel progetto [Example-09] [1]:

 
  • in [1], la vista [vue1.xml];
  • nel file [3], la vista mostra degli errori dovuti alla mancanza di testo nel file [res/values/strings.xml];

In [2], aggiungiamo il testo mancante prelevandolo dal file [res/values/strings.xml] nel progetto [Example-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>
  <!-- vue 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>
  • Sopra, abbiamo aggiunto le righe 6–9;

Ora creiamo la classe [Vue1Fragment], che sarà il frammento responsabile della visualizzazione della vista [vue1.xml]:

  

La classe [Vue1Fragment] sarà la seguente:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}
 
  • riga 10: l'annotazione [@EFragment] garantisce che il frammento utilizzato dall'attività sia effettivamente la classe [Vue1Fragment_]. Tenetelo a mente. Il frammento è associato alla vista [vue1.xml];
  • righe 14–15: il componente identificato da [R.id.editTextNom] viene iniettato nel campo [editTextNom] alla riga 15;
  • righe 18–20: il metodo [doValider] gestisce l'evento 'click' sul pulsante identificato da [R.id.buttonValider];
  • riga 21: il primo parametro di [Toast.makeText] è di tipo [Activity]. Il metodo [Fragment.getActivity()] recupera l'attività in cui si trova il frammento. Si tratta di [MainActivity] poiché, in questa architettura, abbiamo una sola attività che visualizza diverse viste o frammenti;

Nella classe [MainActivity], il gestore dei frammenti è implementato come segue:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // create a fragment
        fragments[i] = new PlaceholderFragment_();
        // you can pass arguments to the
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // a fragment of +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }
 
 ...
  }
  • riga 13: ci sono [FRAGMENTS_COUNT] frammenti: [FRAGMENTS_COUNT-1] frammenti di tipo [PlaceholderFragment] (righe 14-21) e un frammento di tipo [Vue1Fragment_], riga 23 (notare il trattino basso);

Compilare ed eseguire il progetto [Example-09]. La scheda 5 dovrebbe apparire diversa:

1.10.8. Deriva tutti i frammenti dalla stessa classe astratta

Anche il nuovo frammento [Vue1Fragment] deve aggiornarsi quando viene visualizzato. Per farlo, dovremo creare un codice simile a quello creato per il frammento [PlaceholderFragment]. Per evitare ripetizioni, estrarremo ciò che è possibile in una classe astratta da cui erediteranno tutti i frammenti dell'applicazione.

Per farlo, creiamo un nuovo progetto.

1.11. Esempio-10: Derivare tutti i frammenti da una classe astratta

1.11.1. Creazione del progetto

Duplichiamo il progetto [Esempio-09] in [Esempio-10]:

1.11.2. Gestione della modalità debug

Aggiungiamo al progetto l'opzione per mostrare o nascondere i log della modalità debug. Per farlo, aggiungiamo una costante statica alla classe [MainActivity]:


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

1.11.3. La classe padre astratta di tutti i frammenti

  

La classe [AbstractFragment] è la seguente:


package exemples.android;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
 
public abstract class AbstractFragment extends Fragment {
 
  // private data
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;
 
  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;
 
  // activity
  protected MainActivity activity;
 
  // session
  protected Session session;
 
  // manufacturer
  public AbstractFragment() {
    // init
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    ...
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    ...
  }
 
  // local news
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }
 
  // update fragment
  protected void update() {
    ...
    // the daughter class is asked to update itself
    updateFragment();
  }
 
  protected abstract void updateFragment();
}
  • riga 7: la classe [AbstractFragment] estende la classe [Fragment] di Android;
  • Ogni frammento deve essere in grado di aggiornarsi autonomamente. Ecco perché la classe padre [AbstractFragment] richiede che le sue classi figlie abbiano un metodo [updateFragment] (riga 68), che essa chiama (riga 65);
  • riga 19: la classe memorizzerà un riferimento all'attività dell'applicazione;
  • riga 22: la classe memorizzerà un riferimento alla sessione in cui vengono raccolti i dati condivisi dai frammenti e dall'attività;
  • righe 25–33: il costruttore della classe astratta;
  • riga 27: creazione di una copia della costante [MainActivity.IS_DEBUG_ENABLED] nel campo alla riga 16;
  • riga 28: viene memorizzato il nome della classe istanziata, ovvero il nome di una classe figlia;
  • righe 15–22: questi campi hanno l'attributo [protected] in modo che le classi figlie possano accedervi. Si noti che le classi figlie non sono a conoscenza dell'esistenza dei booleani [isVisibleToUser] e [updateDone] (righe 10–11);
  • riga 57: il metodo [getParentInfos] ha l'attributo [protected] in modo che le classi figlie possano chiamarlo;

I metodi [setUserVisibleHint, onDestroyView, onResume] rimangono gli stessi della classe [PlaceholderFragment] del progetto precedente:


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

Il metodo [update] è il seguente:


  // update fragment
  protected void update() {
    // recover activity and session
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}

In base al codice sopra riportato, quando viene eseguito il metodo [update] di un frammento, il frammento è visibile. Questo è importante perché significa che il metodo [Fragment.getActivity] restituisce quindi un riferimento all'attività dell'applicazione (vedere la sezione 1.10.8), che a sua volta fornisce l'accesso alla sessione.

  • righe 4–10: inizializza l'attività e la sessione se non sono già state inizializzate;
  • riga 12: viene chiamato il metodo [updateFragment] della classe figlia. Quando questo metodo viene eseguito, i campi [activity] e [session] a cui ha accesso sono già stati inizializzati;

1.11.4. La classe [PlaceholderFragment]

  

La classe [PlaceholderFragment] è strutturata come segue:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // data
  private boolean initDone;
 
  // data
  private String text;
  private int numVisit;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    super();
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
 ...
  }
 
  // update fragment
  public void updateFragment() {
  ...
  }
 
}
  • Riga 10: La classe [PlaceholderFragment] estende la classe [AbstractFragment]. Con questa architettura, la scrittura di un frammento comporta:
    • scrivere il metodo [@AfterViews], che viene utilizzato per inizializzare il frammento durante il suo primo ciclo di vita o per reimpostarlo se in precedenza si è verificato un [onDestroyView]. La riga 39 è necessaria per gestire correttamente il ciclo di vita del frammento;
    • scrivere il metodo [updateFragment], che aggiorna il frammento appena prima che venga visualizzato. Questo metodo può utilizzare la sessione della sua classe padre;
    • scrivere i gestori di eventi del frammento. Questo è ciò che faremo nei progetti futuri;

I metodi [@AfterViews] e [updateFragment] rimangono gli stessi del progetto precedente:


@AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
  // update fragment
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • Righe 7 e 23: Nei log, visualizziamo le informazioni della classe padre utilizzando il metodo ereditato [getParentInfos];

1.11.5. La classe [Vue1Fragment]

  

La classe [Vue1Fragment] ha la stessa struttura della classe [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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // data
  private int numVisit;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // the visit number is displayed
    Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}
  • Riga 9: La classe [Vue1Fragment] estende la classe [AbstractFragment];
  • righe 18–26: il metodo [@AfterViews] non ha nulla di interessante da fare. Deve comunque essere scritto per impostare il booleano [afterViewsDone] su true, poiché questa informazione viene utilizzata dalla classe padre;
  • righe 42–49: il metodo [updateFragment] consiste nel visualizzare un breve messaggio che mostra il numero di visite (riga 48) e nell'incrementare questo numero nella sessione (righe 44–46);

I lettori sono invitati a provare questo nuovo progetto.

Utilizzeremo questa architettura in tutti i progetti futuri:

  • un'attività e n frammenti;
  • tutti i frammenti estendono la classe [AbstractFragment];
  • i dati da condividere tra i frammenti e tra i frammenti e l'attività sono collocati nella classe [Session];

1.11.6. Associazione Scheda/Frammento

Nella classe [MainActivity], che gestisce le schede, è scritto quanto segue:


// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
tabLayout.setupWithViewPager(mViewPager);

La riga 3 associa il gestore delle schede al contenitore dei frammenti. Abbiamo visto una conseguenza di questa associazione: quando l'utente clicca sulla scheda n. i, il contenitore dei frammenti visualizza il frammento n. i. Non abbiamo visto il contrario: quando chiediamo al contenitore dei frammenti di visualizzare il frammento n. i, la scheda n. i viene selezionata automaticamente.

Per illustrare questo comportamento, aggiungeremo le opzioni [Fragment 1, Fragment 2, ...] al menu corrente. Quando l'utente clicca sull'opzione [Fragment i], chiederemo al contenitore dei frammenti di visualizzare il frammento #i. Verificheremo quindi se la scheda #i è stata selezionata o meno.

Questo passaggio inizia con la modifica del menu dell'applicazione:

 

Il contenuto del file [res/menu/menu_main.xml] cambia come segue:


<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>
  • righe 9–28: le cinque nuove opzioni di menu;
  • le etichette delle opzioni (righe 10, 14, 18, 22, 26) sono definite nel file [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>
  <!-- vue 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>
  <!-- menu -->
  <string name="fragment1">Fragment 1</string>
  <string name="fragment2">Fragment 2</string>
  <string name="fragment3">Fragment 3</string>
  <string name="fragment4">Fragment 4</string>
  <string name="fragment5">Fragment 5</string>
</resources>

Il risultato visivo è il seguente:

  

La gestione dei clic per queste opzioni di menu è gestita nella classe [MainActivity]:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
      case R.id.fragment5: {
        showFragment(4);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
    }
  }
  • Riga 2: il metodo [onOptionsItemSelected] viene chiamato quando si fa clic su una delle opzioni del menu;
  • riga 8: recuperiamo l'ID dell'opzione cliccata;
  • righe 9–36: i diversi casi vengono gestiti da un'istruzione switch;
  • righe 16–36: cliccando sull'opzione [Fragment i] si chiama il metodo [showFragment(i-1)] nelle righe 41–45;
  • riga 43: al contenitore dei frammenti viene richiesto di visualizzare il frammento richiesto;
  • riga 42: verifichiamo innanzitutto che ciò sia possibile (condizione 1) e che sia necessario (condizione 2);

I lettori sono invitati a testare questa nuova versione. Osserviamo che quando richiediamo la visualizzazione del frammento n. i, questo viene effettivamente visualizzato e la scheda n. i viene selezionata.

Ora che abbiamo visto come funziona l'associazione scheda/frammento, esamineremo un altro caso: quello in cui la gestione delle schede è disaccoppiata dalla gestione dei frammenti. Questo è il caso, ad esempio, quando ci sono meno schede che frammenti. Per illustrare questo nuovo caso d'uso, creeremo un nuovo progetto.

1.12. Esempio-11: Schede separate dai frammenti

1.12.1. Creazione del progetto

Duplichiamo il progetto [Esempio-10] in [Esempio-11]:

1.12.2. Obiettivi

La nuova applicazione avrà due schede:

  • La prima scheda mostrerà sempre il frammento [View1];
  • la seconda scheda visualizzerà un frammento selezionato dal menu;

Image

  • in [1], il frammento [View1];
  • in [2], il frammento [PlaceholderFragment] selezionato dall'utente;
  • in [3], le visite continuano a essere conteggiate;

1.12.3. La sessione

  

La nuova sessione sarà la seguente:


package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment;
 
  // getters and setters
...
}
  • Riga 10: Gestiremo noi stessi i clic sulle schede. Quando si clicca su una scheda, dobbiamo caricare il frammento che era visualizzato l'ultima volta che è stata selezionata. Il campo [numFragment] memorizzerà il numero del frammento per la scheda n. 2, un numero compreso nell'intervallo [0, Fragments_COUNT-2]. Quando si clicca sulla scheda n. 2, recupereremo dalla sessione il numero del frammento da visualizzare;

1.12.4. Il menu

  

Il menu [res / menu / menu_main.xml] cambia come segue:


<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 scheda n. 2 visualizzerà uno dei quattro frammenti delle righe 9-24. Il quinto frammento è il frammento [Vue1Fragment], che verrà sempre visualizzato nella scheda n. 1.

1.12.5. La classe [MainActivity]

La classe [MainActivity] deve ora gestire le schede e la navigazione tra di esse, cosa che prima non faceva. Il suo codice cambia come segue:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
 
    // at the start there is only one tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
 
    // event manager
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // a tab has been selected - change the fragment displayed by the fragment container
        ...
      }
 
      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
 
      }
 
      @Override
      public void onTabReselected(TabLayout.Tab tab) {
 
      }
    });
 
...
 
}
  • riga 17: Il primo frammento visualizzato dal contenitore di frammenti sarà il frammento [Vue1Fragment]. Per impostazione predefinita, questo sarà l'ultimo frammento nel contenitore;
  • righe 20–22: poiché non abbiamo stabilito un'associazione tra le schede e il contenitore dei frammenti, dobbiamo gestire le schede autonomamente. Inizialmente, la barra delle schede [tabLayout] alla riga 3 non ha schede;
  • riga 20: creiamo la prima scheda;
  • riga 21: gli assegniamo un titolo. Negli esempi precedenti, i titoli delle schede erano gli stessi dei titoli dei frammenti. Ora non è più così. Di conseguenza, rimuoviamo il metodo [getPageTitle] dal gestore dei frammenti. Non ne abbiamo più bisogno:

    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
}
  • riga 22: la scheda creata viene aggiunta alla barra delle schede. La nostra barra delle schede ora ha una scheda. Cosa visualizza questa scheda? È importante comprendere che le schede e i frammenti sono due concetti distinti. Il frammento visualizzato è sempre quello scelto dal contenitore dei frammenti. Se si cambia scheda e non si richiede al contenitore di modificare il frammento visualizzato, non accade nulla: viene visualizzato lo stesso frammento, ma la scheda selezionata è cambiata. Quindi, in questo caso, il frammento visualizzato è quello scelto riga 17: il frammento [Vue1Fragment];
  • righe 26–30: il metodo da scrivere per gestire il cambio di scheda da parte dell'utente;

Il metodo [onTabSelected] alle righe 26–30 viene attivato ogni volta che c'è un cambio di scheda (se l'utente clicca su una scheda già selezionata, non succede nulla). Il suo codice è il seguente:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // a tab has been selected - change the fragment displayed by the fragment container
        // miter position
        int position = tab.getPosition();
        // fragment number to display
        int numFragment;
        switch (position) {
          case 0:
            // fragment no. [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // fragment no. [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // fragment display
        mViewPager.setCurrentItem(numFragment);
}
  • riga 8: recuperiamo la posizione della scheda su cui è stato cliccato. Qui otterremo un numero 0 o 1;
  • righe 12–15: se è stata cliccata la prima scheda, ci prepariamo a visualizzare il frammento [Vue1Fragment];
  • righe 16–18: negli altri casi (scheda n. 2 cliccata), ci prepariamo a visualizzare nuovamente il frammento che era stato visualizzato l'ultima volta che è stata selezionata la scheda n. 2. Il suo ID è stato quindi memorizzato nella sessione dell'app;
  • riga 21: chiediamo al contenitore dei frammenti di visualizzare il frammento desiderato;

Ora diamo un'occhiata alla gestione delle opzioni del menu (sempre in [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "action_settings selected");
        }
        break;
      }
      case R.id.fragment1: {
        showFragment(0);
        break;
      }
      case R.id.fragment2: {
        showFragment(1);
        break;
      }
      case R.id.fragment3: {
        showFragment(2);
        break;
      }
      case R.id.fragment4: {
        showFragment(3);
        break;
      }
    }
    // item processed
    return true;
}
  • righe 16–31: gestione delle 4 opzioni di menu. Ogni gestore chiama il metodo [showFragment] con il numero del frammento da visualizzare;

Il metodo [showFragment] è il seguente:


  // tab n° 2
  private TabLayout.Tab tab2 = null;
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // if the 2nd tab doesn't yet exist, we create it
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // set the title of the second tab
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
      // the fragment number displayed is set to session
      session.setNumFragment(i);
      // tab 2 is selected - does nothing if already selected
      tab2.select();
    }
}
  • Ricordate che all'avvio dell'applicazione è presente una sola scheda;
  • riga 2: un riferimento alla scheda n. 2, inizialmente nullo;
  • riga 5: le condizioni di visualizzazione non sono cambiate rispetto alla versione precedente;
  • righe 7–10: se la scheda n. 2 non esiste ancora, viene creata (riga 8) e aggiunta alla barra delle schede (riga 9);
  • riga 12: il numero del frammento da visualizzare viene inserito nel titolo della seconda scheda, con numerazione che parte da 1;
  • riga 14: viene visualizzato il frammento desiderato;
  • riga 16: il suo numero viene memorizzato nella sessione;
  • riga 18: viene selezionata la scheda n. 2. Se era già selezionata, non accadrà nulla: il metodo [onTabSelected] non verrà eseguito. Se non era già selezionata, verrà attivato il metodo [onTabSelected]. Questo metodo istruisce quindi il contenitore del frammento a visualizzare il frammento già visualizzato alla riga 14. Un semplice controllo nel metodo [onTabSelected] impedisce questo scenario:

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

I lettori sono invitati a provare questa nuova versione.

1.12.6. Miglioramenti

Ora abbiamo una solida comprensione dei frammenti, del loro ciclo di vita, del concetto di adiacenza dei frammenti e della loro relazione con la barra delle schede. Abbiamo anche un'architettura robusta che ha appena superato il test dell'Esempio 11:

  • un'attività e n frammenti;
  • tutti i frammenti estendono la classe [AbstractFragment];
  • i dati da condividere tra i frammenti e tra i frammenti e l'attività sono collocati nella classe [Session];

In un nuovo progetto, chiariremo le relazioni tra l'attività e i frammenti aggiungendo un'interfaccia.

1.13. Esempio 12: Definizione delle relazioni tra l'attività e i frammenti

In questo esempio, vogliamo definire le relazioni minime tra l'attività e i frammenti. Per farlo, useremo:

  • un'interfaccia [IMainActivity] che definisce ciò che i frammenti possono richiedere all'attività;
  • una classe astratta [AbstractFragment] che definirà lo stato e i metodi che ogni frammento dovrebbe avere;

1.13.1. Creazione del progetto

Duplichiamo il progetto [Esempio-11] in [Esempio-12] seguendo la procedura descritta nella sezione 1.4. Otteniamo il seguente risultato:

1.13.2. L'interfaccia [IMainActivity]

Dagli esempi precedenti, è chiaro che i frammenti necessitano di accedere alla sessione istanziata dall'attività. Inoltre, sebbene non sia visibile in questi esempi, è prevedibile che i gestori di eventi nei frammenti a volte comportino una modifica della vista. All'attività verrà richiesto di eseguire questa modifica. L'interfaccia [IMainActivity] potrebbe quindi apparire così:

  

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

Riga 12: Si noti la presenza di una costante che in precedenza si trovava nella classe [MainActivity]. Vogliamo ridurre l'accoppiamento tra i frammenti e l'attività e limitarlo a un accoppiamento tra [AbstractFragment] e [IMainActivity]. L'attività può quindi essere denominata in modo diverso da [MainActivity]. Poiché la costante [IS_DEBUG_ENABLED] è utilizzata nei frammenti, viene spostata nell'interfaccia [IMainActivity].

1.13.3. La classe astratta [AbstractFragment]

La classe astratta [AbstractFragment] cambia molto poco:


  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
...
  // update fragment
  protected void update() {
    // recover activity and session
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}
  • Righe 6 e 7: manteniamo due tipi di riferimenti all'attività:
    • riga 6: un riferimento all'attività che implementa l'interfaccia [IMainActivity];
    • riga 7: un riferimento all'attività che eredita dalla classe Android [Activity]. Questo vale per tutte le attività;

Questi due riferimenti puntano naturalmente allo stesso oggetto. Tuttavia, questo oggetto viene visto come due tipi diversi. Ciò impedirà il type casting in fase di esecuzione;

  • riga 14: recuperiamo un riferimento all'attività utilizzando il metodo [getActivity];
  • riga 15: se questo riferimento non è nullo, possiamo accedere alla sessione;
  • Righe 16–17: memorizziamo l'attività come implementazione dell'interfaccia [IMainActivity] e della sessione;

1.13.4. Modifica del gestore dei frammenti

L'adattatore di frammenti [SectionsPagerAdapter] nella classe [MainActivity] viene modificato in un punto: invece di gestire frammenti di tipo [Fragment], ora gestisce frammenti di tipo [AbstractFragment]:


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

1.13.5. Modifica della classe [MainActivity]

La classe [MainActivity] deve implementare l'interfaccia [IMainActivity]:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
 
...
  // injection session
  @Bean(Session.class)
  protected Session session;
...
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if(mViewPager.getCurrentItem()!=position){
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  • righe 10–12: il metodo [getSession] esisteva già;
  • righe 15–22: il metodo [navigateToView] visualizza il frammento #[position];
  • riga 17: verifichiamo se c'è qualcosa da fare;
  • riga 19: viene visualizzato il frammento #[position];

A questo punto, esegui l'applicazione. Dovrebbe funzionare.

1.13.6. Modifica della visualizzazione dei frammenti in [MainActivity]

Attualmente, la classe [MainActivity] visualizza un frammento utilizzando l'istruzione:


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

Poiché il metodo [navigateToView] svolge la stessa funzione, sostituisci questo tipo di istruzione ovunque (in 2 punti) con:

navigateToView(...);

Quindi esegui l'app. Dovrebbe funzionare ancora.

1.13.7. Conclusione

D'ora in poi, useremo sempre l'architettura precedente:

  • un'attività che implementa l'interfaccia [IMainActivity];
  • frammenti che estendono la classe [AbstractFragment], il che richiede loro di implementare il metodo [updateFragment]. Questi devono anche avere un metodo [@AfterViews] in cui impostano il valore booleano [afterViewsDone] su true;
  • una sessione che incapsula i dati da condividere tra i frammenti e l'attività;

1.14. Esempio-13: Esempio-05 con frammenti

Nel progetto [Esempio-05] abbiamo introdotto la navigazione tra le viste. In quel caso, si trattava di navigazione tra attività: 1 vista = 1 attività. Qui proponiamo di avere una singola attività con più viste di tipo [AbstractFragment].

1.14.1. Creazione del progetto

Duplichiamo il progetto precedente [Esempio-12] in [Esempio-13] seguendo la procedura descritta nella sezione 1.4. Otteniamo il seguente risultato:

1.14.2. Struttura del progetto

Inizieremo a utilizzare i pacchetti per organizzare il codice. Per ora, possiamo distinguere due ambiti distinti:

  • gestione delle attività;
  • gestione dei frammenti;

Creiamo due pacchetti per essi: [examples.android.activity] e [examples.android.fragments]:

 

Procediamo allo stesso modo per creare il pacchetto [examples.android.fragments]:

In [8], creiamo un terzo pacchetto denominato [architecture] in cui inseriremo le entità [IMainActivity, AbstractFragment, Session, MyPager], che costituiscono gli elementi fondamentali dell'architettura della nostra app. Questo serve a ricordarci che abbiamo effettuato una scelta architettonica specifica. Successivamente, spostiamo gli elementi esistenti del progetto come mostrato in [9]. Ogni spostamento deve essere confermato facendo clic sul pulsante [Refactor].

A questo punto, compiliamo l'applicazione. In [MainActivity] abbiamo i seguenti errori:

 

Durante lo spostamento delle classi nei pacchetti, Android Studio ha apportato le modifiche necessarie al codice dell'applicazione (righe 18–21, ad esempio). Le classi a cui si fa riferimento nelle righe 15 e 17 non sono state spostate. Sono generate dalla libreria Android Annotations. Per queste classi, è necessario modificare manualmente le importazioni. Queste righe diventano quindi:

 

Una volta fatto questo, non ci saranno più errori di compilazione. Esegui l'applicazione. Verrà quindi visualizzato il seguente errore:

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

Questo errore deriva dal manifesto dell'applicazione:

  

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

Le righe 3 e 12 specificano che l'attività designata è [examples.android.MainActivity_]. Tuttavia, poiché l'attività è stata spostata nel pacchetto [activity], la riga 12 deve ora essere:


      android:name=".activity.MainActivity_"

Si noti il punto (.) prima di [activity]. Ancora una volta, Android Studio non è riuscito ad aggiornare il manifesto perché fa riferimento a una classe di Android Annotations che non è stata spostata. L'utilizzo della libreria AA comporta quindi una serie di inconvenienti.

1.14.3. Pulizia del progetto

Nel nuovo progetto:

  • non ci sono più schede, pulsanti fluttuanti o menu;
  • i frammenti [PlaceholderFragment] scompaiono. L'app gestirà due frammenti: [Vue1Fragment], che abbiamo già, e [Vue2Fragment], che dovremo creare;
  • la sessione non è più la stessa;

1.14.3.1. Pulizia dei frammenti

Elimina la classe [PlaceHolderFragment] [1]:

 

Allo stesso modo, eliminare la vista [res/layout/fragment_main.xml] associata a questo frammento [2].

1.14.3.2. Pulizia della sessione

La sessione è attualmente la seguente:


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

Non stiamo salvando nulla da questa sessione.

Compilare il progetto. Le righe che causano errori sono quelle che utilizzavano il contenuto della sessione. Rimuoverle. Nella classe [Vue1Fragment], rimuoviamo anche la variabile [numVisit] dal codice, che diventa il seguente:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}

1.14.3.3. Rimozione delle schede, del pulsante fluttuante e del menu

La rimozione delle schede e del pulsante fluttuante avviene in due punti:

  • nella vista [res/layout/activity-main.xml], che definisce questi elementi e la loro posizione nella vista;
  • nel codice dell'attività [MainActivity];

Anche il menu viene rimosso in due punti:

  • nella vista [res/menu/menu-main.xml], che definisce le opzioni del menu;
  • nel codice dell'attività [MainActivity];

Il codice per la vista [res / layout / activity-main.xml] è attualmente il seguente:


<?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>
  • Rimuovere le righe [28-31, 41-47];
  • rimuovere anche la barra degli strumenti dalle righe 18-24;

Il codice del menu [res / menu / menu_main.xml] è attualmente il seguente:


<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>
  • Rimuoveremo le righe da 9 a 24. Questo lascia un'opzione che non useremo. Semplicemente per fornire un esempio di dichiarazione di un'opzione di menu che può essere replicata tramite copia/incolla;

Nella classe [MainActivity], rimuovi tutto ciò che fa riferimento alle schede, al pulsante fluttuante, alla barra degli strumenti e al menu. Il modo più semplice per trovare questi riferimenti è rimuovere le loro dichiarazioni:


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

e ricompila l'app. Le righe con errori sono quelle che fanno riferimento agli elementi mancanti. Quindi elimina tutte quelle righe. Inoltre, modifica il gestore dei frammenti in modo che non faccia più riferimento al frammento [PlaceholderFragment] che abbiamo eliminato:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • Righe 7–10: Abbiamo rimosso tutta la generazione di frammenti;

A questo punto, non dovrebbero più esserci errori di compilazione. Nella classe [MainActivity], abbiamo ottenuto il seguente codice 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 {
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // manufacturer
  public MainActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }
 
  @AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
 
    // toolbar - this is where the application name is displayed
    setSupportActionBar(toolbar);
 
    // the fragment manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
 
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    navigateToView(FRAGMENTS_COUNT - 1);
 
  }
 
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }
 
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Ci sono ancora alcune modifiche da apportare:

  • eliminare la riga 31, che non è più necessaria;
  • riga 33: impostare l'adiacenza del frammento su 1;
  • riga 76: passare alla vista 0. Questa sarà la prima visualizzata;
  • riga 108: inizializza l'array con il frammento [Vue1Fragment_]:

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

Quindi abbiamo un solo frammento. Esegui l'applicazione. Dovresti ottenere il seguente risultato:

Image

Il pulsante [Validate] dovrebbe funzionare.

1.14.4. Creazione di frammenti e delle relative viste

L'applicazione avrà due viste, quelle del progetto [Example-05]. Abbiamo già la vista [vue1.xml] nel progetto corrente. Ora duplicheremo [vue2.xml] da [Example-05] a [Example-12] (apri entrambi i progetti e copia e incolla tra di essi).

 
  • In [1], la nuova vista. Quando proviamo a modificarla, compaiono degli errori [2]. Dobbiamo modificare il file [strings.xml] [3] per aggiungere le stringhe a cui fa riferimento questa nuova 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>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- vue 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>

Duplichiamo la classe [View1Fragment] in [View2Fragment]:

  

e modificare il codice copiato come segue:


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() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • riga 9: il frammento è associato alla vista [res/layout/view2.xml];
  • riga 10: la classe estende la classe astratta [AbstractFragment];
  • righe 12–20: il metodo richiesto [@AfterViews];
  • righe 23–25: il metodo richiesto [updateFragment];

1.14.5. Implementazione dei frammenti e navigazione tra di essi

L'attività gestirà ora due frammenti. La sua classe [SectionsPagerAdapter] viene aggiornata come segue:


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

L'interfaccia [IMainActivity] gestisce la navigazione tra le viste utilizzando il metodo [navigateToView]. Gestiremo il clic sul pulsante [View 2] nel frammento [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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event managers ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • righe 37–40: il metodo [showVue2] gestisce l'evento 'click' sul pulsante [View #2];
  • riga 39: la navigazione viene eseguita utilizzando il metodo [navigateToView] dell'attività. Ricordiamo che l'attività è stata memorizzata nella classe padre come:

  // activity
protected IMainActivity mainActivity;

e che questa attività sia già stata inizializzata al momento dell'ingresso in qualsiasi gestore di eventi.

  • riga 34: l'istruzione utilizza la variabile [activity] della classe padre, che è un riferimento all'attività come istanza del tipo Android [Activity];

protected Activity activity;

Troviamo un codice simile per il frammento [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() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Righe 24–27: Il metodo [showVue1] gestisce l'evento 'click' sul pulsante [View 1];

Esegui il progetto e verifica che la navigazione tra le viste funzioni.

1.14.6. Definizione della sessione

L'applicazione funziona come segue:

  • Inserisci un nome nella Vista 1;
  • Visualizza questo nome nella Vista 2;

Per consentire alla Vista 1 di passare il nome inserito alla Vista 2, useremo la seguente sessione;


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // name
  private String nom;
 
  // getters and setters
...
}
  • riga 8: il nome inserito;

La classe [MainActivity] inizializzerà la sessione come segue:


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

1.14.7. Codice finale per i frammenti

Nel frammento [Vue1Fragment], modifichiamo il codice per il gestore di clic del pulsante [Validate]:


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 {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
...
  // event managers ----------------------------------
 
  @Click(R.id.buttonValider)
  protected void doValider() {
    // memorizes the name entered
    String nom = editTextNom.getText().toString();
    // we display it
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // enter the name entered in the session
    session.setNom(editTextNom.getText().toString());
    // navigate to view no. 2
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
 
  }
}
  • righe: 31-37: gestiscono il clic sul pulsante [View #2];
  • riga 34: prima di passare alla Vista 2, memorizziamo il nome inserito nella sessione in modo che la nuova vista possa accedervi;

La vista [View2Fragment] si evolve come segue:


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 {
 
  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Quando viene visualizzata la vista n. 2, deve essere visualizzato il nome inserito nella vista n. 1. Sappiamo che, subito dopo la sua visualizzazione, verrà eseguito il metodo [updateFragment]. È quindi in questo metodo (righe 36–42) che possiamo inserire il codice per visualizzare il nome.

  • righe 16–17: dichiarazione dell'unico componente visivo della vista;
  • Riga 39: il nome inserito nella vista n. 1 viene recuperato dalla sessione;
  • Riga 41: l'etichetta [textViewBonjour] viene aggiornata;

Esegui il progetto e verifica che funzioni.

1.14.8. Gestione del ciclo di vita del frammento

Nel frammento [Vue1Fragment], il metodo [@AfterViews] è il seguente:


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

Questo metodo è incompleto. Infatti, dobbiamo sempre tenere conto del caso in cui il frammento venga riciclato dopo un'operazione [onDestroyView]. In questo caso, la vista del Frammento 1 viene rigenerata e qualsiasi nome che possa essere stato inserito in precedenza scomparirà dalla vista. Non vogliamo che ciò accada. Attualmente, il nome inserito rimane visualizzato perché l'adiacenza dei frammenti del Frammento 1 implica che il ciclo di vita del frammento [Vue1Fragment] venga eseguito una sola volta. Tuttavia, è preferibile tenere conto del riciclaggio del frammento.

Esistono diversi modi per risolvere questo problema:

  • possiamo sfruttare il fatto che il metodo [update] viene eseguito sistematicamente ogni volta che il frammento viene visualizzato per aggiornare il nome inserito;
  • è possibile eseguire questo aggiornamento solo quando il metodo [@AfterViews] viene rieseguito. Adotteremo il secondo approccio;

Modifichiamo il codice in [View1Fragment] come segue:


    // visual interface elements
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;
 
    // data
    private String nom;
 
    @AfterViews
    protected void afterViews() {
        // memory
        afterViewsDone = true;
        // log
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // the text displayed is (re)initialized
        editTextNom.setText(nom);
    }
 
    // event managers ----------------------------------
 
...
 
    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // the name entered is noted so that it can be retrieved if the fragment is recycled
        nom = editTextNom.getText().toString();
        // enter the name entered in the session
        session.setNom(nom);
        // navigate to view no. 2
        activity.navigateToView(1);
}
  • riga 27: poiché stiamo per passare dalla vista 1 alla vista 2, salviamo il nome inserito;
  • riga 17: ogni volta che viene eseguito il ciclo di vita del frammento, viene visualizzato nuovamente l'ultimo nome inserito;

Per il frammento [View2Fragment], il codice esistente è sufficiente:


  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • L'unico componente visivo della vista (riga 3) viene aggiornato ogni volta che la vista viene visualizzata (riga 21). Il metodo [@AfterViews] non ha quindi nulla da aggiungere;

1.14.9. Conclusione

A questo punto, abbiamo dimostrato ancora una volta la rilevanza della nostra architettura:

  • un'attività che implementa l'interfaccia [IMainActivity];
  • frammenti che estendono la classe [AbstractFragment], il che richiede loro di implementare il metodo [updateFragment]. Questi devono anche avere un metodo [@AfterViews] in cui impostano il booleano [afterViewsDone] su true;
  • una sessione che incapsula i dati da condividere tra i frammenti e l'attività;

1.15. Esempio 14: un'architettura a due livelli

Realizzeremo un'applicazione a vista singola con la seguente architettura:

1.15.1. Creazione del progetto

Duplichiamo il progetto precedente [Esempio-12] in [Esempio-13] seguendo la procedura descritta nella sezione 1.4. Otteniamo il seguente risultato:

1.15.2. La vista [view1]

L'applicazione avrà una sola vista [view1.xml]. Pertanto, elimineremo l'altra vista [view2.xml] insieme al frammento ad essa associato:

 

Compila l'applicazione. In [MainActivity] compaiono degli errori:

 

Correggi la riga 4 qui sotto nel gestore dei frammenti [SectionsPagerAdapter]


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

La riga 4 sopra diventa:


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

Rimuovi le importazioni che non sono più necessarie [Ctrl-Shift-O]. Non dovrebbero più esserci errori di compilazione. Esegui il progetto: dovrebbe apparire la vista n. 1. Ora la modificheremo.

Creeremo la vista [vue1.xml] che genererà numeri casuali:

 

I suoi componenti sono i seguenti:

N.
ID
Tipo
Ruolo
1
edtNbAleas
Modifica testo
numero di numeri casuali da generare nell'intervallo intero [a,b]
2
edtA
Modifica testo
valore di un
2
edtB
Modifica testo
valore di b
4
btnExecute
Pulsante
avvia la generazione dei numeri
5
ListView
lstAnswers
elenco dei numeri generati in ordine inverso rispetto alla generazione. Il numero generato più di recente viene visualizzato per primo;

Il suo codice XML è il seguente:


<?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 precedente utilizza etichette definite nel file [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>
  <!-- vue 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>

I colori utilizzati in [vue1.xml] sono definiti nel file [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>
  <!-- colors applied -->
  <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 sessione

  

Poiché qui c'è un solo frammento, non è necessario pianificare la comunicazione tra frammenti. La sessione sarà quindi vuota:


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

A questo punto, compila l'applicazione. Verranno visualizzati degli errori sulle righe che utilizzavano elementi della sessione ora vuota. Rimuovi queste righe e verifica che la compilazione non produca più errori.

1.15.4. Il frammento [Vue1Fragment]

  

Modifichiamo il frammento [Vue1Fragment] esistente come segue:


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 {
 
  // visual interface elements
  @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;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // hide error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Qui c'è un solo frammento il cui ciclo di vita verrà eseguito una sola volta, all'avvio dell'applicazione. Per questo motivo, il metodo [@AfterViews] (righe 46–57) e il metodo [updateFragment] (righe 75–81) verranno eseguiti una sola volta all'avvio dell'applicazione;
  • righe 55-56: nascondiamo i due messaggi di errore dalla vista (mostrati di seguito) [1-2];
 
  • righe 59-60: il metodo eseguito quando si fa clic sul pulsante [Esegui];
  • righe 71-73: viene verificata la validità dei dati inseriti;

Il metodo [isPageValid] è il seguente:


  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
...
 
// check the validity of the data entered
  private boolean isPageValid() {
    // enter the number of random numbers
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // enter a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // b input
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // return
    return (nbErreurs == 0);
  }
 
  • Righe 2–4: Questi tre campi vengono inizializzati dal metodo [isPageValid]. Inoltre, questo metodo restituisce true se tutte le voci sono valide, e false in caso contrario. Se una o più voci non sono valide, vengono visualizzati i messaggi di errore associati;

A questo punto, l'applicazione è eseguibile. Verifica la funzionalità del metodo [isPageValid] inserendo dati errati.

1.15.5. Il livello [business]

  

Il livello [business] fornisce la seguente interfaccia [IMetier]:


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

Il metodo [getAleas(a,b,n)] restituisce normalmente n numeri interi casuali nell'intervallo [a,b]. È inoltre progettato per generare un'eccezione una volta ogni tre volte, e tale eccezione è inclusa nei risultati restituiti dal metodo. In definitiva, il metodo restituisce una lista di oggetti di tipo [Exception] o [Integer].

L'implementazione [Metier] di questa interfaccia è la seguente:


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) {
        // object list
        List<Object> réponses = new ArrayList<Object>();
        // some checks
        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"));
        }
        // mistake?
        if (réponses.size() != 0) {
            return réponses;
        }
        // random numbers are generated
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // generate a random exception 1 time / 3
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // otherwise a random number is returned between two bounds [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // result
        return réponses;
    }
}
  • Riga 9: Utilizziamo l'annotazione AA [@EBean] sulla classe [Business] in modo da poter iniettare riferimenti ad essa nel livello [Presentation]. L'attributo (scope = EBean.Scope.Singleton) garantisce che venga creata una sola istanza della classe [Business]. Pertanto, se viene iniettato più volte nel livello [Presentation], viene sempre iniettato lo stesso riferimento;
  • il resto del codice è standard;

Il tipo [AleaException] utilizzato dalla classe [Metier] è il seguente:


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);
    }
 
}
  • Riga 3: La classe [AleaException] estende la classe di sistema [RuntimeException], rendendola un'eccezione non gestita: non deve essere gestita in un blocco try/catch, né deve essere inclusa nelle firme dei metodi;

1.15.6. Ritorno su [MainActivity]

  

[Business] LayerActivityUserView

L'attività implementerà l'interfaccia [IMetier] del livello [business]. Pertanto, il frammento/la vista avrà come controparte solo l'attività.

L'attività [MainActivity] implementa già l'interfaccia [IMainActivity]. Per far sì che implementi anche l'interfaccia [IMetier], possiamo:

  • aggiungere l'interfaccia [IMetier] alle interfacce implementate dall'attività;
  • assicurarci che l'interfaccia [IMainActivity] stessa estenda l'interfaccia [IMetier]. Questo è l'approccio che stiamo adottando;

L'interfaccia [IMainActivity] diventa la seguente:

  

package exemples.android.architecture;
 
import exemples.android.metier.IMetier;
 
public interface IMainActivity extends IMetier {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
}
  • riga 5: l'interfaccia [IMainActivity] estende l'interfaccia [IMetier]

La classe [MainActivity] si evolve come segue:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // injection molding
  @Bean(Metier.class)
  protected IMetier metier;
 
...
  // implémentation IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • righe 11-12: il livello [business] viene iniettato nell'attività. Per farlo, usiamo l'annotazione [@Bean], il cui parametro è la classe che porta l'annotazione [@EBean];
  • riga 2: l'attività implementa l'interfaccia [IMainActivity] e quindi l'interfaccia [IMetier] del livello [business];
  • righe 16–19: implementazione dell'unico metodo dell'interfaccia [IMetier]. Ci limitiamo a delegare la chiamata al livello [business];

1.15.7. Il frammento [Vue1Fragment] rivisitato

  

Il codice della classe [Vue1Fragment] si evolve come segue:


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 {
 
  // visual interface elements
  @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;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
   ...
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
   ...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // will only be executed once when the application is started
    // create the ListView adapter - this requires the [activity] variable to have been initialized
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • Righe 69-70: Imposta l'adattatore per il componente [ListView];

Il componente [ListView] viene utilizzato per visualizzare un elenco di elementi. A tal fine utilizza un adattatore [ListAdapter], a sua volta collegato all'origine dati che alimenta il [ListView]. Per definire l'adattatore per un [ListView], utilizzare il seguente metodo [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

[ListAdapter] è un'interfaccia. La classe [ArrayAdapter] è una classe che implementa questa interfaccia. Il costruttore utilizzato nella riga 69 sopra riportata è il seguente:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] è l'attività che visualizza la [ListView];
  • [resource] è il numero intero che identifica la vista utilizzata per visualizzare un elemento nella [ListView]. Questa vista può essere di qualsiasi complessità. Lo sviluppatore la costruisce in base alle proprie esigenze;
  • [textViewResourceId] è il numero intero che identifica un componente [TextView] nella vista [resource]. La stringa visualizzata verrà mostrata da questo componente;
  • [objects]: l'elenco degli oggetti visualizzati dalla [ListView]. Il metodo [toString] degli oggetti viene utilizzato per visualizzare l'oggetto nella [TextView] identificata da [textViewResourceId] all'interno della vista identificata da [resource].

Il compito dello sviluppatore è quello di creare la vista [resource] che visualizzerà ogni elemento nella [ListView]. Per il caso semplice in cui vogliamo visualizzare solo una singola stringa di caratteri, come in questo caso, Android fornisce la vista identificata da [android.R.layout.simple_list_item_1]. Questa contiene un componente [TextView] identificato da [android.R.id.text1]. Questo è il metodo utilizzato alla riga 69 per creare l'adattatore [ListView]. Questo adattatore deve essere definito una sola volta. Per consentirne il riutilizzo, è stato definito come variabile di istanza della classe (riga 39). Diamo un'altra occhiata alla riga 69:


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

Il primo parametro del costruttore [ArrayAdapter] è l'attività ottenuta da un frammento tramite [getActivity] e memorizzata qui nella variabile [activity] della classe padre. Questo campo non ha sempre un valore. Pertanto, i log mostrano che quando raggiungiamo il metodo [@AfterViews], non è ancora stato inizializzato, quindi non possiamo inserire le righe 69–70 in questo metodo. Nel metodo [updateFragment], ciò è possibile perché sappiamo che quando questo metodo viene eseguito, [activity] non è necessariamente nullo. L'adattatore è qui associato alla fonte dati [reponses] definita alla riga 37;

Il metodo [doExecute] gestisce il clic sul pulsante [Execute]. Il suo codice è il seguente:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // we ask for the random numbers in the activity
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // we create a list of Strings from this data
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // refresh listview
    adapterReponses.notifyDataSetChanged();
  }
  • righe 7-8: vogliamo svuotare la ListView. Per farlo, svuotiamo l'origine dati [reponses] e chiediamo all'adattatore associato alla ListView di aggiornarsi;
  • righe 10-12: prima di eseguire l'azione richiesta, verifichiamo che i valori inseriti siano corretti;
  • riga 14: l'elenco di numeri casuali viene richiesto all'attività. Otteniamo un elenco di oggetti in cui ogni oggetto è di tipo [Integer] o [AleaException];
  • righe 16–22: utilizzando l'elenco di oggetti ottenuto, l'origine dati [reponses] visualizzata dalla ListView viene aggiornata;
  • riga 24: viene richiesto all'adattatore ListView di aggiornarsi;

1.15.8. Esecuzione

Esegui il progetto e verifica che funzioni correttamente.

1.16. Esempio-15: Architettura client/server

Vedremo ora un'architettura comune per un'app Android, in cui l'app Android comunica con servizi web remoti. Avremo quindi la seguente architettura:

Abbiamo aggiunto un livello [DAO] all'app Android per comunicare con il server remoto. Questo livello comunicherà con il server che genera i numeri casuali visualizzati dal tablet Android. Il server avrà la seguente architettura a due livelli:

I client interrogano URL specifici nel livello [web/JSON] e ricevono una risposta testuale in formato JSON (JavaScript Object Notation). In questo caso, il nostro servizio web gestirà un singolo URL della forma [/a/b], che restituirà un numero casuale nell'intervallo [a,b]. Descriveremo l'applicazione nel seguente ordine:

Il server

  • il suo livello [business];
  • il suo servizio [web/JSON] implementato con Spring MVC;

Il client

  • il suo livello [DAO]. Non ci sarà alcun livello [business];

1.16.1. Il server [web/JSON]

Vogliamo realizzare la seguente architettura:

1.16.1.1. Creazione del progetto

Realizzeremo il servizio web utilizzando l'ecosistema Spring [http://spring.io/]. Andiamo sul sito web [http://start.spring.io/] (giugno 2016), che ci permetterà di generare un progetto Gradle con le dipendenze necessarie per il nostro progetto, che non è un progetto Android e per il quale Android Studio non offre alcun supporto:

  • in [1]: scegli un progetto Gradle;
  • in [2-3]: le proprietà della dipendenza JAR generata dal progetto (vedi sotto);
  • in [4]: selezionare la dipendenza web [5] in modo che siano disponibili i binari necessari per il nostro servizio web;
  • in [6]: generare il progetto. Viene quindi generato un file ZIP contenente uno scheletro di progetto Gradle, reso disponibile per il download;

Cosa inserire in [2-3]? Abbiamo già utilizzato le dipendenze Gradle. Ad esempio, quella del progetto precedente era la seguente:

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  ...
}
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
}
  • Riga 22: Una dipendenza è specificata nel formato [groupId:artifactId:version]. Ciò che viene richiesto nel modulo all'indirizzo [http://start.spring.io/]:
    • in [2] è [groupId];
    • in [3] c'è [artifactId];

Decomprimere il file zip ottenuto nella cartella contenente gli altri progetti:

Utilizzando Android Studio, apri il progetto Gradle [server-01] [1-2]. Il progetto aperto si trova in [3] (vista Progetto).

1.16.1.2. Configurazione Gradle

  

Il file Gradle generato (giugno 2016) è il seguente:


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'
 }
}
  • Le righe 14 e 34–38 sono relative all'IDE Eclipse. Le rimuoviamo;
  • Le righe 1–11 e 15 servono ad aggiungere un plugin chiamato [spring-boot] al nostro progetto Gradle. Spring Boot è un progetto all'interno dell'ecosistema Spring [http://projects.spring.io/spring-boot/]. Questo plugin definisce le versioni delle dipendenze più comunemente utilizzate con Spring. Ciò ci permette di omettere la specificazione delle loro versioni (righe 30 e 31). La versione è quindi quella definita dalla versione di Spring Boot utilizzata (riga 3);
  • righe 22–23: la versione Java da utilizzare, in questo caso la versione 1.8;
  • righe 25–27: i repository binari da utilizzare per il download delle dipendenze;
  • riga 26: specifica il Maven Central Repository. Attualmente si tratta del più grande repository binario open-source disponibile;
  • righe 29–32: le dipendenze richieste per il progetto:
  • riga 30: questa dipendenza include tutti i binari necessari per creare un servizio web Spring;
  • riga 31: questa dipendenza include tutti i binari necessari per il testing, in particolare i test JUnit;
  • Una dipendenza [compile] indica che la dipendenza è necessaria per compilare il progetto. Una dipendenza [testCompile] indica che la dipendenza è necessaria solo per l'esecuzione dei test. Non è quindi inclusa nel binario del progetto;

Inizieremo ripulendo il file Gradle:


// spring boot
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}
 
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
 
// binaire du projet
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}
 
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
// dépôts Maven
repositories {
  mavenLocal()
  mavenCentral()
}
 
// dépendances
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • riga 30: abbiamo aggiunto il repository Maven locale per la macchina di sviluppo. Questo viene creato al momento dell'installazione di Maven (vedi sezione 6.10). Se la dipendenza richiesta è già presente nel repository Maven locale, non verrà recuperata dal repository Maven centrale;
  • righe 19–22: un'attività Gradle per generare il binario del progetto. La useremo per vedere cosa viene fatto;
  • In [1-4], eseguire il task [jar] definito nel file [build.gradle] ([1] si trova in alto a destra e sul lato dell'IDE);

Il passaggio precedente crea l'archivio JAR del progetto e lo colloca nella cartella [build/libs] [5]:

  

Il nome dell'archivio deriva direttamente dalle informazioni fornite al task [jar] nel file [build.gradle] (righe 19–22).

Tutte le dipendenze del progetto possono essere visualizzate come segue:

 

Come si può vedere in [1], l'unica dipendenza del progetto [compile('org.springframework.boot:spring-boot-starter-web')] ha comportato l'inclusione di decine di file binari. Spring Boot per il web include le dipendenze di cui un'applicazione web Spring MVC potrebbe aver bisogno. Ciò significa che alcune potrebbero essere superflue. Spring Boot è l'ideale per un tutorial:

  • include le dipendenze di cui probabilmente avremo bisogno;
  • include un server Tomcat incorporato [1], che ci evita di dover distribuire l'applicazione su un server web esterno;

È possibile trovare molti esempi che utilizzano Spring Boot sul sito web dell'ecosistema Spring [http://spring.io/guides].

Ora completeremo il file [build.gradle] come segue:


// spring boot
...
// dépendances
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
 
// plugin pour créer un binaire aux normes Maven dans le dépôt Maven local
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 {
      // change to point to your repo, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • riga 10: importiamo un plugin Gradle chiamato [maven-publish] che ci permette di pubblicare il binario del progetto in un repository Maven in conformità con gli standard Maven;
  • riga 11: un'attività Gradle chiamata [publishing];
  • righe 14–15: le caratteristiche del binario Maven che verrà creato;
  • riga 23: il repository Maven in cui verrà pubblicato, in questo caso un repository Maven locale;

L'aggiunta del plugin [maven-publish] ha creato nuovi task nel progetto Gradle:

Se, in [2], eseguiamo l'attività [publish], il binario del progetto viene creato e installato nella cartella specificata alla riga 23 del file [build.gradle]:

 

Il task [jar] genera il file binario del progetto. Questo file binario non include le dipendenze e pertanto non è eseguibile. È possibile generare un file binario eseguibile che includa tutte le dipendenze. Per farlo, aggiungiamo il seguente codice al file [build.gradle]:


// créer un binaire avec toutes ses dépendances
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
}
  • Riga 6: Inserisci il nome completo della classe eseguibile del progetto:
  

Il codice per questa classe sarà il seguente:


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

Aggiorna il progetto Gradle, quindi esegui il task [fatJar]:

 

Il file binario viene generato nella cartella [build/libs] e può essere eseguito [1-7]:

1.16.1.3. Configurazione del progetto

La configurazione Gradle non è sufficiente. Dobbiamo anche configurare il progetto. Poiché non si tratta di un progetto Android generato dall'IDE, questa configurazione — che non abbiamo ancora effettuato — deve essere eseguita qui.

 
  • In [3-4]: utilizzare JDK 1.8;

Per compilare il progetto, il pulsante disponibile per i progetti Android non è più presente. Utilizzeremo un'opzione di menu [1-2]:

Successivamente, al lettore viene richiesto di creare il seguente progetto. Commenteremo il codice finale del progetto [3].

1.16.1.4. Il livello [business]

  

Il livello [business] segue lo stesso approccio del livello [business] nell'esempio precedente. Avrà la seguente interfaccia [IMetier]:


package exemples.android.server.metier;
 
public interface IMetier {
  // random number in [a,b]
    int getAlea(int a, int b);
}
  • Riga 5: il metodo che genera 1 numero casuale nell'intervallo [a,b]

Il codice della classe [Metier] che implementa questa interfaccia è il seguente:


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) {
    // some checks
    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);
    }
    // result generation
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Non commenteremo la classe: è simile a quella dell'esempio precedente, tranne per il fatto che non genera eccezioni in modo casuale. Si noti l'annotazione Spring [@Service] alla riga 8, che fa sì che Spring istanzi la classe come singola istanza (singleton) e renda il suo riferimento disponibile ad altri componenti Spring. Qui avrebbero potuto essere utilizzate altre annotazioni Spring per ottenere lo stesso effetto. I componenti Spring hanno nomi predefiniti che possono essere specificati come attributo dell'annotazione utilizzata. Senza questo attributo, come in questo caso, il componente Spring prende il nome della classe con la prima lettera minuscola. Pertanto, in questo caso, il componente Spring è denominato [metier] per impostazione predefinita;

La classe [Metier] genera eccezioni di tipo [AleaException]:


package exemples.android.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  public AleaException() {
  }
 
  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }
 
  // getters and setters
....
}
  • riga 3: [AleaException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non gestita (non è necessario gestirla con un try/catch);
  • riga 6: viene aggiunto un codice di errore alla classe [RuntimeException];

1.16.1.5. Il servizio web / JSON

 
  

Il servizio web / JSON è implementato da Spring MVC. Spring MVC implementa il modello architetturale MVC (Model–View–Controller) come segue:

L'elaborazione di una richiesta del client procede come segue:

  1. richiesta - gli URL richiesti hanno il formato http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Il [Dispatcher Servlet] è la classe Spring che gestisce gli URL in entrata. Esso "instradano" l'URL all'azione che dovrebbe gestirlo. Queste azioni sono metodi di classi specifiche chiamate [Controller]. La C in MVC qui si riferisce alla catena [Dispatcher Servlet, Controller, Action]. Se non è stata configurata alcuna azione per gestire l'URL in entrata, il [Dispatcher Servlet] risponderà che l'URL richiesto non è stato trovato (errore 404 NOT FOUND);
  1. l'elaborazione
  • l'azione selezionata può utilizzare i parametri che il [Dispatcher Servlet] le ha passato. Questi possono provenire da diverse fonti:
    • il percorso [/param1/param2/...] dell'URL,
    • i parametri dell'URL [p1=v1&p2=v2],
    • dai parametri inviati dal browser con la sua richiesta;
  • Durante l'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere l'intervento del livello [business] [2b]. Una volta elaborata la richiesta del client, questa può generare diverse risposte. Un esempio classico è:
    • una pagina di errore se la richiesta non è stata elaborata correttamente
    • una pagina di conferma in caso contrario
  • l'azione indica di visualizzare una vista specifica [3]. Questa vista mostrerà i dati noti come modello di vista. Questa è la M in MVC. L'azione creerà questo modello M [2c] e indicherà di visualizzare una vista V [3];
  1. risposta - la vista V selezionata utilizza il modello M costruito dall'azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia questa risposta.

Per un servizio web / JSON, l'architettura precedente viene leggermente modificata:

  • in [4a], il modello, che è una classe Java, viene convertito in una stringa JSON da una libreria JSON;
  • in [4b], questa stringa JSON viene inviata al browser;

Un esempio di serializzazione di un oggetto Java in una stringa JSON e di deserializzazione di una stringa JSON in un oggetto Java è presentato nelle appendici alla Sezione 6.14.

Torniamo al livello [web] della nostra applicazione:

Nella nostra applicazione c'è un solo controller:

  

Il servizio web/JSON invierà ai propri clienti una risposta di tipo [Response] come segue:


package exemples.android.server.web;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}
  • riga 13: il campo [T body] rappresenta la risposta attesa dal client. Abbiamo deciso di utilizzare qui una risposta generica di tipo T, anziché il tipo Integer del numero casuale atteso. Vogliamo poter riutilizzare questa classe in altre situazioni. Durante l'elaborazione della richiesta del client, il server potrebbe incontrare un problema, che viene quindi riassunto negli altri due campi;
    • riga 8: un codice di stato (0 se non ci sono errori);
    • riga 9: se status != 0, un elenco di messaggi di errore — solitamente quelli provenienti dallo stack delle eccezioni, qualora si sia verificata un'eccezione — null se non ci sono errori;

Il controller [WebController] è il seguente:


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 {
 
  // business layer
  @Autowired
  private IMetier metier;
  // mapper JSON
  @Autowired
  private ObjectMapper mapper;
 
  // random numbers
  @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 {
 
    // the answer
    Response<Integer> response = new Response<>();
    // we use the business layer
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // we return the answer
    return mapper.writeValueAsString(response);
  }
 
  private List<String> getMessagesFromException(Throwable e) {
    // message list
    List<String> messages = new ArrayList<String>();
    // browse the exception stack
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // we return the result
    return messages;
  }
 
}
  • riga 17: l'annotazione [@Controller] indica che la classe è un controller MVC i cui metodi gestiscono le richieste per determinati URL nell'applicazione web;
  • righe 21–22: l'annotazione [@Autowired] indica a Spring di iniettare un componente di tipo [IMetier] nel campo. Si tratterà della precedente classe [Metier]. Poiché vi abbiamo aggiunto l'annotazione [@Service], viene trattata come un componente Spring;
  • righe 24–25: facciamo lo stesso con un mappatore JSON che definiremo in seguito. Il nostro servizio web invierà la sua risposta come stringa JSON. Questo mappatore gestirà la serializzazione della risposta in JSON;
  • riga 30: il metodo che genera il numero casuale. Il suo nome non ha importanza. Quando viene eseguito, i suoi parametri sono stati inizializzati da Spring MVC. Vedremo come. Inoltre, se viene eseguito, è perché il server web ha ricevuto una richiesta HTTP GET per l'URL alla riga 28;
  • riga 28: l'annotazione [@RequestMapping] definisce alcune proprietà del metodo annotato:
    • [value]: l'URL accettato dal metodo;
    • [method]: il metodo HTTP accettato dal metodo. Ce ne sono principalmente due: GET e POST. Il metodo [POST] viene utilizzato quando il client desidera allegare un documento alla propria richiesta HTTP;
    • [produces]: imposta una delle intestazioni della risposta HTTP che verrà inviata al client. Qui, tra le intestazioni HTTP inviate con la risposta al client, ce ne sarà una che informa il client che la risposta viene inviata sotto forma di stringa JSON. Questa intestazione non è obbligatoria. Viene fornita al client a scopo informativo se il client si aspetta risposte che possono assumere varie forme;
    • [consumes]: non presente qui. Specifica le intestazioni HTTP che devono accompagnare la richiesta HTTP del client affinché questa venga accettata;
  • riga 29: l'annotazione [@ResponseBody] indica che il risultato prodotto dal metodo deve essere inviato al client. Senza questa annotazione, la risposta del metodo viene trattata come una chiave utilizzata per selezionare la pagina HTML da inviare al client. In un servizio web / JSON, non ci sono pagine HTML;
  • riga 28: l'URL elaborato è della forma /{a}/{b}, dove {x} rappresenta una variabile. Le variabili {a} e {b} vengono assegnate ai parametri del metodo alla riga 30. Ciò avviene tramite l'annotazione @PathVariable("x"). Si noti che {a} e {b} sono componenti di un URL e sono quindi di tipo String. La conversione da String al tipo di parametro potrebbe fallire. Spring MVC genera quindi un'eccezione. Riassumendo: se richiedo l'URL /100/200 in un browser, il metodo getAlea alla riga 30 verrà eseguito con i parametri interi a=100, b=200;
  • riga 36: al livello [business] viene richiesto un numero casuale nell'intervallo [a,b]. Ricordiamo che il metodo [business].getAlea può generare un'eccezione;
  • riga 37: nessun errore;
  • riga 39: codice di errore;
  • riga 40: l'elenco dei messaggi di risposta è quello dello stack delle eccezioni (righe 46–57). Qui sappiamo che lo stack contiene una sola eccezione, ma volevamo dimostrare un metodo più generico;
  • riga 43: la risposta di tipo [Response<Integer>] viene restituita come stringa JSON;

1.16.1.6. Configurazione del progetto Spring

  

Esistono vari modi per configurare Spring:

  • utilizzando file XML;
  • con codice Java;
  • utilizzando una combinazione di entrambi;

Abbiamo scelto di configurare la nostra applicazione web utilizzando il codice Java. La seguente classe [Config] gestisce questa configurazione:


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 {
  // web configuration ------------------------------------
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet();
    return servlet;
  }
 
  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }
 
  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }
 
  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
 
}
  • Riga 12: Indichiamo a Spring in quali pacchetti troverà i due componenti che deve gestire:
    • il componente [Metier] annotato con [@Service] nel pacchetto [exemples.android.server.metier];
    • il componente [WebController] annotato con [@Controller] nel pacchetto [examples.android.server.web];
  • riga 13: l'annotazione [@EnableWebMvc] consente a Spring Boot di gestire automaticamente una serie di configurazioni standard per un'applicazione Spring MVC. Ciò riduce significativamente il carico di lavoro dello sviluppatore;
  • righe 16, 22, 27 e 33: l'annotazione [@Bean] definisce anche i componenti Spring (bean) allo stesso modo delle due annotazioni incontrate (@Service, @Controller). Qui, l'annotazione [@Bean] annota un metodo anziché una classe, e il risultato del metodo è il componente Spring. In assenza di un attributo di denominazione all'interno dell'annotazione [@Bean], il componente Spring creato prende il nome del metodo annotato;
  • righe 16–20: definiscono il bean [dispatcherServlet]. Si tratta di un nome predefinito in Spring MVC che definisce il front controller dell'applicazione MVC, un oggetto attraverso il quale passano tutte le richieste dei client e che le smista (da cui il nome) ai vari [@Controller] nell'applicazione Spring MVC;
  • riga 18: il bean [dispatcherServlet] è un'istanza della classe [DispatcherServlet] fornita da Spring MVC;
  • righe 22–25: il bean [servletRegistrationBean] viene utilizzato per definire quali URL sono accettati dall'applicazione. Alla riga 24, tutti gli URL sono accettati;
  • righe 27–30: il bean [embeddedServletContainerFactory] viene utilizzato per definire il server incorporato nelle dipendenze del progetto che ospiterà l'applicazione web. La riga 29 specifica che si tratta di un server Tomcat e che funzionerà sulla porta 8080. Per impostazione predefinita, i file binari per questo server web sono forniti dalla dipendenza [org.springframework.boot:spring-boot-starter-web] nel file Gradle;

1.16.1.7. Esecuzione del servizio web / JSON

  

Il progetto viene eseguito dalla seguente classe eseguibile [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) {
    // application execution
    SpringApplication.run(Config.class, args);
  }
 
}
  • La classe [Boot] è una classe eseguibile (righe 7–10);
  • riga 9: il metodo statico [SpringApplication.run] è un metodo di [Spring Boot] (riga 4) che avvierà l'applicazione. Il suo primo parametro è la classe Java che configura il progetto. In questo caso, si tratta della classe [Config] appena descritta. Il secondo parametro è l'array di argomenti passati al metodo [main] (riga 7);

L'applicazione web può essere avviata in vari modi, tra cui i seguenti:

 

Nella console compaiono quindi una serie di messaggi di log:

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

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

Ora, apriamo un browser e proviamo l'URL JSON del servizio web:

Ogni volta otteniamo la rappresentazione JSON di un oggetto di tipo [Response<Integer>].

Invece di utilizzare un browser standard, utilizziamo ora l'estensione [ Advanced Rest Client] per il browser Chrome (vedere le appendici, sezione 6.13):

Image

  • in [1], l'URL richiesto;
  • in [2], utilizzando una richiesta GET;
  • in [3], la richiesta viene inviata;

Image

  • in [4], le intestazioni HTTP della risposta del server. Si noti che ciò indica che il documento inviato è una stringa JSON;
  • in [5], la stringa JSON ricevuta;

1.16.1.8. Generazione del JAR eseguibile del progetto

Nella sezione 1.16.1.2, abbiamo mostrato come configurare il file Gradle per generare un eseguibile per l'applicazione con tutte le sue dipendenze. Adattata all'applicazione attuale, questa configurazione diventa la seguente:


// créer un binaire avec toutes ses dépendances
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
}

Per generare questo eseguibile, segui questi passaggi [1-5]:

Per eseguirlo, interrompi il servizio web se è in esecuzione [1], quindi esegui l'archivio [2-4]:

 

Aprire un browser e richiedere l'URL [localhost:8080/100/200]. Si dovrebbero ottenere gli stessi risultati di prima.

1.16.1.9. Gestione dei log

Quando esegui l'archivio eseguibile, noterai che i log sono diversi rispetto a quando esegui il progetto dall'IDE. Vedrai i log in modalità [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)

È possibile gestire il livello di log aggiungendo un file [logback.xml] alla cartella [resources] del progetto:

  

Questo file potrebbe contenere il seguente contenuto:


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

Il livello di log è controllato alla riga 12. Se ora ricompiliamo l'archivio eseguibile ed lo eseguiamo, otteniamo solo log di livello [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. Il client Android per il server web / JSON

Il client Android avrà la seguente architettura:

Il client avrà due componenti:

  1. un livello [Presentazione] (vista+attività) simile a quello che abbiamo studiato nell'Esempio [Esempio-14];
  2. il livello [DAO] che interagisce con il servizio [web / JSON] studiato in precedenza.

1.16.2.1. Creazione del progetto

Duplichiamo il progetto precedente [Esempio-14] in [Esempio-15] seguendo la procedura descritta nella sezione 1.4. Otteniamo il seguente risultato:

Successivamente, il lettore è invitato a creare il seguente progetto.

1.16.2.2. Configurazione Gradle

 

Il file [build.gradle] è il seguente:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}
 
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
 
  // options de packaging nécessaires pour être capable de produire l'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'
  }
}

Ci limiteremo a commentare ciò che non è stato ancora trattato:

  • righe 46–47: inserimento di un plugin AA. Il plugin [rest-spring-api] consente di delegare la comunicazione client/server alla libreria AA;
  • riga 50: la libreria [spring-android-rest-template] è quella utilizzata da AA per gestire la comunicazione client/server. La versione [2.0.0.M3] è una cosiddetta versione "milestone" che non si trova nei normali repository Maven. Pertanto, dobbiamo specificare, nelle righe 56-59, il repository da utilizzare (riga 58) per trovare la libreria;
  • Riga 51: una libreria JSON;
  • righe 33–39: senza questa proprietà, si verificano errori durante la generazione del file binario APK del progetto;

1.16.2.3. Il manifesto dell'applicazione Android

  

È necessario aggiornare il file [AndroidManifest.xml]. Per impostazione predefinita, l'accesso a Internet è disabilitato. Deve essere abilitato utilizzando una direttiva speciale:


<?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>
  • Riga 5: l'accesso a Internet è consentito;

1.16.2.4. Il livello [DAO]

  

1.16.2.4.1. L'interfaccia [IDao] del livello [DAO]

L'interfaccia del livello [DAO] sarà la seguente:


package exemples.android.dao;
 
public interface IDao {
 
  // random number
  int getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
}
  • riga 6: il metodo del servizio web / JSON per ottenere un numero casuale nell'intervallo [a,b] da questo servizio web;
  • riga 9: l'URL del servizio web / JSON per la generazione di numeri casuali;
  • riga 12: impostiamo un timeout massimo per attendere la risposta del server;
  • riga 15: vogliamo impostare un timeout prima di eseguire la richiesta al server, per dare all'utente il tempo di annullare la propria richiesta;

1.16.2.4.2. L'interfaccia [WebClient]
  

L'interfaccia [WebClient] gestisce la comunicazione con il servizio web. Il suo codice è il seguente:


package exemples.android.dao;
 
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // 1 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • Riga 12: [WebClient] è un'interfaccia che la libreria AA implementerà autonomamente utilizzando le annotazioni che aggiungeremo ad essa. Questa interfaccia deve implementare le chiamate agli URL esposti dal servizio web / JSON:

  // random number
  @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 {
  • Riga 11: L'annotazione [@Rest] è un'annotazione AA. Il valore dell'attributo [converters] è un array di convertitori. Qui, il convertitore [MappingJackson2HttpMessageConverter.class] garantisce che quando il server invia una stringa JSON, questa venga automaticamente deserializzata. Pertanto, vediamo alla riga (d) che l'URL [/{a}/{b}] restituisce un tipo String, che in realtà è una stringa JSON (riga b). Con queste informazioni e quelle relative al tipo previsto alla riga 16, l'istanza [WebClient] del client deserializzerà la stringa ricevuta in un tipo [Response<Integer>];
  • riga 15: un'annotazione @Get che indica che l'URL deve essere chiamato utilizzando un metodo HTTP GET. Il parametro dell'annotazione @Get è il formato URL previsto dal servizio web. È sufficiente utilizzare il parametro [value] dell'annotazione @RequestMapping (riga b) del metodo chiamato nel [WebController] del server. Le parentesi graffe {} racchiudono i parametri URL che devono essere passati ai parametri del metodo alla riga 16. La sintassi [@Path("a") int a] fa sì che al parametro [a] del metodo venga assegnato il valore {a} dall'URL. Quando il parametro URL e il parametro del metodo hanno lo stesso nome, come in questo caso, possiamo scrivere più semplicemente [@Path int a];

Nel caso di una richiesta HTTP POST, il metodo di chiamata avrebbe la seguente firma:


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

L'annotazione [@Body] indica il valore inviato. Questo verrà automaticamente serializzato in JSON. Sul lato server, avremo la seguente firma:


  // random numbers
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
  • riga 2: specifica che è prevista una richiesta HTTP POST e che il corpo della richiesta (oggetto inviato) deve essere trasmesso come stringa JSON (attributo consumes);
  • riga 4: il valore inviato verrà recuperato nel parametro [@RequestBody T body] del metodo;

Torniamo al codice della classe [WebClient]:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • Dobbiamo poter specificare l'URL del servizio web da contattare. Questo si ottiene estendendo l'interfaccia [RestClientRootUrl] fornita da AA. Questa interfaccia espone un metodo [setRootUrl(urlServiceWeb)] che ci permette di impostare l'URL del servizio web da contattare;
  • Inoltre, vogliamo controllare la chiamata al servizio web perché vogliamo limitare il tempo di attesa della risposta. Per farlo, estendiamo l'interfaccia [RestClientSupport], che espone il metodo [setRestTemplate] che ci permetterà di:
    • creare noi stessi l'oggetto [RestTemplate], utilizzato per gestire gli scambi client/server;
    • configurare questo oggetto per impostare il timeout massimo di risposta;

1.16.2.4.3. La classe [Response]

Il metodo [getAlea] dell'interfaccia [IDao] restituisce una risposta di tipo [Response] come segue:


package exemples.android.dao;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
...
}

Questa è la classe [Response] già utilizzata sul lato server (sezione 1.16.1.5). Infatti, dal punto di vista della programmazione, è come se il livello [DAO] del client comunicasse direttamente con il [WebController] del servizio web:

La comunicazione di rete tra client e server, così come la serializzazione/deserializzazione degli oggetti Java sul lato client, sono trasparenti per il programmatore.

1.16.2.4.4. Implementazione del livello [DAO]
  

L'interfaccia [IDao] è implementata dalla seguente classe [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 {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // timeout before request execution
  private int delay;
 
// interface 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;
  }
 
}
  • riga 15: annotiamo la classe [Dao] con l'annotazione [@EBean] per trasformarla in un bean AA che possiamo iniettare altrove;
  • righe 19–20: iniettiamo l'implementazione dell'interfaccia [WebClient] che abbiamo descritto. L'annotazione [@RestService] gestisce questa iniezione;
  • gli altri metodi implementano l'interfaccia [IDao] (righe 27–46);

metodo [setTimeout]

Il metodo [setTimeout] è il seguente:


  @Override
  public void setTimeout(int timeout) {
    // set the client request timeout REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // we build the restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
}
  • L'interfaccia [WebClient] sarà implementata da una classe AA utilizzando la dipendenza Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementa la comunicazione del client con il server web/JSON utilizzando una classe [RestTemplate];
  • riga 4: la classe [SimpleClientHttpRequestFactory] è fornita dalla dipendenza [spring-android-rest-template]. Ci consentirà di impostare il timeout massimo per la risposta del server (righe 5-6);
  • riga 8: costruiamo l'oggetto [RestTemplate], che fungerà da canale di comunicazione con il servizio web. Passiamo l'oggetto [factory] appena costruito come parametro ad esso;
  • riga 10: il dialogo client/server può assumere varie forme. Gli scambi avvengono tramite righe di testo e dobbiamo indicare all'oggetto [RestTemplate] cosa fare con ciascuna riga di testo. Per farlo, gli forniamo dei convertitori, ovvero classi in grado di elaborare le righe di testo. La scelta del convertitore avviene generalmente tramite le intestazioni HTTP che accompagnano la riga di testo. In questo caso, sappiamo che stiamo ricevendo solo righe di testo in formato JSON. Inoltre, come abbiamo visto nella sezione 1.16.1.7, il server ha inviato l'intestazione HTTP:

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

Riga 10: l'unico convertitore per [RestTemplate] sarà un convertitore JSON implementato utilizzando la libreria [Jackson]. C'è una particolarità riguardo a questi convertitori: AA richiede di includerli anche nell'annotazione [WebClient]:


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

Riga 1: ci viene richiesto di specificare un convertitore anche se lo stiamo già specificando a livello di codice.

  • Riga 12: L'oggetto [RestTemplate] costruito in questo modo viene iniettato nell'implementazione dell'interfaccia [WebClient], ed è questo oggetto che gestirà la comunicazione client/server;

Metodo [getAlea]

Il metodo [getAlea] è il seguente:


  @Override
  public int getAlea(int a, int b) {
    // service execution
    Response<Integer> info;
    DaoException ex;
    try {
      // waiting
      waitSomeTime(delay);
      // service execution
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // we return the result
        return info.getBody();
      } else {
        // we note the exception
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // we note the exception
      ex = new DaoException(e, 100);
    }
    // we launch the exception
    throw ex;
  }
...
  // private methods -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • riga 8: attendi [delay] millisecondi;
  • riga 10: chiamiamo semplicemente il metodo con la stessa firma nella classe che implementa l'interfaccia [WebClient];
  • riga 11: analizziamo la risposta ricevuta dal server controllandone lo [status];
  • righe 12–14: se non c'è stato alcun errore lato server (status = 0), restituiamo il risultato del metodo;
  • riga 17: se si è verificato un errore lato server (status!=0), prepariamo un'eccezione senza lanciarla. Il server ha inviato un elenco di messaggi di errore. Creiamo un'eccezione con, come unico messaggio, la stringa JSON dell'elenco dei messaggi del server;
  • righe 19–22: altri casi di eccezione;
  • riga 24: quando si raggiunge questo punto, si è necessariamente verificata un'eccezione. Quindi la si genera;

La [DaoException] utilizzata da questo codice è la seguente:


package exemples.android.dao;
 
import java.util.ArrayList;
import java.util.List;
 
public class DaoException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  public DaoException() {
  }
 
  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  // getters and setters
...
}
  • riga 6: [DaoException] è un'eccezione non gestita;

Metodo [setUrlServiceWebJson]

Il metodo [setUrlServiceWebJson] è il seguente:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // we set the URL of the REST service
    webClient.setRootUrl(urlServiceWebJson);
}
  • Riga 4: Impostiamo l'URL del servizio web utilizzando il metodo [setRootUrl] dell'interfaccia [WebClient]. Questo metodo esiste perché questa interfaccia estende l'interfaccia [RestClientRootUrl];

1.16.2.5. Il pacchetto [architecture]

Il pacchetto [architecture] contiene gli elementi che strutturano l'applicazione:

1.16.2.5.1. L'interfaccia [IMainActivity]

L'interfaccia [IMainActivity] elenca i metodi che l'attività dell'applicazione deve implementare:


package exemples.android.architecture;
 
import exemples.android.dao.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // waiting
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
  // response time
  int TIMEOUT = 1000;
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
}
  • riga 5: l'interfaccia [IMainActivity] estende l'interfaccia [IDao];
  • righe 13–16: ai metodi già presenti negli esempi precedenti (righe 7–11), abbiamo aggiunto due metodi per gestire la schermata di caricamento dell'applicazione (righe 14, 16);
  • riga 21: impostiamo un timeout massimo per la risposta del server a 1 secondo;

1.16.2.5.2. La classe [Utils]

Abbiamo raggruppato nella classe [Utils] i metodi di utilità statici che possono essere chiamati da varie parti dell'architettura dell'applicazione:


package exemples.android.architecture;
 
import java.util.ArrayList;
import java.util.List;
 
public class Utils {
 
  // list of exception messages - version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // create a list of error msgs from the exception stack
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }
 
  // exception message list - version 2
  static public String getMessagesForAlert(Throwable th) {
    // build the text to be displayed
    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--;
    }
    // result
    return texte.toString();
  }
 
}
  • righe 9–18: crea un elenco dei messaggi di errore contenuti in un Throwable;
  • righe 21-32: utilizza il metodo precedente per costruire, dall'elenco dei messaggi ottenuto, il testo da visualizzare in un messaggio di avviso Android;
  • righe 27-28: i messaggi vengono numerati. Il numero più piccolo (1) corrisponde all'eccezione iniziale, mentre il numero più alto all'eccezione più recente nello stack delle eccezioni;

1.16.2.5.3. La classe astratta [AbstractFragment]

La classe [AbstractFragment] ha due scopi:

  1. garantire che il metodo [updateFragments] delle classi figlie venga sempre chiamato quando il frammento viene visualizzato, e solo una volta;
  2. esternalizzare lo stato e i metodi delle classi figlie che possono essere esternalizzati;

È lo scopo 2 che ci porta a includere le operazioni di gestione delle immagini di attesa in questa classe: tutti i componenti di un'applicazione Android asincrona devono gestire questo tipo di problema:


  // wait management
  protected void beginWaiting() {
    // we set the hourglass
    mainActivity.beginWaiting();
  }
 
  protected void cancelWaiting() {
    // the hourglass is removed
    mainActivity.cancelWaiting();
}

1.16.2.6. La vista

1.16.2.6.1. La vista [view1.xml]
  

Rispetto all'esempio precedente, la vista [view1.xml] è cambiata come segue:

 
 
  • in [1], l'utente deve specificare l'URL del servizio web e il timeout [2] prima di ogni chiamata al servizio web;
  • in [3], le risposte vengono conteggiate;
  • in [4], l'utente può annullare la propria richiesta;
  • in [5], quando vengono richiesti i numeri, appare un indicatore di caricamento. Scompare una volta che tutti i numeri sono stati ricevuti o l'operazione è stata annullata;

Image

  • In [6], viene verificata la validità dei dati inseriti;

Il lettore è invitato a caricare il file [vue1.xml] dagli esempi. Per il resto di questa sezione, forniamo gli ID dei nuovi componenti:

Image

N.
Tipo
ID
1
Modifica testo
edt_nbaleas
2
TextView
txt_erroreNumeroDiEpisodi
3
Modifica testo
edt_a
4
Modifica testo
edt_b
5
TextView
txt_intervallo_errore
6
Modifica testo
editTextWebServiceUrl
7
TextView
textViewErrorUrl
8
EditText
ritardoModificaTesto
9
TextView
ritardoErroreTextView
10
Pulsante
btn_Esegui
11
Pulsante
btn_Annulla
12
TextView
txt_Risposte
13
ListView
lst_answers

I pulsanti [10-11] sono fisicamente sovrapposti. In un dato momento, solo uno dei due sarà visibile.

1.16.2.6.2. Il frammento [Vue1Fragment]
  

La struttura del frammento [Vue1Fragment] è la seguente:


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 {
 
  // visual interface elements
  @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;
...
  // local data
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone=true;
    // initially no error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // hidden [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // list of answers
    reponses = new ArrayList<>();
  }
...
  • righe 24–49: riferimenti ai componenti della vista [view1.xml] (riga 20);
  • righe 55-69: il metodo [@AfterViews] eseguito quando i riferimenti nelle righe 24-49 sono stati inizializzati;
  • riga 58: non dimenticarlo: è necessario per il ciclo di vita del frammento;
  • righe 60–63: i messaggi di errore sono nascosti;
  • righe 65–66: il pulsante [Cancel] è nascosto (riga 65) e il pulsante [Execute] è visualizzato (riga 66). Si noti che sono fisicamente uno sopra l'altro;
  • Riga 68: il campo alla riga 52 conterrà l'elenco delle stringhe da visualizzare nella ListView delle risposte;

Subito dopo il metodo [@AfterViews], verrà eseguito il seguente metodo [updateFragment]:


  @Override
  protected void updateFragment() {
    // create the response list adapter
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • Righe 4-5: Creazione dell'adattatore ListView per le risposte. Viene memorizzato in una variabile di istanza in modo da essere disponibile per altri metodi della classe;

Facendo clic sul pulsante [Esegui] si avvia l'esecuzione del seguente metodo:


// seizures
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;
 
  // local data
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;
 
  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // we start waiting
    beginWaiting();
  }
 
  @Background(id = "alea")
  void getAlea(int a, int b) {
    // do as little as possible here
    // in any case no display - these must take place in the UiThead
    try {
      // the result is displayed in the UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
  }
  • righe 17–18: cancelliamo l'elenco precedente delle risposte dal server. Per farlo, alla riga 17, cancelliamo l'origine dati [reponses] associata all'adattatore ListView;
  • riga 19: un valore booleano che ci dirà se l'utente ha annullato o meno la propria richiesta;
  • righe 21-22: visualizziamo un contatore impostato a zero per il numero di risposte;
  • righe 24-26: recuperiamo le voci dalle righe [2-6] e ne verifichiamo la validità. Se una di esse non è valida, il metodo viene interrotto (riga 25) e l'utente viene riportato all'interfaccia visiva;
  • righe 28-29: se tutti i dati inseriti sono validi, l'URL del servizio web (riga 28) e il tempo di attesa prima di ogni chiamata al servizio (riga 29) vengono passati all'attività. Queste informazioni sono richieste dal livello [DAO] e si noti che è l'attività a comunicare con esso;
  • righe 31–33: i numeri casuali vengono richiesti uno per uno dal metodo [getAlea] alla riga 39;
  • riga 38: il metodo [getAlea] è annotato con l'annotazione AA [@Background], il che significa che verrà eseguito in un thread (flusso di esecuzione, processo) diverso da quello in cui gira l'interfaccia visiva. È infatti obbligatorio eseguire qualsiasi chiamata Internet in un thread diverso da quello dell'interfaccia visiva. Pertanto, in un dato momento, possono esserci diversi thread:
    • quello che visualizza l'interfaccia utente (UI) e ne gestisce gli eventi,
    • i thread [nbAleas], ognuno dei quali richiede un numero casuale al servizio web. Questi thread vengono avviati in modo asincrono: il thread dell'interfaccia utente avvia un thread [getAlea] (riga 32) che richiede un numero casuale al servizio web e non attende che l'operazione sia completata. Verrà informato del completamento tramite un evento. Pertanto, i thread [nbAleas] verranno avviati in parallelo. È possibile configurare l'applicazione in modo che avvii un solo thread alla volta. In tal caso, esiste una coda di thread da eseguire;

Riga 38: il parametro [id] assegna un nome al thread generato. Qui, i thread [nbAleas] hanno tutti lo stesso nome [alea]. Questo ci permetterà di cancellarli tutti contemporaneamente. Questo parametro è facoltativo se la cancellazione dei thread non viene gestita;

  • Riga 44: viene chiamato il metodo [getAlea] dell'attività. Verrà quindi eseguito in un thread separato dall'interfaccia utente. Questo thread effettuerà la chiamata al servizio web e non attenderà la risposta. Verrà successivamente notificato tramite un evento che la risposta è disponibile. A questo punto, alla riga 44, verrà chiamato il metodo [showInfo] con la risposta ricevuta come parametro;
  • Righe 45–47: L'esecuzione della richiesta web potrebbe generare un'eccezione. Richiediamo quindi che i messaggi di errore dell'eccezione vengano visualizzati in un messaggio di avviso;
  • Riga 35: attendiamo i risultati:
    • verrà visualizzato un indicatore di caricamento;
    • il pulsante [Cancel] sostituirà il pulsante [Execute]. Poiché i thread avviati sono asincroni, il thread dell'interfaccia utente non li attende e la riga 35 viene eseguita prima che essi terminino. Una volta terminato il metodo [beginWaiting], l'interfaccia utente può nuovamente rispondere all'utente, ad esempio a un clic sul pulsante [Annulla]. Se i thread avviati fossero stati sincroni, la riga 35 sarebbe stata raggiunta solo una volta terminati tutti i thread. Annullarli non avrebbe quindi più senso;

Il metodo [showInfo] è il seguente:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // are we done?
      if (nbInfos == nbAleas) {
        // we end the wait
        cancelWaiting();
      }
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // we display the answers
      adapterReponses.notifyDataSetChanged();
    }
}
  • Il metodo [showInfo] viene chiamato all'interno del thread [getAlea] annotato con [@Background]. Questo metodo aggiornerà l'interfaccia utente. Può farlo solo se viene eseguito all'interno del thread dell'interfaccia utente. Questo è il significato dell'annotazione [@UiThread] alla riga 1;
  • riga 2: il metodo riceve un numero casuale;
  • riga 3: il corpo del metodo viene eseguito solo se l'utente non ha annullato la richiesta;
  • righe 5–6: il contatore delle risposte viene incrementato e visualizzato;
  • righe 8–11: se sono state ricevute tutte le risposte previste, l'attesa viene terminata (fine del segnale di attesa; il pulsante [Execute] sostituisce il pulsante [Cancel]);
  • righe 12–15: il numero casuale ricevuto viene aggiunto all'elenco delle risposte visualizzate dal componente [ListView listReponses] e l'elenco viene aggiornato;

Il metodo [showAlert] è il seguente:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

La logica qui è simile a quella del metodo [showInfo]:

  • riga 1: è richiesta l'annotazione [@UiThread];
  • riga 2: il metodo riceve l'eccezione che si è verificata;
  • riga 3: il metodo viene eseguito solo se l'utente non ha annullato la propria richiesta;
  • riga 5: la richiesta dell'utente viene annullata come se avesse cliccato lui stesso sul pulsante [Cancel];
  • riga 7: l'avviso viene visualizzato utilizzando la classe Android [AlertDialog]:
    • [activity]: è l'attività di tipo [Activity] memorizzata nella classe padre [AbstractFragment];
    • [setTitle]: imposta il titolo della finestra di avviso [1];
    • [setMessage]: imposta il messaggio visualizzato dalla finestra di avviso [2];
    • [setNeutral]: imposta il pulsante che chiuderà la finestra di avviso [3];
    • [show]: richiede la visualizzazione della finestra di avviso;
 

Il clic sul pulsante [Annulla] viene gestito con il seguente metodo:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // memory
    hasBeenCanceled=true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea", true);
    // end of wait
    cancelWaiting();
}
  • riga 4: si noti che l'utente ha annullato la propria richiesta;
  • riga 6: annulla tutte le attività identificate dalla stringa [alea]. Il secondo parametro [true] indica che devono essere annullate anche se sono già state avviate. L'identificatore [alea] è quello utilizzato per qualificare il metodo [getAlea] del frammento (riga 1 sotto):

  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

Nota: si è scoperto che la riga 6 del codice del metodo [doAnnuler] non funzionava correttamente. Per questo motivo abbiamo aggiunto la variabile booleana [hasBeenCanceled]. Infatti, in caso di eccezione (server inattivo), la finestra di avviso sarebbe apparsa n volte se avessimo richiesto n numeri casuali.

1.16.2.7. L'attività [MainActivity]

1.16.2.7.1. La vista [activity-main.xml]
  

Rispetto all'esempio precedente, abbiamo aggiunto un'immagine di caricamento alla vista associata alla [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">
      <!-- image d'waiting -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
 
    </android.support.v7.widget.Toolbar>
    <!-- image d'waiting -->
  </android.support.design.widget.AppBarLayout>
...
  • righe 17-21: l'immagine segnaposto;

1.16.2.7.2. L'attività [MainActivity]

[MainActivity] è cambiata poco rispetto a com'era in [Esempio-14]. Per prima cosa, vi iniettiamo il livello [DAO]:


  // dao injection
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // set the [DAO] layer
    setTimeout(TIMEOUT);
}
  • righe 2-3: iniezione del livello [DAO] tramite un'annotazione AA;
  • righe 5-13: codice eseguito dopo questa iniezione;
  • riga 12: impostazione del timeout per il livello [DAO]

Inoltre, l'attività [MainActivity] deve implementare l'interfaccia [IMainActivity], che a sua volta estende l'interfaccia [IDao]:


  // implémentation IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // hold image management
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }
 
  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }
 
  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public int getAlea(int a, int b) {
    // execution
    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. Esecuzione del progetto

Avviare il servizio web (sezione 1.16.1.7) e quindi avviare il client Android:

Image

Per scoprire cosa inserire in [1], segui questi passaggi. Apri un prompt dei comandi e digita il seguente 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

Se avete installato [GenyMotion], la macchina virtuale VirtualBox ha aggiunto degli indirizzi IP al vostro computer (righe 10 e 18). Questi indirizzi sono particolarmente comodi perché non vengono bloccati dal firewall di Windows. La riga 30 mostra l'indirizzo IP del tuo computer su una rete locale. Per utilizzare questo indirizzo, in genere è necessario disabilitare il firewall di Windows. Se sei connesso a una rete Wi-Fi, utilizza l'indirizzo Wi-Fi e, anche in questo caso, disabilita il firewall se ne hai uno.

Prova l'applicazione nei seguenti casi:

  • 100 numeri casuali nell'intervallo [1000, 2000] senza timeout;
  • 2000 numeri casuali nell'intervallo [10000, 20000] senza timeout e annulla l'attesa prima che la generazione sia completata;
  • 5 numeri casuali nell'intervallo [100, 200] con un tempo di attesa di 5000 ms, e annullare l'attesa prima che la generazione sia completata;

1.16.2.9. Gestione dell'annullamento

Per tenere traccia di ciò che accade quando l'utente richiede l'annullamento o quando l'annullamento viene attivato da un'eccezione, aggiungiamo il seguente metodo all'interfaccia [IDao] (vedere la sezione 1.16.2.4.1):


package exemples.android.dao;
 
public interface IDao {
 
  ...
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}

Nella classe [Dao], aggiungiamo il seguente codice:


  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
..
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
...
  // interface IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // service execution
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • riga 9: annotiamo il nome della classe;
  • righe 16–18: scriviamo un log ogni volta che viene chiamato il metodo [getAlea];

Inoltre, nel frammento [Vue1Fragment], aggiungiamo i seguenti log:


  @UiThread
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }
 
  @UiThread
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}
 
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Ogni volta che il frammento [Vue1Fragment] riceve informazioni dal livello [DAO], viene generato un log. Inoltre, quando viene chiamato il metodo [doAnnuler], l'evento viene registrato nel log.

Test 1

Richiediamo 5 numeri anche se il server non è stato avviato. Otteniamo i seguenti log:

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
  • Righe 1–5: il metodo [getAlea] della classe [Dao] viene chiamato cinque volte. Si noti che si tratta di chiamate asincrone effettuate dal frammento [VueFragment] e che il frammento non attende il risultato della chiamata;
  • riga 7: è stata effettuata la prima richiesta HTTP e il frammento [VueFragment] ha ricevuto la sua prima eccezione;
  • riga 8: richiede quindi l'annullamento di tutte le richieste;
  • righe 9–12: tuttavia, vediamo che riceve le quattro eccezioni seguenti. Pertanto, le richieste asincrone in sospeso sono state tutte eseguite;

Test 2

Ora, avviamo il server e richiediamo 5 numeri con un ritardo di 5 secondi, quindi facciamo clic su [Annulla] prima che il ritardo finisca. I log sono i seguenti:

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)
  • righe 1-5: il metodo [getAlea] della classe [Dao] viene chiamato cinque volte;
  • riga 7: l'utente ha richiesto l'annullamento delle richieste;
  • riga 8: vediamo che [Vue1_Fragment] riceve 5 valori. Ancora una volta, le richieste asincrone in sospeso sono state tutte eseguite;

Ecco perché abbiamo dovuto gestire un valore booleano [hasBeenCanceled] per evitare di visualizzare qualsiasi cosa quando era stata richiesta una cancellazione. Nel codice di cancellazione:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea",true);
    // end of wait
    cancelWaiting();
}

Il codice alla riga 10 non funziona come previsto. Ciò potrebbe essere dovuto al fatto che le attività asincrone condividono lo stesso metodo annotato con [@Background]:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Esempio 16: Gestione dell'asincronia con RxAndroid

Ora gestiremo l'asincronia richiesta dalle applicazioni Android utilizzando una libreria chiamata RxJava [http://reactivex.io/] e la sua versione derivata per l'ambiente Android [RxAndroid]. Per farlo, utilizzeremo il corso [Introduzione a RxJava. Applicazione agli ambienti Swing e Android].

1.17.1. Creazione del progetto

Duplichiamo il progetto [Esempio-1] in [Esempio-16]:

1.17.2. Configurazione Gradle

  

Nel file [build.gradle], aggiungiamo la dipendenza dalla libreria [RxAndroid]:


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

1.17.3. Il livello [DAO]

  

1.17.4. L'interfaccia [IDao]

L'interfaccia [IDao] diventa la seguente:


package exemples.android.dao;
 
import rx.Observable;
 
public interface IDao {
 
  // random number
  Observable<Integer> getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • riga 8: il metodo [getAlea] ora restituisce un tipo [Observable] della libreria RxJava (riga 3). Il principio è il seguente:

Uno stream di elementi di tipo Observable<T> viene osservato da uno o più subscriber (osservatori, consumatori) di tipo Subscriber<T>. La libreria RxJava consente al flusso Observable<T> di essere eseguito nel thread T1 e al suo osservatore Subscriber<T> nel thread T2 senza che lo sviluppatore debba preoccuparsi della gestione del ciclo di vita di questi thread e di questioni naturalmente complesse, come la condivisione dei dati tra i thread e la sincronizzazione dei thread per l'esecuzione di un'attività globale. Facilita quindi la programmazione asincrona.

1.17.5. La classe [AbstractDao]

Deriveremo la classe [Dao] dalla seguente classe [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 {
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
 
  // méthodes protégées ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    Response<T> getResponse();
  }
 
  // generic request
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // service execution
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // make the synchronous request and forward the response to the subscriber
          Response<T> response = request.getResponse();
          // mistake?
          int status = response.getStatus();
          if (status != 0) {
            // we note the exception
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // we issue the answer
            subscriber.onNext(response.getBody());
            // we signal the end of the observable
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // we note the exception
          ex = new DaoException(e, 100);
        }
        // exception?
        if (ex != null) {
          // we issue the exception
          subscriber.onError(ex);
        }
      }
    });
  }
 
}
  • La classe [AbstractDao] ha come elemento principale un metodo generico [getResponse] utilizzato per recuperare un [Response<T>] dal server, dove T è il tipo del risultato desiderato dal client HTTP (in questo caso, Integer);
  • Riga 20: L'unico parametro del metodo generico [getResponse] è un'istanza dell'interfaccia generica [IRequest<T>] delle righe 15–17. Questa interfaccia ha un solo metodo [getResponse], ed è proprio questo metodo che restituisce la [Response<T>] desiderata;
  • Grazie ai due punti precedenti, la classe [AbstractDao] può fungere da classe padre per qualsiasi livello [Dao] lato client di un server che invia risposte di tipo [Response<T>];
  • riga 20: il metodo generico [getResponse] restituisce un tipo [Observable<T>] che rappresenta il risultato effettivamente atteso dal client HTTP (in questo caso, un tipo Observable<Integer>);
  • righe 22–51: il metodo statico [rx.Observable.create] crea un tipo [Observable];
  • riga 22: l'unico parametro di questo metodo è un'istanza di tipo [rx.Observable.OnSubscribe<T>], un'interfaccia che dispone dei seguenti metodi:
    • [onNext(T element)]: consente di emettere un elemento di tipo T a un osservatore;
    • [onError(Throwable th)]: consente di emettere un'eccezione a un osservatore;
    • [onCompleted]: consente di indicare a un osservatore che le emissioni sono terminate;

Un tipo [Observable<T>] obbedisce a determinati vincoli:

  • emette i propri elementi utilizzando il metodo [onNext(T element)];
  • il metodo [onCompleted] deve essere chiamato esattamente una volta non appena non ci sono più elementi da emettere all'osservatore;
  • il metodo [onCompleted] non viene chiamato se è stato chiamato il metodo [onError(Throwable th)];

Nel nostro esempio:

  • l'osservatore sarà il frammento [Vue1Fragment]. È l'osservatore che consuma gli elementi emessi dall'[Observable<T>] (elemento o eccezione);
  • il tipo [Observable<T>] creato emetterà un solo elemento (riga 37);
  • riga 29: effettua una richiesta HTTP sincrona al server e ottiene il tipo [Response<T>]. Questa richiesta HTTP è gestita dal tipo [IRequest] passato come parametro al metodo generico [getResponse];
  • riga 31: recupera lo stato della risposta;
  • righe 32–34: se questo stato indica un errore, viene preparata un'eccezione;
  • righe 36–39: se lo stato non è un errore, viene inviata la risposta effettivamente prevista dal client (riga 37) e l'osservatore viene informato che non ci saranno ulteriori emissioni (riga 39);
  • righe 41–44: se la richiesta HTTP termina con un'eccezione, la registra;
  • righe 46–49: se l'eccezione [ex] non è nulla, viene emessa all'osservatore. Qui non è necessario chiamare il metodo [onCompleted] per indicare all'osservatore che non verranno emessi ulteriori elementi. Questo è implicito;

Il punto chiave da trarre da queste spiegazioni è che:

  • il metodo generico [<T> Observable<T> getResponse(final IRequest<T> request)] restituisce un tipo [Observable<T>] che emette un singolo elemento di tipo T oppure un'eccezione;
  • questo metodo accetta come unico parametro un tipo [IRequest<T>] il cui unico metodo [getResponse()] esegue la richiesta HTTP che restituisce il tipo [Response<T>];

1.17.6. La classe [Dao]

La classe [Dao] si evolve come segue:


@EBean
public class Dao extends AbstractDao implements IDao {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // timeout before request execution
  private int delay;
  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
 
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
 
 
  // interface IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        return webClient.getAlea(a, b);
      }
    });
}
...
  • riga 2: la classe [Dao] estende la classe [AbstractDao];
  • riga 24: il metodo [getAlea] ora restituisce un tipo [Observable<Integer>];
  • riga 30: chiamata al metodo generico [getResponse] della classe padre. Viene passato un parametro di tipo [IRequest<Integer>];
  • righe 32–37: implementazione dell'interfaccia [IRequest<Integer>];
  • riga 36: la richiesta HTTP viene effettuata tramite l'interfaccia AA [webClient], come già fatto in precedenza. Sappiamo che otterremo un tipo [Response<Integer>], che è proprio il tipo che il metodo [IRequest<Integer>.getResponse()] deve restituire;
  • riga 36: qui usiamo una funzionalità chiamata chiusura: la capacità di incapsulare al suo interno valori esterni a un'istanza al momento della sua creazione, in questo caso i valori di [a, b] della riga 24. Questo è ciò che permette al metodo [IRequest<Integer>.getResponse()] di non avere parametri. Questi valori sono stati incorporati all'interno del corpo del metodo. E dove normalmente cambieremmo i parametri del metodo (a, b) -> (x, y), qui creiamo una nuova istanza di [IRequest<Integer>] che incapsula i valori di x e y;

1.17.7. La classe [MainActivity]

La classe [MainActivity], che implementa l'interfaccia [IDao], si evolve come segue:


  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // execution
    return dao.getAlea(a, b);
}

1.17.8. La classe [Vue1Fragment]

La classe [Vue1Fragment] si evolve come segue:


  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    getAleasInBackground(a, b);
    // we start waiting
    beginWaiting();
}
  • riga 18: richiediamo numeri casuali dal metodo [getAleasInBackground], così chiamato perché i numeri saranno richiesti in un thread diverso dal thread dell'interfaccia utente;

  private int nbReponses = 0;
  // subscriptions to observables
  private List<Subscription> abonnements;
 
// annotation [Background] unnecessary
  void getAleasInBackground(int a, int b) {
    // initially no response and no subscriptions
    nbReponses = 0;
    abonnements.clear();
    // prepare the observable
    Observable<Integer> response = Observable.empty();
    // merge the results of the various HTTP calls
    // they are executed on an I/O thread
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // the cumulative observable will be observed on the UI thread
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // the observable is executed
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // we add the information to the list of answers
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // error message
          showAlert(th);
          // end waiting
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // end waiting
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
}
  • riga 3: un osservabile ha degli abbonati. Il collegamento tra un abbonato e il processo che osserva è chiamato abbonamento. Qui avremo un solo processo osservato e un solo abbonato. Pertanto, avremo un solo abbonamento. Per principio, lo trattiamo come se potessimo avere più processi osservati monitorati da diversi osservatori, il che comporterebbe più abbonamenti;
  • righe 11–18: configuriamo il processo osservato (osservabile). È importante capire che si tratta solo di configurazione: il processo non viene eseguito;
  • riga 11: iniziamo con un osservabile vuoto, un osservabile che non emette nulla;
  • righe 14–16: a questo osservabile vuoto, aggiungiamo [nbAleas] osservabili, che saranno [nbAleas] richieste HTTP che restituiscono [nbAleas] numeri casuali;
  • Riga 15: come prima, il numero casuale i viene richiesto alla classe [MainActivity]. È importante comprendere che nessuna richiesta HTTP è stata ancora eseguita. Il metodo [mainActivity.getRandom(a, b)] viene eseguito e restituisce un [Observable<Integer>]. Si tratta di un processo che verrà osservato una volta avviato;
  • riga 15: il metodo [subscribeOn(Schedulers.io())] richiede che il processo venga eseguito (quando lo è) su un thread I/O. La libreria RxJava offre diversi tipi di thread. Il thread I/O è adatto alle richieste HTTP;
  • riga 15: l'observable #i viene unito all'observable iniziale della riga 11: da [nbAleas] Observables, ciascuno dei quali emette un elemento, creiamo un observable che emetterà [nbAleas] elementi. Questo è quello che verrà osservato. Questo osservabile emette la notifica [onCompleted] quando tutti gli osservabili che lo compongono hanno emesso le proprie notifiche [onCompleted]. Questo ci eviterà di dover contare le risposte, come abbiamo fatto nella versione precedente, per determinare se abbiamo ricevuto tutti i numeri previsti;
  • riga 18: a questo punto, abbiamo configurato un osservabile che è la composizione di [nbAleas] osservabili, ciascuno in esecuzione su un thread I/O;
  • riga 18: il metodo [observeOn(AndroidSchedulers.mainThread())] specifica su quale thread devono essere osservati i valori emessi dall'osservabile. Qui, il thread [AndroidSchedulers.mainThread())] appartiene alla libreria RxAndroid, non a RxJava. Si riferisce al thread dell'interfaccia utente, noto anche come ciclo di eventi. Questo punto è importante: in un'app Android, la modifica di un componente dell'interfaccia utente può essere effettuata solo sul thread dell'interfaccia utente; in caso contrario, si verifica un'eccezione;
  • righe 19–45: ora che il processo da osservare è stato configurato, lo eseguiamo;
  • riga 21: l'operazione [Observable.subscribe] avvia l'esecuzione del processo osservato. Questa operazione avvierà i processi asincroni [nbAleas] configurati in precedenza. I risultati di questi processi saranno automaticamente resi disponibili all'osservatore sul thread dell'interfaccia utente;
  • Ricordiamo che l'osservabile emette tre tipi di eventi:
    • [onNext]: quando emette un elemento;
    • [onError]: quando incontra un'eccezione;
    • [onCompleted]: quando segnala che non emetterà più;

Il metodo [Observable.subscribe] accetta tre oggetti come parametri: [Action1<Integer>, Action1<Throwable>, Action0], i cui metodi [call] vengono utilizzati per gestire ciascuno di questi tre eventi;

  • righe 21–27: il primo parametro di tipo [Action1<Integer>] viene utilizzato per gestire l'evento [onNext]. Il suo metodo [call] riceve l'elemento emesso dall'osservabile (riga 23);
  • riga 25: riutilizziamo il metodo [showInfo] dell'esempio precedente;
  • righe 27–35: il secondo parametro di tipo [Action1<Throwable>] viene utilizzato per gestire l'evento [onError]. Il suo metodo [call] riceve l'eccezione emessa dall'osservabile (riga 29);
  • riga 31: riutilizziamo il metodo [showAlert] dell'esempio precedente;
  • riga 33: avviamo la procedura per annullare la richiesta dell'utente. Ciò comporta l'annullamento di tutti gli osservabili attualmente in esecuzione;
  • righe 35–41: il terzo parametro di tipo [Action0] viene utilizzato per gestire l'evento [onCompleted]. Il suo metodo [call] non accetta parametri;
  • riga 39: l'attesa viene annullata;

Il metodo [showInfo] si evolve come segue:


  // annotation [UiThread] unnecessary
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // display answers
      adapterReponses.notifyDataSetChanged();
    }
}

Il metodo presenta due modifiche:

  • riga 1: abbiamo rimosso l'annotazione AA [@UiThread];
  • non contiamo più le risposte per determinare se interrompere o meno l'attesa. Ora è l'evento [onCompleted] dell'osservabile a fornire questa informazione;

Il metodo [showAlert] cambia come segue:


  // annotation [UiThread] unnecessary
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • L'unica modifica è nella riga 1: abbiamo rimosso l'annotazione AA [@UiThread];

Infine, il metodo [doAnnuler] cambia come segue:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // asynchronous tasks are cancelled
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // end of wait
    cancelWaiting();
}
  • riga 12: annulla un abbonamento e quindi l'osservazione del processo associato;

1.17.9. Esecuzione

Avvia il servizio web (sezione 1.16.1.7), avvia il client Android e ripeti i test che hai eseguito con l'esempio precedente (sezione 1.16.2.8).

1.17.10. Gestione della cancellazione

Ripetiamo gli stessi test dell'esempio precedente (sezione 1.16.2.9).

Test 1

Richiediamo 5 numeri anche se il server non è stato avviato. Otteniamo i seguenti log:

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

Dopo la riga 7 non ci sono più log, il che dimostra che l'osservatore (Vue1Fragment) non riceve più notifiche dal processo osservato.

Test 2

Ora avviamo il server e richiediamo 5 numeri con un ritardo di 5 secondi, quindi facciamo clic su [Annulla] prima che il ritardo finisca. I log sono i seguenti:

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

Dopo la riga 6 non ci sono più log, il che dimostra che l'osservatore (Vue1Fragment) non riceve più notifiche dal processo osservato.

Questo è il comportamento previsto di una cancellazione. Possiamo quindi rimuovere il booleano [hasBeenCanceled] dal codice [Vue1Fragment] che abbiamo introdotto nell'esempio precedente, poiché la cancellazione non si comportava come previsto.

Il fatto che l'osservatore non riceva più notifiche dopo la cancellazione dell'osservabile non significa che le richieste HTTP stesse siano state cancellate. Per verificarlo, modifichiamo la classe [Dao] come segue:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        Response<Integer> response= webClient.getAlea(a, b);
        if (isDebugEnabled) {
          try {
            Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
          } catch (JsonProcessingException e) {
            Log.d(String.format("%s", className),"erreur désérialisation jSON");
          }
        }
        return response;
      }
    });
}
  • righe 15–21: registriamo il risultato della richiesta HTTP della riga 14;

I log per il test n. 2 sono i seguenti:

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}]
  • righe 1-5: sono state effettuate le 5 richieste;
  • riga 6: l'utente ha annullato;
  • righe 7-11: abbiamo ricevuto con successo le risposte alle cinque richieste HTTP. Tuttavia, poiché l'osservabile è stato annullato, questi elementi non vengono passati all'osservatore;

1.17.11. Conclusione

Nel resto di questo documento, le applicazioni client/server saranno implementate utilizzando la libreria RxAndroid anziché la libreria AA per i seguenti motivi:

  1. RxAndroid può essere utilizzato in un'applicazione Android che non utilizza AA;
  2. RxAndroid non si limita a facilitare le operazioni asincrone. Offre numerosi metodi per creare un nuovo osservabile da un altro. Questi metodi non hanno un equivalente in AA;
  3. Non appena si tenta di derivare una classe annotata da AA, come un frammento, sorgono gravi problemi. Si è quindi costretti ad abbandonare AA e utilizzare la Soluzione 1 per la programmazione asincrona;

I lettori interessati ad approfondire le funzionalità della libreria RxAndroid possono consultare il documento [Introduzione a RxJava. Applicazione agli ambienti Swing e Android]. Il documento illustra l'uso di RxAndroid senza la libreria AA.

1.18. Esempio 17: Componenti per l'inserimento dati

Creeremo un nuovo progetto per illustrare alcuni componenti comunemente utilizzati nei moduli di immissione dati.

1.18.1. Creazione del progetto

Duplichiamo il progetto [Esempio-13] in [Esempio-17]:

Il nuovo progetto avrà una sola vista [view1.xml]. Pertanto, elimineremo la vista [view2.xml] e il relativo frammento [View2Fragment] [2]. Rifletteremo questa modifica nel gestore dei frammenti di [MainActivity]:


  // our fragment manager to be redefined for each application
  // must define the following methods: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Riapri il progetto. Dovrebbe visualizzare la vista n. 1 come prima. Lavoreremo partendo da questo progetto.

1.18.2. La vista XML del modulo

  

La vista generata dal file [vue1.xml] è la seguente:

Image

Il testo XML della vista è il seguente:


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

I componenti principali del modulo sono i seguenti:

  • Riga 2: un layout verticale [ScrollView]. Consente di
  • visualizzare un modulo più grande dello schermo del tablet
  • . È possibile visualizzare l'intero modulo
  • scorrendo;
 
  • righe 125–132: una casella di controllo
  • righe 134–159: un gruppo di tre pulsanti di opzione
  • righe 161–166: una barra di ricerca
  • righe 16–176: un campo di immissione testo
  • righe 178–186: un interruttore sì/no
  • righe 188–195: un campo di immissione dell'ora
  • righe 197–207: una casella di testo multilinea
  • righe 209–215: un elenco a discesa
  • righe 217-225: un campo di immissione data
  • Tutti gli altri componenti sono [TextView] che visualizzano del testo.
 

1.18.3. Le stringhe del modulo

Le stringhe del modulo sono definite nel seguente file [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>
  <!-- vue 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. Il frammento del modulo

  

La classe [View1Fragment] è la seguente:


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;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // the fields of the view displayed by the fragment
  @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;
 
  // drop-down list
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;
 
  @AfterViews
  void afterViews() {
    // check the first button
    radioButton1.setChecked(true);
    // the calendar
    datePicker1.setCalendarViewShown(false);
    // on seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
 
      public void onStopTrackingTouch(SeekBar seekBar) {
      }
 
      public void onStartTrackingTouch(SeekBar seekBar) {
      }
 
      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // the drop-down list
    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() {
    // initialize drop-down list adapter
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }
}
  • righe 22–49: recuperiamo i riferimenti per tutti i componenti del modulo XML [view1] (riga 18);
  • riga 58: il metodo [setChecked] consente di selezionare un pulsante di opzione o una casella di controllo;
  • riga 60: per impostazione predefinita, il componente [DatePicker] visualizza sia un campo di immissione della data che un calendario. La riga 60 rimuove il calendario;
  • riga 62: [SeekBar].setMax() imposta il valore massimo del cursore. Il valore minimo è 0;
  • righe 63–74: gestiamo gli eventi della barra di scorrimento. Per ogni modifica apportata dall'utente, vogliamo visualizzare il valore del cursore nel [TextView] alla riga 49;
  • riga 71: il parametro [progress] rappresenta il valore del cursore;
  • righe 76–79: un elenco di [String] che saranno associate all'elenco a discesa;
  • riga 90: il metodo [updateFragment] del frammento. Quando viene eseguito, la variabile [activity] della classe padre è stata inizializzata;
  • riga 92: l'origine dati [list] è associata all'adattatore dell'elenco a discesa;
  • righe 93–94: il [dataAdapter] è associato all'elenco a discesa [dropDownList];
  • riga 84: il metodo [doValider] è associato al clic sul pulsante [Valider];

Lo scopo del metodo [doValider] è quello di visualizzare i valori inseriti dall'utente. Il suo codice è il seguente:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    // checkbox
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // radio buttons
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // on SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // the input field
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // the switch
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // the date
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // multi-line text
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // by the hour
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // drop-down list
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // display
    doAfficher(messages);
}
  • riga 4: i valori inseriti verranno aggiunti a un elenco di messaggi;
  • riga 6: il metodo [CheckBox].isChecked() determina se una casella di controllo è selezionata o meno;
  • riga 9: il metodo [RadioGroup].getCheckedButtonId() restituisce l'ID del pulsante di opzione selezionato o -1 se nessuno è selezionato;
  • riga 10: il codice [activity.findViewById(id)] recupera il pulsante di opzione selezionato e quindi la sua etichetta;
  • riga 13: il metodo [SeekBar].getProgress() restituisce il valore di un cursore;
  • riga 19: il metodo [Switch].isChecked() determina se un interruttore è On (vero) o Off (falso);
  • riga 22: il metodo [DatePicker].getYear() recupera l'anno selezionato utilizzando un oggetto [DatePicker];
  • riga 23: il metodo [DatePicker].getMonth() restituisce il mese selezionato da un oggetto [DatePicker] nell'intervallo [0,11];
  • riga 24: il metodo [DatePicker].getDayOfMonth() restituisce il giorno del mese selezionato utilizzando un oggetto [DatePicker] nell'intervallo [1,31];
  • riga 30: il metodo [TimePicker].getHour() restituisce l'ora selezionata utilizzando un oggetto [TimePicker];
  • riga 31: il metodo [TimePicker].getMinute() restituisce i minuti selezionati utilizzando un oggetto [TimePicker];
  • riga 34: il metodo [Spinner].getSelectedItemPosition() restituisce la posizione dell'elemento selezionato in un elenco a discesa;
  • riga 35: il metodo [Spinner].getSelectedItem() restituisce l'elemento selezionato in un elenco a discesa;

Il metodo [doAfficher], che visualizza l'elenco dei valori inseriti, è il seguente:


    private void doAfficher(List<String> messages) {
        // we build the poster text
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // we display it
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • riga 1: il metodo riceve un elenco di messaggi da visualizzare;
  • righe 3–6: da questi messaggi viene costruito un oggetto [StringBuilder]. Per la concatenazione di stringhe, il tipo [StringBuilder] è più efficiente del tipo [String];
  • riga 8: una finestra di dialogo visualizza il testo della riga 3:

Image

1.18.5. Esecuzione del progetto

Esegui il progetto e prova i vari componenti di input.

1.19. Esempio 18: Utilizzo di un modello di vista

1.19.1. Creazione del progetto

Creiamo un nuovo progetto [Esempio-18] copiando il progetto [Esempio-13].

1.19.2. Il modello di vista

Vogliamo riutilizzare le due viste del progetto e includerle in un modello:

  

Image

Ciascuna delle due visualizzazioni avrà la stessa struttura:

  • in [1], un'intestazione;
  • in [2], una colonna a sinistra che potrebbe contenere dei link;
  • in [3], un piè di pagina;
  • in [4], il contenuto.

Ciò si ottiene modificando la vista di base dell'attività [activity_main.xml];

Il codice XML per la vista [main] è il seguente:


<?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>
  • L'intestazione [1] è generata dalle righe 38–54;
  • il pannello di sinistra [2] è generato dalle righe 56–84;
  • il piè di pagina [3] è creato dalle righe 86–101;
  • il contenuto [4] è generato dalle righe 78–84;

La vista XML [main] utilizza le informazioni presenti nei file [res/values/colors.xml] e [res/values/strings.xml]:

  

Il file [colors.xml] è il seguente:


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

e il seguente file [strings.xml]:


<?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 contesto di esecuzione per questo progetto ed eseguilo.

1.20. Esempio 19: Il componente [ListView]

Il componente [ListView] consente di ripetere una vista specifica per ogni elemento di un elenco. La vista ripetuta può essere di qualsiasi complessità, da una semplice stringa a una vista che consente di inserire informazioni per ogni elemento dell'elenco. Creeremo la seguente [ListView]:

Image

Ogni vista nell'elenco ha tre componenti:

  • un [TextView] per le informazioni;
  • una [CheckBox];
  • un [TextView] cliccabile;

1.20.1. Creazione del progetto

Creiamo un nuovo progetto [Esempio-19] clonando il progetto [Esempio-18].

  

Svilupperemo il progetto come descritto in [3].

1.20.2. La sessione

  

La sessione memorizza i dati condivisi tra l'attività e i frammenti:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // a list of data
  private List<Data> liste=new ArrayList<>();
 
  // getters and setters
...
}
  • riga 11: l'elenco di dati utilizzato da entrambe le viste;

La classe [Data] è la seguente:


package exemples.android.architecture;
 
public class Data {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
 
    // getters and setters
    ...
}
  • riga 6: il testo che popolerà il primo [TextView] di ogni voce dell'elenco;
  • riga 7: il valore booleano che verrà utilizzato per selezionare o deselezionare la [checkBox] per ogni voce dell'elenco;

1.20.3. La [MainActivity]

Il codice per il metodo [@AfterInject] diventa il seguente:


  // injection session
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<Data> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new Data("Texte n° " + i, false));
    }
}
  • righe 12–15: inizializzazione dell'elenco dei dati presenti nella sessione;

1.20.4. La vista iniziale [View1]

La vista XML [view1.xml] visualizza l'area [1] sopra riportata. Il suo codice è il seguente:


<?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>
  • righe 7–16: il componente [TextView] [2];
  • righe 27–35: il componente [ListView] [4];
  • righe 18–25: il componente [Button] [3];

1.20.5. La vista ripetuta dal [ListView]

La vista ripetuta dal [ListView] è la seguente 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>
  • righe 8–14: il componente [TextView] [1];
  • righe 16–23: il componente [CheckBox] [2];
  • righe 25-35: il componente [TextView] [3];

1.20.6. Il frammento [Vue1Fragment]

  

Il frammento [Vue1Fragment] gestisce la vista XML [vue1]. Il suo codice è il seguente:


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 {
 
  // the fields of the view displayed by the fragment
  @ViewById(R.id.listView1)
  protected ListView listView;
  // list adapter
  private ListAdapter adapter;
  // init done
  private boolean initDone = false;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone = true;
  }
 
  @Click(R.id.button_vue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  public void doRetirer(int position) {
   ...
  }
 
  @Override
  protected void updateFragment() {
    if (!initDone) {
      // associate data with [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // if the fragment has been (re)generated - in this case the ListView must be reconnected to its adapter
    listView.setAdapter(adapter);
    // if other fragments have changed the data source - in this case, refresh the ListView
    adapter.notifyDataSetChanged();
  }
}
  • riga 15: la vista XML [view1] è associata al frammento;
  • righe 26–30: il metodo [@AfterViews] non fa nulla. Tuttavia, è necessario impostare la variabile [afterViewsDone] su true perché viene utilizzata dalla classe padre [AbstractFragment];
  • righe 42–53: il metodo [updateFragment], che viene chiamato ogni volta che il frammento diventa visibile. Il metodo è stato scritto qui come se il frammento potesse uscire dall’adiacenza del frammento visualizzato e quindi azzerare il proprio ciclo di vita. In questo caso non è così, ma lo sarebbe se l’applicazione avesse 3 frammenti con un’adiacenza pari a 1;
  • riga 44: l'adattatore [ListView] deve essere inizializzato una sola volta;
  • riga 46: associamo un [ListAdapter] a questo [ListView]. Creeremo questa classe. Deriva dalla classe [ArrayAdapter], che abbiamo già utilizzato per associare dati a un [ListView]. Passiamo varie informazioni al costruttore [ListAdapter]:
    • un riferimento all'attività corrente,
    • l'identificatore della vista che verrà istanziata per ogni elemento dell'elenco,
    • una fonte di dati per popolare l'elenco,
    • un riferimento al frammento. Questo verrà utilizzato per gestire un clic su un link [Rimuovi] nel [ListView] tramite il metodo [doRemove] alla riga 38;
  • Riga 50: L'adattatore viene associato alla [ListView]. Allo stesso tempo, l'origine dati [lists] viene associata alla [ListView]. Questa operazione viene eseguita qui ogni volta che viene visualizzata la vista n. 1. In realtà, deve essere eseguita solo una volta che il metodo [@AfterViews] è stato eseguito. Qui, l'istruzione viene eseguita troppo spesso. Abbiamo bisogno di una variabile booleana che ci indichi che il metodo [@AfterViews] è stato appena eseguito e che il [ListView] deve quindi essere ricollegato al suo adattatore;
  • Riga 52: Aggiorniamo la [ListView]. In questo esempio, ciò non ha alcuna utilità poiché solo la vista n. 1 può modificare l'origine dati della [ListView]. Consideriamo un caso più generale in cui anche la vista n. 2 potrebbe modificare l'origine dati della [ListView]. Incontreremo esempi di questo tipo più avanti in questo documento. In questo caso, quando si passa dalla vista n. 2 alla vista n. 1, la [ListView] nella vista n. 1 deve essere aggiornata;

1.20.7. Il [ListAdapter] della [ListView]

La classe [ListAdapter]

  • configura l'origine dati della [ListView];
  • gestisce la visualizzazione dei vari elementi nella [ListView];
  • gestisce gli eventi di questi elementi;

Il codice è il seguente:


package exemples.android.fragments;
 
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
 
    // execution context
    private Context context;
    // the id of the layout displaying a line in the list
    private int layoutResourceId;
    // list data
    private List<Data> data;
    // the fragment that displays the [ListView]
    private Vue1Fragment fragment;
    // the adapter
    final ListAdapter adapter = this;
 
    // manufacturer
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // memorize information
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.data = data;
        this.fragment = fragment;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • riga 5: la classe [ListAdapter] estende la classe [ArrayAdapter];
  • riga 19: il costruttore;
  • riga 20: non dimenticare di chiamare il costruttore della classe padre [ArrayAdapter] con i primi tre parametri;
  • righe 22–25: memorizziamo le informazioni del costruttore;
  • riga 29: il metodo [getView] verrà chiamato ripetutamente da [ListView] per generare la vista per l'elemento n. [position]. Il risultato [View] restituito è un riferimento alla vista creata.

Il codice per il metodo [getView] è il seguente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // create the current ListView line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the text
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // the checkbox
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // the [Remove] link
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {
 
            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // manage the click on the checkbox
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // we return the line
        return row;
}
  • Riga 2: il metodo accetta tre parametri. Ne useremo solo il primo;
  • riga 4: creiamo la vista per l'elemento #[position]. Si tratta della vista [list_data] il cui ID è stato passato come secondo parametro al costruttore. Quindi recuperiamo i riferimenti ai componenti della vista che abbiamo appena istanziato;
  • riga 6: recuperiamo il riferimento a [TextView] n. 1;
  • riga 7: gli assegniamo il testo proveniente dalla fonte dati che è stata passata come terzo parametro al costruttore;
  • riga 9: recuperiamo il riferimento a [CheckBox] #2;
  • riga 10: lo selezioniamo o deselezioniamo utilizzando un valore proveniente dalla fonte dati della [ListView];
  • riga 12: recuperiamo il riferimento a [TextView] n. 3;
  • righe 13–18: gestiamo il clic sul link [Rimuovi];
  • riga 16: il metodo [Vue1Fragment].doRetirer gestisce questo clic. Ha più senso che sia il frammento che visualizza la [ListView] a gestire questo evento. Esso ha una visione d'insieme che la classe [ListAdapter] non possiede. Il riferimento al frammento [Vue1Fragment] è stato passato come quarto parametro al costruttore della classe;
  • Righe 20–25: Gestiscono il clic sulla casella di controllo. L'azione eseguita su di essa si riflette nei dati che visualizza. Questo per il seguente motivo: [ListView] è un elenco che mostra solo una parte dei suoi elementi. Pertanto, un elemento dell'elenco a volte è nascosto e a volte visualizzato. Quando l'elemento #i deve essere visualizzato, viene chiamato il metodo [getView] della riga 2 sopra per la posizione #i. La riga 10 ricalcolerà lo stato della casella di controllo in base ai dati a cui è collegata. Pertanto, deve memorizzare lo stato della casella di controllo nel tempo;

1.20.8. Rimozione di un elemento dall'elenco

Il clic sul link [Rimuovi] viene gestito nel frammento [Vue1Fragment] dal seguente metodo [doRetirer]:


  public void doRetirer(int position) {
    // remove element n° [position] from the list
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // note the scroll position to return to it
    // read
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    int firstPosition = listView.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // refresh the [ListView]
    adapter.notifyDataSetChanged();
    // we position ourselves at the right spot on the ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • riga 1: Otteniamo la posizione nella [ListView] del link [Rimuovi] su cui è stato cliccato;
  • riga 3: recupera l'elenco dei dati;
  • riga 4: rimuovi l'elemento nella posizione [position];
  • riga 15: aggiorniamo la [ListView]. Senza questo, nulla cambia visivamente.
  • Righe 5–13, 17: un processo piuttosto complesso. Senza di esso, accade quanto segue:
    • la [ListView] visualizza le righe 15–18 dell'elenco dei dati,
    • la riga 16 viene eliminata,
    • la riga 15 sopra la resetta completamente, e la [ListView] visualizza quindi le righe 0–3 dell'elenco dei dati;

Con le righe precedenti, l'eliminazione avviene e la [ListView] rimane posizionata sulla riga successiva a quella eliminata.

1.20.9. La vista XML [View2]

Il codice XML per la vista è il seguente:


<?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>
  • righe 6–15: componente [TextView] n. 1;
  • righe 26–33: componente [TextView] n. 2;
  • righe 17-24: componente [Button] n. 3;

1.20.10. Il frammento [Vue2Fragment]

123

Il frammento [Vue2Fragment] gestisce la vista XML [vue2]. Il suo codice è il seguente:


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 {
 
    // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
    @AfterViews
    void initFragment(){
        // memory
        afterViewsDone=true;
    }
 
  @Click(R.id.button_vue1)
    void navigateToView1() {
        // navigate to view 1
        mainActivity.navigateToView(0);
    }
 
    @Override
    protected void updateFragment() {
        // displays list items selected in view 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);
    }
}

Il codice importante si trova nel metodo [updateFragment] alla riga 32:

  • riga 34: calcoliamo il testo da visualizzare nel [TextView] n. 2;
  • righe 35–39: percorriamo l'elenco dei dati visualizzati da [ListView]. È memorizzato nell'attività;
  • riga 36: se l'elemento di dati i è stato selezionato, l'etichetta associata viene aggiunta a uno [StringBuilder];
  • riga 41: il [TextView] visualizza il testo calcolato;

1.20.11. Esecuzione

Creare una configurazione di esecuzione per questo progetto ed eseguirlo.

1.20.12. Miglioramento

Nell'esempio precedente, abbiamo utilizzato un'origine dati List<Data> in cui la classe [Data] era la seguente:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...
 
}

Nella riga 7 abbiamo utilizzato una variabile booleana per gestire le caselle di controllo degli elementi nella [ListView]. Spesso la [ListView] deve visualizzare dati che possono essere selezionati spuntando una casella, anche se l'elemento nell'origine dati non dispone di un campo booleano corrispondente a quella casella. In tal caso, è possibile procedere come segue:

La classe [Data] diventa la seguente:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String texte;
 
    // manufacturer
    public Data(String texte) {
        this.texte = texte;
    }
 
    // getters and setters
...
}

Creiamo una classe [CheckedData] derivata dalla precedente:


package exemples.android.fragments;
 
public class CheckedData extends Data {
 
    // checked item
    private boolean isChecked;
 
    // manufacturer
    public CheckedData(String text, boolean isChecked) {
        // parent
        super(text);
        // local
        this.isChecked = isChecked;
    }
 
    // getters and setters
...
}

Quindi basta sostituire il tipo [Data] con il tipo [CheckedData] in tutto il codice (MainActivity, ListAdapter, View1Fragment, View2Fragment). Ad esempio, in [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

Il progetto per questa versione è disponibile con il nome [Example-19B].

1.21. Esempio-20: Utilizzo di un menu

1.21.1. Creazione del progetto

Duplichiamo il progetto [Esempio-19B] nel progetto [Esempio-20]:

3

Rimuoveremo i pulsanti dalle viste 1 e 2 e li sostituiremo con le opzioni di menu [1-2].

1.21.2. La definizione XML dei menu

  

Il file [res/menu/menu_vue1] definisce il menu per 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>

Le voci di menu sono definite dalle seguenti informazioni:

  • android:id: l'identificatore dell'elemento;
  • android:title: l'etichetta della voce;
  • app:showsAsAction: indica se la voce di menu può essere inserita nella barra delle azioni dell'attività. [ifRoom] indica che la voce deve essere inserita nella barra delle azioni se c'è spazio a sufficienza;
  • un'opzione di menu può essere essa stessa un sottomenu (il tag <menu>, righe 25, 29);

Il file [res / menu / menu_vue2] definisce il menu per 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. Gestione dei menu nella classe astratta [AbstractFragment]

Estrarremo la gestione del menu nella classe padre [AbstractFragment] delle due viste:


package exemples.android.architecture;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
 
import java.util.ArrayList;
import java.util.List;
 
public abstract class AbstractFragment extends Fragment {
 
  // data accessible to daughter classes
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
  // session
  protected Session session;
 
  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;
 
  // manufacturer
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // retrieve # menu options if not already done
    if (!initDone) {
      // retrieve the # menu options
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // transfer the list of options to a table
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memory
      initDone = true;
    }
 
    // the girl fragment is asked to stand
    updateFragment();
  }
 
 
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }
 
  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }
 
  // update girl class
  protected abstract void updateFragment();
}
  • riga 42: i log mostrano che il metodo [onCreateOptionsMenu] viene chiamato ogni volta che il frammento viene visualizzato. Viene chiamato molto tardi, in particolare dopo che è stato chiamato il metodo [updateFragment]. Ciò suggerisce che potrebbe essere utilizzato per aggiornare il frammento. È quello che faremo qui (riga 63);
  • riga 42: il metodo ha due parametri:
    • [menu]: che è un menu vuoto;
    • [inflater]: uno strumento che ci permette di creare il menu dalla sua descrizione iniziale. Qui non useremo questa opzione perché useremo un'annotazione AA che lo farà per noi;
  • riga 44: memorizziamo il menu. Ne avremo bisogno in seguito;
  • righe 52–53: memorizziamo gli ID di tutte le voci del menu nell'array della riga 28;
  • righe 55–57: i log mostrano che quando viene chiamato il metodo [onCreateOptionsMenu], il metodo [Fragment.getActivity()] restituisce l'attività associata al frammento;
  • riga 55: memorizziamo l'attività come istanza della classe Android [Activity];
  • riga 56: memorizziamo l'attività come istanza dell'interfaccia [IMainActivity];
  • riga 57: memorizziamo la sessione;
  • riga 59: notiamo che la classe è già stata inizializzata, quindi non dobbiamo farlo di nuovo (riga 50);
  • riga 63: chiediamo al frammento figlio di aggiornarsi. Ciò è possibile perché il frammento è sia visibile che associato alla sua vista e al suo menu;

Il metodo [getMenuOptions], che recupera gli ID delle voci di menu, è il seguente:


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // scroll through all menu items
    for (int i = 0; i < menu.size(); i++) {
      // item n° i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // if item n° i is a sub-menu, then start again
      if (menuItem.hasSubMenu()) {
        // recursivity
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
}

Il metodo [setAllMenuOptions] consente di nascondere o mostrare tutte le opzioni del menu;


  protected void setAllMenuOptions(boolean isVisible) {
    // update all menu options
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

Il metodo [setMenuOptions] consente di nascondere o mostrare determinate opzioni del menu;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

La classe [MenuItemState] è la seguente:

  

package exemples.android.architecture;
 
public class MenuItemState {
 
  // menu option identify
  private int menuItemId;
  // option visibility
  private boolean isVisible;
 
  // manufacturers
  public MenuItemState() {
 
  }
 
  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }
 
  // getters and setters
...
}

1.21.4. Gestione del menu nel frammento [View1Fragment]

La classe [Vue1Fragment] diventa la seguente:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
 
...
 
  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  @OptionsItem(R.id.actionValider)
  void valider() {
    // a message is displayed
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }
 
  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // we change state
    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() {
    // we change state
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }
 
  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // we change state
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...
 
  @Override
  protected void updateFragment() {
    ....
    // update the menu
    //setMenuOptions(...)
  }
}
  • riga 2: il menu [res/menu/menu_vue1.xml] è associato al frammento;
  • riga 48: quando viene eseguito il metodo [updateFragment], anche il menu può essere aggiornato per riflettere il nuovo stato del frammento;
  • riga 7: l'annotazione [@OptionsItem(R.id.navigationVue2)] annota il metodo che deve essere eseguito quando si fa clic sull'opzione di menu [Navigazione / Vista 2];
  • righe 19–25: per nascondere un ramo del menu, è sufficiente nascondere la sua opzione radice;
  • riga 24: le opzioni radice [menuNavigation, menuActions] vengono mostrate o nascoste;
  • riga 40: per mostrare un'opzione in un ramo del menu, non devi solo mostrare quell'opzione, ma anche tutte le opzioni incontrate quando si passa dall'opzione foglia alla radice del menu;

1.21.5. Gestione del menu nel frammento [Vue2Fragment]

È possibile trovare un codice simile nel frammento della Vista 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 {
 
  // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // navigate to view 1
    mainActivity.navigateToView(0);
  }
 
  @Override
  protected void updateFragment() {
    // displays list items selected in view 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);
    // update the menu
    // setMenuOptions(...)
  }
}
  • riga 35: visualizza l'opzione [Navigazione / Vista 1];
  • righe 17-20: quando si fa clic sull'opzione [Navigazione / Vista 1], viene chiamato il metodo [navigateToView1];

1.21.6. Esecuzione

Creare un contesto di runtime per questo progetto ed eseguirlo.

1.22. Esempio-21: Rifattorizzazione della classe [AbstractFragment]

L'esempio precedente ci ha mostrato che quando il frammento ha un menu, il suo metodo [onCreateOptionsMenu] è un buon punto in cui chiedere al frammento di aggiornarsi:

  • viene chiamato esattamente una volta quando il frammento sta per essere visualizzato;
  • quando viene chiamato, vengono stabilite le associazioni del frammento con la sua attività, la sua vista e il suo menu;

Per dimostrarlo, torneremo all'Esempio 12, che presenta molti frammenti la cui adiacenza può essere modificata. In quell'esempio, i frammenti non avevano un menu. Assoceremo loro un menu vuoto.

1.22.1. Creazione del progetto

Duplichiamo il progetto [Esempio-12] nel progetto [Esempio-21]:

1.22.2. Il menu dei frammenti

  

Il menu aggiunto per i frammenti sarà vuoto:


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

Ciò che è necessario comprendere in questo caso è che l'attività dispone già di un proprio menu [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>

Quando un'attività dispone già di un menu, il menu associato ai frammenti viene aggiunto al menu dell'attività: si hanno quindi a disposizione le opzioni di entrambi i menu. In questo caso, il menu dei frammenti sarà vuoto. Verrà quindi visualizzato solo il menu dell'attività.

1.22.3. I frammenti

  

Riutilizziamo la classe astratta [AbstractFragment] dell'esempio precedente (vedere la sezione 1.21.3). Associamo il menu [menu_fragment] ai due frammenti:


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

In entrambi i frammenti [PlaceholderFragment] e [Vue1Fragment], rimuoviamo ogni riferimento alla vecchia classe astratta [AbstractFragment].

1.22.4. Esecuzione

Esegui l'app e verifica che funzioni. Controlla i log per vedere quando viene eseguito il metodo [onCreateOptionsMenu] della classe [AbstractFragment]. Ora è questo metodo che chiama il metodo [updateFragment] dei frammenti figli.

1.23. Esempio 22: Salvataggio/ripristino dello stato dell'attività e dei frammenti

1.23.1. Il problema

Qui affrontiamo il problema della rotazione del dispositivo Android (verticale <--> orizzontale). Per illustrarlo, riprendiamo il precedente Esempio 21:

Image

Se ruotiamo il dispositivo [1], otteniamo la seguente nuova visualizzazione:

Image

Possiamo notare che:

  • in [1], la scheda [Fragment #3] è scomparsa;
  • in [2], il testo visualizzato è effettivamente quello del Frammento n. 3, ma il contatore delle visite è errato;

Durante questa rotazione, i log sono i seguenti:

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
  • riga 1: possiamo vedere che l'attività è stata completamente ricostruita;
  • righe 3–7: lo stesso vale per i cinque frammenti gestiti dall'attività;
  • riga 21: il frammento n. 3 sta per essere visualizzato. Vediamo che prima dell'incremento il conteggio delle visite è 0;

Possiamo quindi spiegare il risultato ottenuto dopo la rotazione come segue:

  • la classe [MainActivity] crea inizialmente una barra delle schede con una singola scheda denominata [View 1]. Questa è la scheda visibile;
  • Dopo la rotazione del dispositivo, il gestore di pagine [mViewPager] visualizza nuovamente lo stesso frammento, che in questo caso è il frammento n. 3. È importante ricordare che le schede e i frammenti sono concetti diversi e hanno cicli di vita diversi. Verrà eseguito il metodo [updateFragment] del frammento n. 3:

  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • Riga 7: L'ID dell'ultima visita viene letto dalla sessione. Tuttavia, la sessione — come tutto il resto — è stata resettata e l'ID della visita è stato azzerato. Questo spiega il risultato visualizzato nel frammento n. 3;

1.23.2. Metodi per salvare/ripristinare l'attività e i frammenti

1.23.2.1. Soluzione 1: backup manuale

Quando il dispositivo ruota, vengono chiamati due metodi dell'attività:


// backup / restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // backup activity status
    // ....
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
     // restoring activity
    // ...
  }
  • Righe 2–8: Il metodo [onSaveInstanceState] viene chiamato dal sistema durante la rotazione. È qui che l'attività può essere salvata. Se non viene fatto nulla, non viene salvato nulla. Lo stato dell'attività deve essere salvato nel parametro [Bundle outState] passato al metodo. La classe [Bundle] assomiglia a un dizionario. Dispone dei metodi [putString, putInt, putLong, putBoolean, putChar, ...] con due parametri: void putT(String key, T value);
  • Righe 10–16: Il metodo [onCreate] viene chiamato quando viene creata l'attività. Se lo stato dell'attività è stato salvato, questo stato salvato le viene passato nel parametro [Bundle savedInstanceState]. Per recuperare i valori salvati, sono disponibili metodi come [getString, getInt, getLong, getBoolean, getChar, ...] con un unico parametro: T getT(String key);

I frammenti dispongono di questi stessi due metodi per salvare il proprio stato.

Useremo queste informazioni per salvare e ripristinare lo stato dell’Esempio 21. Per farlo, duplichiamo il progetto [Example-21] in [Example-22].

1.23.2.2. Soluzione 2: salvataggio automatico

La documentazione di Android indica che, quando il dispositivo viene ruotato, è possibile impedire la distruzione di un frammento utilizzando l'istruzione: [Fragment].setRetainInstance(true). Diversi articoli su [StackOverflow] raccomandano di utilizzare questa istruzione solo per i frammenti privi di interfaccia visiva [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]. Ho testato questa affermazione su due esempi: Esempio-17 (Sezione 1.18 — un'applicazione a frammento singolo che visualizza un modulo) ed Esempio-21 (Sezione 1.22 — un'applicazione a cinque frammenti). In entrambi i casi, l'applicazione di questa singola istruzione a tutti i frammenti dell'applicazione si è rivelata insufficiente per ripristinare correttamente la vista visualizzata quando il dispositivo veniva ruotato. Piuttosto che creare due modelli, uno basato su [setRetainInstance(true)] e un altro basato su [setRetainInstance(false)] — che è il valore predefinito — ho deciso di seguire le raccomandazioni di [StackOverflow] e mantenere il valore predefinito false per il metodo [setRetainInstance(boolean)]. L'istruzione: [Fragment].setRetainInstance(true) non è mai stata utilizzata nel resto di questo documento.

1.23.3. Il metodo di backup/ripristino per il progetto [Esempio-22]

Il progetto [Example-22] si evolve come segue:

  

Appaiono due nuove classi:

  • [PlaceHolderFragmentState], che memorizzerà lo stato di un frammento di tipo [PlaceHolderFragment];
  • [Vue1FragmentState], che memorizzerà lo stato di un frammento di tipo [Vue1Fragment];

Queste classi sono le seguenti:


package exemples.android;
 
public class Vue1FragmentState {
  // status Vue1Fragment
  private boolean hasBeenVisited=false;
  // getters and setters
...
}
  • riga 5: il valore booleano [hasBeenVisited] è vero se il frammento [Vue1Fragment] è stato visitato (visualizzato) almeno una volta. Questo campo è stato creato per l'esempio perché il frammento [Vue1Fragment] non ha nulla da salvare;

La classe [PlaceHolderFragmentState] è la seguente:


package exemples.android;
 
public class PlaceHolderFragmentState {
  // whether visited or not
  private boolean hasBeenVisited;
  // display text
  private String text;
 
  // getters and setters
...
}
  • riga 5: vediamo il valore booleano [hasBeenVisited];
  • riga 7: il testo visualizzato dal frammento nel momento in cui deve essere salvato. Abbiamo visto che questo testo andava perso durante la rotazione;

Lo stato dei frammenti verrà memorizzato nella sessione, e l'attività sarà responsabile del salvataggio e del ripristino di questa sessione. La sessione si evolve come segue:


package exemples.android;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in the second tab
  private int numFragment = -1;
  // selected tab no
  private int selectedTab = 0;
  // n° current view
  private int currentView;
 
  // fragment backups ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
 
  // manufacturer
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // getters and setters
...
}
  • riga 18: lo stato del frammento [Vue1Fragment];
  • riga 19: lo stato dei frammenti di tipo [PlaceHolderFragment];
  • righe 22–27: nel costruttore della sessione, vengono inizializzati i campi delle righe 18 e 19;
  • righe 12–15: compaiono due nuovi campi:
    • riga 13: il numero dell'ultima scheda selezionata;
    • riga 15: il numero dell'ultimo frammento visualizzato;

L'attività salva/ripristina la sessione come segue:


  // backup / restore management ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // log
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // session recovery
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // log
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    }
}
  • riga 8: la sessione viene salvata come stringa JSON;
  • riga 29: ripristina la sessione dalla stringa JSON;

Per gestire il salvataggio e il ripristino dei frammenti, la classe astratta [AbstractFragment] si evolve come segue:


// backup / restore management -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // the fragment will be hidden - save it
      saveFragment();
      saveFragmentDone = true;
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the fragment must be restored
    fragmentHasToBeInitialized = true;
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // parent
    super.onSaveInstanceState(outState);
    // save fragment only if visible
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }
 
  // girls' classes
  protected abstract void updateFragment();
 
  protected abstract void saveFragment();
  • Decidiamo di salvare lo stato dei frammenti nella sessione in due momenti:
    • righe 2–14: quando il frammento passa da visibile a nascosto;
    • righe 29–42: quando il sistema indica che il frammento deve essere salvato e il frammento è visibile (riga 38);

Questo meccanismo evita di salvare più spesso del necessario. Infatti, poiché abbiamo salvato lo stato del frammento i quando è passato da visibile a nascosto, quando il frammento j viene visualizzato e si verifica una rotazione, non è necessario salvare nuovamente il frammento i. Se non è stato visualizzato nuovamente dall'ultimo salvataggio, allora il suo stato non è cambiato. È necessario salvare solo lo stato del frammento j. Questo meccanismo presenta anche un altro vantaggio: non è solo durante la rotazione del dispositivo che dobbiamo salvare lo stato di un frammento. Esiste anche il caso della pura navigazione tra i frammenti, ad esempio in un sistema a schede. In tali casi, vogliamo recuperare un frammento nello stato in cui si trovava quando è stato visualizzato l'ultima volta. Questo stato potrebbe essere parzialmente scomparso se il frammento è stato in qualche momento rimosso dalla vicinanza dei frammenti visualizzati. Il frammento non viene quindi ricostruito completamente, ma lo è la sua vista associata. Il salvataggio eseguito quando il frammento è diventato nascosto verrà utilizzato per ripristinare l'ultimo stato di questa vista;

  • righe 10, 40: per evitare di effettuare due salvataggi consecutivi, si utilizza il booleano [saveFragmentDone] per indicare che è stato effettuato un salvataggio;
  • righe 9, 39: al frammento figlio viene chiesto di salvare il proprio stato. Il metodo [saveFragment] è astratto (riga 47). Spetta quindi alle classi figlie implementarlo;
  • righe 16–26: il metodo [onActivityCreated] viene utilizzato per impostare il booleano [fragmentHasToBeInitialized] su true. Questo perché il frammento figlio deve sapere che deve reinizializzare completamente lo stato del frammento a partire da uno stato che troverà nella sessione;

Sempre nella classe [AbstractFragment], il metodo [onCreateOptionsMenu] cambia come segue:


// fragment update
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // the girl fragment is asked to update itself
    updateFragment();
    // backup to do
    saveFragmentDone = false;
  }
  • riga 14: abbiamo visto che il valore booleano [saveFragmentDone] è stato impostato su true quando è stato eseguito un salvataggio. A un certo punto, deve essere reimpostato su false. Quando viene eseguito il metodo [updateFragment] (riga 12) del frammento figlio, questo diventa visibile. Tuttavia, è quando un frammento è visibile che deve essere salvato, in particolare nel momento in cui passa dallo stato visibile a quello nascosto. Impostiamo quindi il booleano [saveFragmentDone] su false in modo che il salvataggio possa avvenire;

1.23.4. Salvataggio del frammento [Vue1Fragment]

I frammenti vengono salvati nel metodo [saveFragment] chiamato dalla classe padre [AbstractFragment]:


// save fragment status
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // in-session saving of fragment status
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • righe 9–11: salvataggio dello stato del frammento nella sessione. Quando viene chiamato il metodo [saveFragment], il frammento è visibile. Pertanto, il valore booleano [hasBeenVisited] deve essere impostato su true (riga 10);

1.23.5. Salvataggio del frammento [PlaceHolderFragment]

I frammenti vengono salvati nel metodo [saveFragment] chiamato dalla classe padre [AbstractFragment]:


  @Override
  public void saveFragment() {
    // save fragment state in session
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • righe 4–7: salvano lo stato del frammento nella sessione;
  • riga 5: viene salvato il testo attualmente visualizzato da [TextView] textViewInfo;
  • riga 6: il valore booleano [hasBeenVisited] del frammento viene impostato su true;
  • riga 7: lo stato del frammento viene salvato nella sessione nell'array [placeHolderFragmentStates]. L'indice dell'elemento da inizializzare è il numero della sezione del frammento meno uno;

1.23.6. Ripristino del frammento [Vue1Fragment]

I frammenti vengono ripristinati nel metodo [updateFragment]:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // restoration?
    if (fragmentHasToBeInitialized) {
      // restoration condition
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // navigation?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // increment visit no
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // the visit number is displayed
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // change n° current view
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • Righe 8–12: Ripristino dello stato del frammento. Il valore booleano [fragmentHasToBeInitialized] è stato inizializzato dalla classe padre [AbstractFragment]. Quando è vero, il frammento è stato appena ricostruito e deve essere reinizializzato. È qui che ciò avviene. In questo specifico esempio, non c'è nulla da fare. Abbiamo semplicemente dimostrato che è possibile recuperare il valore del booleano [hasBeenVisited] dallo stato salvato del frammento (riga 10);
  • riga 11: non dimenticare di reimpostare [fragmentHasToBeInitialized] su false, in modo che quando torneremo a questo frammento in un secondo momento senza che il dispositivo abbia subito rotazioni, non venga eseguita un'inizializzazione non necessaria del frammento;
  • righe 18–26: incrementare il contatore delle visite. Qui c'è una sfida: quando si ripristina il frammento, non vogliamo incrementare questo contatore. Dobbiamo distinguere qui tra:
    • una semplice navigazione che riporta l'utente alla scheda [View 1];
    • un ripristino quando l'utente ruota il dispositivo mentre è visualizzata la scheda [View 1];

Distinguiamo questi due casi utilizzando il numero di vista memorizzato nella sessione. Questo numero è quello dell'ultima vista visualizzata (riga 28).

  • riga 18: si verifica una navigazione anziché un aggiornamento se il numero dell'ultima vista differisce da quello della vista corrente;
  • righe 21–25: incremento del contatore delle visite e visualizzazione dello stesso;

1.23.7. Ripristino del [PlaceHolderFragment]

I frammenti vengono ripristinati nel metodo [updateFragment]:


  // data
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...
 
public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // which fragment is it?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // does the fragment need to be initialized?
    if (fragmentHasToBeInitialized) {
      // initial text
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // navigation?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // increment visit no
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // modified text
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // we are dealing with a restoration
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // text display
    textViewInfo.setText(newText);
    // current view
    session.setCurrentView(numView);
}
  • righe 15-16: determinare il numero della vista che viene aggiornata;
  • righe 18-22: caso in cui il frammento si trovi in un ciclo di salvataggio/ripristino dopo un cambiamento di orientamento del dispositivo. Deve essere ripristinato qui. Ciò comporta generalmente il ripristino di determinati campi del frammento;
  • riga 20: il campo [text] alla riga 2 deve contenere il testo iniziale visualizzato dal frammento: [Hello world from section i]. Deve essere rigenerato qui;
  • riga 21: notare che il frammento è stato inizializzato;
  • righe 24–36: come per il frammento [Vue1Fragment] precedente, il contatore delle visite non deve essere incrementato durante un ripristino. Come prima, dobbiamo distinguere tra navigazione e ripristino;
  • righe 32–36: caso di ripristino;
  • riga 34: lo stato del frammento prima della rotazione del dispositivo viene recuperato dalla sessione;
  • riga 35: viene recuperato il testo che era visualizzato in quel momento;
  • riga 38: questo testo viene visualizzato nuovamente;
  • riga 40: il numero della nuova vista visualizzata viene annotato nella sessione;

1.23.8. Gestione delle schede

Le sezioni precedenti non hanno trattato la gestione delle schede. Tuttavia, nell'Esempio 21 abbiamo riscontrato un problema durante la rotazione del dispositivo: è stata mantenuta solo la prima scheda [Vista 1]. La seconda scheda è andata persa.

Risolviamo questo problema nella classe [MainActivity] come segue:


@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
 ...
 
    // 1st tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2nd tab ?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }
 
    // which tab to select?
    tabLayout.getTabAt(session.getSelectedTab()).select();
 
...
 
  }
  • righe 14–16: creazione della prima scheda;
  • righe 18-23: creazione della seconda scheda. Per determinare se crearla, controlliamo nella sessione il numero del frammento visualizzato nella scheda 2. Se questo numero non è -1 (il suo valore iniziale), allora viene creata la seconda scheda. A questo punto, abbiamo due schede, con la prima selezionata di default;
  • riga 26: recuperiamo dalla sessione il numero della scheda che era selezionata prima del salvataggio/ripristino e la riselezioniamo. Se il campo [selectedTab] non è stato ancora inizializzato dal codice, viene utilizzato il suo valore iniziale pari a 0;