Skip to content

1. Imparare la programmazione Android

Il PDF del documento è disponibile |QUI|.

Gli esempi tratti dal documento sono disponibili |QUI|.

1.1. Introduction

1.1.1. Contenuto

Il presente documento è una rielaborazione di diversi documenti esistenti:

  1. Android per sviluppatori J2EE;
  2. Introduzione alla programmazione dei tablet Android attraverso esempi pratici;
  3. Controllare un Arduino con un tablet Android;
  4. Introduzione alla programmazione dei tablet Android con esempi pratici - versione 2

e introduce le seguenti novità:

  • il documento 1 presentava un'architettura denominata AVAT (Attività-Viste-Azioni-Compiti) per facilitare la programmazione asincrona in un'applicazione Android. In questo documento, la libreria standard RxJava viene utilizzata per gestire le azioni asincrone;
  • il documento 2 utilizzava IDE Eclipse con un plugin Android. Questo documento utilizza Android Studio;
  • il documento 3 è stato ripreso così com’è;
  • il documento 4 utilizzava la libreria [Android Annotations] (AA) con IntelliJ IDE Community Edition. Il presente documento riprende integralmente il documento 4 con le seguenti differenze:
    • IDE è ora Android Studio;
    • il sistema di build è Gradle per tutti i progetti client o server (nel documento 4, a volte si utilizzava Maven)
    • la programmazione asincrona viene realizzata con la libreria RxJava (nel documento 4 si utilizzava la libreria AA);
  • il presente documento approfondisce argomenti non trattati o trattati solo marginalmente 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 vengono fattorizzati numerosi elementi che si ritrovano regolarmente in questo tipo di client. Questo scheletro viene ripreso in tutti gli esempi a partire dal capitolo 2. È la parte davvero innovativa del documento.

Vengono presentati i seguenti esempi:

Exemple
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 viste
6
Navigazione tramite schede
7
Utilizzo della libreria [Android Annotations] con Gradle
8 à 12
Gestione dei frammenti in un'applicazione Android
13
Navigazione tra le viste rivisitata
14
Architettura a due livelli
15
Architettura client/server
16
Gestire l'asincronia con RxJava
17, 17B
Componenti per l'immissione dei 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
App meteo
Chap 2
Struttura di base di un client Android che comunica con un servizio web / jSON. In essa vengono raggruppati numerosi elementi che si ritrovano regolarmente in questo tipo di client Android.
Chap 3
Gestione degli appuntamenti di uno studio medico
Chap 4
Esercizio pratico - Gestione di una busta paga di base
Chap 5
Esercizio pratico - Controllo delle schede Arduino

Questo documento è stato utilizzato durante l'ultimo anno della scuola di ingegneria IstiA dell'Università di Angers [istia.univ-angers.fr]. Ciò spiega il tono a volte un po' particolare del testo. I due esercizi applicativi sono testi tratti da TP di cui vengono fornite solo le linee guida della soluzione. Spetta al lettore elaborarla.

Il codice sorgente degli esempi è disponibile |ICI|. Per eseguire questi esempi, è necessario seguire la procedura descritta nel paragrafo 6.12.

Questo documento è un testo introduttivo alla programmazione Android. Non pretende di essere esaustivo. Si rivolge essenzialmente ai principianti.

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

1.1.2. Prerequisiti

Il prerequisito per un utilizzo ottimale del documento è una buona padronanza del linguaggio Java.

1.1.3. Gli strumenti utilizzati

Gli esempi che seguono 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 (cfr. paragrafo 6.8);
  • il gestore di emulatori Android Genymotion (cfr. paragrafo 6.9);
  • il gestore di dipendenze Maven (cfr. paragrafo 6.10);
  • IDE [Android Studio] (cfr. paragrafo 6.11);

1.2. Esempio-01: importazione di un esempio Android

1.2.1. Creazione del progetto

Creiamo con Android Studio un primo progetto Android. Per prima cosa creiamo una cartella vuota in cui inserire tutti i nostri progetti:

  

quindi creiamo un progetto con Android Studio. Per prima cosa importeremo uno degli esempi forniti con IDE [1-5]:

 

Image

L'importazione del progetto può causare errori dovuti alla mancata corrispondenza tra l'ambiente utilizzato durante la creazione del progetto e quello utilizzato qui per la sua esecuzione. È un'occasione per vedere come risolvere questo tipo di errori. 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"
}

// La build di esempio utilizza più directory per
// separare il codice standard e comune dal
// il codice principale dell’esempio.
List<String> dirs = [
    'main',     // codice di esempio principale; qui troverete le parti più interessanti.
    'common',   // componenti riutilizzati da più esempi
    'template'] // codice boilerplate generato dal processo di creazione dei modelli di esempio

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 della versione SDK 21. Sostituiamo questa versione con la versione 23 di cui disponiamo.

Nel file [build.gradle], Android Studio fornisce i seguenti suggerimenti:

 

Per accettare i suggerimenti, si seleziona [alt-entrée] sul suggerimento:

 

Potrebbe verificarsi anche 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 (la 2.10, riga 6 qui sotto):


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

e quella indicata nel file [<projet>/gradle/wrapper/gradle-wrapper.properties]:


#Mercoledì 10 aprile 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, occorre sostituire 2,8 con 2,10.

Per accedere al file [<projet>/gradle/wrapper/gradle-wrapper.properties], è necessario utilizzare la prospettiva del progetto:

Una volta apportata questa correzione, è possibile compilare l’applicazione [1], avviare l’emulatore Genymotion [2] e quindi eseguire il progetto [3]:

 

Image

Arrestiamo l'applicazione:

  

Ora possiamo chiudere il progetto. Ne creeremo uno nuovo.

  

1.2.2. Alcune considerazioni su IDE

1.2.2.1. Le viste

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

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

Nella maggior parte dei casi lavoreremo con la prospettiva [Android]. Quando duplicheremo un progetto in un altro, avremo bisogno della prospettiva [Project].

1.2.2.2. Gestione dell'esecuzione

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

Il pulsante [Rerun] [3] interrompe l'esecuzione del progetto [2] e poi lo riavvia [1].

1.2.2.3. Gestione della cache

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

Con Android 2.1 (maggio 2016), l’operazione precedente doveva essere ripetuta più volte e talvolta ciò non era sufficiente a risolvere l’anomalia rilevata. La soluzione è stata quella di disabilitare la tecnologia [Instant Run]:

  • in [3-4], tutto è stato disattivato;

In tutto ciò che segue, abbiamo lavorato con questa configurazione della cache e non abbiamo riscontrato alcun problema.

1.2.2.4. Gestione dei log

Durante l’esecuzione di un progetto, nel monitor Android vengono visualizzati dei log:

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:

  • si cancellano i log;
  • sul dispositivo Android, si esegue l’azione di cui si desiderano i log;
  • i log che compaiono in seguito 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. Con [4] è possibile selezionare un livello specifico.

I log sono molto utili per capire in quali momenti dell’esecuzione di un progetto vengono visualizzati determinati metodi. Vi faremo spesso ricorso. Prendiamo il codice della classe [MainActivity] del progetto [Exemple-01]:

 

package com.example.android.pdfrendererbasic;

import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity {

    public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_real);
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction()
                    .add(R.id.container, new PdfRendererBasicFragment(),
                            FRAGMENT_PDF_RENDERER_BASIC)
                    .commit();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_info:
                new AlertDialog.Builder(this)
                        .setMessage(R.string.intro_message)
                        .setPositiveButton(android.R.string.ok, null)
                        .show();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Nell’esempio sopra riportato, i metodi [onCreate, ligne 14] e [onCreateOptionsMenu, ligne 26] appartengono alla classe padre [Activity] (riga 9). Vengono chiamati in diversi momenti del ciclo di vita dell’applicazione. A volte vengono eseguiti più volte. Anche leggendo la documentazione, a volte è difficile stabilire se un metodo del ciclo di vita di questo tipo verrà eseguito prima o dopo un metodo scritto dall’utente. Tuttavia, questa informazione è spesso importante da conoscere. È quindi possibile inserire dei log come quelli riportati 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()) {
      ...
  }
}
  • nelle righe 7, 14 e 21 si utilizza la classe [Log]. Questa classe consente di scrivere log sulla 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 dei log. Per poterle distinguere, si utilizza questo primo argomento. Il secondo argomento è il messaggio da scrivere sulla console dei log;

Se eseguiamo nuovamente il progetto [Exemple-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

Si nota così che il metodo [onCreate], che crea l’attività Android, viene eseguito prima del metodo [onCreateOptionsMenu], che crea il menu dell’applicazione.

Ora, se si fa clic sull’opzione di menu nell’emulatore Android [1]:

  

nella console dei log viene aggiunto il seguente messaggio:


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

Nel prosieguo, aggiungeremo spesso nel codice Android delle istruzioni di log. Nella maggior parte dei casi, non le commenteremo. Sono presenti solo per invitare il lettore a consultare la console dei log al fine di comprendere progressivamente il ciclo di vita di un’applicazione Android.

1.2.2.5. Gestione dell'emulatore [Genymotion]

A volte l’emulatore Genymotion va in crash e non è più possibile riavviarlo. Ciò è dovuto al fatto che alcuni processi di VirtualBox sono rimasti attivi nel Task Manager. Aprite il Task Manager [Ctrl-Alt-Supp] ed eliminate tutti i processi di VirtualBox presenti:

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

1.2.2.6. Gestione del file binario APK creato

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

Esistono due versioni: una denominata [debug] e l'altra denominata [debug-unaligned]. È necessario utilizzare la prima, poiché l'altra è una versione intermedia. Il file binario .pak generato con [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 con Android Studio un nuovo progetto Android [1-12]:

 

In [13], si esegue l'applicazione. Si ottiene quindi la schermata [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 da IDE con gli elementi della sua configurazione. Si tratta di un file minimale che arricchiremo progressivamente.

  • righe 3-12: le caratteristiche dell’applicazione Android;
  • righe 22-25: le sue dipendenze. È soprattutto qui che apporteremo modifiche in base agli esempi studiati;

1.3.2. Il manifesto dell'applicazione

  

Il file [AndroidManifest.xml] [1] definisce le caratteristiche del 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 durante la creazione del progetto:

  • la riga 3 del manifesto (package) deriva dall'inserimento [4] riportato sopra. In questo pacchetto vengono generate automaticamente alcune classi;
  • la riga 10 del manifesto (nome dell’attività) deriva dall’inserimento [1] sopra riportato;

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 indicata;
  • riga 6: l'icona [2] dell'applicazione. Può essere modificata;
  • riga 7: il nome 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 di caratteri utilizzate dall'applicazione. Riga 2: il nome dell'applicazione deriva dall'inserimento effettuato durante la creazione 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 comparire nell’elenco delle applicazioni che è possibile avviare sul dispositivo Android.

1.3.3. L'attività principale

 

Un'applicazione Android si basa su una o più attività. In questo caso è stata generata un'attività [1]: [MainActivity]. Un'attività può visualizzare una o più viste a seconda del proprio tipo. 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 al momento della creazione dell’attività. Ciò avviene prima della visualizzazione della vista associata all’attività;
  • riga 10: viene chiamato il metodo [onCreate] della classe padre. È necessario farlo sempre;
  • 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 formattazione. Quello scelto per impostazione predefinita è il tipo [RelativeLayout]. In questo tipo di contenitore, i componenti sono posizionati gli uni rispetto agli altri (a destra di, a sinistra di, sotto, sopra);
  • righe m-p: un componente di tipo [TextView] che serve a visualizzare il testo;
  • riga n: il testo visualizzato. Si sconsiglia di inserire testo fisso nelle viste. È preferibile spostare questi testi 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, 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] che si trova nella cartella [res / layout] sarà identificata da [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] è l'identificatore della 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];

Ricordiamo quindi che quando si fa riferimento a [R.layout.activity_main] nel codice, si fa riferimento a un attributo della classe [R]. Il IDE ci aiuta a conoscere 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 [Edit Configurations];
  • il progetto è stato creato con una configurazione [app] che elimineremo [2] per ricrearla;
  • in [3], creare una nuova configurazione di esecuzione;
  
  • in [4], selezionare [Android Application];

Image

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

    <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 [Show Chooser Dialog] che consente di scegliere il dispositivo su cui eseguire l'applicazione (emulatore, tablet);
  • in [9], si indica che questa scelta deve essere memorizzata;
  • confermare la configurazione;
  
  • in [11], avviare il gestore degli emulatori [Genymotion] (cfr. paragrafo 6.9);
  • in [12], selezionare un emulatore di tablet e avviare [13];
  • in [14], eseguire la configurazione di esecuzione [app];
  • in [15] viene visualizzata la schermata di selezione del dispositivo di esecuzione. Qui è disponibile un solo dispositivo: 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 fa parte dei metodi che possono essere chiamati durante il ciclo di vita di un'attività. La documentazione Android ne fornisce l'elenco:

 
  • [1]: il metodo [onCreate] viene chiamato all’avvio dell’attività. È in questo metodo che si associa l’attività a una vista e si recuperano i riferimenti dei suoi componenti;
  • [2-3]: vengono quindi chiamati i metodi [onStart, onResume]. Si nota che il metodo [onResume] è l’ultimo metodo ad essere eseguito prima di arrivare allo stato [4] dell’attività in esecuzione;

1.4. Esempio-03: riscrittura del progetto [Exemple-02] con la libreria [Android Annotations]

Ora introdurremo la libreria [Android Annotations] che semplifica lo sviluppo di applicazioni Android. A tal fine, duplichiamo l’esempio [Exemple-02] in [Exemple-03] seguendo la procedura [1-16].

  • in [1], selezionate la vista [Project] per visualizzare l’intero progetto Android;

Nota: tra [14] e [15], si è passati da una prospettiva [Android] a una prospettiva [Project] (cfr. paragrafo 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 ripreso l’intera configurazione di [Exemple-02]:

 

In [19] otteniamo lo stesso risultato di [Exemple-02], ma con un nuovo nome.

Introdurremo ora la libreria [Android Annotations], che per comodità chiameremo AA. Questa libreria introduce nuove classi per annotare i sorgenti Android. Queste annotazioni saranno utilizzate da un processore che creerà nuove classi Java nel modulo, classi che parteciperanno alla compilazione dello stesso allo stesso titolo delle classi scritte dallo sviluppatore. Si ottiene così la seguente catena di compilazione:

Per prima cosa inseriremo nel file [build.gradle] le dipendenze relative al compilatore di annotazioni AA (il processore di cui sopra):


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 costituiscono la libreria AA;

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

  • elaborazione delle annotazioni Android, che dà origine a nuove classi;
  • compilazione di tutte le classi del progetto;

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    // A partire dalla versione 0.11 del plugin Gradle per Android, è necessario utilizzare 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 inseriremo una prima annotazione della libreria AA nella classe [MainActivity]:

  

La classe [MainActivity] è attualmente 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);
  }
}

Abbiamo già spiegato questo codice al paragrafo 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] ed è proprio questa classe che costituirà la vera e propria attività. 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à;

Fatto ciò, possiamo compilare il progetto [1]:

 
  • in [2], si vede la classe [MainActivity_] generata nella cartella [app / build / generated / source / apt / debug];

La classe [MainActivity_] generata è la seguente:


//
// DO NOT EDIT THIS FILE.
// Generato utilizzando AndroidAnnotations 4.0.0.
// 
// È possibile creare un'opera più ampia che contenga questo file e distribuirla secondo i termini di propria scelta.
//


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 cercheremo di spiegare il codice delle classi generate da AA. Esse gestiscono la complessità che le annotazioni cercano di nascondere. Tuttavia, a volte può essere utile esaminarlo quando si vuole capire come vengono «tradotte» le annotazioni che si utilizzano.

Ora è possibile eseguire nuovamente la configurazione [app]. Si ottiene lo stesso risultato di prima. Partiremo ora da questo progetto, che duplicheremo per illustrare i concetti fondamentali della programmazione Android.

1.5. Esempio-04: viste ed eventi

1.5.1. Creazione del progetto

Seguiremo la procedura descritta per duplicare [Exemple-02] in [Exemple-03] al paragrafo 1.4:

Noi:

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

1.5.2. Creazione di una vista

Ora modificheremo, con l’editor grafico, la vista visualizzata dal progetto [Exemple-04]:

  • in [1-4], creare una nuova vista XML;
  • in [5], assegnare un nome alla vista;
  • in [6], specificare il tag radice della vista. In questo caso, scegliamo un contenitore [RelativeLayout]. All’interno di questo contenitore, i componenti vengono posizionati gli uni rispetto agli altri: «a destra di», «a sinistra di», «sotto», «sopra»;
  

Il file [vue1.xml] generato da [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 tutta la sua altezza (riga 4);
  • in [1], selezionate la scheda [Design] nella vista [vue1.xml] visualizzata;
  • in [2-4], passare alla modalità tablet;
  • in [5], impostare la scala 1 del tablet;
  • in [6], selezionate la modalità "orizzontale" per il tablet;
  • la schermata [7] riassume le scelte effettuate.
  • in [1], selezionare un [Large Text] e trascinarlo sulla vista [2];
  • in [3], fare doppio clic sul componente;
  • in [4], modificare il testo visualizzato. Anziché inserirlo direttamente nella vista XML, lo esternalizzeremo nel file [res / values / string.xml]
  • nel file [5], si aggiunge un nuovo valore nel file [strings.xml];
  • nel file [8], si assegna un identificativo alla stringa;
  • in [9], si inserisce il valore della stringa;
  • in [10], la nuova visualizzazione dopo la convalida della fase precedente;
  • dopo aver fatto doppio clic sul componente, si modifica il suo identificativo [11];
  • in [12]; nelle proprietà del componente, si modifica la dimensione dei caratteri [50sp];
  • in [13], la nuova visualizzazione;

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 nell'interfaccia grafica si trovano alle righe 10, 11 e 14. Gli altri attributi di [TextView] sono valori predefiniti oppure derivano dal posizionamento del componente nella vista;
  • righe 7-8: le dimensioni del componente corrispondono a quelle del testo che contiene (wrap_content) in altezza e 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 più a destra (riga 12);

In generale, le dimensioni esatte dei margini sinistro, destro, superiore e inferiore saranno impostate direttamente nel XML.

Procedendo allo stesso modo, creare la seguente vista [1]:

 

I componenti sono i seguenti:

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

Posizionare i componenti l'uno rispetto all'altro può rivelarsi un'operazione frustrante, poiché le reazioni dell'editor grafico sono talvolta imprevedibili. 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:

  • nel componente [1], il bordo superiore (top) del componente è allineato rispetto al bordo inferiore (bottom) del componente [textViewTitreVue1] a una distanza di 50 pixel da [3] (top);
  • in [2], il bordo sinistro (left) del componente è allineato rispetto al bordo sinistro del contenitore a una distanza di 50 pixel da [3] (left);

Il componente [editTextNom] deve essere posizionato 60 pixel a destra del componente [textView1] e allineato al bordo inferiore di quest'ultimo;

 
  • nel componente [1], il bordo sinistro (left) del componente è allineato rispetto al bordo destro (right) del componente [textView1] a una distanza di 60 pixel dal componente [2] (left). È allineato al bordo inferiore (bottom:bottom) del componente [textView1] [1];

Il componente [buttonValider] deve essere posizionato a 60 pixel a destra del componente [editTextNom] e allineato al bordo inferiore di quest’ultimo;

 
  • nel componente [1], il bordo sinistro (left) del componente è allineato rispetto al bordo destro (right) del componente [editTextNom] a una distanza di 60 pixel dal componente [2] (left). È allineato al bordo inferiore del componente (bottom:bottom) [editTextNom] [1];

Il componente [buttonVue2] deve essere posizionato 50 pixel sotto il componente [textView1] e allineato a sinistra rispetto a quest’ultimo;

 
  • nel componente [1], il bordo sinistro (left) del componente è allineato rispetto al bordo sinistro (left) del componente [textView1] ed è posizionato sotto (top:bottom) a una distanza di 50 pixel dal componente [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>

Qui si ritrova tutto ciò che è stato realizzato graficamente. Un altro modo per creare una vista consiste quindi nel scrivere direttamente questo file. Una volta acquisita una certa dimestichezza, questa operazione può risultare più veloce rispetto all’utilizzo dell’editor grafico.

  • alla riga 38 si trova un'informazione che non abbiamo mostrato. È fornita tramite le proprietà del componente [editTextNom] [1]:
 

Tutti i testi provengono 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 l'attività [MainActivity] in modo che questa vista venga visualizzata all'avvio dell'applicazione:


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: ora l'attività visualizza la vista [vue1.xml];

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: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 la comparsa della tastiera non appena viene visualizzata la vista [vue1]. Infatti, quest'ultima presenta un campo di immissione che ha il focus al momento della visualizzazione della vista. Questo focus fa apparire per impostazione predefinita la tastiera virtuale;

Eseguire l'applicazione e verificare che venga visualizzata effettivamente la vista [vue1.xml]:

Image

1.5.3. Gestione degli eventi

Gestiamo ora il clic sul pulsante [Valider] della vista [Vue1]:

Image

Il codice di [MainActivity] viene modificato 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 {

  // gli elementi dell'interfaccia visiva
  @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");
  }

  // gestore degli eventi
  @Click(R.id.buttonValider)
  protected void doValider() {
    // viene visualizzato il nome inserito
    Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

}
  • righe 17-18: si associa il campo [protected EditText editTextNom] al componente identificativo [R.id.editTextNom] dell’interfaccia visiva. Il campo associato al componente deve essere accessibile nella classe derivata [MainActivity_] e, per questo motivo, non può avere 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 inserire caratteri accentati negli identificatori [id]. AA non li gestisce correttamente.

  • riga 32: l'annotazione [@Click(R.id.buttonValider)] indica il metodo che gestisce l'evento «Click» sul pulsante con identificativo [R.id.buttonValider]. Anche questo identificativo 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 un testo sullo schermo,
    • il primo parametro di makeText è l'attività,
    • il secondo parametro è il testo da visualizzare nella finestra che verrà visualizzata da makeText,
    • il terzo parametro è la durata della finestra visualizzata: Toast.LENGTH_LONG o Toast.LENGTH_SHORT;
  • alla riga 26, l’annotazione [@AfterViews] indica il metodo da eseguire quando tutti i campi annotati da [@ViewById] sono stati inizializzati. È importante sapere quando questi campi vengono inizializzati. Ad esempio, nel metodo [onCreate] è possibile utilizzare il riferimento della riga 18? Per rispondere a questa domanda, abbiamo inserito dei log;

Eseguite il progetto [Exemple-04] e verificate che avvenga qualcosa quando cliccate sul pulsante [Valider]. 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

Ne deduciamo che, quando viene eseguito il metodo [onCreate], i campi contrassegnati da [@ViewById] non sono ancora inizializzati. Ancora una volta, il lettore alle prime armi è incoraggiato 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 [Vue n° 2] non è stato utilizzato. Ci proponiamo di utilizzarlo creando una seconda vista e mostrando come navigare da una vista all’altra. Esistono diversi modi per risolvere questo problema. Quello proposto qui consiste nell’associare ogni vista a un’attività. Un altro metodo consiste nell’avere un’unica attività di tipo [AppCompatActivity] che visualizzi viste di tipo [Fragment]. Questo sarà il metodo utilizzato nelle applicazioni future.

1.6.1. Creazione del progetto

Si duplica il progetto [Exemple-04] in [Exemple-05]. A tal fine, si seguirà la procedura descritta per duplicare [Exemple-02] in [Exemple-03] al paragrafo 1.4 e riportata nel paragrafo 1.5.

1.6.2. Aggiunta di una seconda attività

Per gestire una seconda vista, creeremo una seconda attività. Sarà questa a gestire la vista n. 2. In questo caso si applica il modello «una vista = un’attività». Esistono altri modelli possibili.

123

Image

  • in [1-4], si crea una nuova attività;

Image

  • in [5], il nome della classe che verrà generata;
  • in [6], il nome della vista (vue2.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>

Si tratta di una vista attualmente vuota con un gestore di layout di tipo [RelativeLayout] (riga 2). Alla riga 11 si vede 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. Navigazione dalla vista n. 1 alla vista n. 2

Torniamo al codice della classe [MainActivity] che visualizza la vista n. 1. Il passaggio alla vista n. 2 al momento non è gestito:

  

Lo gestiamo nel modo seguente:


  // passare alla vista n. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // si passa alla vista n. 2 passandole il nome inserito nella vista n. 1
    // si crea un Intent
    Intent intent = new Intent();
    // si associa questo Intent a un'attività
    intent.setClass(this, SecondActivity.class);
    // si associano delle informazioni a questo Intent
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // si avvia l'attività di tipo [SecondActivity] passandole l'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 eseguire per il cambio di vista:

  1. riga 6: creare un oggetto di tipo [Intent]. Questo oggetto consentirà di specificare sia l’attività da avviare sia le informazioni da passarle;
  2. riga 8: associare l’Intent a un’attività, in questo caso un’attività di tipo [SecondActivity] che avrà il compito di visualizzare la vista n. 2. È importante ricordare che l’attività [MainActivity] visualizza invece la vista n. 1. Quindi abbiamo una vista = un’attività. Dovremo definire il tipo [SecondActivity];
  3. riga 10: facoltativamente, inserire delle informazioni nell’oggetto [Intent]. Queste sono destinate all’attività [SecondActivity] che verrà avviata. I parametri di [Intent.putExtra] sono (Oggetto chiave, Oggetto valore). Si noti che il metodo [EditText.getText()], che restituisce il testo inserito nel campo di immissione, non restituisce un tipo [String] ma un tipo [Editable]. È necessario utilizzare il metodo [toString] per ottenere il testo inserito;
  4. riga 12: avviare l'attività definita dall'oggetto [Intent].

Eseguire il progetto [Exemple-05] e verificare che venga visualizzata correttamente la vista n. 2 (al momento vuota):

1.6.4. Creazione della vista n. 2

 
  • in [1-2], eliminiamo la vista [main.xml] che non ci serve più, quindi modifichiamo la vista [vue2.xml] nel modo seguente:
 

I componenti sono i seguenti:

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 1

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>

Eseguite il progetto [Exemple-05] e verificate che venga visualizzata la nuova vista cliccando sul pulsante [Vue n° 2].

1.6.5. L'attività [SecondActivity]

In [MainActivity] abbiamo scritto il seguente codice:


    // si passa alla vista n. 2
    protected void navigateToView2() {
        // si passa alla vista n. 2 passandole il nome inserito nella vista n. 1
        // si crea un Intent
        Intent intent = new Intent();
        // si associa questo Intent a un'attività
        intent.setClass(this, SecondActivity.class);
        // si associano delle informazioni a questo Intent
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // si avvia l'attività di tipo [SecondActivity] passandole l'Intent
        startActivity(intent);
}

Alla riga 9, abbiamo inserito per [SecondActivity] delle informazioni che non sono state utilizzate. Ora le utilizziamo e ciò avviene nel codice di [SecondActivity]:

  

Il codice di [SecondActivity] si evolve 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 {

  // componenti dell'interfaccia visiva
  @ViewById
  protected TextView textViewBonjour;

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

  @AfterViews
  protected void afterViews() {
    // si recupera l'Intent, se presente
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // si recupera il nome
        String nom = extras.getString("NOM");
        if (nom != null) {
          // lo si visualizza
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }

}
  • riga 11: si utilizza l’annotazione [@EActivity] per indicare che la classe [SecondActivity] è un’attività associata alla vista [vue2.xml];
  • righe 15-16: si recupera un riferimento al componente [TextView] identificato da [R.id.textViewBonjour]. Qui non è stato scritto [@ViewById(R.id.textViewBonjour)]. In questo caso, AA presuppone che l’identificatore del componente sia identico al campo annotato, in questo caso il campo [textViewBonjour];
  • riga 23: l'annotazione [@AfterViews] annota un metodo che deve essere eseguito dopo che i campi annotati da [@ViewById] sono stati inizializzati. Nel metodo [OnCreate] (riga 19) non è possibile utilizzare questi campi poiché non sono ancora stati inizializzati. Nel progetto [Exemple-05] si passa da un'attività all'altra e, a prima vista, non era chiaro se il metodo annotato [@AfterViews] sarebbe stato eseguito una sola volta all'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] ha un metodo [getIntent] che restituisce l’oggetto [Intent] associato all’attività;
  • riga 28: il metodo [Intent.getExtras] restituisce un tipo [Bundle] che è una sorta di dizionario contenente le informazioni associate all’oggetto [Intent] dell’attività;
  • riga 31: si recupera il nome contenuto nell'oggetto [Intent] dell'attività;
  • riga 34: lo si visualizza.

Promemoria: i campi contrassegnati dall'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] ed è questa classe che costituirà la vera e propria attività. Questo ci porta ad apportare delle modifiche in:

[MainActivity]


  // si passa alla vista n. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // si associa questo Intent a un'attività
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • alla riga 6, occorre 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, occorre sostituire [SecondActivity] con [SecondActivity_];

Provate questa nuova versione. Digitate un nome nella vista n. 1 e verificate 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 illustrata in precedenza:

  • inserire il codice di navigazione nell’attività [SecondActivity] che visualizza la vista n. 2;
  • scrivere il metodo [@AfterViews] nell’attività [MainActivity] che visualizza la vista n. 1;

Il codice di [SecondActivity] viene modificato come segue:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // si crea un Intent per l'attività [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // si recupera l'Intent dell'attività corrente [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // si inserisce il nome nell’Intent di [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // si avvia [MainActivity]
      startActivity(intent1);
    }
}
  • righe 1-2: si associa il metodo [navigateToView1] al clic sul pulsante [btn_vue1];
  • riga 4: si crea un nuovo [Intent];
  • riga 5: si associa all’attività [MainActivity_];
  • riga 7: si recupera l'Intent associato a [SecondActivity];
  • riga 9: si recuperano le informazioni di questo Intent;
  • riga 12: la chiave [NOM] viene recuperata da [intent2] per essere inserita in [intent1] con lo stesso valore associato;
  • riga 15: viene avviata l'attività [MainActivity_].

Nel codice di [MainActivity] viene aggiunto il seguente metodo [@AfterViews]:


  @AfterViews
  protected void afterViews() {
    // si recupera l'intent, se esiste
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // si recupera il nome
        String nom = extras.getString("NOM");
        if (nom != null) {
          // lo si visualizza
          editTextNom.setText(nom);
        }
      }
    }
}

Apportate queste modifiche e testate l’applicazione. Ora, quando si torna dalla vista n. 2 alla vista n. 1, dovrebbe comparire il nome inserito inizialmente, cosa che finora non avveniva.

1.6.7. Ciclo di vita delle attività

Nel paragrafo 1.3.5 abbiamo presentato il ciclo di vita di un'attività. Qui abbiamo due attività e si passa dall'una all'altra durante l'esecuzione. Queste attività contengono due metodi di cui non si sa bene quando vengono chiamati l’uno rispetto all’altro: [onCreate] e [afterViews]. È importante saperlo. A tal fine, aggiungiamo dei log in entrambe le attività:

Pertanto, nella classe [MainActivity], scriviamo:


  // produttore
  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’applicazione, otteniamo 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 [Vue n° 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 [Vue n° 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 nuovamente istanziata. Quando si fa clic sul pulsante [Vue n° 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 nuovamente istanziata.

Le due attività vengono quindi ricreate sistematicamente ogni volta che si cambia attività.

Scopriremo ora un'architettura con un'unica 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 tramite schede

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

1.7.1. Creazione del progetto

Creiamo un nuovo progetto:

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

Il progetto creato è quindi il seguente:

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

È stata creata automaticamente una configurazione di esecuzione [app], con il nome del modulo, [2b]:

 

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

Image

1.7.2. Configurazione Gradle

Il progetto [Exemple-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'è una novità rispetto a quanto già visto: 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 all'attività [MainActivity] del progetto. In modalità [design], la vista è la seguente:

Image

Contiene i seguenti componenti:

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

In modalità [text], 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>

Si ritrovano gli elementi descritti in precedenza:

  • righe 2-49: la definizione del componente [main_content] (riga 5), che costituisce l’intera vista. Si nota che si tratta di un layout (gestore di disposizione dei componenti) di tipo [CoordinatorLayout] (riga 2);
  • righe 11-33: il contenitore [appbar] (riga 12). Si tratta di un layout di tipo [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 che 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, si trova in basso a destra della vista completa;

Non cercheremo di comprendere il significato di tutti gli attributi di questi componenti. Li useremo così come sono. È con l’esperienza e spesso in modalità [design] che se ne scopre il ruolo. In questa modalità, si scopre che i componenti hanno diverse decine di attributi. In generale, solo alcuni sono inizializzati, mentre gli altri mantengono un valore predefinito.

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

  

Questi valori sono indicati alle righe 15-16, 23, 39, 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>
  <!-- Margini predefiniti dello schermo, secondo le linee guida di progettazione Android. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
</resources>

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

Analogamente, l'annotazione:

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

1.7.4. L'attività

  

Il codice generato per l'attività è all'altezza della visione descritta in precedenza: è complesso. Lo analizzeremo in più fasi.

1.7.4.1. La gestione dei frammenti e delle schede

Il codice di [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 {

  // il gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // il contenitore dei frammenti 
  private ViewPager mViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // genitore
    super.onCreate(savedInstanceState);
    // vista
    setContentView(R.layout.activity_main);
    // barra degli strumenti
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // il gestore dei frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // il contenitore dei frammenti è associato al gestore dei frammenti
    // ovvero il frammento n. i del contenitore di frammenti è il frammento n. i fornito dal gestore di frammenti
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // anche la barra delle schede è associata al contenitore di frammenti
    // ovvero la scheda n. i visualizza il frammento n. i del contenitore
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }


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

  // il gestore dei frammenti
  // è a lui che vengono richiesti i frammenti da visualizzare nella vista principale
  // deve definire i metodi [getItem] e [getCount]; gli altri sono facoltativi
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • riga 28: Android fornisce un contenitore di viste di tipo [android.support.v4.view.ViewPager] (riga 12). A questo contenitore deve essere fornito un gestore di viste o frammenti. È lo sviluppatore a 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 alla creazione dell'attività;
  • riga 35: la vista [activity_main.xml] è associata all’attività;
  • riga 37: si recupera il riferimento al componente [toolbar] della vista tramite il suo identificatore;
  • riga 38: questa barra degli strumenti diventa la barra delle azioni (un concetto Android) dell'attività;
  • riga 40: viene istanziato il gestore dei frammenti. Il parametro del costruttore è la classe Android [android.support.v4.app.FragmentManager] (riga 10);
  • riga 44: nella vista [activity_main.xml] si recupera il riferimento al contenitore dei frammenti tramite il suo identificatore;
  • riga 45: il gestore dei frammenti viene associato al contenitore dei frammenti. Ciò significa che quando si richiederà al contenitore dei frammenti di visualizzare il frammento n. i, questo verrà richiesto al gestore dei frammenti;
  • riga 48: si recupera un riferimento alla barra delle schede tramite il suo identificativo;
  • riga 49: il gestore delle schede è associato al contenitore dei frammenti. Ciò significa che quando si cliccherà sulla scheda n. i, il contenitore visualizzerà il frammento n. i. L'associazione tra il gestore delle schede e il contenitore dei frammenti ci evita qualsiasi gestione delle schede. Pertanto non dobbiamo 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 saranno più frammenti che schede. In questo caso, non si effettua tale associazione.

Il gestore di frammenti [SectionsPagerAdapter] è il seguente:


// il gestore dei frammenti
  // è a lui che vengono richiesti i frammenti da visualizzare nella vista principale
  // deve definire i metodi [getItem] e [getCount] - gli altri sono facoltativi
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

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

    // numero del frammento e posizione
    @Override
    public Fragment getItem(int position) {
      // si istanzia un frammento [PlaceHolder] e lo si restituisce
      return PlaceholderFragment.newInstance(position + 1);
    }

    // indica il numero di frammenti gestiti
    @Override
    public int getCount() {
      return 3;
    }

    // facoltativo - assegna un titolo ai frammenti gestiti
    @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’applicazione dipendono da quest’ultima. Il gestore di frammenti è definito dallo sviluppatore;
  • riga 5: il gestore di frammenti estende la classe Android [android.support.v4.app.FragmentPagerAdapter]. Il costruttore ci viene imposto. Dobbiamo definire almeno i due metodi seguenti:
    • 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 delle righe 27-33 saranno i titoli delle schede;

  • righe 18-21: getCount indica 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 identici, del tipo [PlaceholderFragment];
  • righe 24-35: getPageTitle(int i) restituisce il titolo del frammento n. i;

1.7.4.2. I frammenti visualizzati

  

I frammenti dell’attività hanno qui tutti lo stesso tipo e sono tutti associati alla vista XML [fragment_main] seguente:


<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 di tipo [RelativeLayout];
  • righe 11-14: l'unico componente della vista (frammento): un [TextView] identificato da [section_label];

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


// un frammento
  public static class PlaceholderFragment extends Fragment {
      // un testo visualizzato nel frammento
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
    }

    // restituisce un frammento con un'informazione: il numero del frammento passato come parametro
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // frammento
      PlaceholderFragment fragment = new PlaceholderFragment();
      // informazioni incorporate
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // risultato
      return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // la vista [fragment_main] viene istanziata
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // il [TextView] è stato trovato
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // il suo contenuto viene modificato
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // viene restituita la vista
      return rootView;
    }
  }
  • riga 2: la classe [PlaceholderFragment] estende la classe Android [Fragment]. In genere è sempre così;
  • 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] della 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] di cui si recupera il riferimento tramite il suo identificatore;
  • riga 29: si visualizza un testo nel [TextView];
    • [getString] è un metodo della classe padre [AppCompatActivity];
    • il primo argomento è un numero di componente. [R.string.section_format] indica il numero del componente identificato da [section_format] nel file [res / values / strings.xml] (riga 4 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>
  • (continua)
    • la riga (d) sopra riportata %1$d indica che l’argomento n. 1 (%1) deve essere formattato come numero intero ($d);
    • il secondo argomento di [getString] è il valore da assegnare all'argomento $1 della riga (d) sopra;
    • [getArguments] fornisce il riferimento al bundle degli argomenti del frammento. È importante ricordare che ogni argomento è stato creato con il seguente bundle (righe f-h):

    // restituisce un frammento contenente un'informazione: il numero del frammento passato come parametro
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // frammento
      PlaceholderFragment fragment = new PlaceholderFragment();
      // informazione incorporata
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // risultato
      return fragment;
}
  • (continua)
    • getArguments().getInt(ARG_SECTION_NUMBER) restituirà quindi il valore [sectionNumber] delle righe (g) e (b) sopra riportate;
  • riga 31: si restituisce 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: un elemento del menu identificato 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 precedente corrisponde alla seguente schermata (il menu si trova in alto a destra nella finestra di esecuzione Android):

 

Questo menu è gestito nel modo seguente nell'attività [MainActivity]:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate il menu; questa operazione aggiunge voci alla barra delle azioni, se presente.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Gestisci qui i clic sulle voci della barra delle azioni. La barra delle azioni
    // gestirà automaticamente i clic sul pulsante Home/Su, purché
    // si specifichi un'attività padre 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 opzioni;
  • 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: si indica che il menu è stato creato;
  • righe 8-21: il metodo [onOptionsItemSelected] viene eseguito non appena si fa clic su un'opzione del menu;
  • riga 13: il riferimento dell’opzione di menu selezionata;
  • righe 16-18: se l'opzione selezionata è quella con identificativo [action_settings], non viene eseguita alcuna operazione e si indica che l'evento è stato gestito (riga 17);
  • riga 20: l'evento viene passato alla classe padre;

Per capire meglio cosa succede con questo menu, aggiungiamo dei log nel codice precedente:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "création menu en cours");
    // Carica il menu; questa operazione aggiunge voci alla barra delle azioni, se presente.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Gestisci qui i clic sulle voci della barra delle azioni. La barra delle azioni
    // gestirà automaticamente i clic sul pulsante Home/Su, purché
    // si specifichi un'attività padre in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // attività principale
    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 supporto Android, quella di una busta.

Questo componente è gestito nella classe [MainActivity] nel modo seguente:


    // pulsante fluttuante
    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: si recupera il riferimento al pulsante fluttuante nella vista associata all'attività (activity_main);
  • righe 3-9: gli si associa un gestore per gestire il clic su di esso;
  • riga 6: la classe [Snackbar] consente di visualizzare messaggi temporanei nella vista tramite il suo metodo [Snackbar.make]. Il primo argomento è una vista dalla quale [Snackbar] cercherà una vista padre in cui visualizzare il messaggio. In questo caso, [view] è la vista del busta su cui è stato fatto clic (riga 5). La vista padre che verrà individuata sarà 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 e attivare così un'azione. In questo caso non è associata alcuna azione al clic sul messaggio. Infine, il metodo [show] visualizza il messaggio;

Il clic sul pulsante fluttuante produce 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 clicca sulla scheda n. i, il frammento n. i viene visualizzato nel contenitore delle viste. Ciò è evidente dal testo visualizzato in [4]. Si può notare inoltre che è possibile passare da una scheda all'altra trascinando la vista verso destra o verso sinistra con il mouse (swipe). Vedremo che è possibile controllare questo comportamento.

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], si vede che il metodo [onCreateView] e quelli successivi vengono eseguiti al primo visualizzazione del frammento e ogni volta che l'attività deve visualizzarlo nuovamente;

Per seguire il ciclo di vita dell’attività e dei frammenti, aggiungiamo i seguenti log nel codice di [MainActivity]:


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

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

  // un frammento
  public static class PlaceholderFragment extends Fragment {
    // un testo visualizzato nel frammento
    private static final String ARG_SECTION_NUMBER = "section_number";

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

    // restituisce un frammento con un'informazione: il numero del frammento passato come parametro
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // frammento
      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: istanziazione 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 è necessario ricordare il codice che regola la creazione dei frammenti:


  // il gestore dei frammenti
  // è a lui che vengono richiesti i frammenti da visualizzare nella vista principale
  // deve definire i metodi [getItem] e [getCount] - gli altri sono facoltativi
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

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

    // numero del frammento e posizione
    @Override
    public Fragment getItem(int position) {
      // si istanzia un frammento [PlaceHolder] e lo si rende visibile
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • righe 11-15: un frammento viene istanziato da [newInstance] ogni volta che il contenitore di 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. Ricordiamo che è il frammento n. 2 quello visualizzato;

Ora clicchiamo sulla scheda n. 3. Qui non c’è alcun log. Probabilmente perché il frammento n. 3 da visualizzare era già stato istanziato. Ora torniamo alla scheda n. 1. I log sono quindi i 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 ripete per gli altri due frammenti.

Da questi log si evince che:

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

Ciò che è importante sapere e che i log confermano è che, per impostazione predefinita, quando viene visualizzato un frammento n. i, i frammenti i-1 e i+1 vengono istanziati, se non lo sono già. Questo spiega, ad esempio, perché all’avvio, quando occorre visualizzare il frammento n. 1, vengono invece istanziati e inizializzati i frammenti 1 e 2. I log mostrano inoltre che il metodo [getItem(i)] viene chiamato una sola volta, anche se il frammento n. i viene visualizzato più volte. Sembra quindi che il contenitore di frammenti [ViewPager], che deve visualizzare il frammento n. i, lo richieda una sola volta al gestore di frammenti [SectionsPagerAdapter]. Successivamente non lo richiede più e continua a utilizzare quello che ha ottenuto.

Infine, i log forniscono indicazioni 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. Non è quindi possibile utilizzarlo per aggiornare il frammento 2. Tuttavia, l’utente potrebbe aver eseguito, con il frammento 1, un’operazione il cui risultato dovrebbe essere visualizzato dal frammento 2. Si nota che il metodo [onCreateView] non potrà essere utilizzato per aggiornare il frammento 2. Sarà necessario trovare un’altra soluzione;

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

1.8.1. Creazione del progetto

Duplicheremo il progetto [Exemple-06] in [Exemple-07] per inserire in quest'ultimo le annotazioni Android. A tal fine, seguire la procedura descritta nel paragrafo 1.4. Si ottiene il seguente risultato:

1.8.2. Configurazione Gradle

 

Modifichiamo il file [build.gradle] come segue:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // A partire dalla versione 0.11 del plugin Gradle per Android, è necessario utilizzare 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 l'utilizzo della libreria [Android Annotations] (cfr. paragrafo 1.4).

1.8.3. Aggiunta delle prime annotazioni AA

Creeremo delle annotazioni AA nel file [MainActivity]:

  

La classe [MainActivity] si evolve come segue:


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

  // il gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // il contenitore dei frammenti
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // il gestore delle schede
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // il pulsante galleggiante
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;


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

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

    // barra degli strumenti
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // il gestore dei frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // il contenitore dei frammenti è associato al gestore dei frammenti
    // cioè il frammento n. i del contenitore di frammenti è il frammento n. i fornito dal gestore di frammenti
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // anche la barra delle schede è associata al contenitore di frammenti
    // ovvero la scheda n. i visualizza il frammento n. i del contenitore
    tabLayout.setupWithViewPager(mViewPager);

    // pulsante fluttuante
    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 inserito nel campo [tabLayout]. Si tratta del gestore delle schede;
  • righe 14-15: il componente identificato da [R.id.fab] viene inserito nel campo [fab]. Si tratta del pulsante fluttuante;
  • righe 23-50: il codice che in precedenza si trovava nel metodo [onCreate] viene spostato in un metodo con nome qualsiasi, ma annotato con [@AfterViews] (riga 23). Nel metodo così annotato, si ha la certezza che tutti i componenti dell'interfaccia visiva annotati con [@ViewById] siano stati inizializzati;
  • sono stati inoltre inseriti dei log per monitorare il ciclo di vita dell’attività;

Si ricorda che l'annotazione [@EActivity] genererà una classe [MainActivity_] che costituirà la vera e propria attività del progetto. È quindi necessario modificare il file [AndroidManifest.xml] nel modo 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_"
      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, eseguite nuovamente il progetto e verificate che venga visualizzata l’interfaccia con le schede.

1.8.4. Riscrittura dei frammenti

Rivedremo la gestione dei frammenti del progetto. Per il momento 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, introduciamo le annotazioni AA per i frammenti.

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

  

Nell’esempio sopra riportato, compare la classe [PlaceholderFragment], che è stata esternalizzata al di fuori della classe [MainActivity]. Essa viene 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;

// un frammento è una vista visualizzata da un contenitore di frammenti
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // componente dell’interfaccia visiva
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // numero del frammento
  private static final String ARG_SECTION_NUMBER = "section_number";

  // costruttore
  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)));
    // genitore
    super.onResume();
    // visualizzazione
    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'identificativo della vista XML associata al frammento, in questo caso la vista [fragment_main.xml];
  • righe 19-20: inseriscono nel campo [textViewInfo] il riferimento al componente di [fragment_main.xml] identificato da [R.id.section_label], che è di tipo [TextView] (riga (l) di seguito):

<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 della visualizzazione della vista associata al frammento. È possibile utilizzarlo per aggiornare l'interfaccia visiva che verrà visualizzata;
  • riga 47: è necessario richiamare il metodo omonimo della classe padre;
  • riga 49: non è chiaro se il metodo [onResume] possa essere eseguito o meno prima dell’inizializzazione del campo alla riga 20. I log inseriti per monitorare il ciclo di vita del frammento ce lo diranno. Per il momento, a titolo precauzionale, si esegue un test di nullità;
  • riga 51: si aggiornano le informazioni del campo [textViewInfo] con l’argomento intero passato al frammento al momento della sua creazione;

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


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private Fragment[] fragments;
    // numero di frammenti
    private static final int FRAGMENTS_COUNT = 3;
    // n. del frammento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // produttore
    public SectionsPagerAdapter(FragmentManager fm) {
      // genitore
      super(fm);
      // inizializzazione della tabella dei frammenti
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // si crea un frammento
        fragments[i] = new PlaceholderFragment_();
        // è possibile passare argomenti al frammento
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }

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

    // restituisce il numero di frammenti gestiti
    @Override
    public int getCount() {
      return fragments.length;
    }

    // facoltativo - assegna un titolo ai frammenti gestiti
    @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’inizializzazione dell’array dei frammenti avviene nel costruttore. Sono di tipo [PlaceholderFragment_] (riga 18) e non [PlaceholderFragment]. La classe [PlaceholderFragment] è stata infatti annotata con un'annotazione AA e darà origine a una classe [PlaceholderFragment_] derivata da [PlaceholderFragment]; è questa la classe che l'attività deve utilizzare. A ogni frammento creato viene passato un argomento intero che verrà visualizzato dal frammento stesso;
  • righe 42-45: sono stati modificati i titoli dei frammenti. Poiché questi corrispondono anche ai titoli delle schede, dovremmo vedere un cambiamento nella barra delle schede;

Compiliamo [Make] e [1] in questo progetto:

 
  • in [2], si nota 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]);

Eseguite il progetto [Exemple-07] e verificate che funzioni ancora.

1.8.5. Analisi 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'unica attività;
  • riga 2: metodo [afterViews] dell'attività: i suoi campi annotati da [@ViewById] vengono inizializzati;
  • righe 3-5: creazione 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 qui si trova la risposta a una domanda posta in precedenza: il metodo [onResume] del frammento 1, ad esempio (riga 12), viene eseguito dopo il metodo [afterViews] dello stesso frammento (riga 11). Pertanto, quando il metodo [onResume] viene eseguito, può utilizzare i campi annotati da [@ViewById]. Potremo quindi scrivere il metodo [onResume] nel modo seguente:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // genitore
    super.onResume();
    // visualizzazione
    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. Ricordiamo che questo frammento era stato istanziato all’avvio dell’applicazione;
  • righe 4-5: viene eseguito il metodo [onResume] del frammento n. 3. Si ricorda che è il frammento n. 2 ad essere visualizzato;

Ora passiamo dalla scheda 2 alla scheda 3. Non c’è alcun log. Pertanto, nessuno dei metodi [onCreateView, afterViews, onResume] del frammento n. 3 viene eseguito. Il testo [Hello World from section:3] viene visualizzato correttamente solo perché era già stato creato nella fase precedente, durante la visualizzazione del frammento n. 2. Ricordiamo infatti che in quella fase era stato eseguito il metodo [onResume] del frammento n. 3. Ci si rende conto qui che, proprio come il metodo [onCreateView], nemmeno il metodo [onResume] può essere utilizzato per aggiornare il frammento 3. Se fosse stato necessario 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

Si nota che tutti i metodi del frammento 1 sono stati eseguiti. Si nota che il metodo getItem non è stato chiamato. Come già detto, questo metodo viene chiamato una sola volta per ogni frammento;

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

Sorprendente, vero? Tutti i metodi del frammento n. 3 vengono rieseguiti.

Per comprendere questi fenomeni, occorre ricordare che, per impostazione predefinita, quando il contenitore dei frammenti sta per visualizzare il frammento i, inizializza i frammenti i-1, i e i+1. Rileggiamo i log alla luce di questa informazione.

Innanzitutto, i log all'avvio dell'applicazione:

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

Si passa ora 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 lo sono già dalla fase precedente. Il frammento 3 viene inizializzato alle righe 2-5.

Si passa 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. Ma, poiché sono già stati inizializzati nella fase precedente, ciò non avviene. Ciò che qui non si vede è che il frammento 1, che non è adiacente al frammento 3, perde il proprio stato, che non viene conservato in memoria.

Si passa 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. Lo è già dalla fase precedente. In questa stessa fase, lo stato del frammento 1 era andato perso. Viene quindi reinizializzato alle righe 1-4. Ciò che qui non si vede è 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 lo sono già dalla fase precedente. Il frammento 3 viene inizializzato alle righe 1-4.

Cosa abbiamo imparato?

  • che la gestione predefinita dei frammenti è molto particolare e che è necessario conoscerla se non si vuole perdere la pazienza. È possibile 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, poiché non è certo che vengano eseguiti;

1.8.6. onDestroyView

Il metodo [onDestroyView] fa parte del ciclo di vita dei frammenti (cfr. paragrafo 1.7.6):

Si nota che nel ciclo di vita di un frammento:

  • il metodo [onCreateView] può essere eseguito più volte;
  • prima di tornare successivamente al metodo [onCreateView], è necessario passare obbligatoriamente attraverso il metodo [onDestroyView] [2];

Inseriremo questi metodi nei frammenti per seguirne 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;

// un frammento è una vista visualizzata da un contenitore di frammenti
@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)));
    // genitore
    super.onDestroyView();
  }

}

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

Passiamo 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 del 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ò implica che quando l'utente tornerà al frammento 1 o a un frammento adiacente, il ciclo di vita di tale frammento verrà rieseguito;

Si torna 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 frammento 1 viene rieseguito poiché era stato sottoposto a un [onDestroyView];
  • riga 5: ora è il frammento 3 a vedere eseguito il proprio metodo [onDestroyView]. Anche in questo caso, quando l’utente tornerà al frammento 3 o a un frammento adiacente, il ciclo di vita di tale 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 questo 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;

....

// un frammento è una vista visualizzata da un contenitore di frammenti
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // componente dell'interfaccia visiva
  @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 frammento 1 diventa visibile. Si nota inoltre che diventa visibile prima dell’esecuzione del 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 è visibile (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 è visibile (riga 3);

Cosa abbiamo imparato?

  • il metodo [setUserVisibleHint] viene eseguito una volta con la proprietà [isVisibleToUser] impostata su true, per il frammento che verrà visualizzato;
  • non è possibile stabilire quando verrà eseguito questo metodo 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 tale 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 n. i, esegue, se non lo ha già fatto, il ciclo di vita dei frammenti adiacenti i-1 e i+1. Questo comportamento può essere controllato tramite il metodo [ViewPager].setOffscreenPageLimit:

// offset dei frammenti
    [ViewPager].setOffscreenPageLimit(n);

Con l’istruzione sopra riportata,

  1. quando il contenitore di frammenti [ViewPager] sta per visualizzare il frammento n. i, esegue, se non è già stato fatto, il ciclo di vita dei frammenti adiacenti dell’intervallo [i-n, i+n];
  2. se successivamente viene visualizzato il frammento j:
    • lo stesso fenomeno si ripete per i frammenti adiacenti dell’intervallo [j-n, j+n];
    • i frammenti inizializzati nella fase 1 e che non si trovano più nell’adiacenza [j-n, j+n] del nuovo frammento possono quindi essere sottoposti a un’operazione [onDestroyView]. Tuttavia, ho potuto osservare in altre applicazioni, in particolare in quella del capitolo 3, che ciò non avveniva sistematicamente;

Modifichiamo il metodo [MainActivity.afterViews] nel modo seguente:


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

    // barra degli strumenti
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // il gestore dei frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // il contenitore dei frammenti è associato al gestore dei frammenti
    // cioè il frammento n. i del contenitore dei frammenti è il frammento n. i fornito dal gestore dei frammenti
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // si inibisce lo scorrimento tra i frammenti
    mViewPager.setSwipeEnabled(false);

    // offset dei frammenti
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);

    // la barra delle schede è associata anche al contenitore dei frammenti
    // cioè la scheda n. i visualizza il frammento n. i del contenitore
    tabLayout.setupWithViewPager(mViewPager);

    // pulsante fluttuante
    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 pari al numero totale di frammenti meno 1. In questo modo, all’avvio, quando il contenitore dei frammenti visualizzerà il frammento n. 1, inizializzerà contemporaneamente i frammenti 2, 3, …, n con n = 1 + mSectionsPagerAdapter.getCount() - 1=mSectionsPagerAdapter.getCount(). Verranno quindi inizializzati tutti i frammenti. Quando la finestra di visualizzazione si sposterà su un altro frammento, il contenitore dei frammenti:
    • rileverà che tutti i frammenti adiacenti al nuovo frammento sono già inizializzati e quindi non provvederà alla loro inizializzazione;
    • poiché anche l’area adiacente al nuovo frammento copre la totalità dei frammenti, nessuno di essi verrà «disinizializzato» dal contenitore dei frammenti;

In totale, dovremmo vedere tutti i frammenti istanziati e inizializzati all’avvio dell’applicazione e poi mai più in seguito. È proprio ciò che verifichiamo ora 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: viene eseguito il ciclo di vita dei tre frammenti;

Passiamo ora 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 metodi del loro ciclo di vita non vengono più eseguiti. Si tratta di un funzionamento molto prevedibile dei frammenti che ne facilita enormemente l’utilizzo.

Quello che vogliamo trovare è un modo per aggiornare un frammento che sta per essere visualizzato, indipendentemente dalla disposizione 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 e non per gli altri;
  • questo evento può verificarsi prima o dopo il ciclo di vita del frammento. Dipende dall’adiacenza dei frammenti scelta dallo sviluppatore. Si tratta di un problema, poiché se il ciclo di vita non si è ancora verificato, ciò significa che il frammento non può essere aggiornato tramite il metodo [setUserVisibleHint, true];

I log all’avvio dell’applicazione, quando l’adiacenza dei frammenti 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
  • si nota che quando il frammento 1 diventa visibile, la sua vista non è ancora stata creata. Non è quindi possibile modificarla. Ciò potrà avvenire nel corso del ciclo di vita del frammento, ad esempio nei metodi [onCreateView] (riga 11) o [onResume] (righe 13-14). Poiché utilizziamo le annotazioni AA, normalmente non è necessario scrivere il metodo [onCreateView]. È quindi il metodo [onResume] che sembra il più adatto in questo caso per aggiornare il frammento 1;

Quando siamo passati dalla scheda 1 alla scheda 2, i log sono stati 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] della riga 4 per aggiornare il frammento 2;

Quando siamo passati dalla scheda 2 alla scheda 3, i log sono stati 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] della riga 2 per aggiornare il frammento 3;

Quando siamo passati dalla scheda 3 alla scheda 1, i log sono stati 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, occorre utilizzare il metodo [onResume] del frammento 1 (righe 6-7) per aggiornare il frammento 1.

Quindi, in questo esempio, si vede che per aggiornare un frammento che verrà visualizzato, sono disponibili 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, ciò che chiameremo una visita. Sarà quindi necessario aggiornare la sua visualizzazione ogni volta che verrà visualizzato. Questo è proprio il problema che stiamo cercando di risolvere.

Prima di ciò, esaminiamo l’ultima fase del ciclo di vita di un’attività o di un frammento, ovvero quella in cui viene eliminato. Il sistema può decidere di eliminare un’attività se altre attività con priorità maggiore richiedono risorse non disponibili. Per liberare tali risorse, il sistema provvederà a eliminare alcune attività. Verrà quindi chiamato il metodo [onDestroy] dell’attività e dei frammenti.

1.8.9. OnDestroy

Consentiremo all’utente di eliminare l’attività tramite un’opzione di menu [5]. A tal fine, aggiungiamo una nuova opzione di menu nel 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>

Si esegue un semplice copia/incolla della prima opzione di menu e si adatta il risultato (righe 9 e 10). Il testo di questa nuova opzione viene aggiunto nel 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], si gestisce il clic sull'opzione [Terminate]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Gestisci qui i clic sugli elementi della barra delle azioni. La barra delle azioni
    // gestirà automaticamente i clic sul pulsante Home/Su, purché
    // si specifichi un'attività padre 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");
      //al termine dell’attività
      finish();
      return true;
    }
    // principale
    return super.onOptionsItemSelected(item);
}
  • righe 14-19: si esegue un copia/incolla delle righe 10-13 e si adatta il codice alla nuova opzione;
  • riga 17: l’attività viene terminata tramite azione software;

Ora eseguiamo questa nuova versione, quindi, 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: clic sull'opzione [Terminate];
  • riga 4: viene chiamato il metodo [onDestroy] dell’attività;
  • righe 4-5: viene chiamato il metodo [onDestroyView] del frammento 1 e successivamente il suo metodo [onDestroy];
  • righe 6-9: questa operazione si ripete per gli altri due frammenti;

Si ricordi quindi che il metodo [onDestroy] dell'attività e dei frammenti viene chiamato quando l'attività sta per essere eliminata dal sistema, dallo sviluppatore o dall'utente. È possibile utilizzare questo metodo per salvare informazioni, ad esempio localmente sul tablet, in modo da poterle recuperare quando l'utente riavvierà l'applicazione.

1.9. Esempio-08: aggiornamento di un frammento con un numero variabile di frammenti adiacenti

1.9.1. Creazione del progetto

Si duplica il progetto [Exemple-07] in [Exemple-08]. A tal fine, si seguirà la procedura descritta per duplicare [Exemple-02] in [Exemple-03] al paragrafo 1.4.

1.9.2. Riscrittura del frammento [PlaceholderFragment]

Il nuovo codice del 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;

// un frammento è una vista visualizzata da un contenitore di frammenti
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // componente dell'interfaccia visiva
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // dati
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

  // numero del frammento
  private static final String ARG_SECTION_NUMBER = "section_number";

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


  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // log
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // testo iniziale
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inizializzazione completata
      initDone = true;
    }
    // visualizzazione testo corrente
    textViewInfo.setText(text);
  }


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

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

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

  // aggiornamento frammento
  public void update() {
    // il lavoro da svolgere dipende dal numero della visita
    if (numVisit > 1) {
      // registro
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // testo modificato
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // informazioni locali per i log
  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 si utilizzava per inizializzare il testo del frammento (riga 42). Lo si fa ancora, ma per assicurarsi che avvenga una sola volta, si gestisce un valore booleano [initDone] (riga 44) per indicare che l’inizializzazione è stata effettuata e non deve essere ripetuta;
  • righe 56-59: introduciamo il metodo [onDestroyView] per segnalare 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], invece, non viene sempre eseguito dopo il metodo [@AfterViews]. I log hanno dimostrato 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]. Viene eseguito o l’uno o l’altro. Per precauzione, si imposterà un valore booleano [updateDone] quando è stato effettuato un aggiornamento;

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


  // dati
  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) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // numero di visite
    if (isVisibleToUser) {
      // incremento
      numVisit++;
      // aggiornamento frammento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // il frammento verrà nascosto
      updateDone = false;
    }
  }

  @Override
  public void onResume() {
    // genitore
    super.onResume();
    // registro
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // aggiornamento
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • riga 14: si memorizza lo stato di visibilità o meno 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 passato a true;
  • righe 26-28: se il frammento sta per essere nascosto, si reimposta il valore booleano [updateDone] a false. Abbiamo infatti bisogno di un evento per reimpostare a false il valore booleano [updateDone], che era stato impostato a true non appena viene chiamato il metodo [update], in modo che possano essere effettuati nuovi aggiornamenti. A tal fine sfruttiamo il fatto che il frammento non sia più visibile. Quando tornerà visibile, l’aggiornamento del frammento dovrà essere eseguito nuovamente;
  • righe 32-42: i log mostrano che, a seconda dell’adiacenza scelta per i frammenti, il metodo [onResume] può essere eseguito anche se il frammento non è visibile. Se non è visibile, non si esegue l’aggiornamento (riga 39) e, come fatto per [setMenuVisibility], si gestisce il valore booleano [updateDone].

Infine, il metodo [onDestroyView] è il seguente:


  @Override
  public void onDestroyView() {
    // genitore
    super.onDestroyView();
    // aggiornamento indicatore
    afterViewsDone = false;
    // registro
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

Il metodo [onDestroyView] viene eseguito quando termina un ciclo di vita del frammento. In seguito potrà riprendere un altro ciclo.

  • riga 6: il metodo [onDestroyView] rimuove qualsiasi collegamento con la vista associata al frammento. Verrà ricreato nel prossimo ciclo di vita del frammento. Per il momento, dobbiamo impostare il valore booleano [afterViews] su false, per indicare che il collegamento con la vista non esiste più;

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


    // numero di frammenti
  private final int FRAGMENTS_COUNT = 5;
  // adiacenza dei frammenti
  private final int OFF_SCREEN_PAGE_LIMIT=2;


  // gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

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

    ....

    // offset dei frammenti
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

...
}

I log all'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] fino a false;
  • riga 14: il metodo [setUserVisibleHint] del frammento 1 viene eseguito con [visibleToUser] fino a true;
  • righe 15-17: viene chiamato il metodo [afterViews] dei 3 segmenti adiacenti. Si tratta quindi di un caso in cui questo metodo viene chiamato dopo che un frammento è diventato visibile (il frammento 1 alla riga 14);
  • righe 18-20: viene chiamato il metodo [onResume] dei 3 segmenti adiacenti;

Si passa 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é l’adiacenza dei frammenti è spostata di una posizione verso destra, il frammento 4 viene richiesto dal contenitore dei frammenti;
  • riga 2: viene chiamato il metodo [setUserVisibleHint] del frammento 4 con [visibleToUser] a false;
  • riga 3: il metodo [setUserVisibleHint] del frammento 1 viene chiamato con [visibleToUser] a false. Infatti, il frammento 1 è ora nascosto;
  • riga 4: il metodo [setUserVisibleHint] del frammento 2 viene chiamato con [visibleToUser] a true. Il frammento 2 è ora visibile;
  • righe 5-6: il ciclo di vita del frammento 4 prosegue;

Si passa 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é l’adiacenza dei frammenti è spostata di una posizione verso destra, il frammento 5 viene richiesto dal contenitore dei frammenti;
  • riga 2: il metodo [setUserVisibleHint] del frammento 5 viene chiamato con [visibleToUser] a false;
  • riga 3: il metodo [setUserVisibleHint] del frammento 2 viene chiamato da [visibleToUser] a false. Infatti, il frammento 2 è ora nascosto;
  • riga 4: il metodo [setUserVisibleHint] del frammento 3 viene chiamato con [visibleToUser] a true. Il frammento 3 è ora visibile;
  • righe 5-6: il ciclo di vita del frammento 5 prosegue;

Si passa 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. Questo è già stato fatto due fasi prima;
  • riga 3: il frammento 1 esce dall’adiacenza del frammento 4 visualizzato. Viene eseguito il suo metodo [onDestroyView]. La prossima volta che verrà visualizzato, il suo ciclo di vista [onCreateView, afterViews, onResume] verrà rieseguito;

Si passa 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 non viene eseguito il ciclo di vita del frammento 5. Questo è già stato fatto due passaggi prima;
  • riga 3: il frammento 2 esce dall’adiacenza del frammento 5 visualizzato. Viene eseguito il suo metodo [onDestroyView];

Si passa 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. Infatti, aveva perso la connessione con la sua vista;
  • righe 2, 5, 8, 9: per lo stesso motivo viene rieseguito il ciclo di vita del frammento 2;
  • righe 10-11: i frammenti 4 e 5 escono dall'adiacenza del frammento visualizzato;
  • riga 7: il frammento 1 viene aggiornato;
 

I log non hanno mai mostrato che i metodi [setUserVisibleHint] e [onResume] tentassero entrambi di aggiornare il frammento. È o l'uno o l'altro. Il lettore è invitato a effettuare ulteriori test e a monitorare i log per comprendere appieno il concetto di adiacenza e il ciclo di vita dei frammenti.

Ora, ipotizziamo un’adiacenza totale ed eseguiamo gli stessi test.

In [MainActivity]:


  // numero di frammenti
  private final int FRAGMENTS_COUNT = 5;
  // adiacenza dei frammenti
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

I log all’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 viene eseguito;
  • il frammento 1 viene visualizzato alla riga 18;

Si passa 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 viene nascosto;
  • riga 2: viene visualizzato il frammento 2;

Si passa 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 è visibile;

Si passa 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 è visibile;

Si passa 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 è visibile;

Si passa 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 è visibile;
  • riga 3: il frammento 1 viene aggiornato;

Si passa 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: viene visualizzato il frammento 4;
  • riga 3: il frammento 4 viene aggiornato;

Si nota che, con l'adiacenza totale, il comportamento dei frammenti è molto più prevedibile.

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


  // numero di frammenti
  private final int FRAGMENTS_COUNT = 5;
  // adiacenza dei frammenti
private final int OFF_SCREEN_PAGE_LIMIT = 0;

I log all’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 si vede che il contenitore di frammenti ha richiesto 2 frammenti, i nn. 1 e 2. Tutto avviene 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 diversi frammenti. Queste interazioni modificano lo stato dell’applicazione. Con «stato dell’applicazione» si intende l’insieme delle informazioni che essa memorizza nel corso del suo ciclo di vita. Si pone quindi il seguente problema:

  • quando l’utente interagisce con il frammento i, l’applicazione passa da uno stato E1 a uno stato E2;
  • un'azione dell'utente sul frammento i fa visualizzare il frammento j;
  • come aggiornare il frammento j con lo stato attuale E2 dell’applicazione;

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

Esistono diverse soluzioni a questo problema. Ne abbiamo vista una: il frammento i può trasmettere 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++) {
        // si crea un frammento
        fragments[i] = new PlaceholderFragment_();
        // è possibile passare argomenti al frammento
        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 farà apparire il frammento j, il nostro codice non viene chiamato. Viene eseguito esclusivamente il codice di sistema. Vedremo in un prossimo progetto come intercettare il clic su una scheda, ma per ora seguiremo un’altra strada.

Abbiamo parlato dello stato dell’applicazione: l’insieme dei dati gestiti dall’applicazione nel corso del tempo. In questo caso l’applicazione è costituita da un’attività e da n frammenti, tutti istanziati una sola volta all’avvio dell’applicazione e la cui durata corrisponde a quella dell’applicazione stessa. Pertanto, ciascuno di questi elementi, o più elementi insieme, può essere un candidato per memorizzare lo 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 proprio in essa.

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


  // aggiornamento del frammento
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // il lavoro da svolgere dipende dal numero della visita
    if (numVisit > 1) {
      // registro
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // testo modificato
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // informazioni locali per i log
  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 [getInfos] visualizza parte dello stato dell'applicazione;

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


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: si nota che nei metodi [setUserVisibleHint] viene chiamato [getActivity()==null] se il frammento non è ancora visibile (isVisibleToUser==false);
  • riga 19: si nota che quando il flusso di esecuzione raggiunge il metodo [update] del frammento 1, il metodo [getActivity] restituisce correttamente l’attività;

Quando si imposta l’adiacenza dei frammenti a 4 (adiacenza totale), 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 -- accettazione ed eliminazione
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

Si ottengono gli stessi risultati. Se ne deduce che, non appena il frammento è visibile, il metodo [getActivity] restituisce l’attività del frammento. Si nota inoltre che quando l’esecuzione raggiunge il metodo [update] del frammento che sta per essere visualizzato, il metodo [getActivity] restituisce correttamente un valore.

Per illustrare la comunicazione tra frammenti, realizziamo un nuovo progetto.

1.10. Esempio-09: comunicazione tra frammenti, swipe e scrolling

1.10.1. Creazione del progetto

Duplichiamo il progetto [Exemple-07] in [Exemple-08]. A tal fine, seguiremo la procedura descritta per duplicare [Exemple-02] in [Exemple-03] al paragrafo 1.4.

1.10.2. La sessione

In questo nuovo progetto, vogliamo che i frammenti visualizzino il numero totale di frammenti visualizzati dall’utente. A tal fine, è necessario gestire 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 in una sessione vengono inseriti i dati da condividere tra diverse viste richieste dallo stesso utente. Il fatto di incapsulare 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 {
  // numero di frammenti visitati
  private int numVisit;

  // getters e 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] indica l’ambito (o durata) della classe così annotata. In questo caso, l’attributo [scope = EBean.Scope.Singleton] fa sì che la classe [Session] sia un singleton: verrà istanziata una sola volta all’avvio dell’applicazione. Il riferimento a una classe annotata con [EBean] può quindi essere iniettato in un’altra classe. Si tratta del concetto di iniezione delle dipendenze;

1.10.3. L'attività [MainActivity]

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


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

  ...

  // Iniezione della sessione
  @Bean(Session.class)
  protected Session session;

  // numero di frammenti
  private final int FRAGMENTS_COUNT = 5;
  // adiacenza dei frammenti
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

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

    // inizializzazione della sessione
    session.setNumVisit(0);
  }

...
  • righe 7-8: iniezione del riferimento al singleton della sessione tramite l’annotazione [@Bean]. Il parametro dell’annotazione è la classe del bean da iniettare. Il campo così annotato non può avere l’ambito [private];
  • riga 15: l'annotazione [@AfterInject] serve a indicare un metodo da chiamare una volta completate tutte le iniezioni della classe. Pertanto, quando si entra nel metodo [afterInject] alla riga 16, il riferimento della riga 8 è già stato inizializzato;
  • riga 20: si azzera il contatore delle visite;

1.10.4. Il frammento [PlaceholderFragment]

Il frammento [PlaceholderFragment] si evolve come segue:


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

....

  // sessione
  protected Session session;

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // numero di visite
    if (isVisibleToUser) {
      // aggiornamento frammento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // il frammento verrà nascosto
      updateDone = false;
    }
  }

  // aggiornamento frammento
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // sessione
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // incremento numero di visite
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // testo modificato
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • riga 7: la sessione;
  • righe 35-37: sappiamo che quando si entra nel metodo [update], il metodo [getActivity] restituisce correttamente l'attività. Ne approfittiamo per recuperare la sessione e memorizzarla localmente (riga 36);
  • righe 39-41: per incrementare il numero della visita, lo recuperiamo dalla sessione. Avremmo potuto inserire questo codice nel metodo [setUserVisibleHint] a partire dalla riga 19, poiché sappiamo che a quel punto il metodo [getActivity] restituisce l’attività. Qui si decide di non attribuire alcun ruolo particolare a questo metodo e di spostare il codice specifico di un frammento nel metodo [update], che è stato creato proprio a questo scopo;
  • riga 43: visualizza il numero della visita;

Quando si esegue questa applicazione con 5 frammenti, con un’adiacenza di 2 frammenti, 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: si nota che il metodo [afterInject] dell'attività viene eseguito prima del metodo [afterViews];

Il lettore è invitato a provare questa nuova applicazione.

1.10.5. Disattivare lo swipe o lo scorrimento

Nell’applicazione precedente, quando si scorre l’emulatore Android con il mouse verso sinistra o destra, la vista corrente lascia il posto alla vista di destra o di sinistra a seconda dei casi. Questo comportamento predefinito non è sempre auspicabile. Impareremo a disattivare lo scorrimento delle viste (swipe).

Torniamo alla vista principale XML [activity_main]:

  

Nel codice XML della vista si trova quello del contenitore dei frammenti:


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

  // il gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // il contenitore dei frammenti
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

Alla riga 12, il contenitore dei frammenti è di tipo [android.support.v4.view.ViewPager] (riga 1). Per disattivare la scansione, è necessario derivare questa classe nel modo seguente:

  

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 {

  // controllo dello swipe
  private boolean isSwipeEnabled;

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

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

  // metodi da ridefinire per gestire lo swipe
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // scorrimento autorizzato?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // scorrimento autorizzato?
    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);
  • in caso di scorrimento con la mano, possono essere chiamati i gestori di eventi delle righe 24 e 34. Entrambi restituiscono un valore booleano. È sufficiente che restituiscano il valore booleano [false] per inibire lo scorrimento;
  • riga 11: il valore booleano che serve a indicare se si accetta o meno lo scorrimento con la mano.

Fatto ciò, occorre 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] si scrive:

  

  <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: si utilizza la nuova classe. In [MainActivity], il codice si evolve come segue:


package exemples.android;

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

  // il gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // il contenitore di frammenti
  @ViewById(R.id.container)
  protected MyPager mViewPager;

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // il contenitore di frammenti è associato al gestore di frammenti
    // ovvero il frammento n. i del contenitore di frammenti è il frammento n. i fornito dal gestore di frammenti
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // si disabilita lo scorrimento tra i frammenti
    mViewPager.setSwipeEnabled(false);
    // anche la barra delle schede è associata al contenitore dei frammenti
...
  • riga 12: il gestore delle pagine ha ora il tipo [MyPager];
  • riga 23: si può scegliere se disattivare o meno lo scorrimento con il mouse.

Provate questa nuova versione. Disattivate o meno lo scorrimento e osservate la differenza di comportamento delle viste quando le trascinate a destra o a sinistra con il mouse. In tutte le applicazioni future, lo scorrimento sarà disattivato. Non lo ricorderemo più.

1.10.6. Disattivare lo scorrimento tra i frammenti

Continuiamo con un miglioramento del gestore delle schede. Quando si passa dalla scheda 1 alla scheda 4, si vedono scorrere le due schede intermedie 2 e 3. In gergo Android questo fenomeno è chiamato smoothScrolling. Questo comportamento può diventare fastidioso se ci sono molte schede. È possibile disattivarlo aggiungendo il seguente codice nel gestore dei frammenti [MyPager]:


// controlla lo scorrimento
  private boolean isSwipeEnabled;
  // controlla lo scorrimento
  private boolean isScrollingEnabled;

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

  // setter
...

  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 tramite il metodo [setCurrentItem] sopra indicato (riga 9). [position] è il numero del frammento da visualizzare;

  • riga 10: viene chiamato il metodo [setCurrentItem] della classe padre. Il secondo argomento di [false] richiede una transizione immediata tra il frammento precedente e quello nuovo (senza scorrimento), mentre per [true] è prevista una transizione tramite scrolling. In questo caso, il secondo argomento è il valore del campo della riga 4, campo che lo sviluppatore può impostare con il metodo delle righe 16-18;

Se si desidera disattivare lo scorrimento, la classe [MainActivity] sarà la seguente:


...
    // offset dei frammenti
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // si inibisce lo scorrimento tra i frammenti
    mViewPager.setSwipeEnabled(false);

    // nessuno scorrimento
    mViewPager.setScrollingEnabled(false);
...

Eseguite nuovamente il progetto e verificate che non ci sia più scrolling tra le schede 1 e 4, ad esempio. Di seguito, disabiliteremo sempre lo scorrimento. Non lo ripeteremo.

1.10.7. Un nuovo frammento

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

Per prima cosa copiamo la vista [vue1.xml] dal progetto [Exemple-04] nel progetto [Exemple-09] [1]:

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

In [2], si aggiungono i testi mancanti prelevandoli dal file [res/values/strings.xml] del progetto [Exemple-04]


<resources>
  <string name="app_name">Exemple-07</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
</resources>
  • sopra, sono state aggiunte le righe 6-9;

Ora creiamo la classe [Vue1Fragment] che sarà il frammento incaricato di visualizzare la 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 {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // gestore eventi
  @Click(R.id.buttonValider)
  protected void doValider() {
    // viene visualizzato il nome inserito
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}

  • riga 10: l'annotazione [@EFragment] fa sì che il frammento utilizzato dall'attività sia in realtà la classe [Vue1Fragment_]. È importante tenerlo presente. Il frammento è associato alla vista [vue1.xml];
  • righe 14-15: il componente identificato da [R.id.editTextNom] viene inserito nel campo [editTextNom] della 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()] consente di ottenere 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 si evolve come segue:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private Fragment[] fragments;
    // numero del frammento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // costruttore
    public SectionsPagerAdapter(FragmentManager fm) {
      // genitore
      super(fm);
      // inizializzazione dell'array dei frammenti
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // si crea un frammento
        fragments[i] = new PlaceholderFragment_();
        // è possibile passare argomenti al frammento
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // un frammento di +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

 ...
  }
  • riga 13: è presente [FRAGMENTS_COUNT] frammenti: [FRAGMENTS_COUNT-1] frammenti di tipo [PlaceholderFragment] (righe 14-21) e un frammento di tipo [Vue1Fragment_], riga 23 (attenzione al trattino basso);

Compilate ed eseguite il progetto [Exemple-09]. La scheda n. 5 dovrebbe essere diversa:

1.10.8. Derivare tutti i frammenti da una stessa classe astratta

Anche il nuovo frammento [Vue1Fragment] deve essere aggiornato quando viene visualizzato. A tal fine, dovremo creare un codice simile a quello creato per il frammento [PlaceholderFragment]. Per evitare ripetizioni, fattorizzeremo ciò che è possibile in una classe astratta da cui erediteranno tutti i frammenti dell’applicazione.

A tal fine creiamo un nuovo progetto.

1.11. Esempio 10: far derivare tutti i frammenti da una classe astratta

1.11.1. Creazione del progetto

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

1.11.2. Gestione della modalità debug

Aggiungiamo al progetto la possibilità di visualizzare o meno i log della modalità debug. A tal fine, aggiungiamo una costante statica alla classe [MainActivity]:


  // modalità debug
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. La classe astratta padre 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 {

  // dati privati
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;

  // dati  accessibili alle classi figlie
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;

  // attività
  protected MainActivity activity;

  // sessione
  protected Session session;

  // costruttore
  public AbstractFragment() {
    // inizializzazione
    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) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }

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

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

  // informazioni locali
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }

  // aggiornamento frammento
  protected void update() {
    ...
    // si richiede alla classe figlia di aggiornarsi
    updateFragment();
  }

  protected abstract void updateFragment();
}
  • riga 7: la classe [AbstractFragment] estende la classe Android [Fragment];
  • ogni frammento deve poter essere aggiornato. Per questo motivo la classe padre [AbstractFragment] impone alle sue classi figlie la presenza di un metodo [updateFragment] (riga 68) che essa stessa 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 sono 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 della riga 16;
  • riga 28: si memorizza il nome della classe istanziata, ovvero il nome di una classe figlia;
  • righe 15-22: questi campi hanno l'attributo [protected] affinché le classi figlie possano accedervi. Si noti che le classi figlie ignorano l’esistenza dei valori booleani [isVisibleToUser] e [updateDone] (righe 10-11);
  • riga 57: il metodo [getParentInfos] ha l’attributo [protected] affinché le classi figlie possano richiamarlo;

I metodi [setUserVisibleHint, onDestroyView, onResume] rimangono analoghi a quelli presenti nella classe [PlaceholderFragment] del progetto precedente:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    // memoria
    this.isVisibleToUser = isVisibleToUser;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // caso in cui il frammento diventerà visibile
    if (isVisibleToUser) {
      // aggiornamento del frammento
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // si esce dal frammento
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // genitore
    super.onDestroyView();
    // aggiornamento indicatore
    afterViewsDone = false;
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }

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

Il metodo [update] è il seguente:


  // aggiornamento frammento
  protected void update() {
    // si recuperano l'attività e la sessione
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // si richiede alla classe figlia di aggiornarsi
    updateFragment();
}

In base al codice sopra riportato, quando viene eseguito il metodo [update] di un frammento, quest’ultimo è visibile. Questo è importante perché significa che il metodo [Fragment.getActivity] restituisce quindi un riferimento all'attività dell'applicazione (cfr. paragrafo 1.10.8), il che consente successivamente di accedere alla sessione.

  • righe 4-10: si inizializzano l’attività e la sessione se non è già stato fatto;
  • riga 12: si chiama il metodo [updateFragment] della classe figlia. Quando questo verrà eseguito, i campi [activity] e [session] a cui ha accesso saranno già stati inizializzati;

1.11.4. La classe [PlaceholderFragment]

  

La classe [PlaceholderFragment] si evolve come segue:


package exemples.android;

import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;

// un frammento è una vista visualizzata da un contenitore di frammenti
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {

  // componente dell'interfaccia visiva
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;

  // dati
  private boolean initDone;

  // dati
  private String text;
  private int numVisit;

  // numero del frammento
  private static final String ARG_SECTION_NUMBER = "section_number";

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


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

  // aggiornamento frammento
  public void updateFragment() {
  ...
  }

}
  • riga 10: la classe [PlaceholderFragment] estende la classe [AbstractFragment]. Con questa architettura, la scrittura di un frammento consiste nel:
    • scrivere il metodo [@AfterViews], che serve a inizializzare il frammento durante il suo primo ciclo di vita o a reinizializzarlo se in precedenza è stato eseguito un [onDestroyView]. La riga 39 è obbligatoria per gestire correttamente il ciclo di vita del frammento;
    • scrivere il metodo [updateFragment] che aggiornerà il frammento appena prima della sua visualizzazione. Questo metodo può utilizzare la sessione della sua classe padre;
    • scrivere i gestori di eventi del frammento. È ciò che faremo nei progetti futuri;

I metodi [@AfterViews] e [updateFragment] rimangono analoghi a quelli del progetto precedente:


@AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // testo iniziale
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // inizializzazione completata
      initDone = true;
    }
    // visualizzazione testo corrente
    textViewInfo.setText(text);
  }

  // aggiornamento frammento
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // incremento numero di visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // testo modificato
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }

  // informazioni locali per i log
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • righe 7 e 23: nei log vengono visualizzate le informazioni della classe padre con il metodo ereditato [getParentInfos];

1.11.5. La classe [Vue1Fragment]

  

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

  // elementi dell'interfaccia visiva
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // dati
  private int numVisit;

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

  // gestore eventi
  @Click(R.id.buttonValider)
  protected void doValider() {
    // viene visualizzato il nome inserito
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // informazioni locali per i log
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }

  // aggiornamento frammento
  @Override
  protected void updateFragment() {
    // incremento del numero di visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // viene visualizzato il numero della visita
    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 alcuna funzione rilevante. È comunque necessario scriverlo per impostare il valore 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 della visita (riga 48) e nell'incrementare tale numero nella sessione (righe 44-46);

Il lettore è invitato a testare questo nuovo progetto.

Riprenderemo 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 schede / frammenti

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


// la barra delle schede è associata anche al contenitore dei frammenti
// ovvero la scheda n. i visualizza il frammento n. i del contenitore
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 si chiede al contenitore dei frammenti di visualizzare il frammento n. i, la scheda n. i viene automaticamente selezionata.

Per illustrare questo comportamento, aggiungeremo le opzioni [Fragment 1, Fragment 2, ...] al menu attuale. Quando l’utente cliccherà sull’opzione [Fragment i], verrà richiesto al contenitore dei frammenti di visualizzare il frammento n. i. Verificheremo quindi se la scheda n. i è stata selezionata o meno.

Questa fase inizia con la modifica del menu dell’applicazione:

 

Il contenuto del file [res / menu / menu_main.xml] si evolve 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 del menu;
  • i nomi delle opzioni (righe 10, 14, 18, 22, 26) sono definiti 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>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
  <!-- 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 del clic su queste opzioni di menu avviene nella classe [MainActivity]:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // elaborazione delle opzioni di menu
    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;
      }
    }
    // voce elaborata
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // si cambia il frammento visualizzato
      mViewPager.setCurrentItem(i);
    }
  }
  • riga 2: il metodo [onOptionsItemSelected] viene chiamato quando si verifica un clic su una delle opzioni di menu;
  • riga 8: viene recuperato l'identificativo dell'opzione su cui si è cliccato;
  • righe 9-36: i diversi casi vengono gestiti dal metodo switch;
  • righe 16-36: il clic sull'opzione [Fragment i] rimanda al metodo [showFragment(i-1)] delle righe 41-45;
  • riga 43: si richiede al contenitore dei frammenti di visualizzare il frammento richiesto;
  • riga 42: si verifica preventivamente che sia possibile farlo (condizione 1) e che sia necessario (condizione 2);

Il lettore è invitato a testare questa nuova versione. Si nota che quando si richiede la visualizzazione del frammento n. i, questo viene correttamente visualizzato e la scheda n. i viene a sua volta selezionata.

Ora che abbiamo visto come funziona l’associazione tra schede e frammenti, ci occuperemo di un altro caso: quello in cui la gestione delle schede è dissociata da quella dei frammenti. È il caso, ad esempio, in cui il numero delle schede è inferiore a quello dei frammenti. Per illustrare questo nuovo caso d’uso, realizzeremo un nuovo progetto.

1.12. Esempio 11: schede separate dai frammenti

1.12.1. Creazione del progetto

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

1.12.2. Obiettivi

La nuova applicazione avrà due schede:

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

Image

  • in [1], il frammento [Vue1];
  • in [2], il frammento di tipo [PlaceholderFragment] scelto dall'utente;
  • in [3], si continua a contare le visite;

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 {
  // numero di frammenti visitati
  private int numVisit;
  // n. del frammento di tipo [PlaceholderFragment] visualizzato nella seconda scheda
  private int numFragment;

  // getter e setter
...
}
  • riga 10: gestiremo noi stessi il clic sulle schede. Quando si clicca su una scheda, è necessario ripristinare il frammento che era visualizzato l'ultima volta che è stata selezionata. Il campo [numFragment] memorizzerà il numero di tale frammento per la scheda n. 2, un numero contenuto in [0, Fragments_COUNT-2]. Quando si cliccherà sulla scheda n. 2, cercheremo nella sessione il numero del frammento da visualizzare;

1.12.4. Il menu

  

Il menu [res / menu / menu_main.xml] si evolve 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 sarà 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 finora non faceva. Il suo codice si evolve come segue:


  // il gestore delle schede
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...

    // nessuno scorrimento
    mViewPager.setScrollingEnabled(false);

    // visualizzazione Vista1
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

    // all'inizio c'è una sola scheda
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);

    // gestore eventi
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // è stata selezionata una scheda - si modifica il frammento visualizzato tramite il contenitore dei frammenti
        ...
      }

      @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 definizione, sarà l’ultimo frammento del contenitore;
  • righe 20-22: poiché non è stata effettuata alcuna associazione tra le schede e il contenitore dei frammenti, dobbiamo gestire le schede autonomamente. Inizialmente, la barra delle schede [tabLayout] della riga 3 non contiene alcuna scheda;
  • riga 20: si crea la prima scheda;
  • riga 21: gli si assegna un titolo. Negli esempi precedenti, il titolo delle schede era il titolo dei frammenti. Ora non è più così. Di conseguenza, si rimuove il metodo [getPageTitle] dal gestore dei frammenti. Non ne abbiamo più bisogno:

    // facoltativo - assegna un titolo ai frammenti gestiti
    @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 indipendenti. Il frammento visualizzato è sempre quello scelto dal contenitore dei frammenti. Se si cambia scheda e non si richiede al contenitore di cambiare il frammento visualizzato, non succede nulla: viene visualizzato sempre lo stesso frammento, ma la scheda selezionata è cambiata. Quindi, in questo caso, il frammento visualizzato è quello scelto alla 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] delle righe 26-30 viene attivato non appena si verifica 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");
        }
        // è stata selezionata una scheda - si modifica il frammento visualizzato dal contenitore di frammenti
        // posizione della scheda
        int position = tab.getPosition();
        // numero del frammento da visualizzare
        int numFragment;
        switch (position) {
          case 0:
            // numero del frammento [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // numero del frammento [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // visualizzazione frammento
        mViewPager.setCurrentItem(numFragment);
}
  • riga 8: si recupera la posizione della scheda su cui è stato cliccato. Qui si otterrà un numero 0 o 1;
  • righe 12-15: se è stata cliccata la prima scheda, ci si prepara a visualizzare il frammento [Vue1Fragment];
  • righe 16-18: negli altri casi (se è stata cliccata la scheda n. 2), ci si prepara a visualizzare nuovamente il frammento che era visualizzato l'ultima volta che è stata selezionata la scheda n. 2. Il numero di quest'ultimo era stato quindi inserito nella sessione dell'applicazione;
  • riga 21: si richiede al contenitore dei frammenti di visualizzare il frammento desiderato;

Vediamo ora la gestione delle opzioni di menu (sempre in [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // elaborazione delle opzioni di menu
    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;
      }
    }
    // voce elaborata
    return true;
}
  • righe 16-31: gestione delle 4 opzioni del menu. Ogni gestore chiama il metodo [showFragment] con il numero del frammento da visualizzare;

Il metodo [showFragment] è il seguente:


  // la scheda n. 2
  private TabLayout.Tab tab2 = null;

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // se la seconda scheda non esiste ancora, la si crea
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // si imposta il titolo della seconda scheda
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // si modifica il frammento visualizzato
      mViewPager.setCurrentItem(i);
      // il numero del frammento visualizzato viene memorizzato nella sessione
      session.setNumFragment(i);
      // si seleziona la scheda 2 - non si esegue alcuna operazione se questa è già selezionata
      tab2.select();
    }
}
  • si ricorda che all'avvio dell'applicazione è presente una sola scheda;
  • riga 2: un riferimento alla scheda n. 2, null all'avvio;
  • 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: nel titolo della seconda scheda viene inserito il numero del frammento che verrà visualizzato, con una numerazione che parte da 1;
  • riga 14: viene visualizzato il frammento desiderato;
  • riga 16: il suo numero viene inserito nella sessione;
  • riga 18: viene selezionata la scheda n. 2. Se fosse già selezionata, non accadrà nulla: il metodo [onTabSelected] non verrà eseguito. Se non fosse già selezionata, verrà attivato il metodo [onTabSelected]. Questo metodo richiede quindi al contenitore dei frammenti di visualizzare il frammento già visualizzato alla riga 14. Un semplice test nel metodo [onTabSelected] evita questo caso:

        // visualizzazione del frammento solo se necessario
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Si invita il lettore a provare questa nuova versione.

1.12.6. Miglioramenti

Ora abbiamo una buona comprensione dei frammenti, del loro ciclo di vita, del concetto di adiacenza dei frammenti e della loro relazione con la barra delle schede. Disponiamo inoltre di 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 specificheremo le relazioni tra l’attività e i frammenti aggiungendo un’interfaccia.

1.13. Esempio 12: codificare le relazioni tra attività e frammenti

In questo esempio, vogliamo definire le relazioni minime tra attività e frammenti. A tal fine, utilizzeremo:

  • un'interfaccia [IMainActivity] che definirà 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 [Exemple-11] in [Exemple-12] seguendo la procedura descritta al paragrafo 1.4. Otteniamo il seguente risultato:

1.13.2. L'interfaccia [IMainActivity]

Dagli esempi precedenti emerge che i frammenti devono poter accedere alla sessione istanziata dall’attività. Inoltre, sebbene non sia visibile in questi esempi, è prevedibile che i gestori degli eventi dei frammenti terminino talvolta con un cambio di vista. Si chiederà all’attività di effettuare tale cambio. L’interfaccia [IMainActivity] potrebbe quindi essere la seguente:

  

package exemples.android;

public interface IMainActivity {

  // accesso alla sessione
  Session getSession();

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

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;
}

Alla riga 12, si noti la presenza di una costante che in precedenza si trovava nella classe [MainActivity]. Si desidera ridurre l’accoppiamento tra i frammenti e l’attività, limitandolo a un accoppiamento tra [AbstractFragment] e [IMainActivity]. L’attività potrà quindi avere un nome 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] subisce modifiche minime:


  // dati  accessibili alle classi figlie
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;

  // attività
  protected IMainActivity mainActivity;
  protected Activity activity;

...
  // aggiornamento frammento
  protected void update() {
    // si recuperano l'attività e la sessione
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // si richiede alla classe figlia di aggiornarsi
    updateFragment();
}
  • righe 6 e 7: vengono gestiti due tipi di riferimento 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 ovviamente allo stesso oggetto. Tuttavia, quest’ultimo viene visto con due tipi diversi. Ciò ci eviterà la conversione di tipo in fase di esecuzione;

  • riga 14: si recupera un riferimento all’attività tramite il metodo [getActivity];
  • riga 15: se il riferimento non è nullo, allora è possibile accedere alla sessione;
  • righe 16-17: si memorizzano l’attività come implementazione dell’interfaccia [IMainActivity] e la sessione;

1.13.4. Modifica del gestore dei frammenti

Il gestore di frammenti [SectionsPagerAdapter] nella classe [MainActivity] viene modificato in un unico punto: invece di gestire frammenti di tipo [Fragment], ora gestisce frammenti di tipo [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private AbstractFragment[] fragments;
    // numero del frammento
    private static final String ARG_SECTION_NUMBER = "section_number";

    // costruttore
    public SectionsPagerAdapter(FragmentManager fm) {
      // genitore
      super(fm);
      // inizializzazione dell'array dei frammenti
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // un frammento di +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

    // numero di posizione del frammento
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }

    // restituisce il numero di frammenti gestiti
    @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{

...
  // sessione di iniezione
  @Bean(Session.class)
  protected Session session;
...
  // getter della sessione
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // visualizza la vista posizione
    if(mViewPager.getCurrentItem()!=position){
      // visualizzazione del frammento
      mViewPager.setCurrentItem(position);
    }
  }

  • righe 10-12: il metodo [getSession] esisteva già;
  • righe 15-22: il metodo [navigateToView] fa visualizzare il frammento n. [position];
  • riga 17: si verifica se c'è qualcosa da fare;
  • riga 19: viene visualizzato il frammento n. [position];

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

1.13.6. Modifica della visualizzazione dei frammenti in [MainActivity]

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


    // visualizzazione Vista1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Poiché il metodo [navigateToView] esegue la stessa operazione, si sostituisce questo tipo di istruzione ovunque (in 2 punti) con:

navigateToView(...);

Eseguire quindi l'applicazione. Dovrebbe funzionare come prima.

1.13.7. Conclusione

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

  • un'attività che implementi l'interfaccia [IMainActivity];
  • frammenti che estendono la classe [AbstractFragment], il che li obbliga a implementare il metodo [updateFragment]. Questi devono inoltre disporre di un metodo [@AfterViews] in cui impostano il valore booleano [afterViewsDone] a true;
  • una sessione che incapsula i dati da condividere tra i frammenti e l’attività;

1.14. Esempio-13: Esempio-05 con i frammenti

Nel progetto [Exemple-05] abbiamo introdotto la navigazione tra le viste. Si trattava allora di una navigazione tra attività: 1 vista = 1 attività. Qui ci proponiamo di avere un’unica attività con più viste di tipo [AbstractFragment].

1.14.1. Creazione del progetto

Duplichiamo il progetto precedente [Exemple-12] in [Exemple-13] seguendo la procedura descritta nel paragrafo 1.4. Otteniamo il seguente risultato:

1.14.2. Strutturazione del progetto

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

  • la gestione dell'attività;
  • la gestione dei frammenti;

Per questi creiamo due pacchetti, [exemples.android.activity] e [exemples.android.fragments]:

 

Procediamo allo stesso modo per creare il pacchetto [exemples.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 applicazione. Questo per ricordarci che abbiamo effettuato una scelta architettonica precisa. Successivamente, spostare gli elementi esistenti del progetto come indicato in [9]. Ogni spostamento deve essere confermato cliccando sul pulsante [Refactor].

A questo punto, compilate l’applicazione. In [MainActivity] compaiono 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 interessate dalle righe 15 e 17 non sono state spostate. Sono generate dalla libreria Android Annotations. Per queste classi, è necessario modificare manualmente i imports. Queste righe diventano quindi:

 

Una volta fatto ciò, non ci sono più errori di compilazione. Eseguite l’applicazione. Si ottiene quindi 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 indicano che l’attività designata è [exemples.android.MainActivity_]. Tuttavia, poiché l’attività è stata spostata nel pacchetto [activity], la riga 12 deve ora essere:


      android:name=".activity.MainActivity_"

Attenzione al punto (.) che precede [activity]. Ancora una volta, Android Studio non è riuscito ad aggiornare il manifesto perché questo fa riferimento a una classe 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 né menu;
  • i frammenti [PlaceholderFragment] scompaiono. L'applicazione gestirà due frammenti: [Vue1Fragment], che già abbiamo, e [Vue2Fragment], che dovremo creare;
  • la sessione non è più la stessa;

1.14.3.1. Pulizia dei frammenti

Eliminare 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 {
  // numero di frammenti visitati
  private int numVisit;
  // numero del frammento di tipo [PlaceholderFragment] visualizzato nella seconda scheda
  private int numFragment;

  // getter e setter

  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 conserviamo nulla di questa sessione.

Compilate il progetto. Le righe errate sono quelle che utilizzavano il contenuto della sessione. Eliminatele. Nella classe [Vue1Fragment] eliminate 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 {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

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

  // gestore eventi
  @Click(R.id.buttonValider)
  protected void doValider() {
    // viene visualizzato il nome inserito
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }


  // aggiornamento del frammento
  @Override
  protected void updateFragment() {
  }
}

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

La rimozione delle schede e del pulsante mobile 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 la rimozione del menu avviene in due punti:

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

Il codice della 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>
  • si eliminano le righe [28-31, 41-47];
  • si elimina 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>
  • si elimineranno le righe da 9 a 24. Si lascerà così un'opzione che non verrà utilizzata. Semplicemente per avere un esempio di dichiarazione di un'opzione di menu che potrà essere riprodotta tramite copia/incolla;

Nella classe [MainActivity] si elimina tutto ciò che fa riferimento alle schede, al pulsante mobile, alla barra degli strumenti e al menu. Per individuare questi riferimenti, il modo più semplice è eliminare la loro dichiarazione:


  // gestore delle schede
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // il pulsante galleggiante
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

e ricompilare l’applicazione. Le righe errate sono quelle che fanno riferimento agli elementi rimossi. Eliminare quindi tutte queste righe. Inoltre, modificare il gestore dei frammenti in modo che non faccia più riferimento al frammento [PlaceholderFragment] che abbiamo eliminato:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private AbstractFragment[] fragments;

    // costruttore
    public SectionsPagerAdapter(FragmentManager fm) {
      // genitore
      super(fm);
    }

    // posizione del frammento n.
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // restituisce il numero di frammenti gestiti
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • righe 7-10: è stata eliminata l’intera generazione dei frammenti;

A questo punto, non dovrebbero più esserci errori di compilazione. Nella classe [MainActivity], si è 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 {

  // il contenitore dei frammenti
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// la barra degli strumenti
@ViewById(R.id.toolbar)
protected Toolbar toolbar;

  // sessione di iniezione
  @Bean(Session.class)
  protected Session session;

  // numero di frammenti
  private final int FRAGMENTS_COUNT = 5;
  // adiacenza dei frammenti
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

  // modalità debug
  public static final boolean IS_DEBUG_ENABLED = true;

  // gestore dei frammenti
  private SectionsPagerAdapter mSectionsPagerAdapter;

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

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

    // barra degli strumenti - qui viene visualizzato il nome dell'applicazione
    setSupportActionBar(toolbar);

    // il gestore dei frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // il contenitore dei frammenti è associato al gestore dei frammenti
    // cioè il frammento n. i del contenitore di frammenti è il frammento n. i fornito dal gestore dei frammenti
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // offset dei frammenti
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // si disabilita lo scorrimento tra i frammenti
    mViewPager.setSwipeEnabled(false);

    // senza scorrimento
    mViewPager.setScrollingEnabled(false);

    // visualizzazione Vista1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

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

  // getter sessione
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // visualizzazione della vista "posizione"
    if (mViewPager.getCurrentItem() != position) {
      // visualizzazione del frammento
      mViewPager.setCurrentItem(position);
    }
  }

  // il gestore dei frammenti
  // è a lui che vengono richiesti i frammenti da visualizzare nella vista principale
  // deve definire i metodi [getItem] e [getCount] - gli altri sono facoltativi
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private AbstractFragment[] fragments;

    // costruttore
    public SectionsPagerAdapter(FragmentManager fm) {
      // genitore
      super(fm);
    }

    // numero di posizione del frammento
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // restituisce il numero di frammenti gestiti
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Restano alcune modifiche da apportare:

  • eliminare la riga 31, che non è più necessaria;
  • riga 33: impostare 1 come adiacenza dei frammenti;
  • riga 76: passare alla vista 0. Sarà questa la prima ad essere visualizzata;
  • riga 108: inizializzate l'array con il frammento [Vue1Fragment_]:

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

Abbiamo quindi un solo frammento. Eseguite l'applicazione. Dovreste ottenere il seguente risultato:

Image

Il pulsante [Valider] dovrebbe funzionare.

1.14.4. Creazione dei frammenti e delle viste associate

L'applicazione avrà due viste, quelle del progetto [Exemple-05]. Nel progetto attuale è già presente la vista [vue1.xml]. Ora duplichiamo [vue2.xml] da [Exemple-05] a [Exemple-12] (aprite i due progetti ed eseguite il copia/incolla) tra di essi.

 
  • in [1], la nuova vista. Quando si prova a modificarla, compaiono degli errori in [2]. È necessario 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>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- vista 2 -->
  <string name="btn_vue2">Vue n° 2</string>
  <string name="titre_vue2">Vue n° 2</string>
  <string name="btn_vue1">Vue n° 1</string>
</resources>

Duplichiamo la classe [Vue1Fragment] in [Vue2Fragment]:

  

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

  // aggiornamento frammento
  @Override
  protected void updateFragment() {
  }
}
  • riga 9: il frammento è associato alla vista [res / layout / vue2.xml];
  • riga 10: la classe estende la classe astratta [AbstractFragment];
  • righe 12-20: il metodo obbligatorio [@AfterViews];
  • righe 23-25: metodo obbligatorio [updateFragment];

1.14.5. Implementazione dei frammenti e della navigazione tra di essi

L'attività gestirà d'ora in poi due frammenti. La sua classe [SectionsPagerAdapter] viene modificata come segue:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

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

    ...
}

L'interfaccia [IMainActivity] garantisce la navigazione tra le viste tramite il metodo [navigateToView]. Gestiremo il clic sul pulsante [Vue n° 2] del 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 {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

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

  // gestori di eventi ----------------------------------
  @Click(R.id.buttonValider)
  protected void doValider() {
    // viene visualizzato il nome inserito
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

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

  // aggiornamento del frammento
  @Override
  protected void updateFragment() {
  }
}
  • righe 37-40: il metodo [showVue2] gestisce l'evento «clic» sul pulsante [Vue n° 2];
  • riga 39: la navigazione avviene tramite il metodo [navigateToView] dell’attività. Si ricorda che l’attività è stata memorizzata nella classe padre nella forma:

  // attività
protected IMainActivity mainActivity;

e che tale attività è già stata inizializzata quando si accede a un 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;

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

  // gestori di eventi ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // aggiornamento frammento
  @Override
  protected void updateFragment() {
  }
}
  • righe 24-27: il metodo [showVue1] gestisce l'evento "clic" sul pulsante [Vue n° 1];

Eseguite il progetto e verificate che la navigazione tra le viste funzioni correttamente.

1.14.6. Definizione della sessione

Il funzionamento dell'applicazione è il seguente:

  • inserimento di un nome nella vista n. 1;
  • visualizzazione di tale nome nella vista n. 2;

Affinché la vista n. 1 possa comunicare il nome inserito alla vista n. 2, utilizzeremo la seguente sessione:


package exemples.android.architecture;

import org.androidannotations.annotations.EBean;

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

  // getter e setter
...
}
  • riga 8: il nome inserito;

La classe [MainActivity] inizializzerà la sessione nel modo seguente:


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

1.14.7. Scrittura finale dei frammenti

Nel frammento [Vue1Fragment] modifichiamo il codice del gestore del clic sul pulsante [Valider]:


package exemples.android.fragments;

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

@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // elementi dell'interfaccia visiva
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

...
  // gestori di eventi ----------------------------------

  @Click(R.id.buttonValider)
  protected void doValider() {
    // si memorizza il nome inserito
    String nom = editTextNom.getText().toString();
    // lo si visualizza
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // si inserisce il nome inserito nella sessione
    session.setNom(editTextNom.getText().toString());
    // si passa alla vista n. 2
    mainActivity.navigateToView(1);
  }

  // aggiornamento del frammento
  @Override
  protected void updateFragment() {

  }
}
  • righe: 31-37: si gestisce il clic sul pulsante [Vue n° 2];
  • riga 34: prima di passare alla vista n. 2, inseriamo il nome immesso nella sessione in modo che la nuova vista possa accedervi;

La vista [Vue2Fragment] 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 {

  // componenti dell'interfaccia visiva
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

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

  // gestori di eventi ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // aggiornamento frammento
  @Override
  protected void updateFragment() {
    // si recupera il nome inserito nella sessione
    String nom = session.getNom();
    // lo si visualizza
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Quando viene visualizzata la vista n. 2, è necessario visualizzare il nome inserito nella vista n. 1. Sappiamo che subito dopo la sua visualizzazione verrà eseguito il suo metodo [updateFragment]. È quindi in questo metodo (righe 36-42) che possiamo inserire il codice per la visualizzazione del 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: la dicitura [textViewBonjour] viene modificata;

Eseguite il progetto e verificate che funzioni.

1.14.8. Gestione del ciclo di vita dei frammenti

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


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

Questo metodo è incompleto. Infatti, occorre sempre prevedere il caso in cui il frammento venga riciclato dopo un'operazione [onDestroyView]. In questo caso, la vista del frammento 1 viene rigenerata e il nome che potrebbe essere stato inserito in precedenza scomparirà dalla vista. Non è questo il risultato desiderato. Attualmente il nome inserito rimane visualizzato perché l’adiacenza dei frammenti di 1 fa sì che il ciclo di vita del frammento [Vue1Fragment] venga eseguito una sola volta. È tuttavia preferibile prevedere il caso del riciclaggio del frammento.

Esistono diversi modi per risolvere questo problema:

  • si può sfruttare il fatto che il metodo [update] venga eseguito sistematicamente ogni volta che il frammento viene visualizzato per aggiornare il nome inserito;
  • è possibile effettuare tale aggiornamento solo quando il metodo [@AfterViews] viene rieseguito. È quest’ultima soluzione che adottiamo;

Modifichiamo il codice di [Vue1Fragment] nel modo seguente:


    // elementi dell'interfaccia visiva
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;

    // dati
    private String nom;

    @AfterViews
    protected void afterViews() {
        // memoria
        afterViewsDone = true;
        // registro
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // si (ri)inizializza il testo visualizzato
        editTextNom.setText(nom);
    }

    // gestori di eventi ----------------------------------

...

    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // si annota il nome inserito per poterlo recuperare se il frammento viene riciclato
        nom = editTextNom.getText().toString();
        // si inserisce il nome immesso nella sessione
        session.setNom(nom);
        // si passa alla vista n. 2
        activity.navigateToView(1);
}
  • riga 27: quando ci si appresta a passare dalla vista 1 alla vista 2, si memorizza il nome inserito;
  • riga 17: ad ogni nuova esecuzione del ciclo di vita del frammento, viene visualizzato nuovamente l’ultimo nome inserito;

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


  // componenti dell'interfaccia visiva
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;

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

  // aggiornamento del frammento
  @Override
  protected void updateFragment() {
    // si recupera il nome inserito nella sessione
    String nom = session.getNom();
    // lo si visualizza
    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 nuovamente dimostrato la validità della nostra architettura:

  • un'attività che implementa l'interfaccia [IMainActivity];
  • frammenti che estendono la classe [AbstractFragment], il che li obbliga a implementare il metodo [updateFragment]. Questi devono inoltre disporre di 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.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 [Exemple-12] in [Exemple-13] seguendo la procedura descritta nel paragrafo 1.4. Otteniamo il seguente risultato:

1.15.2. La vista [vue1]

L'applicazione avrà solo una vista [vue1.xml]. Pertanto, eliminiamo l'altra vista [vue2.xml] e il relativo frammento associato:

 

Compilate l'applicazione. In [MainActivity] compaiono alcuni errori:

 

Correggete la riga 4 riportata di seguito nel gestore dei frammenti [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

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

La riga 4 sopra riportata diventa:


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

Eliminare le importazioni ormai superflue [Ctrl-Shift-O]. Non dovrebbero più esserci errori di compilazione. Eseguire il progetto: dovrebbe apparire la vista n. 1. Ora la modificheremo.

Creeremo la vista [vue1.xml] che consentirà di generare numeri casuali:

 

I suoi componenti sono i seguenti:

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

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 le 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>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="list_reponses">Liste des réponses</string>
  <string name="btn_executer">Exécuter</string>
  <string name="aleas">Génération de N nombres aléatoires</string>
  <string name="txt_nbaleas">Valeur de N :</string>
  <string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
  <string name="txt_b">"b : "</string>
  <string name="txt_dummy">Dummy</string>
  <string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
  <string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>

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>
  <!-- colori dell'app -->
  <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é in questo caso è presente un solo frammento, non è necessario prevedere alcuna 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, compilate l'applicazione. Verranno visualizzati degli errori sulle righe che utilizzavano elementi della sessione ora vuota. Eliminate queste righe e verificate che la compilazione non produca più errori.

1.15.4. Il frammento [Vue1Fragment]

  

Modifichiamo il frammento [Vue1Fragment] esistente nel modo seguente:


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 {

  // gli elementi dell'interfaccia visiva
  @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;

  // elenco delle risposte a un comando
  private List<String> reponses = new ArrayList<>();
  // adattatore della listview
  private ArrayAdapter<String> adapterReponses;

  // i campi di immissione
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
    // memoria
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // si nascondono i  messaggi di errore
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
    // si nascondono eventuali messaggi di errore precedenti
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // si verifica la validità dei dati inseriti
    if (!isPageValid()) {
      return;
    }
  }

  // si verifica la validità dei dati inseriti
  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, i metodi [@AfterViews] (righe 46-57) e [udateFragment] (righe 75-81) verranno eseguiti una sola volta all’avvio dell’applicazione;
  • righe 55-56: si nascondono i due messaggi di errore della vista (rappresentati di seguito) [1-2];
 
  • righe 59-60: il metodo eseguito quando si fa clic sul pulsante [Exécuter];
  • righe 71-73: si verifica la validità dei dati inseriti;

Il metodo [isPageValid] è il seguente:


  // i dati inseriti
  private int nbAleas;
  private int a;
  private int b;

...

// si verifica la validità dei dati inseriti
  private boolean isPageValid() {
    // inserimento del numero di numeri casuali
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // errore?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // inserimento di a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // errore?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // inserimento di b
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // errore?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // torna indietro
    return (nbErreurs == 0);
  }

  • righe 2-4: questi tre campi vengono inizializzati dal metodo [isPageValid]. Inoltre, questo metodo restituisce true se tutti i dati inseriti sono validi, false in caso contrario. Se alcuni dati inseriti non sono validi, vengono visualizzati i relativi messaggi di errore;

A questo punto, l’applicazione è eseguibile. Verificare il funzionamento del metodo [isPageValid] inserendo dati errati.

1.15.5. Il livello [métier]

  

Il livello [métier] presenta 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]. È stato inoltre previsto che, una volta su tre, restituisca un'eccezione, anch'essa inclusa nelle risposte fornite dal metodo. Alla fine, il metodo restituisce un elenco 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) {
        // l'elenco degli oggetti
        List<Object> réponses = new ArrayList<Object>();
        // alcune verifiche
        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"));
        }
        // errore?
        if (réponses.size() != 0) {
            return réponses;
        }
        // si generano i numeri casuali
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // si genera un'eccezione casuale 1 volta su 3
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // altrimenti si restituisce un numero casuale compreso tra due limiti [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // risultato
        return réponses;
    }
}
  • riga 9: si utilizza l'annotazione AA [@EBean] sulla classe [Metier] per poter inserire i riferimenti di quest'ultima nel livello [Présentation]. L'attributo (scope = EBean.Scope.Singleton) fa sì che la classe [Metier] venga istanziata in un unico esemplare. Viene quindi sempre iniettato lo stesso riferimento se lo si inietta più volte nel livello [Présentation];
  • 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], il che la rende un'eccezione non controllata: non è necessario gestirla in un try/catch, né inserirla nella firma dei metodi;

1.15.6. L'attività [MainActivity] rivisitata

  

Livello

[metier]

Attività

Vista

Utente

L'attività implementerà l'interfaccia [IMetier] del livello [métier]. Pertanto, il frammento/la vista avrà come unico interlocutore l'attività.

L'attività [MainActivity] implementa già l'interfaccia [IMainActivity]. Affinché implementi anche l'interfaccia [IMetier], è possibile:

  • aggiungere l’interfaccia [IMetier] alle interfacce implementate dall’attività;
  • fare in modo che l'interfaccia [IMainActivity] estenda a sua volta l'interfaccia [IMetier]. È questa la strada che seguiamo;

L'interfaccia [IMainActivity] diventa la seguente:

  

package exemples.android.architecture;

import exemples.android.metier.IMetier;

public interface IMainActivity extends IMetier {

  // accesso alla sessione
  Session getSession();

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

  // modalità debug
  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 {

  ...

  // iniezione della sessione
  @Bean(Session.class)
  protected Session session;

  // iniezione business
  @Bean(Metier.class)
  protected IMetier metier;

...
  // implementazione IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • righe 11-12: il livello [métier] viene inserito nell'attività. A tal fine si utilizza l’annotazione AA [@Bean], il cui parametro è la classe che porta l’annotazione AA [@EBean];
  • riga 2: l'attività implementa l'interfaccia [IMainActivity] e quindi l'interfaccia [IMetier] del livello [métier];
  • righe 16-19: implementazione dell'unico metodo dell'interfaccia [IMetier]. Ci si limita a delegare la chiamata al livello [métier];

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 {

  // elementi dell'interfaccia visiva
  @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;

  // elenco delle risposte a un comando
  private List<String> reponses = new ArrayList<>();
  // adattatore della listview
  private ArrayAdapter<String> adapterReponses;

  // i dati inseriti
  private int nbAleas;
  private int a;
  private int b;

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

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

  // si verifica la validità dei dati inseriti
  private boolean isPageValid() {
   ...
  }

  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // verrà eseguito una sola volta all'avvio dell'applicazione
    // si crea l'adattatore di ListView - a tal fine è necessario che la variabile [activity] sia stata inizializzata
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • righe 69-70: si imposta l'adattatore del componente di tipo [ListView];

Il componente [ListView] serve a visualizzare un elenco di elementi. Lo fa tramite un adattatore di tipo [ListAdapter], a sua volta collegato alla fonte di dati che deve alimentare il [ListView]. Per definire l’adattatore di un [ListView], è disponibile il seguente metodo [ListView.setAdapter]:


public void setAdapter (ListAdapter adapter)

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


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] è l’attività che visualizza [ListView];
  • [resource] è il numero intero che identifica la vista utilizzata per visualizzare un elemento del [ListView]. Questa vista può avere qualsiasi livello di complessità. È lo sviluppatore a costruirla 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 dal [ListView]. Il metodo [toString] degli oggetti viene utilizzato per visualizzare l’oggetto nella vista [TextView] identificata da [textViewResourceId] nella vista identificata da [resource].

Il compito dello sviluppatore è quello di creare la vista [resource] che visualizzerà ogni elemento del [ListView]. Nel caso semplice in cui si desideri visualizzare solo una semplice 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 di [ListView]. Questo adattatore deve essere definito una sola volta. Per consentirne il riutilizzo, è stato definito come variabile di istanza della classe (riga 39). Rivediamo la 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 in un frammento tramite [getActivity] e che qui è stata memorizzata nella variabile [activity] della classe padre. Questo campo non ha sempre un valore. Pertanto, i log mostrano che quando si entra nel metodo [@AfterViews], esso non è ancora stato inizializzato e quindi non è possibile inserire le righe 69-70 in questo metodo. Nel metodo [updateFragment] ciò è possibile poiché si sa che, quando questo metodo viene eseguito, è necessariamente presente [activity!=null]. L’adattatore è qui associato alla fonte dati [reponses] definita alla riga 37;

Il metodo [doExecuter] gestisce il clic sul pulsante [Exécuter]. Il suo codice è il seguente:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // si nascondono eventuali messaggi di errore precedenti
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // si cancellano le risposte precedenti
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // si verifica la validità dei dati inseriti
    if (!isPageValid()) {
      return;
    }
    // si richiedono i numeri casuali all'attività
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // si crea un elenco di stringhe a partire da questi dati
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // si aggiorna la ListView
    adapterReponses.notifyDataSetChanged();
  }
  • righe 7-8: si desidera svuotare il ListView. A tal fine, si svuota la fonte dati [reponses] e si richiede all'adattatore associato al ListView di aggiornarsi;
  • righe 10-12: prima di eseguire l’azione richiesta, si verifica che i valori inseriti siano corretti;
  • riga 14: si richiede all’attività l’elenco dei numeri casuali. Si ottiene un elenco di oggetti in cui ogni oggetto è di tipo [Integer] o [AleaException];
  • righe 16-22: a partire dall'elenco di oggetti ottenuto, si aggiorna la fonte dati [reponses] visualizzata da ListView;
  • riga 24: si richiede all'adattatore di ListView di aggiornarsi;

1.15.8. Esecuzione

Eseguire il progetto e verificarne il corretto funzionamento.

1.16. Esempio 15: architettura client/server

Affrontiamo un'architettura comune per un'applicazione Android, quella in cui l'applicazione Android comunica con servizi web remoti. Avremo ora la seguente architettura:

All’applicazione Android è stato aggiunto un livello [DAO] per comunicare con il server remoto. Esso comunicherà con il server che genera i numeri casuali visualizzati dal tablet Android. Questo server avrà la seguente architettura a due livelli:

I clienti interrogano alcuni URL del livello [web / jSON] e ricevono una risposta testuale nel formato jSON (JavaScript Object Notation). In questo caso, il nostro servizio web elaborerà un unico URL di tipo [/a/b] che restituirà un numero casuale compreso nell'intervallo [a,b]. Descriveremo l'applicazione nel seguente ordine:

Il server

  • il suo livello [métier];
  • il suo servizio [web / jSON] implementato con Spring MVC;

Il client

  • il suo livello [DAO]. Non ci sarà alcun livello [métier];

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/]. Accediamo al sito [http://start.spring.io/] (giugno 2016) che ci consentirà di generare un progetto Gradle con le dipendenze necessarie al nostro progetto, che non è un progetto Android e per la cui realizzazione Android Studio non offre alcun supporto:

  • in [1]: scegliete un progetto Gradle;
  • in [2-3]: le caratteristiche della dipendenza jar generata dal progetto (vedi sotto);
  • in [4]: selezionare la dipendenza web [5] affinché siano disponibili i binari necessari al nostro servizio web;
  • in [6]: generare il progetto. Verrà quindi generato il file zip di un progetto Gradle di base, che sarà disponibile per il download;

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

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // A partire dalla versione 0.11 del plugin Gradle per Android, è necessario utilizzare 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 si presenta nella forma [groupId:artifactId:version]. Ciò che viene richiesto nel modulo del sito [http://start.spring.io/]:
    • in [2] è [groupId];
    • in [3] è [artifactId];

Decomprimere nella cartella degli altri progetti il file zip sopra indicato:

Con Android Studio, apri il progetto Gradle [server-01] [1-2]. Il progetto aperto è [3] (prospettiva Project).

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 riguardano IDE Eclipse. Le eliminiamo;
  • le righe da 1 a 11 e la riga 15 servono ad aggiungere un plugin denominato [spring-boot] al nostro progetto Gradle. Spring Boot è un progetto dell’ecosistema Spring [http://projects.spring.io/spring-boot/]. Questo plugin definisce le versioni delle dipendenze più comunemente utilizzate con Spring. Ciò consente di non specificarne le versioni (righe 30 e 31). La versione è quindi quella definita dalla versione di Spring Boot utilizzata (riga 3);
  • righe 22-23: la versione di Java da utilizzare, in questo caso la versione 1.8;
  • righe 25-27: i repository di binari da utilizzare per scaricare le dipendenze;
  • riga 26: indica il repository centrale di Maven. Attualmente è il più grande repository di binari open source disponibile;
  • righe 29-32: le dipendenze necessarie al progetto:
  • riga 30: questa dipendenza include tutti i file binari necessari per creare un servizio web Spring;
  • riga 31: questa dipendenza include tutti i file binari necessari per i test, in particolare per i test JUnit;
  • una dipendenza [compile] indica che tale dipendenza è necessaria per la compilazione del progetto. Una dipendenza [testCompile] indica che tale dipendenza è necessaria esclusivamente per l'esecuzione dei test. In tal caso, non viene inclusa nel binario del progetto;

Eseguiamo una prima pulizia del file Gradle:


// Spring Boot
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}

// plugin
apply plugin: 'java'
apply plugin: 'spring-boot'

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

// versioni Java
sourceCompatibility = 1.8
targetCompatibility = 1.8

// repository Maven
repositories {
  mavenLocal()
  mavenCentral()
}

// dipendenze
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 della postazione di sviluppo. Questo viene creato al momento dell’installazione di Maven (cfr. paragrafo 6.10). Se la dipendenza richiesta è già presente nel repository Maven locale, non verrà richiesta al repository Maven centrale;
  • righe 19-22: un'attività Gradle che consente di generare il binario del progetto. La useremo per vedere cosa viene fatto;
  • in [1-4], eseguite l'attività [jar] definita nel file [build.gradle] ([1] si trova in alto a destra e accanto a IDE);

L'operazione precedente crea l'archivio jar del progetto e lo inserisce nella cartella [build / libs] [5]:

  

Il nome dell'archivio deriva direttamente dalle informazioni fornite all'attività [jar] nel file [build.gradle] (righe 19-22).

L'insieme delle dipendenze del progetto può essere visualizzato come segue:

 

Si può notare in [1] che 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 ha incluso le dipendenze di cui un'applicazione web Spring MVC avrà probabilmente bisogno. Ciò significa che alcune potrebbero essere superflue. Spring Boot è l'ideale per un tutorial:

  • fornisce le dipendenze di cui probabilmente avremo bisogno;
  • fornisce un server Tomcat integrato [1], il che ci evita di dover distribuire l’applicazione su un server web esterno;

Sul sito dell’ecosistema Spring [http://spring.io/guides] si trovano numerosi esempi che utilizzano Spring Boot.

Ora completiamo il file [build.gradle] nel modo seguente:


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

// plugin per creare un binario conforme agli standard Maven nel repository Maven locale
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 {
      // modificare per puntare al proprio repository, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • riga 10: si importa un plugin Gradle denominato [maven-publish] che consente di pubblicare il binario del progetto in un repository Maven nel rispetto degli standard Maven;
  • riga 11: un'attività Gradle denominata [publishing];
  • righe 14-15: le caratteristiche del binario Maven che verrà creato;
  • riga 23: il repository Maven su cui verrà pubblicato, in questo caso un repository Maven locale;

L'aggiunta del plugin [maven-publish] ha creato nuove attività nel progetto Gradle:

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

 

L'attività [jar] consente di generare il file binario del progetto. Questo file binario è privo delle sue dipendenze e quindi non eseguibile. È possibile generare un file binario con tutte le sue dipendenze e che sia eseguibile. A tal fine, aggiungiamo al file [build.gradle] il seguente codice:


// creare un binario con tutte le sue dipendenze
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: occorre inserire il nome completo della classe eseguibile del progetto:
  

Il codice di 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);
    }
}

Aggiornare il progetto Gradle, quindi eseguire l'attività [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. È necessario configurare anche il progetto. Poiché non si tratta di un progetto Android generato da IDE, questa configurazione, che finora non era stata effettuata, deve essere eseguita in questa fase.

 
  • in [3-4]: utilizzare una versione 1.8 di JDK;

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

Di seguito, il lettore è invitato a creare il progetto riportato di seguito. Commentiamo il codice finale del progetto [3].

1.16.1.4. Il livello [métier]

  

Il livello [métier] riprende lo spirito del livello [métier] dell’esempio precedente. Avrà la seguente interfaccia [IMetier]:


package exemples.android.server.metier;

public interface IMetier {
  // numero casuale in [a,b]
    int getAlea(int a, int b);
}
  • riga 5: il metodo che genera un numero casuale in [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) {
    // alcune verifiche
    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);
    }
    // generazione del risultato
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Non commentiamo la classe: è analoga a quella vista nell'esempio precedente, tranne per il fatto che non genera eccezioni in modo casuale. Si noti semplicemente, alla riga 8, l'annotazione Spring [@Service] che farà sì che Spring istanzi la classe in un unico esemplare (singleton) e renda il suo riferimento disponibile per altri componenti Spring. Altre annotazioni Spring avrebbero potuto essere utilizzate qui 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 dalla classe con la prima lettera minuscola. Pertanto, in questo caso, il componente Spring porta per impostazione predefinita il nome [metier];

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


package exemples.android.server.metier;

public class AleaException extends RuntimeException {

  // codice di errore
  private int code;

  // costruttori
  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;
  }

  // getter e setter
....
}
  • riga 3: [AleaException] estende la classe [RuntimeException]. Si tratta quindi di un'eccezione non controllata (non è obbligatorio gestirla con un try/catch);
  • riga 6: alla classe [RuntimeException] viene aggiunto un codice di errore;

1.16.1.5. Il servizio web / jSON

 
  

Il servizio web / jSON è implementato da Spring MVC. Spring MVC implementa il modello di architettura denominato MVC (Modello – Vista – Controller) nel modo seguente:

L’elaborazione di una richiesta da parte di un cliente avviene nel modo seguente:

  1. richiesta – le URL richieste hanno la forma http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... La [Dispatcher Servlet] è la classe di Spring che gestisce le URL in entrata. Essa "instradano" l'URL verso l'azione che deve elaborarla. Queste azioni sono metodi di classi specifiche denominate [Contrôleurs]. La parte "C" di MVC è in questo caso la stringa [Dispatcher Servlet, Contrôleur, Action]. Se non è stata configurata alcuna azione per gestire l’URL in entrata, il servlet [Dispatcher Servlet] risponderà che l’URL richiesto non è stato trovato (errore 404 NOT FOUND);
  1. elaborazione
  • l'azione selezionata può utilizzare i parametri parami che il servlet [Dispatcher Servlet] le ha trasmesso. Questi possono provenire da diverse fonti:
    • dal percorso [/param1/param2/...] dell’URL,
    • dai parametri [p1=v1&p2=v2] del URL,
    • dai parametri inviati dal browser insieme alla sua richiesta;
  • nell'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere il livello [metier] [2b]. Una volta elaborata la richiesta del cliente, 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 richiede la visualizzazione di una determinata vista [3]. Questa vista visualizzerà i dati denominati modello della vista. Si tratta della M di MVC. L’azione creerà questo modello M [2c] e richiederà la visualizzazione di una vista V [3];
  1. risposta: la vista V selezionata utilizza il modello M creato dall’azione per inizializzare le parti dinamiche della risposta HTML che deve inviare al client, quindi invia tale risposta.

Per un servizio web / jSON, l’architettura precedente viene leggermente modificata:

  • in [4a], il modello, che è una classe Java, viene trasformato 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 nella stringa jSON e di deserializzazione di una stringa jSON in un oggetto Java è riportato negli allegati al paragrafo 6.14.

Torniamo al livello [web] della nostra applicazione:

Nella nostra applicazione è presente 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> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getter e setter
...
}
  • riga 13: il campo [T body] è la risposta attesa dal cliente. 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 cliente, il server può 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 della pila delle eccezioni se si è 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 {

  // livello business
  @Autowired
  private IMetier metier;
  // mappatore JSON
  @Autowired
  private ObjectMapper mapper;

  // numeri casuali
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
  public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {

    // la risposta
    Response<Integer> response = new Response<>();
    // si utilizza il livello di business
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // si restituisce la risposta
    return mapper.writeValueAsString(response);
  }

  private List<String> getMessagesFromException(Throwable e) {
    // elenco dei messaggi
    List<String> messages = new ArrayList<String>();
    // si esamina lo stack delle eccezioni
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // si restituisce il risultato
    return messages;
  }

}
  • riga 17: l'annotazione [@Controller] indica che la classe è un controller MVC i cui metodi gestiscono le richieste per alcune URL dell'applicazione web;
  • righe 21-22: l'annotazione [@Autowired] richiede a Spring di iniettare nel campo un componente di tipo [IMetier]. Si tratterà della precedente classe [Metier]. Poiché abbiamo applicato a quest’ultima l’annotazione [@Service], essa viene gestita 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 sotto forma di stringa jSON. Sarà questo mappatore a eseguire 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 della riga 28;
  • riga 28: l'annotazione [@RequestMapping] definisce alcune proprietà del metodo annotato:
    • [value]: il 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 cliente desidera allegare un documento alla propria richiesta HTTP;
    • [produces]: imposta una delle intestazioni della risposta HTTP che verrà inviata al cliente. In questo caso, tra le intestazioni HTTP inviate con la risposta al cliente, ce ne sarà una che gli comunicherà che la risposta gli viene inviata sotto forma di stringa jSON. Questa intestazione non è obbligatoria. Viene fornita a titolo informativo al cliente qualora quest’ultimo si aspetti risposte che possono assumere diverse forme;
    • [consumes]: non è presente in questo caso. Serve a indicare le intestazioni HTTP che devono accompagnare la richiesta HTTP del cliente affinché venga accettata;
  • riga 29: l'annotazione [@ResponseBody] indica che il risultato prodotto dal metodo deve essere inviato al cliente. Senza questa annotazione, la risposta del metodo è considerata come una chiave che consente di selezionare la pagina HTML da inviare al cliente. In un servizio web / jSON non sono presenti pagine HTML;
  • riga 28: l’URL elaborata ha la 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 una URL e sono quindi di tipo String. La conversione da String al tipo dei parametri potrebbe non riuscire. In tal caso, Spring MVC genera un'eccezione. Riassumiamo: se con un browser richiedo l’URL /100/200, il metodo getAlea della riga 30 verrà eseguito con i parametri interi a=100, b=200;
  • riga 36: si richiede al livello [métier] un numero casuale nell’intervallo [a,b]. Si ricorda che il metodo [metier].getAlea può generare un'eccezione;
  • riga 37: nessun errore;
  • riga 39: codice di errore;
  • riga 40: l’elenco dei messaggi della risposta corrisponde a quello dello stack delle eccezioni (righe 46-57). In questo caso, sappiamo che lo stack contiene una sola eccezione, ma abbiamo voluto mostrare un metodo più generico;
  • riga 43: la risposta di tipo [Response<Integer>] viene restituita sotto forma di stringa jSON;

1.16.1.6. Configurazione del progetto Spring

  

Esistono diversi modi per configurare Spring:

  • con i file XML;
  • con codice Java;
  • con una combinazione dei due;

Abbiamo scelto di configurare la nostra applicazione web con codice Java. È la seguente classe [Config] a garantire 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 {
  // configurazione web ------------------------------------
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet();
    return servlet;
  }

  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }

  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }

  // mappatore jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }

}
  • riga 12: si indica 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 come [@Controller], nel pacchetto [exemples.android.server.web];
  • riga 13: l'annotazione [@EnableWebMvc] consente a Spring Boot di effettuare autonomamente una serie di configurazioni standard per un'applicazione Spring MVC. Ciò alleggerisce di conseguenza il carico di lavoro dello sviluppatore;
  • righe 16, 22, 27 e 33: anche l’annotazione [@Bean] definisce componenti (bean) Spring, proprio come le due annotazioni incontrate in precedenza (@Service, @Controller). In questo caso l’annotazione [@Bean] annota un metodo e non una classe, ed è il risultato del metodo a costituire il componente Spring. In assenza di un attributo di denominazione all’interno dell’annotazione [@Bean], il componente Spring creato prende il nome dal metodo annotato;
  • righe 16-20: definiscono il bean [dispatcherServlet]. Si tratta di un nome predefinito di 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] dell’applicazione Spring MVC;
  • riga 18: il bean [dispatcherServlet] è un'istanza della classe [DispatcherServlet] fornita da Spring MVC;
  • righe 22-25: il bean [servletRegistrationBean] serve a definire quali URL sono accettati dall'applicazione. Alla riga 24, vengono accettati tutti i URL;
  • righe 27-30: il bean [embeddedServletContainerFactory] serve a definire il server incorporato nelle dipendenze del progetto che deve ospitare l'applicazione web. La riga 29 indica che si tratta di un server Tomcat e che questo funzionerà sulla porta 8080. Per impostazione predefinita, i file binari di questo server web sono forniti dalla dipendenza [org.springframework.boot:spring-boot-starter-web] del file Gradle;

1.16.1.7. Esecuzione del servizio web / jSON

  

Il progetto viene eseguito a partire 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) {
    // esecuzione dell'applicazione
    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, la classe [Config] che abbiamo appena descritto. Il secondo parametro è l'array di argomenti passato al metodo [main] (riga 7);

È possibile avviare l’applicazione web in vari modi, tra cui il seguente:

 

Nella console compaiono quindi una serie 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 [/**] su handler di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler di tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • righe 12-14: viene avviato il server Tomcat integrato;
  • righe 15-19: viene caricata e configurata la servlet [DispatcherServlet] di Spring MVC;
  • riga 20: viene rilevato il URL [/{a}/{b}] del server web;

Ora apriamo un browser e proviamo l'URL del servizio web / jSON:

Otteniamo ogni volta la rappresentazione jSON di un oggetto di tipo [Response<Integer>].

Invece di utilizzare un browser standard, prendiamo ora l’estensione [Advanced Rest Client] del browser Chrome (vedi allegati, paragrafo 6.13):

Image

  • in [1], l’URL richiesto;
  • in [2], tramite un GET;
  • in [3], si invia la richiesta;

Image

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

1.16.1.8. Generazione del file jar eseguibile del progetto

Nel paragrafo 1.16.1.2 abbiamo illustrato come configurare il file Gradle per generare un eseguibile dell’applicazione con tutte le sue dipendenze. Adattata all’applicazione in questione, questa configurazione diventa la seguente:


// creare un binario con tutte le sue dipendenze
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, è possibile procedere come segue [1-5]:

Per eseguirlo, si arresta il servizio web se è in esecuzione [1], quindi si esegue l’archivio [2-4]:

 

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

1.16.1.9. Gestione dei log

Quando si esegue l’archivio eseguibile, si nota che i log non sono gli stessi di quelli generati eseguendo il progetto dal file IDE. I log in modalità [DEBUG] sono i seguenti:


...
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 dei log aggiungendo un file [logback.xml] nella cartella [resources] del progetto:

  

Questo file potrebbe avere il seguente contenuto:


<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!--viene assegnato per impostazione predefinita il tipo
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- controllo del livello dei log -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Il livello dei log viene controllato alla riga 12. Se ora si rigenera l'archivio eseguibile e lo si esegue, si ottengono 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 del server web / jSON

Il client Android avrà la seguente struttura:

Il client avrà due componenti:

  1. un livello [Présentation] (vista+attività) analogo a quello che abbiamo esaminato nell'esempio [Exemple-14];
  2. il livello [DAO] che si interfaccia con il servizio [web / jSON] che abbiamo esaminato in precedenza.

1.16.2.1. Creazione del progetto

Duplichiamo il progetto precedente [Exemple-14] in [Exemple-15] seguendo la procedura descritta nel paragrafo 1.4. Otteniamo il seguente risultato:

Di seguito, il lettore è invitato a creare il progetto riportato di seguito.

1.16.2.2. Configurazione Gradle

 

Il file [build.gradle] è il seguente:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // A partire dalla versione 0.11 del plugin Gradle per Android, è necessario utilizzare 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'
    }
  }

  // opzioni di packaging necessarie per poter generare il file 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'
  }
}

Commentiamo solo ciò che non è già stato trattato:

  • righe 46-47: inserimento di un plugin AA. Il plugin [rest-spring-api] consente di delegare alla libreria AA le comunicazioni client/server;
  • riga 50: la libreria [spring-android-rest-template] è quella utilizzata da AA per garantire gli scambi client/server. La versione [2.0.0.M3] è una versione cosiddetta «milestone» che non si trova nei consueti repository Maven. Pertanto, alle righe 56-59, è necessario specificare il repository da utilizzare (riga 58) per trovare la libreria;
  • riga 51: una libreria jSON;
  • righe 33-39: senza questa proprietà, compaiono degli errori al momento della generazione del binario APK del progetto;

1.16.2.3. Il manifesto dell'applicazione Android

  

Il file [AndroidManifest.xml] deve essere modificato. Infatti, per impostazione predefinita, gli accessi a Internet sono disabilitati. È necessario attivarli tramite 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: gli accessi a Internet sono consentiti;

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 {

  // numero casuale
  int getAlea(int a, int b);

  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // tempo di attesa massimo (ms) per la risposta del server
  void setTimeout(int timeout);

  // tempo di attesa in millisecondi del client prima della richiesta
  void setDelay(int delay);

}
  • riga 6: il metodo del servizio web / jSON per ottenere un numero casuale nell’intervallo [a,b] di questo servizio web;
  • riga 9: l'URL del servizio web / jSON per la generazione di numeri casuali;
  • riga 12: si imposta un tempo di attesa massimo per la risposta del server;
  • riga 15: si desidera impostare un tempo di attesa prima dell’esecuzione della 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] si occupa di comunicare 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 numero casuale nell'intervallo [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 grazie alle annotazioni che vi inseriremo. Questa interfaccia deve implementare le chiamate a URL esposte dal servizio web / jSON:

  // numero casuale
  @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. In questo caso, il convertitore [MappingJackson2HttpMessageConverter.class] fa sì che, quando il server invia una stringa jSON, questa venga automaticamente deserializzata. Si vede quindi nella riga (d) che URL [/{a}/{b}] restituisce un tipo String che è in realtà una stringa jSON (riga b). Con queste informazioni e quelle relative al tipo atteso alla riga 16, l’istanza [WebClient] del client deserializzerà la stringa che riceverà in un tipo [Response<Integer>];
  • riga 15: un'annotazione AA che indica che URL deve essere chiamata con un metodo HTTP GET. Il parametro dell'annotazione [@Get] è il formato di URL previsto dal servizio web. È sufficiente riprendere il parametro [value] dall’annotazione [@RequestMapping] (riga b) del metodo chiamato nel controller [WebController] del server. Le parentesi graffe {} racchiudono i parametri di URL che devono essere ripresi nei parametri del metodo alla riga 16. La sintassi [@Path("a") int a] fa sì che il parametro [a] del metodo venga assegnato al valore {a} di URL. Quando il parametro di URL e quello del metodo hanno lo stesso nome, come in questo caso, è possibile 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] che indica il valore inviato. Questo verrà automaticamente serializzato in jSON. Sul lato server, si avrà la seguente firma:


  // numeri casuali
  @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: si specifica che ci si aspetta una richiesta HTTP POST e che il corpo di tale richiesta (oggetto inviato) deve essere trasmesso sotto forma di una 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. Ciò si ottiene estendendo l’interfaccia [RestClientRootUrl] fornita da AA. Questa interfaccia espone un metodo [setRootUrl(urlServiceWeb] che consente di impostare l’URL del servizio web da contattare;
  • inoltre, vogliamo controllare la chiamata al servizio web poiché intendiamo limitare il tempo di attesa della risposta. A tal fine, estendiamo l’interfaccia [RestClientSupport] che espone il metodo [setRestTemplate] che ci consentirà di:
    • creare noi stessi l'oggetto [RestTemplate], che serve a gestire gli scambi client/server;
    • configurare tale oggetto per impostare il tempo massimo di attesa della risposta;

1.16.2.4.3. La classe [Response]

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


package exemples.android.dao;

import java.util.List;

public class Response<T> {

    // ----------------- proprietà
    // stato dell'operazione
    private int status;
    // eventuali messaggi di errore
    private List<String> messages;
    // il corpo della risposta
    private T body;

    // costruttori
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getter e setter
...
}

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

La comunicazione di rete tra client e server, così come la serializzazione e la 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 con la 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 {

  // cliente del servizio REST
  @RestService
  protected WebClient webClient;

  // mappatore jSON
  private ObjectMapper mapper = new ObjectMapper();
  // tempo di attesa prima dell'esecuzione della richiesta
  private int delay;

// interfaccia 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 potremo iniettare altrove;
  • righe 19-20: iniettiamo l'implementazione dell'interfaccia [WebClient] che abbiamo descritto. È l'annotazione [@RestService] che garantisce 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) {
    // si imposta il timeout delle richieste del client REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // si crea il restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // si imposta il convertitore jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // si imposta il restTemplate del client web
    webClient.setRestTemplate(restTemplate);
}
  • l'interfaccia [WebClient] verrà implementata da una classe AA che utilizza la dipendenza Gradle [org.springframework.android:spring-android-rest-template]. [spring-android-rest-template] implementa il dialogo tra il client e il server web / jSON tramite una classe di tipo [RestTemplate];
  • riga 4: la classe [SimpleClientHttpRequestFactory] è fornita dalla dipendenza [spring-android-rest-template]. Ci consentirà di impostare il tempo massimo di attesa per la risposta del server (righe 5-6);
  • riga 8: creiamo l’oggetto di tipo [RestTemplate] che fungerà da supporto per la comunicazione con il servizio web. Gli passiamo come parametro l’oggetto [factory] appena creato;
  • riga 10: il dialogo client/server può assumere diverse forme. Gli scambi avvengono tramite righe di testo e dobbiamo indicare all’oggetto di tipo [RestTemplate] cosa deve fare con quella riga di testo. A tal fine, gli forniamo dei convertitori, ovvero classi in grado di elaborare le righe di testo. La scelta del convertitore avviene in genere tramite le intestazioni HTTP che accompagnano la riga di testo. In questo caso, sappiamo che riceviamo esclusivamente righe di testo nel formato jSON. Inoltre, come visto nel paragrafo 1.16.1.7, il server inviava l’intestazione HTTP:

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

Alla riga 10, l’unico convertitore per [RestTemplate] sarà un convertitore jSON implementato con la libreria [Jackson]. C'è una stranezza riguardo a questi convertitori: AA ci impone di includerlo anche nell'annotazione del client web [WebClient]:


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

Alla riga 1, siamo obbligati a specificare un convertitore anche se lo specifichiamo già a livello di programmazione.

  • riga 12: l’oggetto [RestTemplate] così costruito viene inserito nell’implementazione dell’interfaccia [WebClient] ed è proprio questo oggetto che gestirà il dialogo client/server;

Metodo [getAlea]

Il metodo [getAlea] è il seguente:


  @Override
  public int getAlea(int a, int b) {
    // esecuzione del servizio
    Response<Integer> info;
    DaoException ex;
    try {
      // in attesa
      waitSomeTime(delay);
      // esecuzione del servizio
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // restituzione del risultato
        return info.getBody();
      } else {
        // si registra l'eccezione
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // si registra l'eccezione
      ex = new DaoException(e, 100);
    }
    // si genera l'eccezione
    throw ex;
  }
...
  // metodi privati -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • riga 8: si attendono [delay] millisecondi;
  • riga 10: ci si limita a chiamare il metodo con la stessa firma nella classe che implementa l'interfaccia [WebClient];
  • riga 11: si analizza la risposta ottenuta dal server controllandone il [status];
  • righe 12-14: se non si è verificato alcun errore lato server (status=0), si restituisce il risultato del metodo;
  • riga 17: se si è verificato un errore lato server (status!=0), si prepara un'eccezione senza lanciarla. Il server ha trasmesso un elenco di messaggi di errore. Si crea un'eccezione con, come unico messaggio, la stringa jSON presente nell'elenco dei messaggi del server;
  • righe 19-22: altri casi di eccezione;
  • riga 24: quando si arriva a questo punto, si è verificata necessariamente un'eccezione. Quindi la si genera;

L’eccezione [DaoException] utilizzata da questo codice è la seguente:


package exemples.android.dao;

import java.util.ArrayList;
import java.util.List;

public class DaoException extends RuntimeException {

  // codice di errore
  private int code;

  // costruttori
  public DaoException() {
  }

  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  // getter e setter
...
}
  • riga 6: l'eccezione [DaoException] è un'eccezione non controllata;

Metodo [setUrlServiceWebJson]

Il metodo [setUrlServiceWebJson] è il seguente:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // si imposta il URL del servizio REST
    webClient.setRootUrl(urlServiceWebJson);
}
  • riga 4: si imposta l'URL del servizio web tramite il metodo [setRootUrl] dell'interfaccia [WebClient]. Questo metodo esiste proprio perché tale interfaccia estende l’interfaccia [RestClientRootUrl];

1.16.2.5. Il pacchetto [architecture]

Il pacchetto [architecture] raggruppa 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 {

  // accesso alla sessione
  Session getSession();

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

  // attesa
  void beginWaiting();

  void cancelWaiting();

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;
  // tempo di attesa della risposta
  int TIMEOUT = 1000;
  // adiacenza dei frammenti
  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 l'immagine di attesa dell'applicazione (righe 14, 16);
  • riga 21: si imposta un tempo massimo di attesa per la risposta del server a 1 secondo;

1.16.2.5.2. La classe [Utils]

Nella classe [Utils] sono stati raggruppati metodi di utilità statici che possono essere richiamati da diverse parti dell’architettura dell’applicazione:


package exemples.android.architecture;

import java.util.ArrayList;
import java.util.List;

public class Utils {

  // elenco dei messaggi di un'eccezione - versione 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // si crea un elenco con i messaggi di errore della pila delle eccezioni
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // elenco dei messaggi di un'eccezione - versione 2
  static public String getMessagesForAlert(Throwable th) {
    // si costruisce il testo da visualizzare
    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--;
    }
    // risultato
    return texte.toString();
  }

}
  • righe 9-18: crea l'elenco dei messaggi di errore contenuti in un Throwable;
  • righe 21-32: si basa sul metodo precedente per costruire, a partire dall'elenco di messaggi ottenuto, il testo da visualizzare in un messaggio di avviso Android;
  • righe 27-28: i messaggi sono numerati. Il numero più basso (1) corrisponde all’eccezione iniziale e il numero più alto all’eccezione più recente nella pila delle eccezioni;

1.16.2.5.3. La classe astratta [AbstractFragment]

La classe [AbstractFragment] ha due funzioni:

  1. garantire che il metodo [updateFragments] delle classi figlie venga sempre chiamato durante la visualizzazione del frammento e che ciò avvenga una sola volta;
  2. fattorizzare lo stato e i metodi delle classi figlie che possono essere fattorizzati;

È proprio la funzione 2 che ci porta a inserire in questa classe le operazioni di gestione dell’immagine di attesa: tutti i frammenti di un’applicazione Android asincrona devono gestire questo tipo di problematica:


  // gestione dell'attesa
  protected void beginWaiting() {
    // viene visualizzata la clessidra
    mainActivity.beginWaiting();
  }

  protected void cancelWaiting() {
    // si rimuove la clessidra
    mainActivity.cancelWaiting();
}

1.16.2.6. La vista

1.16.2.6.1. La vista [vue1.xml]
  

Rispetto all'esempio precedente, la vista [vue1.xml] si evolve come segue:

 
 
  • in [1], l'utente deve specificare l'URL del servizio web e il tempo di attesa [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], viene visualizzato un indicatore di attesa quando vengono richiesti i numeri. Scompare quando sono stati ricevuti tutti o quando l'operazione è stata annullata;

Image

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

All'utente viene richiesto di caricare il file [vue1.xml] dagli esempi. Di seguito riportiamo l'identificativo dei nuovi componenti:

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

I pulsanti [10-11] sono fisicamente sovrapposti. In un determinato momento, ne verrà visualizzato solo uno dei due.

1.16.2.6.2. Il frammento [Vue1Fragment]
  

Lo scheletro del frammento [Vue1Fragment] è il 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 {

  // gli elementi dell'interfaccia visiva
  @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;
...
  // dati locali
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;

  @AfterViews
  void afterViews() {
    // memoria
    afterViewsDone=true;
    // all'inizio nessun messaggio di errore
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // pulsante [Annuler] nascosto
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // elenco delle risposte
    reponses = new ArrayList<>();
  }
...
  • righe 24-49: i riferimenti ai componenti della vista [vue1.xml] (riga 20);
  • righe 55-69: il metodo [@AfterViews] eseguito quando sono stati inizializzati i riferimenti delle righe 24-49;
  • riga 58: da non dimenticare - necessaria per il ciclo di vita del frammento;
  • righe 60-63: i messaggi di errore vengono nascosti;
  • righe 65-66: si nasconde il pulsante [Annuler] (riga 65) e si visualizza il pulsante [Exécuter] (riga 66). Si ricorda che fisicamente si trovano uno sopra l’altro;
  • riga 68: il campo della riga 52 conterrà l'elenco delle stringhe da visualizzare tramite il ListView delle risposte;

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


  @Override
  protected void updateFragment() {
    // si crea l'adattatore dell'elenco delle risposte
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • righe 4-5: si crea l'adattatore ListView delle risposte. Viene memorizzato in una variabile di istanza per essere disponibile agli altri metodi della classe;

Il «clic» sul pulsante [Exécuter] provoca l'esecuzione del seguente metodo:


// i dati inseriti
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;

  // dati locali
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;

  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // si cancellano le risposte precedenti
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // si azzera il contatore delle risposte
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // si verifica la validità dei dati inseriti
    if (!isPageValid()) {
      return;
    }
    // inizializzazione attività
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // si richiedono i numeri casuali
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // si avvia l'attesa
    beginWaiting();
  }

  @Background(id = "alea")
  void getAlea(int a, int b) {
    // qui bisogna fare il meno possibile
    // in ogni caso nessuna visualizzazione - queste devono avvenire nell'UiThead
    try {
      // si visualizza il risultato nel file UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // l'eccezione viene visualizzata in UiThread
      showAlert(e);
    }
  }
  • righe 17-18: si cancella il precedente elenco di risposte dal server. A tal fine, alla riga 17, si svuota la fonte dati [reponses] associata all’adattatore ListView;
  • riga 19: un valore booleano che ci servirà per sapere se l’utente ha annullato o meno la sua richiesta;
  • righe 21-22: si visualizza un contatore pari a zero per il numero di risposte;
  • righe 24-26: si recuperano i dati inseriti nelle righe [2-6] e se ne verifica la validità. Se uno di essi non è valido, il metodo viene interrotto (riga 25) e l’utente viene reindirizzato all’interfaccia visiva;
  • righe 28-29: se i dati inseriti sono tutti validi, si trasmette all’attività l’URL del servizio web (riga 28) e il tempo di attesa prima di ogni chiamata al servizio (riga 29). Queste informazioni sono necessarie al livello [DAO] e si ricorda che è l’attività a comunicare con esso;
  • righe 31-33: i numeri casuali vengono richiesti uno alla volta al metodo [getAlea] della riga 39;
  • riga 38: il metodo [getAlea] è annotato con l’annotazione AA [@Background], il che fa sì che venga eseguito in un altro thread (flusso di esecuzione, processo) diverso da quello in cui viene eseguita l'interfaccia visiva. È infatti obbligatorio eseguire qualsiasi chiamata Internet in un thread diverso da quello dell'interfaccia visiva. Pertanto, in un dato momento, si potranno avere diversi thread:
    • quello che visualizza l’interfaccia visiva UI (User Interface) e ne gestisce gli eventi,
    • i thread [nbAleas], ciascuno dei quali richiede un numero casuale al servizio web. Questi thread vengono avviati in modo asincrono: il thread UI avvia un thread [getAlea] (riga 32) che richiede un numero casuale al servizio web e non attende il completamento dell’operazione. Il completamento gli verrà segnalato 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, viene creata una coda dei thread da eseguire;

Riga 38: il parametro [id] assegna un nome al thread generato. In questo caso, i thread [nbAleas] hanno tutti lo stesso nome, [alea]. Questo ci consentirà di annullarli tutti contemporaneamente. Questo parametro è facoltativo se non si gestisce l’annullamento del thread;

  • riga 44: viene chiamato il metodo [getAlea] dell’attività. Verrà quindi chiamato in un thread separato da quello di UI. Quest’ultimo effettuerà la chiamata al servizio web e non attenderà la risposta. Verrà avvisato in un secondo momento tramite un evento che la risposta è disponibile. È in quel momento che, alla riga 44, verrà chiamato il metodo [showInfo] con la risposta ricevuta come parametro;
  • righe 45-47: l’esecuzione della richiesta web può generare un’eccezione. Si richiede quindi la visualizzazione dei messaggi di errore dell’eccezione in un messaggio di avviso;
  • riga 35: si attende l’arrivo dei risultati:
    • verrà visualizzato un indicatore di attesa;
    • il pulsante [Annuler] sostituirà il pulsante [Exécuter]. Poiché i thread avviati sono asincroni, il thread di UI non li attende e la riga 35 viene eseguita prima del loro completamento. Una volta terminato il metodo [beginWaiting], il thread UI può nuovamente rispondere alle richieste dell'utente, come il clic sul pulsante [Annuler]. Se i thread avviati fossero stati sincroni, si arriverebbe alla riga 35 solo una volta terminati tutti i thread. L'annullamento di questi ultimi non avrebbe quindi più senso;

Il metodo [showInfo] è il seguente:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // un'ulteriore informazione
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // abbiamo finito?
      if (nbInfos == nbAleas) {
        // si conclude l'attesa
        cancelWaiting();
      }
      // si aggiunge l'informazione all'elenco delle risposte
      reponses.add(0, String.valueOf(alea));
      // visualizziamo le risposte
      adapterReponses.notifyDataSetChanged();
    }
}
  • il metodo [showInfo] viene chiamato all’interno del thread [getAlea] annotato da [@Background]. Questo metodo aggiornerà l’interfaccia visiva UI. Può farlo solo se viene eseguito all'interno del thread di UI. 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: si incrementa il contatore delle risposte e lo si visualizza;
  • righe 8-11: se sono state ricevute tutte le risposte attese, si termina l’attesa (fine del segnale di attesa, il pulsante [Exécuter] sostituisce il pulsante [Annuler]);
  • righe 12-15: si aggiunge il numero casuale ricevuto all’elenco delle risposte visualizzato dal componente [ListView listReponses] e lo si aggiorna;

Il metodo [showAlert] è il seguente:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // si annulla tutto
      doAnnuler();
      // la visualizziamo
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

Si riscontra una logica analoga a quella del metodo [showInfo]:

  • riga 1: l'annotazione [@UiThread] è obbligatoria;
  • riga 2: il metodo riceve l'eccezione che si è verificata;
  • riga 3: il metodo viene eseguito solo se l'utente non ha annullato la sua richiesta;
  • riga 5: si annulla la richiesta dell’utente come se avesse cliccato lui stesso sul pulsante [Annuler];
  • riga 7: si visualizza l'avviso 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 [Annuler] viene gestito con il seguente metodo:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // memoria
    hasBeenCanceled=true;
    // si annulla l'attività asincrona
    BackgroundExecutor.cancelAll("alea", true);
    // fine dell'attesa
    cancelWaiting();
}
  • riga 4: si rileva che l'utente ha annullato la richiesta;
  • riga 6: annulla tutte le attività identificate dalla stringa [alea]. Il secondo parametro [true] indica che tali attività devono essere annullate anche se sono già state avviate. L'identificatore [alea] è quello utilizzato per qualificare il metodo [getAlea] del frammento (riga 1 di seguito):

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

Nota: si è riscontrato che la riga 6 del codice del metodo [doAnnuler] non funzionava correttamente. Per questo motivo è stato aggiunto il valore booleano [hasBeenCanceled]. Infatti, in caso di eccezione (server non disponibile), veniva visualizzata n volte la finestra di avviso se erano stati richiesti 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 attesa nella vista associata all’attività [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">
      <!-- immagine di attesa -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>

    </android.support.v7.widget.Toolbar>
    <!-- immagine di attesa -->
  </android.support.design.widget.AppBarLayout>
...
  • righe 17-21: l'immagine di attesa;

1.16.2.7.2. L'attività [MainActivity]

L'attività [MainActivity] presenta poche differenze rispetto a com'era in [Exemple-14]. Innanzitutto, le viene inserito il livello [DAO]:


  // iniezione dao
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // configurazione del livello [DAO]
    setTimeout(TIMEOUT);
}
  • righe 2-3: inserimento del livello [DAO] tramite un'annotazione AA;
  • righe 5-13: codice eseguito dopo tale inserimento;
  • riga 12: si imposta il timeout del livello [DAO]

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


  // implementazione IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // viene visualizzata la vista posizione
    if (mViewPager.getCurrentItem() != position) {
      // visualizzazione del frammento
      mViewPager.setCurrentItem(position);
    }
  }

  // gestione dell'immagine di attesa
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }

  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }

  // implementazione IDao --------------------------------------------------------------------

  @Override
  public int getAlea(int a, int b) {
    // esecuzione
    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 (paragrafo 1.16.1.7), quindi avviare il client Android:

Image

Per sapere cosa inserire in [1], procedere come segue. Aprire una finestra di comando e digitare 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 gli indirizzi IP al vostro computer (righe 10 e 18). Questi indirizzi sono particolarmente pratici poiché non vengono bloccati dal firewall di Windows. La riga 30 riporta l'indirizzo IP del vostro computer su una rete locale. Per utilizzare questo indirizzo, in genere è necessario disattivare il firewall di Windows. Se siete connessi a una rete Wi-Fi, utilizzate l'indirizzo Wi-Fi e, anche in questo caso, disattivate il firewall se ne avete uno.

Prova l'applicazione nei seguenti casi:

  • 100 numeri casuali nell’intervallo [1000, 2000] senza tempo di attesa;
  • 2000 numeri casuali nell’intervallo [10000, 20000] senza tempo di attesa e annullate l’attesa prima della fine della generazione;
  • 5 numeri casuali nell'intervallo [100, 200] con un tempo di attesa di 5000 ms e annullate l'attesa prima della fine della generazione;

1.16.2.9. Gestione dell'annullamento

Per monitorare ciò che accade quando l’utente richiede l’annullamento o quando questo viene richiesto a causa di un’eccezione, aggiungiamo il seguente metodo all’interfaccia [IDao] (cfr. paragrafo 1.16.2.4.1):


package exemples.android.dao;

public interface IDao {

  ...

  // modalità debug
  void setDebugMode(boolean isDebugEnabled);
}

Nella classe [Dao], aggiungiamo il codice seguente:


  // modalità debug
  private boolean isDebugEnabled;
  // nome della classe
  private String className;
..
  // costruttore
  public Dao() {
    // nome della classe
    className = getClass().getSimpleName();
  }
...
  // interfaccia 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));
    }
    // esecuzione del servizio
    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 un'informazione dal livello [DAO], viene emesso un log. Inoltre, quando viene chiamato il metodo [doAnnuler], si registra l'evento.

Test 1

Vengono richiesti 5 numeri mentre il server non è stato avviato. Si ottengono 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 ricorda che si tratta di chiamate asincrone effettuate dal frammento [VueFragment] e che quest’ultimo non attende il risultato della propria chiamata;
  • riga 7: la prima richiesta HTTP è stata effettuata e il frammento [VueFragment] ha ricevuto la sua prima eccezione;
  • riga 8: richiede quindi l'annullamento di tutte le richieste;
  • righe 9-12: si nota tuttavia che riceve le quattro eccezioni successive. Pertanto, le richieste asincrone che erano in attesa sono state tutte eseguite;

Test 2

Ora avviamo il server e richiediamo 5 numeri con un intervallo di 5 secondi, quindi clicchiamo su [Annuler] prima della scadenza di tale intervallo. 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: si nota che [Vue1_Fragment] riceve 5 valori. Ancora una volta, tutte le richieste asincrone in sospeso sono state eseguite;

Questo è il motivo per cui abbiamo dovuto gestire un valore booleano [hasBeenCanceled] per evitare di visualizzare qualcosa quando era stata richiesta un'annullamento. Nel codice dell'annullamento:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memoria
    hasBeenCanceled = true;
    // si annulla l'attività asincrona
    BackgroundExecutor.cancelAll("alea",true);
    // fine dell'attesa
    cancelWaiting();
}

il codice della riga 10 non funziona come previsto. È possibile che ciò sia dovuto al fatto che le attività asincrone condividono lo stesso metodo annotato [@Background]:


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

1.17. Esempio 16: gestire l’asincronia con RxAndroid

Ci proponiamo ora di gestire l’asincronia necessaria alle applicazioni Android con una libreria denominata RxJava [http://reactivex.io/] e la sua versione derivata per l’ambiente Android [RxAndroid]. A tal fine, utilizzeremo il corso [Introduction à RxJava. Application aux environnements Swing et Android].

1.17.1. Creazione del progetto

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

1.17.2. Configurazione Gradle

  

In [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 {

  // numero casuale
  Observable<Integer> getAlea(int a, int b);

  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // tempo massimo di attesa (ms) per la risposta del server
  void setTimeout(int timeout);

  // tempo di attesa in millisecondi del client prima della richiesta
  void setDelay(int delay);

  // modalità debug
  void setDebugMode(boolean isDebugEnabled);
}
  • riga 8: il metodo [getAlea] restituisce ora un tipo [Observable] della libreria RxJava (riga 3). Il principio è il seguente:

Un flusso di elementi di tipo Observable<T> viene osservato da uno o più sottoscrittori (abbonati, osservatori, consumatori) di tipo Subscriber<T>. La libreria RxJava consente al flusso Observable<T> di essere eseguito in un thread T1 e al suo osservatore Subscriber<T> in un thread T2 senza che lo sviluppatoredebba preoccuparsi di gestire il ciclo di vita di questi thread e di problemi naturalmente complessi, come la condivisione dei dati tra thread e la loro sincronizzazione 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 {

  // mappatore jSON
  private ObjectMapper mapper = new ObjectMapper();

  // metodi protetti ----------------------------------------------------------
  // interfaccia generica
  protected interface IRequest<T> {
    Response<T> getResponse();
  }

  // richiesta generica
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // esecuzione del servizio
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // esecuzione del servizio
        try {
          // si esegue la richiesta in modo sincrono e si inoltra la risposta all'abbonato
          Response<T> response = request.getResponse();
          // errore?
          int status = response.getStatus();
          if (status != 0) {
            // si registra l'eccezione
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // si invia la risposta
            subscriber.onNext(response.getBody());
            // si segnala la fine dell'osservabile
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // si registra l'eccezione
          ex = new DaoException(e, 100);
        }
        // eccezione?
        if (ex != null) {
          // si genera l'eccezione
          subscriber.onError(ex);
        }
      }
    });
  }

}
  • la classe [AbstractDao] ha come elemento principale un metodo generico [getResponse] che serve a ottenere dal server un tipo [Response<T>] 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. Quest’ultima ha un solo metodo [getResponse] ed è proprio questo metodo a fornire la risposta [Response<T>] desiderata;
  • grazie ai due elementi precedenti, la classe [AbstractDao] può fungere da classe padre per qualsiasi livello client [Dao] 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 del tipo [rx.Observable.OnSubscribe<T>], un'interfaccia che possiede i seguenti metodi:
    • [onNext(T element)]: consente di inviare a un osservatore un elemento di tipo T;
    • [onError(Throwable th)]: consente di inviare un'eccezione a un osservatore;
    • [onCompleted]: consente di segnalare a un osservatore la fine delle emissioni;

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

  • invia i propri elementi tramite il metodo [onNext(T element)];
  • il metodo [onCompleted] deve essere chiamato una sola volta non appena non ci sono più elementi da trasmettere 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]. È questo che riceve gli elementi inviati dal [Observable<T>] (elemento o eccezione);
  • il tipo [Observable<T>] creato emetterà un solo elemento (riga 37);
  • riga 29: effettua una richiesta sincrona HTTP al server e ottiene il tipo [Response<T>]. Questa richiesta HTTP è gestita dal tipo [IRequest] passato come parametro al metodo generico [getResponse];
  • riga 31: si recupera il status dalla risposta;
  • righe 32-34: se questo status è quello di un errore, si prepara un'eccezione;
  • righe 36-39: se questo status non è quello di un errore, allora si invia la risposta effettivamente attesa dal cliente (riga 37) e si comunica all'osservatore che non ci saranno più altre trasmissioni (riga 39);
  • righe 41-44: se la richiesta HTTP termina con un'eccezione, la si registra;
  • righe 46-49: se l’eccezione [ex] è diversa da null, allora la si invia all’osservatore. In questo caso non è necessario chiamare il metodo [onCompleted] per indicare all'osservatore che non ci saranno più emissioni di elementi. È implicito;

Da queste spiegazioni si ricava che:

  • il metodo generico [<T> Observable<T> getResponse(final IRequest<T> request)] restituisce un tipo [Observable<T>] che emette solo un elemento di tipo T oppure un'eccezione;
  • che questo metodo accetta come unico parametro un tipo [IRequest<T>], il cui unico metodo [getResponse()] implementa l'accesso 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 {

  // cliente del servizio REST
  @RestService
  protected WebClient webClient;

  // tempo di attesa prima dell'esecuzione della richiesta
  private int delay;
  // modalità debug
  private boolean isDebugEnabled;
  // nome della classe
  private String className;

  // costruttore
  public Dao() {
    // nome della classe
    className = getClass().getSimpleName();
  }


  // interfaccia 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));
    }
    // esecuzione client web
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // attesa
        waitSomeTime(delay);
        // chiamata sincrona HTTP
        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. Gli viene passato un parametro di tipo [IRequest<Integer>];
  • righe 32-37: implementazione dell’interfaccia [IRequest<Integer>];
  • riga 36: si esegue la richiesta HTTP tramite l’interfaccia AA [webClient], come era stato fatto in precedenza. Sappiamo che otterremo un tipo [Response<Integer>], che è proprio il tipo che deve restituire il metodo [IRequest<Integer>.getReponse()];
  • riga 36: qui si utilizza una proprietà denominata closure: la capacità di incapsulare in un’istanza valori esterni ad essa al momento della sua creazione, in questo caso i valori di [a, b] della riga 24. È questo che permette al metodo [IRequest<Integer>.getReponse()] di non avere parametri. Questi sono stati inseriti nel corpo del metodo. E laddove normalmente si modificherebbero i parametri del metodo (a,b) -> (x,y), qui si crea 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:


  // implementazione IDao --------------------------------------------------------------------

  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // esecuzione
    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() {
    // si cancellano le risposte precedenti
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // si azzera il contatore delle risposte
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // si verifica la validità dei dati inseriti
    if (!isPageValid()) {
      return;
    }
    // inizializzazione attività
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // si richiedono i numeri casuali
    getAleasInBackground(a, b);
    // si avvia l'attesa
    beginWaiting();
}
  • riga 18: i numeri casuali vengono richiesti al metodo [getAleasInBackground], così denominato perché i numeri vengono richiesti in un thread diverso da quello dell’interfaccia utente;

  private int nbReponses = 0;
  // gli abbonamenti agli osservabili
  private List<Subscription> abonnements;

// annotazione [Background] non necessaria
  void getAleasInBackground(int a, int b) {
    // all'inizio nessuna risposta e nessun abbonamento
    nbReponses = 0;
    abonnements.clear();
    // si prepara l'osservabile
    Observable<Integer> response = Observable.empty();
    // si uniscono i risultati delle diverse chiamate HTTP
    // vengono eseguiti su un thread di I/O
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // l'observable cumulativo verrà osservato sul thread dell'UI
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // si esegue l'osservabile
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // si aggiunge l'informazione all'elenco delle risposte
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // messaggio di errore
          showAlert(th);
          // fine attesa
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // fine attesa
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // viene visualizzata l'eccezione in UiThread
      showAlert(e);
    }
}
  • riga 3: un osservabile ha degli abbonati. Il legame tra un abbonato e il processo che osserva è chiamato abbonamento (Subscription). In questo caso avremo un solo processo osservato e un solo abbonato. Avremo quindi un solo abbonamento. In linea di principio, facciamo come se potessimo avere più processi osservati da diversi osservatori, il che comporterebbe più abbonamenti;
  • righe 11-18: si configura il processo osservato (osservabile). È importante capire che si tratta solo di una configurazione: il processo non viene eseguito;
  • riga 11: si parte da un osservabile vuoto, un osservabile che non emette nulla;
  • righe 14-16: a questo osservabile vuoto si aggiungono gli osservabili [nbAleas], che saranno le richieste [nbAleas] che restituiranno i numeri casuali [nbAleas];
  • riga 15: come in precedenza, il numero casuale n. i viene richiesto alla classe [MainActivity]. È fondamentale comprendere che, a questo punto, non viene ancora eseguita alcuna richiesta HTTP. Viene eseguito il metodo [mainActivity.getAlea(a, b)], che restituisce un tipo [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 sarà) su un thread di I/O. La libreria RxJava offre diversi tipi di thread. Quello di I/O è adatto alle chiamate HTTP;
  • riga 15: l'osservabile n. i viene fuso con l'osservabile iniziale della riga 11: da osservabili [nbAleas] che emettono ciascuno un elemento, si crea un osservabile che ne emetterà [nbAleas]. Sarà questo ad essere osservato. Questo osservabile emette la notifica [onCompleted] quando tutti gli osservabili che lo compongono avranno emesso la propria notifica [onCompleted]. Ciò ci eviterà di dover contare le risposte, come avevamo fatto nella versione precedente, per sapere se abbiamo ricevuto tutti i numeri attesi;
  • riga 18: a questo punto, abbiamo configurato un osservabile che è la composizione di [nbAleas] osservabili, ciascuno dei quali viene eseguito su un thread di I/O;
  • riga 18: il metodo [observeOn(AndroidSchedulers.mainThread())] serve a specificare su quale thread deve avvenire l’osservazione dei valori emessi dall’osservabile. In questo caso, il thread [AndroidSchedulers.mainThread())] appartiene alla libreria RxAndroid e non a RxJava. Indica il thread dell'interfaccia utente, chiamato anche event loop. Questo punto è importante: in un'applicazione Android, la modifica di un componente dell'interfaccia utente può avvenire solo nel thread dell'interfaccia utente, altrimenti si verifica un'eccezione;
  • righe 19-45: ora che il processo da monitorare è stato configurato, lo si esegue;
  • riga 21: è l'operazione [Observable.subscribe] che avvia l'esecuzione del processo osservato. Questa operazione avvierà i processi asincroni [nbAleas] configurati in precedenza. I risultati di questi ultimi saranno automaticamente messi a disposizione dell'osservatore sul thread dell'interfaccia utente;
  • ricordiamo che l’osservabile emette tre tipi di eventi:
    • [onNext]: quando emette un elemento;
    • [onError]: quando si verifica un'eccezione;
    • [onCompleted]: quando segnala che non emetterà più;

Il metodo [Observable.subscribe] ha come parametri tre oggetti [Action1<Integer>, Action1<Throwable>, Action0], i cui metodi [call] servono a gestire ciascuno di questi tre eventi;

  • righe 21-27: il primo parametro di tipo [Action1<Integer>] serve a gestire l'evento [onNext]. Il suo metodo [call] riceve l'elemento che è stato emesso dall'osservabile (riga 23);
  • riga 25: si riutilizza il metodo [showInfo] dell’esempio precedente;
  • righe 27-35: il secondo parametro di tipo [Action1<Throwable>] serve a gestire l'evento [onError]. Il suo metodo [call] riceve l'eccezione generata dall'osservabile (riga 29);
  • riga 31: si riutilizza il metodo [showAlert] dell'esempio precedente;
  • riga 33: si avvia la procedura di annullamento della richiesta dell’utente. Ciò consisterà nell’annullare tutti gli osservabili attualmente in esecuzione;
  • righe 35-41: il terzo parametro di tipo [Action0] serve a gestire l'evento [onCompleted]. Il suo metodo [call] non riceve alcun parametro;
  • riga 39: si annulla l’attesa;

Il metodo [showInfo] si evolve come segue:


  // annotazione [UiThread] non necessaria
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // un'ulteriore informazione
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // si aggiunge l'informazione all'elenco delle risposte
      reponses.add(0, String.valueOf(alea));
      // si visualizzano le risposte
      adapterReponses.notifyDataSetChanged();
    }
}

Il metodo presenta due modifiche:

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

Il metodo [showAlert] si evolve come segue:


  // annotazione [UiThread] inutile
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // si annulla tutto
      doAnnuler();
      // si pubblica
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • l'unica modifica è alla riga 1: è stata rimossa l'annotazione AA [@UiThread];

Infine, il metodo [doAnnuler] si evolve come segue:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // registro
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memoria
    hasBeenCanceled = true;
    // si annullano le attività asincrone
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // fine dell'attesa
    cancelWaiting();
}
  • riga 12: annulla un abbonamento e quindi il monitoraggio del processo associato;

1.17.9. Esecuzione

Avviare il servizio web (paragrafo 1.16.1.7), avviare il client Android e ripetere i test effettuati con l'esempio precedente (paragrafo 1.16.2.8).

1.17.10. Gestione della cancellazione

Si ripetono gli stessi test dell’esempio precedente (paragrafo 1.16.2.9).

Test 1

Si richiedono 5 numeri mentre il server non è stato avviato. Si ottengono 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 intervallo di 5 secondi, quindi clicchiamo su [Annuler] prima della fine di tale intervallo. 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 in caso di annullamento. È quindi possibile rimuovere dal codice di [Vue1Fragment] la variabile booleana [hasBeenCanceled] che avevamo introdotto nell’esempio precedente, poiché l’annullamento non funzionava come previsto.

Il fatto che l’osservatore non riceva più notifiche dopo l’annullamento dell’osservabile non significa che le richieste HTTP siano a loro volta annullate. Per verificarlo, modifichiamo la classe [Dao] nel modo seguente:


  @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));
    }
    // esecuzione client web
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // attesa
        waitSomeTime(delay);
        // chiamata sincrona HTTP
        Response<Integer> response= webClient.getAlea(a, b);
        if (isDebugEnabled) {
          try {
            Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
          } catch (JsonProcessingException e) {
            Log.d(String.format("%s", className),"erreur désérialisation jSON");
          }
        }
        return response;
      }
    });
}
  • righe 15-21: registriamo il risultato della richiesta HTTP della riga 14;

I log per il test n. 2 sono quindi 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: si ricevono correttamente le risposte delle cinque richieste HTTP. Tuttavia, a causa dell’annullamento dell’osservabile, questi elementi non vengono trasmessi all’osservatore;

1.17.11. Conclusione

Nel prosieguo del presente documento, le applicazioni client/server saranno realizzate utilizzando la libreria RxAndroid anziché la libreria AA per i seguenti motivi:

  1. RxAndroid può essere utilizzata in un'applicazione Android che non utilizza AA;
  2. RxAndroid non si limita a facilitare le operazioni asincrone. Offre infatti numerosi metodi per creare un nuovo osservabile a partire da un altro. Questi metodi non hanno un equivalente in AA;
  3. non appena si desidera derivare una classe annotata con AA, come ad esempio un frammento, si incontrano seri problemi. Si è quindi costretti ad abbandonare AA e a ricorrere alla soluzione 1 per la programmazione asincrona;

Il lettore interessato ad approfondire le possibilità offerte dalla libreria RxAndroid potrà consultare il documento [Introduction à RxJava. Application aux environnements Swing et Android]. In esso si utilizza RxAndroid senza la libreria AA.

1.18. Esempio 17: componenti per l’inserimento dei dati

Scriveremo un nuovo progetto per presentare alcuni componenti comunemente utilizzati nei moduli di immissione dati.

1.18.1. Creazione del progetto

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

Il nuovo progetto avrà una sola vista, [vue1.xml]. Pertanto, eliminiamo la vista [vue2.xml] e i relativi frammenti associati [Vue2Fragment] e [2]. Inseriamo questa modifica nel gestore dei frammenti di [Mainactivity]:


  // il nostro gestore di frammenti deve essere ridefinito per ogni applicazione
  // deve definire i seguenti metodi: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // i frammenti
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Rieseguire il progetto. Dovrebbe visualizzarsi la vista n. 1 come in precedenza. Lavoreremo a partire 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 [ScrollView] verticale. Consente di
  • presentare un modulo più grande dello schermo del
  • tablet. È possibile visualizzare l'intero modulo tramite
  • scorrimento;
 
  • righe 125-132: una casella di selezione
  • righe 134-159: un gruppo di tre pulsanti di opzione
  • righe 161-166: una barra di ricerca
  • righe 16-176: un campo di immissione
  • righe 178-186: un selettore sì/no
  • righe 188-195: un campo di immissione dell'ora
  • righe 197-207: un campo di immissione multilinea
  • righe 209-215: un menu a tendina
  • righe 217-225: un campo di immissione data
  • tutti gli altri componenti sono [TextView] che visualizzano dei testi.
 

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>
  <!-- vista 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="formulaire_checkbox">Cases à cocher</string>
  <string name="formulaire_radioButton">Boutons Radio</string>
  <string name="formulaire_seekBar">Seek Bar</string>
  <string name="formulaire_saisie">Champ de saisie</string>
  <string name="formulaire_bool">Booléen</string>
  <string name="formulaire_date">Date</string>
  <string name="formulaire_time">Heure</string>
  <string name="formulaire_multilignes">Champ de saisie multilignes</string>
  <string name="formulaire_listview">Liste</string>
  <string name="formulaire_combo">Liste déroulante</string>
  <string name="formulaire_checkbox1">1</string>
  <string name="formulaire_checkbox2">2</string>
  <string name="formulaire_radiobutton1">1</string>
  <string name="formulaire_radionbutton2">2</string>
  <string name="formulaire_radiobutton3">3</string>
  <string name="formulaire_switch"></string>
  <string name="formulaire_valider">Valider</string>
</resources>

1.18.4. Il frammento del modulo

  

La classe [Vue1Fragment] è 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;

// un frammento è una vista visualizzata da un contenitore di frammenti
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // i campi della vista visualizzata dal frammento
  @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;

  // elenco a discesa
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // si seleziona il primo pulsante
    radioButton1.setChecked(true);
    // il calendario
    datePicker1.setCalendarViewShown(false);
    // il 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));
      }
    });
    // l'elenco a discesa
    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() {
    // inizializzazione dell'adattatore dell'elenco a discesa
    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: si recuperano i riferimenti di tutti i componenti del modulo XML [vue1] (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 elimina il calendario;
  • riga 62: [SeekBar].setMax() consente di impostare il valore massimo della barra di regolazione. Il valore minimo è 0;
  • righe 63-74: si gestiscono gli eventi della barra di regolazione. Si desidera, ad ogni modifica effettuata dall'utente, visualizzare il valore della barra di regolazione nel campo [TextView] della riga 49;
  • riga 71: il parametro [progress] rappresenta il valore della barra di regolazione;
  • righe 76-79: un elenco di [String] che verrà associato al menu a tendina;
  • riga 90: il metodo [updateFragment] del frammento. Quando viene eseguito, la variabile [activity] della classe padre è stata inizializzata;
  • riga 92: la fonte dati [list] è associata all’adattatore dell’elenco a discesa;
  • righe 93-94: l'adattatore [dataAdapter] viene associato all'elenco a discesa [dropDownList];
  • riga 84: si associa il metodo [doValider] al clic sul pulsante [Valider];

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


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // elenco dei messaggi da visualizzare
    List<String> messages = new ArrayList<>();
    // casella di controllo
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // i pulsanti di opzione
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // il SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // il campo di immissione
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // l'interruttore
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // la data
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // il testo su più righe
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // l'ora
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // elenco a discesa
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // visualizzazione
    doAfficher(messages);
}
  • riga 4: i valori inseriti verranno raccolti in un elenco di messaggi;
  • riga 6: il metodo [CheckBox].isCkecked() consente di verificare se una casella è spuntata o meno;
  • riga 9: il metodo [RadioGroup].getCheckedButtonId() consente di ottenere l’ID del pulsante di opzione che è stato selezionato oppure -1 se nessuno è stato selezionato;
  • riga 10: il codice [activity.findViewById(id)] consente di individuare il pulsante di opzione selezionato e di ottenere così la sua etichetta;
  • riga 13: il metodo [SeekBar].getProgress() consente di ottenere il valore di una barra di scorrimento;
  • riga 19: il metodo [Switch].isChecked() consente di verificare se un switch è On (vero) o Off (falso);
  • riga 22: il metodo [DatePicker].getYear() consente di ottenere l’anno selezionato tramite un oggetto [DatePicker];
  • riga 23: il metodo [DatePicker].getMonth() consente di ottenere il mese selezionato con un oggetto [DatePicker] nell'intervallo [0,11];
  • riga 24: il metodo [DatePicker].getDayOfMonh() consente di ottenere il giorno del mese selezionato con un oggetto [DatePicker] nell'intervallo [1,31];
  • riga 30: il metodo [TimePicker].getHour() consente di ottenere l'ora selezionata con un oggetto [TimePicker];
  • riga 31: il metodo [TimePicker].getMinute() consente di ottenere i minuti selezionati con un oggetto [TimePicker];
  • riga 34: il metodo [Spinner].getSelectedItemPosition() consente di ottenere la posizione dell'elemento selezionato in un elenco a discesa;
  • riga 35: il metodo [Spinner].getSelectedItem() consente di ottenere l'oggetto selezionato in un elenco a discesa;

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


    private void doAfficher(List<String> messages) {
        // si costruisce il testo da visualizzare
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // lo si visualizza
        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 creato un oggetto [StringBuilder]. Per concatenare 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

Eseguire il progetto e testare i diversi componenti di immissione dati.

1.19. Esempio 18: utilizzo di un modello di vista

1.19.1. Creazione del progetto

Creiamo un nuovo progetto [Exemple-18] duplicando il progetto [Exemple-13].

1.19.2. Il modello delle viste

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

  

Image

Ciascuna delle due viste sarà strutturata allo stesso modo:

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

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

Il codice XML della 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] si ottiene con le righe 38-54;
  • la banda sinistra [2] si ottiene dalle righe 56-84;
  • il piè di pagina [3] si ottiene dalle righe 86-101;
  • il contenuto [4] è ricavato 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 file [strings.xml] è il seguente:


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

Creare un contesto di esecuzione per questo progetto ed eseguirlo.

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 di caratteri a una vista che consente di inserire informazioni per ogni elemento dell’elenco. Creeremo il seguente [ListView]:

Image

Ogni vista dell'elenco è composta da tre elementi:

  • un [TextView] di informazioni;
  • un [CheckBox];
  • un [TextView] cliccabile;

1.20.1. Creazione del progetto

Creiamo un nuovo progetto [Exemple-19] duplicando il progetto [Exemple-18].

  

Procederemo con l'evoluzione del progetto come indicato in [3].

1.20.2. La sessione

  

La sessione memorizza i dati condivisi tra attività e 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 {
  // un elenco di dati
  private List<Data> liste=new ArrayList<>();

  // getter e setter
...
}
  • riga 11: l'elenco dei dati utilizzato da entrambe le viste;

La classe [Data] è la seguente:


package exemples.android.architecture;

public class Data {

    // dati
    private String texte;
    private boolean isChecked;

    // costruttore
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }

    // getter e setter
    ...
}
  • riga 6: il testo che alimenterà il primo [TextView] di ciascun elemento dell'elenco;
  • riga 7: il valore booleano che servirà a selezionare o meno il [checkBox] di ciascun elemento dell'elenco;

1.20.3. L'attività [MainActivity]

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


  // Iniezione della sessione
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // si crea un elenco di dati
    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 [Vue1]

La vista XML [vue1.xml] visualizza l'area [1] sopra indicata. 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 da [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 {

  // i campi della vista visualizzata dal frammento
  @ViewById(R.id.listView1)
  protected ListView listView;
  // l'adattatore dell'elenco
  private ListAdapter adapter;
  // inizializzazione completata
  private boolean initDone = false;

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

  @Click(R.id.button_vue2)
  void navigateToView2() {
    // si passa alla vista 2
    mainActivity.navigateToView(1);
  }

  public void doRetirer(int position) {
   ...
  }

  @Override
  protected void updateFragment() {
    if (!initDone) {
      // si associano i dati a [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // caso in cui il frammento sia stato (ri)generato - in questo caso è necessario ricollegare il ListView al proprio adattatore
    listView.setAdapter(adapter);
    // caso in cui altri frammenti abbiano modificato la fonte dei dati - in questo caso è necessario aggiornare il ListView
    adapter.notifyDataSetChanged();
  }
}
  • riga 15: la vista XML [vue1] è associata al frammento;
  • righe 26-30: il metodo [@AfterViews] non esegue alcuna operazione. È tuttavia necessario per impostare la variabile [afterViewsDone] su true, poiché quest’ultima viene utilizzata dalla classe padre [AbstractFragment];
  • righe 42-53: il metodo [updateFragment], che viene chiamato ogni volta che il frammento sta per diventare visibile. Il metodo è stato scritto qui come se il frammento potesse uscire dall’adiacenza del frammento visualizzato e quindi reinizializzare il proprio ciclo di vita. In questo caso non è così, ma lo sarebbe se l’applicazione dovesse avere 3 frammenti con adiacenza pari a 1;
  • riga 44: l’adattatore di [ListView] deve essere inizializzato una sola volta;
  • riga 46: a questo [ListView] viene associato un adattatore di tipo [ListAdapter]. Ora creeremo questa classe. Essa deriva dalla classe [ArrayAdapter] che abbiamo già avuto modo di utilizzare per associare dati a un [ListView]. Passiamo diverse informazioni al costruttore di [ListAdapter]:
    • un riferimento all’attività corrente,
    • l’identificatore della vista che verrà istanziata per ogni elemento dell’elenco,
    • una fonte di dati per alimentare l'elenco,
    • un riferimento al frammento. Questo verrà utilizzato per gestire il clic su un link [Retirer] del [ListView] tramite il metodo [doRetirer] della riga 38;
  • riga 50: l’adattatore viene associato a [ListView]. Allo stesso tempo, la fonte dati [listes] viene associata a [ListView]. Questa operazione verrà eseguita ogni volta che viene visualizzata la vista n. 1. In realtà, dovrebbe essere eseguita solo quando è stato eseguito il metodo [@AfterViews]. In questo caso l’istruzione viene eseguita troppo spesso. Si avverte la necessità di una variabile booleana che indichi che il metodo [@AfterViews] è stato appena eseguito e che, di conseguenza, il [ListView] debba essere nuovamente associato al proprio adattatore;
  • riga 52: si aggiorna il [ListView]. In questo esempio, ciò non serve a nulla poiché solo la vista n. 1 può modificare la fonte dati del [ListView]. Consideriamo un caso più generale in cui anche la vista n. 2 potrebbe modificare la fonte dati del [ListView]. Si incontreranno esempi di questo tipo più avanti nel presente documento. In questo caso, quando si passa dalla vista n. 2 alla vista n. 1, il [ListView] della vista n. 1 deve essere aggiornato;

1.20.7. L'adattatore [ListAdapter] del [ListView]

La classe [ListAdapter]

  • configura la fonte dati del [ListView];
  • gestisce la visualizzazione dei diversi elementi del [ListView];
  • gestisce gli eventi di questi elementi;

Il suo codice è il seguente:


package exemples.android.fragments;

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

    // il contesto di esecuzione
    private Context context;
    // l'ID del layout di visualizzazione di una riga dell'elenco
    private int layoutResourceId;
    // i dati dell'elenco
    private List<Data> data;
    // il frammento che visualizza il [ListView]
    private Vue1Fragment fragment;
    // l'adattatore
    final ListAdapter adapter = this;

    // produttore
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // si memorizzano le informazioni
        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: si memorizzano le informazioni del costruttore;
  • riga 29: il metodo [getView] verrà chiamato ripetutamente da [ListView] per generare la vista dell'elemento n. [position]. Il risultato restituito da [View] è un riferimento alla vista creata.

Il codice del metodo [getView] è il seguente:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // si crea la riga corrente del ListView
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // il testo
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // la casella di controllo
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // il link [Retirer]
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // gestiamo il clic sulla casella di selezione
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // si visualizza la riga
        return row;
}
  • riga 2: il metodo riceve tre parametri. Ne useremo solo il primo;
  • riga 4: si crea la vista dell’elemento n. [position]. Si tratta della vista [list_data], il cui ID è stato passato come secondo parametro al costruttore. Successivamente si recuperano i riferimenti dei componenti della vista appena istanziata;
  • riga 6: si recupera il riferimento del [TextView] n. 1;
  • riga 7: gli viene assegnato un testo proveniente dalla fonte di dati passata come terzo parametro al costruttore;
  • riga 9: si recupera il riferimento del [CheckBox] n. 2;
  • riga 10: lo si contrassegna o meno con un valore proveniente dalla fonte dati del [ListView];
  • riga 12: si recupera il riferimento del [TextView] n. 3;
  • righe 13-18: si gestisce il clic sul link [Retirer];
  • riga 16: è il metodo [Vue1Fragment].doRetirer che gestirà questo clic. Sembra infatti più logico far gestire questo evento dal frammento che visualizza il [ListView]. Esso dispone di una visione d’insieme che la classe [ListAdapter] non possiede. Il riferimento al frammento [Vue1Fragment] era stato passato come quarto parametro al costruttore della classe;
  • righe 20-25: si gestisce il clic sulla casella di controllo. L’azione effettuata su di essa si riflette sul dato che visualizza. Ciò per il seguente motivo. Il [ListView] è un elenco che visualizza solo una parte dei suoi elementi. Pertanto, un elemento dell’elenco a volte è nascosto, a volte visualizzato. Quando l’elemento n. i deve essere visualizzato, viene chiamato il metodo [getView] della riga 2 sopra riportata per la posizione n. i. La riga 10 ricalcolerà lo stato della casella di controllo in base al dato a cui è collegata. È quindi necessario che quest'ultima memorizzi lo stato della casella di controllo nel corso del tempo;

1.20.8. Rimuovere un elemento dall’elenco

Il clic sul link [Retirer] viene gestito nel frammento [Vue1Fragment] dal seguente metodo [doRetirer]:


  public void doRetirer(int position) {
    // si rimuove l'elemento n. [position] dall'elenco
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // si registra la posizione di scorrimento per tornarci
    // leggere
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // posizione del primo elemento visibile completamente o parzialmente
    int firstPosition = listView.getFirstVisiblePosition();
    // offset Y di questo elemento rispetto alla parte superiore di ListView
    // misura l'altezza della parte eventualmente nascosta
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // si aggiorna il [ListView]
    adapter.notifyDataSetChanged();
    // ci si posiziona nel punto corretto del ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • riga 1: si riceve la posizione nel [ListView] del link [Retirer] su cui è stato cliccato;
  • riga 3: si recupera l'elenco dei dati;
  • riga 4: si rimuove l'elemento con n. [position];
  • riga 15: si aggiorna il [ListView]. Senza questa operazione, visivamente non cambia nulla.
  • righe 5-13, 17: un'operazione piuttosto complessa. Senza di essa, accade quanto segue:
    • il [ListView] visualizza le righe 15-18 dell'elenco dei dati,
    • si elimina la riga 16,
    • la riga 15 sopra lo reinizializza completamente e il [ListView] visualizza quindi le righe 0-3 dell'elenco dei dati;

Con le righe sopra indicate, l’eliminazione viene eseguita e il [ListView] rimane posizionato sulla riga successiva a quella eliminata.

1.20.9. La vista XML [Vue2]

Il codice XML della 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: il componente [TextView] n. 1;
  • righe 26-33: il componente [TextView] n. 2;
  • righe 17-24: il 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 {

    // i campi della vista
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

    @AfterViews
    void initFragment(){
        // memoria
        afterViewsDone=true;
    }

  @Click(R.id.button_vue1)
    void navigateToView1() {
        // si passa alla vista 1
        mainActivity.navigateToView(0);
    }

    @Override
    protected void updateFragment() {
        // vengono visualizzati gli elementi dell'elenco selezionati nella vista 1
        StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
        for (Data data : mainActivity.getListe()) {
            if (data.isChecked()) {
                texte.append(String.format("(%s)", data.getTexte()));
            }
        }
        texte.append("]");
        txtResultats.setText(texte);
    }
}

Il codice rilevante si trova nel metodo [updateFragment] alla riga 32:

  • riga 34: si calcola il testo da visualizzare nel [TextView] n. 2;
  • righe 35-39: si scorre l’elenco dei dati visualizzati dal [ListView]. Esso è memorizzato nell’attività;
  • riga 36: se il dato n. i è stato selezionato, si aggiunge la dicitura associata in un tipo [StringBuilder];
  • riga 41: il [TextView] visualizza il testo calcolato;

1.20.11. Esecuzione

Creare una configurazione di esecuzione per questo progetto ed eseguirla.

1.20.12. Miglioramento

Nell'esempio precedente abbiamo utilizzato una fonte dati List<Data> in cui la classe [Data] era la seguente:


package exemples.android.fragments;

public class Data {

    // dati
    private String texte;
    private boolean isChecked;

    // produttore
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...

}

Alla riga 7, era stato utilizzato un valore booleano per gestire la casella di controllo degli elementi di [ListView]. Spesso il [ListView] deve visualizzare dati che è possibile selezionare spuntando una casella, senza che l’elemento della fonte dati disponga di un campo booleano corrispondente a tale casella. È quindi possibile procedere come segue:

La classe [Data] diventa la seguente:


package exemples.android.fragments;

public class Data {

    // dati
    private String texte;

    // costruttore
    public Data(String texte) {
        this.texte = texte;
    }

    // getter e setter
...
}

Si crea una classe [CheckedData] derivata dalla precedente:


package exemples.android.fragments;

public class CheckedData extends Data {

    // elemento selezionato
    private boolean isChecked;

    // costruttore
    public CheckedData(String text, boolean isChecked) {
        // genitore
        super(text);
        // locale
        this.isChecked = isChecked;
    }

    // getter e setter
...
}

È quindi sufficiente sostituire ovunque nel codice (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment), il tipo [Data] con il tipo [CheckedData]. Ad esempio, in [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // si crea un elenco di dati
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

Il progetto di questa versione vi viene fornito con il nome [Exemple-19B].

1.21. Esempio 20: utilizzare un menu

1.21.1. Creazione del progetto

Duplichiamo il progetto [Exemple-19B] nel progetto [Exemple-20]:

3

Elimineremo i pulsanti dalle viste 1 e 2 per sostituirli con le opzioni di menu [1-2].

1.21.2. La definizione XML dei menu

  

Il file [res / menu / menu_vue1] definisce il menu della 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>

Gli elementi del menu sono definiti dalle seguenti informazioni:

  • android:id: l'identificatore dell'elemento;
  • android:title: il testo dell'elemento;
  • 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ò a sua volta essere un sottomenu (tag <menu>, righe 25, 29);

Il file [res / menu / menu_vue2] definisce il menu della vista n. 2:


<menu xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto"
      xmlns:tools="http://schemas.android.com/tools"
      tools:context=".activity.MainActivity">
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationVue1"
        android:title="@string/navigationVue1"/>
    </menu>
  </item>
</menu>

1.21.3. La gestione del menu nella classe astratta [AbstractFragment]

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

  // dati  accessibili alle classi figlie
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;

  // attività
  protected IMainActivity mainActivity;
  protected Activity activity;

  // sessione
  protected Session session;

  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;

  // costruttore
  public AbstractFragment() {
    // inizializzazione
    className = getClass().getSimpleName();
    // registro
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memoria
    this.menu = menu;
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // si recuperano le # opzioni del menu se non è già stato fatto
    if (!initDone) {
      // si recuperano le # opzioni del menu
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // si trasferisce l'elenco delle opzioni in un array
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // attività
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memoria
      initDone = true;
    }

    // si richiede al frammento figlio di attivarsi
    updateFragment();
  }


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }

  // visualizzazione delle opzioni di menu -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }

  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }

  // aggiornamento della classe figlia
  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. È proprio ciò che faremo qui (riga 63);
  • riga 42: il metodo ha due parametri:
    • [menu]: che è un menu vuoto;
    • [inflater]: uno strumento che permette di creare il menu a partire dalla sua descrizione iniziale. Qui non useremo questa possibilità perché utilizzeremo un'annotazione AA che lo farà per noi;
  • riga 44: memorizziamo il menu. Ne avremo bisogno in seguito;
  • righe 52-53: memorizziamo nell’array della riga 28 gli identificatori di tutti gli elementi del menu;
  • 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: si osserva che l'inizializzazione della classe è già stata effettuata per evitare di doverla ripetere (riga 50);
  • riga 63: si richiede al frammento figlio di aggiornarsi. Ciò è possibile poiché il frammento è allo stesso tempo visibile e associato alla propria vista e al proprio menu;

Il metodo [getMenuOptions], che consente di ottenere gli identificatori degli elementi di un menu, è il seguente:


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // si scorrono tutte le voci del menu
    for (int i = 0; i < menu.size(); i++) {
      // voce n. i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // se la voce n. i è un sottomenu, allora si ricomincia
      if (menuItem.hasSubMenu()) {
        // ricorsività
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
}

Il metodo [setAllMenuOptions] consente di nascondere/mostrare tutte le opzioni del menu;


  protected void setAllMenuOptions(boolean isVisible) {
    // si aggiornano tutte le opzioni del menu
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

Il metodo [setMenuOptions] consente di nascondere/mostrare alcune delle opzioni del menu;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // si aggiornano alcune opzioni del menu
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

La classe [MenuItemState] è la seguente:

  

package exemples.android.architecture;

public class MenuItemState {

  // ID dell'opzione di menu
  private int menuItemId;
  // visibilità dell'opzione
  private boolean isVisible;

  // costruttori
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // getter e setter
...
}

1.21.4. La gestione del menu nel frammento [Vue1Fragment]

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() {
    // si passa alla vista 2
    mainActivity.navigateToView(1);
  }

  @OptionsItem(R.id.actionValider)
  void valider() {
    // si visualizza un messaggio
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }

  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // si cambia stato
    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() {
    // si cambia stato
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }

  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // si cambia stato
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...

  @Override
  protected void updateFragment() {
    ....
    // si aggiorna il 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)] indica il metodo che deve essere eseguito quando si fa clic sull'opzione di menu [Navigation / Vue 2];
  • righe 19-25: per nascondere un ramo del menu, è sufficiente nascondere l'opzione radice di tale ramo;
  • riga 24: si mostrano/nascondono le opzioni radice [menuNavigation, menuActions];
  • riga 40: per visualizzare un'opzione di un ramo del menu, è necessario visualizzare non solo quest'ultima, ma anche tutte le opzioni che si incontrano risalendo dall'opzione foglia alla radice del menu;

1.21.5. La gestione del menu nel frammento [Vue2Fragment]

Si trova un codice simile nel frammento della vista n. 2:


package exemples.android.fragments;

import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;

@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {

  // i campi della vista
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // si passa alla vista 1
    mainActivity.navigateToView(0);
  }

  @Override
  protected void updateFragment() {
    // si visualizzano gli elementi dell'elenco selezionati nella vista 1
    StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
    for (CheckedData data : session.getListe()) {
      if (data.isChecked()) {
        texte.append(String.format("(%s)", data.getTexte()));
      }
    }
    texte.append("]");
    txtResultats.setText(texte);
    // si aggiorna il menu
    // setMenuOptions(...)
  }
}
  • riga 35: viene visualizzata l’opzione [Navigation / Vue 1];
  • righe 17-20: quando si fa clic sull'opzione [Navigation / Vue1], viene chiamato il metodo [navigateToView1];

1.21.6. Esecuzione

Creare un contesto di esecuzione per questo progetto ed eseguirlo.

1.22. Esempio 21: refactoring della classe astratta [AbstractFragment]

L'esempio precedente ci ha mostrato che, quando il frammento dispone di un menu, il suo metodo [onCreateOptionsMenu] è un punto ideale per richiedere 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, riprendiamo l’esempio 12, che presenta la particolarità di avere molti frammenti di cui è possibile modificare l’adiacenza. In quell’esempio, i frammenti non avevano un menu. Ora assoceremo loro un menu vuoto.

1.22.1. Creazione del progetto

Duplichiamo il progetto [Exemple-12] nel progetto [Exemple-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 occorre 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 si aggiunge a quello dell'attività: si hanno quindi a disposizione le opzioni di entrambi i menu. In questo caso, il menu dei frammenti sarà vuoto. Di conseguenza, sarà visibile solo il menu dell'attività.

1.22.3. I frammenti

  

Riprendiamo la classe astratta [AbstractFragment] dell’esempio precedente (cfr. paragrafo 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 {

Nei due frammenti [PlaceholderFragment] e [Vue1Fragment], eliminiamo i riferimenti alla vecchia classe astratta [AbstractFragment].

1.22.4. Esecuzione

Eseguite l’applicazione e verificate che funzioni. Controllate i log per vedere quando viene eseguito il metodo [onCreateOptionsMenu] della classe [AbstractFragment]. Ora è questo metodo a chiamare 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 l’esempio 21 precedente:

Image

Se ruotiamo il dispositivo [1], otteniamo la seguente nuova visualizzazione:

Image

Si nota che:

  • in [1], la scheda [Fragment n° 3] è scomparsa;
  • in [2], il testo visualizzato è effettivamente quello del frammento n. 3, ma il contatore delle visite non è corretto;

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: si nota che l’attività è stata completamente ricostruita;
  • righe 3-7: lo stesso vale per i cinque frammenti gestiti dall’attività;
  • riga 21: verrà visualizzato il frammento n. 3. Si nota che, prima dell’incremento, il numero della visita è 0;

È quindi possibile spiegare il risultato ottenuto dopo la rotazione nel modo seguente:

  • la classe [MainActivity] crea inizialmente una barra delle schede con una sola scheda, denominata [Vue 1]. È questa la scheda che si vede;
  • dopo la rotazione del dispositivo, il gestore di pagine [mViewPager] visualizza nuovamente lo stesso frammento, ovvero in questo caso il frammento n. 3. È importante ricordare che le schede e i frammenti sono concetti diversi e hanno un ciclo di vita diverso. Verrà eseguito il metodo [updateFragment] del frammento n. 3:

  public void updateFragment() {
    // registro
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // incremento del numero di visita
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // testo modificato
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • riga 7: l’ultimo numero della visita viene letto dalla sessione. Tuttavia, questa, come tutto il resto, è stata ricostruita e il numero della visita è stato azzerato. Ciò spiega il risultato visualizzato nel frammento n. 3;

1.23.2. Metodi di salvataggio/ripristino dell’attività e dei frammenti

1.23.2.1. Soluzione 1: salvataggio manuale

Durante la rotazione del dispositivo, vengono richiamati due metodi dell’attività:


// gestione salvataggio/ripristino dell'attività ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // genitore
    super.onSaveInstanceState(outState);
    // salvataggio dello stato dell'attività
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // genitore
    super.onCreate(savedInstanceState);
     // ripristino dell'attività
    // ...
  }
  • righe 2-8: il metodo [onSaveInstanceState] viene richiamato dal sistema durante la rotazione. È qui che può avvenire il salvataggio dell’attività. Se non si interviene, nulla viene salvato. Il salvataggio dello stato dell’attività deve avvenire nel parametro [Bundle outState] passato al metodo. La classe [Bundle] è simile a un dizionario. Dispone di metodi [putString, putInt, putLong, putBoolean, putChar, ...] con due parametri: void putT(String key, T value);
  • righe 10-16: il metodo [onCreate] viene chiamato al momento della creazione dell’attività. Se lo stato di quest’ultima è stato salvato, tale salvataggio viene passato al metodo tramite il parametro [Bundle savedInstanceState]. Per recuperare i valori salvati, sono disponibili metodi come [getString, getInt, getLong, geBoolean, getChar, ...] con un parametro: T getT(String key);

Anche i frammenti dispongono di questi due stessi metodi per salvare il proprio stato.

Utilizzeremo queste informazioni per salvare e ripristinare lo stato dell’esempio 21. A tal fine, duplichiamo il progetto [Exemple-21] in [Exemple-22].

1.23.2.2. Soluzione 2: salvataggio automatico

La documentazione Android indica che, durante la rotazione del dispositivo, è possibile evitare la distruzione di un frammento utilizzando l’istruzione: [Fragment].setRetainInstance(true). Diversi articoli di [StackOverflow] raccomandano di utilizzare questa istruzione solo per i frammenti senza 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 istruzione su due esempi: Esempio-17 (paragrafo 1.18 – un’applicazione con un frammento che visualizza un modulo) ed Esempio-21 (paragrafo 1.22), un’applicazione con cinque frammenti. In entrambi i casi, questa singola istruzione applicata a tutti i frammenti dell’applicazione si è rivelata insufficiente per ripristinare correttamente la vista visualizzata durante la rotazione del dispositivo. Anziché 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 di mantenere il valore predefinito false del metodo [setRetainInstance(boolean )]. L'istruzione: [Fragment].setRetainInstance(true) non è mai stata utilizzata nel prosieguo di questo documento.

1.23.3. Il metodo di salvataggio/ripristino del progetto [Exemple-22]

Il progetto [Exemple-22] si evolve come segue:

  

Si notano due nuove classi:

  • [PlaceHolderFragmentState], che memorizzerà lo stato di un frammento di tipo [PlaceHolderFragment];
  • [Vue1FragmentState], che memorizzerà lo stato del frammento di tipo [Vue1Fragment];

Queste classi sono le seguenti:


package exemples.android;

public class Vue1FragmentState {
  // stato Vue1Fragment
  private boolean hasBeenVisited=false;
  // getter e setter
...
}
  • riga 5: il valore booleano [hasBeenVisited] è vero se il frammento [Vue1Fragment] è stato visitato (visualizzato) almeno una volta. Questo campo è stato creato a scopo esemplificativo poiché il frammento [Vue1Fragment] non ha nulla da salvare;

La classe [PlaceHolderFragmentState] è la seguente:


package exemples.android;

public class PlaceHolderFragmentState {
  // stato visitato o meno
  private boolean hasBeenVisited;
  // testo visualizzato
  private String text;

  // getter e setter
...
}
  • riga 5: si ritrova il valore booleano [hasBeenVisited];
  • riga 7: il testo visualizzato dal frammento nel momento in cui deve essere salvato. Abbiamo visto che questo testo è andato perso durante la rotazione;

Lo stato dei frammenti verrà memorizzato nella sessione e sarà l’attività a occuparsi di salvare/ripristinare tale sessione. La sessione si evolve nel modo seguente:


package exemples.android;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // numero di frammenti visitati
  private int numVisit;
  // numero del frammento di tipo [PlaceholderFragment] visualizzato nella seconda scheda
  private int numFragment = -1;
  // numero della scheda selezionata
  private int selectedTab = 0;
  // numero della vista corrente
  private int currentView;

  // salvataggio dei frammenti ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];

  // costruttore
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // getter e setter
...
}
  • riga 18: lo stato del frammento [Vue1Fragment];
  • riga 19: lo stato dei frammenti di tipo [PlaceHolderFragment];
  • righe 22-27: nel costruttore della sessione, si inizializzano 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 nel modo seguente:


  // gestione salvataggio/ripristino dell'attività ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // genitore
    super.onSaveInstanceState(outState);
    // salvataggio della sessione
    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) {
    // genitore
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // ripristino sessione
      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 sotto forma della sua stringa jSON;
  • riga 29: si ripristina la sessione a partire dalla sua stringa jSON;

Per gestire il salvataggio e il ripristino dei frammenti, la classe astratta [AbstractFragment] viene modificata come segue:


// gestione salvataggio/ripristino -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // il frammento verrà nascosto - lo si salva
      saveFragment();
      saveFragmentDone = true;
    }
    // memoria
    this.isVisibleToUser = isVisibleToUser;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // genitore
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // il frammento deve essere ripristinato
    fragmentHasToBeInitialized = true;
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // genitore
    super.onSaveInstanceState(outState);
    // salvataggio del frammento solo se visibile
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // classi figlie
  protected abstract void updateFragment();

  protected abstract void saveFragment();
  • si decide 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 è necessario eseguire un salvataggio del frammento e che questo è visibile (riga 38);

Questo meccanismo evita di effettuare salvataggi più spesso del necessario. Infatti, poiché lo stato del frammento i è stato salvato quando è passato da visibile a nascosto, quando il frammento j viene visualizzato e si esegue una rotazione, è inutile salvare nuovamente il frammento i. Se non è stato visualizzato nuovamente dall’ultimo salvataggio, il suo stato non è cambiato. Deve essere salvato solo lo stato del frammento j. Questo meccanismo presenta anche un altro vantaggio: non è solo durante la rotazione del dispositivo che è necessario salvare lo stato di un frammento. Esiste anche il caso della semplice navigazione tra i frammenti, ad esempio in un sistema a schede. In tal caso, si desidera ritrovare un frammento nello stato in cui è stato lasciato l’ultima volta che è stato visualizzato. Questo stato potrebbe essere in parte andato perso se il frammento è uscito, in un determinato momento, dall’adiacenza dei frammenti visualizzati. Il frammento non viene quindi ricostruito nella sua interezza, ma lo è invece la vista ad esso associata. Il salvataggio effettuato quando il frammento è stato nascosto servirà a recuperare l’ultimo stato di tale vista;

  • righe 10, 40: per evitare di effettuare due salvataggi consecutivi, si utilizza il valore booleano [saveFragmentDone] per indicare che è stato effettuato un salvataggio;
  • righe 9, 39: si richiede al frammento figlio 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 valore booleano [fragmentHasToBeInitialized] su vero. Infatti, 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] viene modificato come segue:


// aggiornamento del frammento
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memoria
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // si richiede al frammento figlio di aggiornarsi
    updateFragment();
    // salvataggio da eseguire
    saveFragmentDone = false;
  }
  • riga 14: abbiamo visto che il valore booleano [saveFragmentDone] era passato a vrai quando era stato effettuato un salvataggio. A un certo punto deve tornare a faux. Quando viene eseguito il metodo [updateFragment] (riga 12) del frammento figlio, quest’ultimo diventa visibile. È proprio quando è visibile che un frammento deve essere salvato, nel momento specifico in cui passa dallo stato visibile a quello nascosto. Si imposta quindi il valore booleano [saveFragmentDone] su false affinché il salvataggio possa avvenire;

1.23.4. Salvataggio del frammento [Vue1Fragment]

Il salvataggio dei frammenti avviene nel metodo [saveFragment] chiamato dalla classe madre [AbstractFragment]:


// salvataggio dello stato del frammento
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // salvataggio dello stato del frammento nella sessione
    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 richiamato il metodo [saveFragment], il frammento è visibile. È quindi necessario impostare il valore booleano [hasBeenVisited] su vrai (riga 10);

1.23.5. Salvataggio del frammento [PlaceHolderFragment]

Il salvataggio dei frammenti avviene nel metodo [saveFragment] richiamato dalla classe padre [AbstractFragment]:


  @Override
  public void saveFragment() {
    // si sta salvando lo stato del frammento nella sessione
    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: salvataggio in sessione dello stato del frammento;
  • riga 5: viene salvato il testo attualmente visualizzato da [TextView] textViewInfo;
  • riga 6: il valore booleano [hasBeenVisited] del frammento viene impostato a vrai;
  • riga 7: lo stato del frammento viene memorizzato nella tabella [placeHolderFragmentStates]. Il numero dell’elemento da inizializzare è il numero di sezione del frammento meno uno;

1.23.6. Ripristino del frammento [Vue1Fragment]

Il ripristino dei frammenti avviene nel metodo [updateFragment]:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // ripristino?
    if (fragmentHasToBeInitialized) {
      // ripristino dello stato
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // navigazione?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // incremento n. visita
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // visualizza il numero della visita
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // modifica del numero della vista corrente
    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 assume il valore vrai, il frammento è stato appena ricostruito ed è necessario reinizializzarlo. È qui che avviene l'operazione. In questo specifico esempio, non c’è nulla da fare. Abbiamo semplicemente dimostrato che è possibile recuperare il valore della variabile booleana [hasBeenVisited] dallo stato salvato del frammento (riga 10);
  • riga 11: non bisogna dimenticare di riportare il valore di [fragmentHasToBeInitialized] a faux, in modo che, quando in seguito si tornerà su questo frammento senza che vi sia stata una rotazione del dispositivo, non si effettui un’inutile reinizializzazione del frammento;
  • righe 18-26: incremento del contatore delle visite. Qui c’è una difficoltà: quando si esegue un ripristino del frammento, non si vuole incrementare questo contatore. Dobbiamo distinguere in questo caso tra:
    • una semplice navigazione che riporta l'utente alla scheda [Vue 1];
    • un ripristino quando l'utente ruota il proprio dispositivo mentre è visualizzata la scheda [Vue 1];

Si distinguono questi due casi grazie al numero di vista memorizzato nella sessione. Questo numero corrisponde a quello dell’ultima vista visualizzata (riga 28).

  • riga 18: si verifica la navigazione e non il ripristino se il numero dell'ultima vista è diverso da quello della vista corrente;
  • righe 21-25: incremento del contatore delle visite e sua visualizzazione;

1.23.7. Recupero del frammento [PlaceHolderFragment]

Il ripristino dei frammenti avviene nel metodo [updateFragment]:


  // dati
  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()));
    }
    // di quale frammento si tratta?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // il frammento deve essere inizializzato?
    if (fragmentHasToBeInitialized) {
      // testo iniziale
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // navigazione?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // incremento del numero di visite
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // testo modificato
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // si tratta di un ripristino
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // visualizzazione testo
    textViewInfo.setText(newText);
    // vista corrente
    session.setCurrentView(numView);
}
  • righe 15-16: si determina il numero della vista che si sta aggiornando;
  • righe 18-22: caso in cui il frammento si trovi in un ciclo di salvataggio/ripristino a seguito di un cambiamento di orientamento del dispositivo. In questo caso è necessario ripristinarlo. Si tratta generalmente di ripristinare alcuni campi del frammento;
  • riga 20: il campo [text] della riga 2 deve contenere il testo iniziale visualizzato dal frammento: [Hello world from section i]. In questo caso deve essere rigenerato;
  • riga 21: si nota che l’inizializzazione del frammento è stata effettuata;
  • righe 24-36: come in precedenza per il frammento [Vue1Fragment], l'incremento del contatore delle visite non deve avvenire durante un ripristino. Come in precedenza, occorre distinguere tra navigazione e ripristino;
  • righe 32-36: caso del ripristino;
  • riga 34: lo stato del frammento prima della rotazione del dispositivo viene recuperato dalla sessione;
  • riga 35: qui viene recuperato il testo che era visualizzato in quel momento;
  • riga 38: questo testo viene nuovamente visualizzato;
  • riga 40: si registra nella sessione il numero della nuova vista visualizzata;

1.23.8. Gestione delle schede

I paragrafi precedenti non hanno affrontato la gestione delle schede. Tuttavia, nell’esempio 21 abbiamo riscontrato un problema durante la rotazione del dispositivo: veniva conservata solo la prima scheda [Vue 1]. La seconda scheda, invece, andava persa.

Risolviamo questo problema nella classe [MainActivity] nel modo seguente:


@AfterViews
  protected void afterViews() {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // barra degli strumenti
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // prima scheda
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // seconda scheda?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }

    // quale scheda selezionare?
    tabLayout.getTabAt(session.getSelectedTab()).select();

...

  }
  • righe 14-16: creazione della prima scheda;
  • righe 18-23: creazione della seconda scheda. Per stabilire se è necessario crearla, si controlla nella sessione il numero del frammento visualizzato nella scheda 2. Se tale numero è diverso da -1, il suo valore iniziale, allora viene creata la seconda scheda. A questo punto, si hanno due schede, di cui per impostazione predefinita è selezionata la prima;
  • riga 26: si recupera dalla sessione il numero della scheda che era selezionata prima del salvataggio/ripristino e la si seleziona nuovamente. Se il campo [selectedTab] non è stato ancora inizializzato dal codice, viene utilizzato il suo valore iniziale 0;