Skip to content

2. Struttura di base di un client Android che comunica con un servizio web / jSON

Proponiamo ora uno scheletro di applicazione Android che comunica con uno o più servizi web / jSON. Si tratta del progetto [client-android-skel] che si trova nella cartella [architecture] degli esempi:

  

L’analisi di questa applicazione base ci offrirà l’occasione di rivedere alcuni punti che abbiamo affrontato negli esempi precedenti. Questa applicazione fungerà da struttura di base per tutte le applicazioni future. È stata realizzata dopo numerose iterazioni. Il suo obiettivo è quello di fattorizzare in classi astratte il maggior numero possibile di elementi delle applicazioni che realizzeremo prossimamente, per evitare di dover scrivere sempre lo stesso tipo di codice, che differisce solo per alcuni dettagli. Le sue caratteristiche sono le seguenti:

  • la comunicazione asincrona con il server web / jSON avviene tramite la libreria RxJava;
  • il ciclo di vita di un frammento (aggiornamento, salvataggio, ripristino) è gestito dalla sua classe padre [AbstractFragment], che in momenti precisi richiama determinati metodi delle sue classi figlie. La classe figlia non deve quindi preoccuparsi delle fasi del ciclo di vita, ma solo di implementare alcuni metodi imposti dalla sua classe padre;
  • il ciclo di vita dell’attività (salvataggio / ripristino) è gestito da una classe astratta [AbstractActivity], che a sua volta impone all’attività figlia di implementare determinati metodi;
  • la classe [AbstractActivity] è in grado di gestire un’applicazione con o senza schede, con o senza immagine di attesa, con o senza autenticazione di base presso il server web / jSON. La presenza o meno di questi elementi viene determinata tramite configurazione;

Questo scheletro è stato utilizzato per tutti gli esempi successivi. A causa della loro diversità, ciò che funzionava per un esempio poteva non funzionare per quello successivo. Poiché lo scheletro è stato utilizzato per un totale di sette esempi, si sono verificate numerose iterazioni. Se lo si utilizzasse per un ottavo esempio, è possibile che anche in questo caso le specificità di questo nuovo esempio generino nuovi errori. Tuttavia, l’utilizzo di questo modello semplificherà notevolmente la scrittura degli esempi successivi. Infatti, la gestione del ciclo di vita di un frammento (aggiornamento, salvataggio, ripristino), unita al concetto di adiacenza dei frammenti, è particolarmente complessa. In questo caso, è completamente nascosta nella classe [AbstractFragment].

2.1. Architettura del client Android

Il client Android proposto si basa sulla seguente architettura:

  • il livello [DAO] implementa un'interfaccia [IDao]. È questa che comunica con il server web / jSON;
  • c'è una sola attività che implementa anche l'interfaccia [IDao]. Le viste si rivolgono a essa per raggiungere il server;
  • le viste sono implementate da frammenti;

Il progetto Android riflette questa architettura:

  

Presenteremo uno per uno i diversi elementi di questo progetto.

2.2. La configurazione Gradle

 

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 {
    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 'io.reactivex:rxandroid:1.2.0'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}
  • tutti i numeri di versione sono soggetti a modifiche. È comunque possibile partire dai numeri attuali se si configura Android Studio in modo che queste versioni degli strumenti Android (righe 15-16, 47-48) siano effettivamente presenti (cfr. paragrafo 6.11);

2.3. Il manifesto dell'applicazione

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="client.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 3: si modificherà il pacchetto dell'applicazione;
  • righe 10, 15: si imposterà il valore della voce [app_name] nel file [res / values / strings.xml]. Per il momento è il seguente:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- nome dell'applicazione -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. L’organizzazione del codice Java

  
  • [architecture] raggruppa gli elementi principali di organizzazione del codice;
  • [activity] contiene l'unica attività dell'applicazione;
  • [fragments] raggruppa i frammenti o le viste dell'applicazione;
  • [dao] raggruppa gli elementi di comunicazione con il server web / jSON;

2.5. Elementi dell'attività

 

Image

2.5.1. La vista associata all'attività

La vista [activity_main.xml] associata all'attività è la 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>

  <!-- contenitore di frammenti -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
  • riga 29: si utilizza un contenitore di frammenti specifico;

L'attività dispone inoltre di un menu [res / menu / menu_main.xml] per la propria vista:


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

Per il momento è vuoto. Lo sviluppatore lo completerà se necessario.

2.5.2. Il contenitore di frammenti [MyPager]

  

package client.android.architecture;

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 scorrimento
  private boolean isSwipeEnabled;
  // controllo dello scorrimento
  private boolean isScrollingEnabled;

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

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

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

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

Questa classe estende la classe standard Android [ViewPager] esclusivamente per gestire lo swipe (riga 11) e lo scorrimento (riga 13) tra le viste.

  • righe 26-43: i metodi che disabilitano lo swipe se questo è stato disattivato;
  • righe 46-49: ridefinizione del metodo [setCurrentItem] che serve a cambiare la vista visualizzata. Se lo scorrimento è stato disabilitato, il cambio di vista avverrà senza scorrimento. Da notare che lo sviluppatore può aggirare questa modalità di funzionamento utilizzando il metodo [setCurrentItem(int position, boolean smoothScrolling)], che gli consente di specificare lo scorrimento desiderato;

2.5.3. La classe [CoreState]

  

La classe [CoreState] è la classe padre degli stati dei diversi frammenti:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// da fare: aggiungere qui le sottoclassi di [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • riga 16: ogni frammento ha nel proprio stato un valore booleano [hasBeenVisited] che indica se è già stato visitato o meno. Ciò è necessario perché a volte, al primo visualizzazione di un frammento, ci sono operazioni particolari da eseguire;
  • riga 18: il progetto [client-android-skel] salva e ripristina automaticamente i menu dei frammenti, se ne hanno uno. Nella tabella MenuItemState[] menuOptionsState viene memorizzato lo stato di visibilità o meno di tutte le opzioni del menu;
  • righe 10-13: come è stato fatto in [Exemple-22], lo stato dell’attività e dei suoi frammenti verrà salvato nella sessione, che a sua volta verrà salvata sotto forma di una stringa jSON. Vedremo che la sessione memorizza un array di elementi di tipo [CoreState]. Se non si interviene, verrà salvata la stringa jSON di tipo [CoreState]. Noi, invece, vogliamo salvare gli stati dei frammenti, ovvero gli stati derivati da [CoreState]. Affinché venga generata la stringa jSON del tipo derivato e non quella del tipo padre, è necessario dichiarare i tipi derivati come indicato alle righe 10-13. La classe [CoreState] è una delle classi dell’architettura che lo sviluppatore deve modificare per ogni nuova applicazione (righe 10-13);

2.5.4. L'interfaccia [IMainActivity]

  

L’interfaccia [IMainActivity] definisce ciò che i frammenti possono richiedere all’attività nella seguente architettura:

Image


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione (da modificare) -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 0;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = false;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = false;

  // numero di frammenti dell'applicazione
  int FRAGMENTS_COUNT = 0;

  // da fare: aggiungere qui le costanti e altri metodi
}
  • riga 6: l'interfaccia [IMainActivity] estende l'interfaccia [IDao] del livello [DAO];
  • riga 9: è l'attività che fornisce l'accesso alla sessione sotto forma di un'istanza dell'interfaccia [ISession];
  • riga 12: è tramite questa attività che si cambia vista. Il secondo parametro è l'azione che provoca questo cambio di vista, uno dei valori SUBMIT, NAVIGATION, RESTORE;
  • righe 15-17: è l'attività che gestisce l'immagine di attesa;
  • riga 22: per il debug dell’applicazione;
  • riga 25: per non attendere troppo a lungo se il server non risponde più;
  • riga 28: in fase di debug, si imposterà un valore di alcuni secondi per avere il tempo di annullare l’operazione con il server e vedere cosa succede;
  • riga 31: a true se il servizio jSON richiede un'autenticazione di base;
  • riga 34: adiacenza dei frammenti;
  • riga 37: a vrai se l'applicazione presenta delle schede;
  • riga 39: a vrai se l'applicazione comunica con un server web / jSON e si desidera mostrare un'immagine di attesa durante gli scambi;
  • riga 43: il numero di frammenti gestiti dall'applicazione;

L'interfaccia [IMainActivity] è il secondo elemento dell'architettura che lo sviluppatore deve completare (riga 45).

2.5.5. L'interfaccia [IDao]

L'interfaccia [IMainActivity] estende la seguente interfaccia [IDao]:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // utente
  void setUser(String user, String mdp);

  // timeout del client
  void setTimeout(int timeout);

  // autenticazione di base
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  // da fare: dichiarare qui la propria interfaccia
}
  • riga 24: lo sviluppatore completerà l’interfaccia in questo punto;

2.5.6. La sessione

  

La classe [Session] incapsula gli elementi condivisi dall'attività e dai frammenti. Essa implementa la seguente interfaccia [ISession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;

public interface ISession {

  // numero dell'ultima vista visualizzata
  int getPreviousView();

  void setPreviousView(int numView);

  // ultimo stato di una vista
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // azione in corso
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // stati di tutte le viste -
  // non utilizzato dal codice ma necessario per la serializzazione/deserializzazione jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // numero dell'ultima scheda selezionata
  int getPreviousTab();

  void setPreviousTab(int position);

  // navigazione in base alla selezione della scheda
  boolean isNavigationOnTabSelectionNeeded();

  void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}

Introduciamo l'interfaccia [ISession] per imporre la presenza di determinati metodi nella sessione:

  • righe 7-10: il numero dell'ultima vista (frammento) visualizzata;
  • righe 12-15: lo stato di una vista specifica;
  • righe 17-24: introduciamo il concetto di azione in corso. Ce ne sono quattro (riga 17):
    • RESTORE: è in corso un salvataggio/ripristino. Non vi è alcun cambio di vista;
    • NAVIGATION: è in corso una navigazione. In questo contesto, per navigazione si intende un cambio di vista in cui la nuova vista può essere ripristinata a partire dal suo ultimo stato salvato nella sessione;
    • SUBMIT: si assegnerà il tipo [SUBMIT] a un'azione in corso quando si verifica un cambio di vista e la nuova vista dipende dallo stato dell'attività in generale e non dal suo stato specifico; A volte è difficile distinguere tra NAVIGATION e SUBMIT. In tal caso, si considererà il caso più generale, ovvero SUBMIT;
    • NONE: valore dell’azione quando questa non ha ancora ricevuto il suo primo valore;
  • righe 26-30: gli stati dell’attività e dei frammenti saranno memorizzati in un array di tipo CoreState[]. Affinché questo venga gestito correttamente durante le serializzazioni/deserializzazioni jSON, è necessario che disponga di un getter e di un setter;
  • righe 32-35: numero dell'ultima scheda selezionata. Viene utilizzato durante il ciclo di salvataggio/ripristino per riselezionare la scheda che era selezionata prima della rotazione del dispositivo;
  • righe 37-40: gestione di un valore booleano che indica se la selezione di una scheda deve essere accompagnata da un cambio di frammento;

L'interfaccia [ISession] è implementata dalla seguente classe astratta [AbstractSession]:


package client.android.architecture.core;

import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class AbstractSession implements ISession {
  // numero della vista precedente
  private int preViousView;

  // stato delle viste
  private CoreState[] coreStates = new CoreState[0];

  // azione in corso
  private Action action = Action.NONE;

  // scheda selezionata in precedenza
  private int previousTab;

  // navigazione in base alla selezione della scheda
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // costruttore
  public AbstractSession() {
    // si inizializza la tabella degli stati dei frammenti
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // interfaccia ISession ---------------------------------------------------------
  @Override
  public int getPreviousView() {
    return preViousView;
  }

  @Override
  public void setPreviousView(int numView) {
    this.preViousView = numView;
  }

  @Override
  public CoreState getCoreState(int numView) {
    return coreStates[numView];
  }

  @Override
  public void setCoreState(int numView, CoreState coreState) {
    coreStates[numView] = coreState;
  }

  @Override
  public Action getAction() {
    return action;
  }

  @Override
  public void setAction(Action action) {
    this.action = action;
  }

  @Override
  public CoreState[] getCoreStates() {
    return coreStates;
  }

  @Override
  public void setCoreStates(CoreState[] coreStates) {
    this.coreStates = coreStates;
  }

  @Override
  public int getPreviousTab() {
    return previousTab;
  }

  @Override
  public void setPreviousTab(int position) {
    this.previousTab = position;
  }

  @Override
  public boolean isNavigationOnTabSelectionNeeded() {
    return navigationOnTabSelectionNeeded;
  }

  @Override
  public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
    this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
  }
}
  • riga 9: il numero della vista che era visualizzata prima di quella attualmente visualizzata. Questa informazione è utile quando è possibile accedere a una vista da più punti. È tipicamente il caso della navigazione tramite schede. La vista visualizzata può quindi sapere quale fosse la vista precedente;
  • riga 12: la tabella degli stati di tutti i frammenti visualizzati dall’attività;
  • riga 18: il numero della scheda precedentemente selezionata. Svolge un ruolo analogo a quello del numero della vista precedente della riga 9. Questa informazione è utile quando si verifica una rotazione del dispositivo e si deve tornare alla scheda che era selezionata prima della rotazione;
  • riga 22: un valore booleano che indica se la selezione di una scheda deve comportare un cambiamento del frammento visualizzato. È importante sapere che il progetto [client-android-skel] gestisce separatamente le schede e i frammenti per poter essere utilizzato nei casi in cui il numero di schede sia inferiore al numero di frammenti. Esistono due tipi di selezione:
    • una selezione effettuata dall’utente quando clicca su una scheda. In questo caso, generalmente il frammento visualizzato deve cambiare;
    • una selezione software tramite il metodo [Tablayout.Tab.select()]. In questo caso, la modifica del frammento visualizzato non è sempre auspicabile. Ecco due esempi:
      • durante la rotazione del dispositivo, l’attività viene ricreata e con essa anche le schede. Tuttavia, quando viene creata la prima scheda, questa subisce automaticamente un’operazione software [select]. Non è quindi auspicabile cambiare il frammento visualizzato poiché ci si trova in una fase di ricreazione dell’attività in cui il frammento visualizzato alla fine non sarà necessariamente quello associato alla prima scheda;
      • poiché la gestione delle schede è separata da quella dei frammenti, si potrebbe voler aggiornare le schede (eliminazione, aggiunta) senza interferire con i frammenti ad esse associati. Tuttavia, alcune di queste operazioni possono, anche in questo caso, innescare un'operazione software implicita [select] su una delle schede. Questa selezione non deve quindi necessariamente tradursi in una navigazione verso il frammento associato;
  • riga 21: il campo [navigationOnTabSelectionNeeded] non è destinato a essere salvato durante le operazioni di salvataggio dell’attività e dei suoi frammenti. L'annotazione [@JsonIgnore] fa sì che il campo venga ignorato durante le operazioni di serializzazione/deserializzazione jSON;
  • righe 25-31: il costruttore inizializza l'array degli stati dei frammenti [FRAGMENTS_COUNT] dell'applicazione. Gli elementi di questo array vengono inizializzati con il campo [hasBeeenVisited=false]. Questa informazione viene utilizzata per determinare se si tratta o meno della prima visita al frammento;

La classe [Session] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // dati da condividere tra i frammenti stessi e tra i frammenti e l'attività
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
  // non dimenticare i getter e i setter necessari per la serializzazione/deserializzazione in jSON
}
  • riga 5: la classe [Session] estende la classe [AbstractSession] che abbiamo appena visto. Lo sviluppatore inserirà in essa gli elementi da condividere tra i frammenti stessi e tra i frammenti e l’attività. Si noti che la classe [Session] non è più annotata dall’annotazione AA [@EBean]. È diventata una classe normale;

2.5.7. La classe astratta [AbstractActivity]

  

2.5.7.1. Squelette

La classe [AbstractActivity] è una classe di oltre 300 righe. La analizzeremo per fasi. La sua struttura di base è la seguente:


package client.android.architecture;

import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // livello [DAO]
  private IDao dao;
  // la sessione
  protected Session session;

  // il contenitore dei frammenti
  protected MyPager mViewPager;
  // la barra degli strumenti
  private Toolbar toolbar;
  // l'immagine di caricamento
  private ProgressBar loadingPanel;
  // barra delle schede
  protected TabLayout tabLayout;

  // il gestore dei frammenti o delle sezioni
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // nome della classe
  protected String className;
  // mappatore jSON
  private ObjectMapper jsonMapper;

  // costruttore
  public AbstractActivity() {
    // nome della classe
    className = getClass().getSimpleName();
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // implementazione IMainActivity --------------------------------------------------------------------
  ...

  // ciclo di vita - salvataggio/ripristino dell'attività ------------------------------------
  ...

  // gestione dell'immagine di attesa ---------------------------------
  ...

  // interfaccia IDao -----------------------------------------------------
  ...

  // gestore dei frammenti --------------------------------
  ...

  // classi figlie
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

  protected abstract int getFirstView();

}

La classe [AbstractActivity]:

  • implementa l'interfaccia [IMainActivity] (righe 21, 55);
  • gestisce il salvataggio e il ripristino dell’attività e dei suoi frammenti durante la rotazione del dispositivo (riga 58);
  • gestisce l'immagine di attesa durante uno scambio con il server web / jSON (riga 61);
  • implementa l'interfaccia IDao del livello [DAO] (riga 64);
  • implementa il gestore dei frammenti (riga 67);
  • impone alle sue classi figlie la presenza di sei metodi (righe 71-81);

2.5.7.2. Implementare l'interfaccia [IMainActivity]

L'implementazione dell'interfaccia [IMainActivity] (cfr. paragrafo 2.5.4) è la seguente:


  // implementazione IMainActivity --------------------------------------------------------------------
  @Override
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position, ISession.Action action) {
    if (IS_DEBUG_ENABLED) {
      Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
    }
    // visualizzazione del nuovo frammento
    mViewPager.setCurrentItem(position);
    // si registra l'azione in corso durante questo cambio di vista
    session.setAction(action);
}

2.5.7.3. Salvataggio dello stato dell’attività e dei suoi frammenti

Lo stato dell’attività e dei suoi frammenti è interamente contenuto nella sessione. Si tratta quindi di salvare quest’ultima. Riprendiamo qui quanto già fatto nel progetto [Exemple-22] (cfr. paragrafo 1.23):


  // gestione del salvataggio/ripristino dell'attività ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // genitore
    super.onSaveInstanceState(outState);
    // salvataggio della sessione sotto forma di stringa jSON
    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();
      }
    }
}

2.5.7.4. Ripristino dello stato dell’attività e dei suoi frammenti

Si tratta di ripristinare la sessione. Procediamo come illustrato in [Exemple-22]:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // genitore
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // qualcosa da ripristinare?
    if (savedInstanceState != null) {
      // recupero 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();
        }
      }
    } else {
      // sessione
      session = new Session();
    }
...
  • righe 10-26: se il parametro [Bundle savedInstanceState] della riga 2 non è null, allora la sessione viene ripristinata (righe 12-17);
  • righe 26-29: il caso in cui il parametro [Bundle savedInstanceState] della riga 2 sia null corrisponde al primo avvio dell'attività. Viene quindi creata una sessione vuota;

2.5.7.5. Inizializzazione del livello [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // genitore
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // livello [DAO]
    dao = getDao();
    if (dao != null) {
      // configurazione del livello [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // classi figlie
  protected abstract IDao getDao();
....
}
  • riga 11: viene richiesto un riferimento al livello [DAO] all’attività figlia (riga 21);
  • righe 14-17: se il livello [DAO] esiste, lo si configura in base alle informazioni contenute nell’interfaccia [IMainActivity];

2.5.7.6. Inizializzazione della vista associata all’attività

La vista associata all'attività è stata illustrata al paragrafo 2.5.1:


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

  <!-- contenitore di frammenti -->
  <client.android.architecture.core.MyPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="20dp"
    android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>

Questa vista viene inizializzata con il seguente codice:


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // genitore
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // vista associata
    setContentView(R.layout.activity_main);
    // componenti della vista ---------------------
    // barra degli strumenti
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // immagine di caricamento?
    if (IS_WAITING_ICON_NEEDED) {
      // si aggiunge l'immagine di caricamento
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // creazione di ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // aggiunta di ProgressBar alla barra degli strumenti
      toolbar.addView(loadingPanel);
    }
...
  • riga 11: la vista XML [activity_main] è associata all'attività;
  • righe 14-15: la barra degli strumenti è integrata e supportata;
  • righe 17-27: eventuale aggiunta di un'immagine di attesa: se il valore booleano [IS_WAITING_ICON_NEEDED] è vero nell'interfaccia [IMainActivity];
  • riga 23: creazione dell'immagine di attesa di tipo [ProgressBar], a cui fa riferimento il campo [loadingPanel];
  • riga 24: inizialmente questa immagine è nascosta;
  • riga 26: viene aggiunta alla barra degli strumenti;

2.5.7.7. Gestione delle schede

L'interfaccia [IMainActivity] può richiedere una barra delle schede. Questa viene aggiunta e gestita nel modo seguente:


// barra delle schede
  protected TabLayout tabLayout;
...

    // barra delle schede?
    if (ARE_TABS_NEEDED) {
      // si aggiunge la barra delle schede
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // nessuna navigazione sulla selezione fino alla visualizzazione di un frammento
      session.setNavigationOnTabSelectionNeeded(false);
      // creazione barra delle schede
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // aggiunta della barra delle schede alla barra delle applicazioni
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // gestore eventi della barra delle schede
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // è stata selezionata una scheda
          if (IS_DEBUG_ENABLED) {
            Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
              tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
          }
          if (session.isNavigationOnTabSelectionNeeded()) {
            // posizione della scheda
            int position = tab.getPosition();
            // memoria
            session.setPreviousTab(position);
            // visualizzazione del frammento associato?
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  // classi figlie
  protected abstract void navigateOnTabSelected(int position);
...
  • righe 12-48: aggiunta e gestione di una barra delle schede;
  • riga 6: l’aggiunta della barra delle schede avviene se la costante [ARE_TABS_NEEDED] è impostata su vrai nell’interfaccia [IMainActivity];
  • riga 12: durante la creazione della barra delle schede, possono verificarsi operazioni [Tablayout.Tab.select] implicite (non sono provocate dall’utente). Si imposta il valore booleano [session.navigationOnTabSelectionNeeded] su faux per impedire qualsiasi navigazione durante queste selezioni errate. Spetterà allo sviluppatore selezionare il frammento da visualizzare con il metodo [navigateToView]. Il valore booleano [session.navigationOnTabSelectionNeeded] verrà reimpostato a vrai quando tale frammento verrà visualizzato (cfr. classe AbstractFragment);
  • riga 14: creazione di una barra delle schede indicata dal campo [tabLayout]. Utilizziamo una barra delle schede personalizzata [CustomTabLayout] su cui torneremo;
  • riga 15: definiamo i colori dei titoli delle schede. Questi sono riportati nel seguente file [res / color / tab_txt.xml]:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
  <item android:state_selected="true" android:color="#FFFF00" />
  <item android:state_selected="false" android:color="#FFFFFF" />
</selector>
    • riga (c): il colore del titolo della scheda quando questa è selezionata;
    • riga (d): il colore del titolo della scheda quando non è selezionata;

Questo file è ovviamente modificabile. I codici esadecimali dei colori sono disponibili, ad esempio, qui.

  • righe 17-18: aggiunta di questa barra delle schede alla barra delle applicazioni presente nella vista XML [activity_main];
  • righe 20-47: gestore degli eventi della barra delle schede;
  • righe 22-36: viene gestito solo l’evento [onTabSelected]. Corrisponde a un clic sulla scheda [Tab tab] passata come parametro al metodo oppure a un’operazione software [TabLayout.Tab.select];
  • riga 30: posizione della scheda selezionata;
  • riga 32: questa posizione viene memorizzata nella sessione;
  • riga 34: ora si tratta di visualizzare il frammento associato a questa scheda. Solo la classe figlia (riga 52) può effettuare questa associazione. Si noti che non si associa la barra delle schede al contenitore di frammenti [mViewPager] come è stato fatto in alcuni esempi esaminati. Qui si separa completamente la gestione della barra delle schede da quella dei frammenti. Per questo motivo, quando si fa clic su una scheda, è necessario specificare quale vista si desidera visualizzare;
  • riga 28: si distingue la selezione della scheda con o senza navigazione. In generale, quando l’utente clicca su una scheda, si desidera la navigazione, mentre in caso di selezione software non la si desidera. È lo sviluppatore a distinguere questi due casi con l’elemento [session.navigationOnTabSelectionNeeded]. Quando la navigazione non viene effettuata, il numero dell’ultima scheda selezionata non viene salvato nella sessione. Spetterà allo sviluppatore farlo;

2.5.7.8. Il gestore delle schede [CustomTabLayout]

  

Utilizziamo un gestore di schede personalizzato per poter visualizzare i titoli delle schede con diversi tipi di carattere. La classe [CustomTabLayout] è la seguente:


package client.android.architecture.custom;

import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

public class CustomTabLayout extends TabLayout {
  private Typeface mTypeface;

  public CustomTabLayout(Context context) {
    super(context);
    init();
  }

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

  public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init();
  }

  private void init() {
    mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
  }

  @Override
  public void addTab(Tab tab) {
    super.addTab(tab);

    ViewGroup mainView = (ViewGroup) getChildAt(0);
    ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());

    int tabChildCount = tabView.getChildCount();
    for (int i = 0; i < tabChildCount; i++) {
      View tabViewChild = tabView.getChildAt(i);
      if (tabViewChild instanceof TextView) {
        ((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
      }
    }
  }

}
  • la personalizzazione del carattere dei titoli delle schede avviene alle righe 30 e 44;

Il file [fonts] è il seguente:

  

Fonti:

  • il codice della classe [CustomTabLayout] è stato trovato in URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
  • i caratteri sono stati trovati in URL e [https://www.fontsquirrel.com/fonts/roboto];

2.5.7.9. Ultime inizializzazioni


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // classe padre
    super.onCreate(savedInstanceState);
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // istanziazione del gestore dei frammenti
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // 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 = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // si inibisce lo scorrimento tra i frammenti
    mViewPager.setSwipeEnabled(false);
    // adiacenza dei frammenti
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // viene visualizzata la prima vista
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // si passa il controllo all'attività figlia
    onCreateActivity();
  }
...
  // classi figlie
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • righe 10-19: qui si ritrova del codice che ricorre spesso negli esempi studiati;
  • righe 21-23: visualizzazione della primissima vista. Esistono senza dubbio diversi modi per distinguere questo caso. Qui abbiamo sfruttato il fatto che, per la primissima vista, il valore dell’azione che provoca il cambio di vista è NONE;
  • riga 22: non si formulano ipotesi sul primo frammento da visualizzare. Nei nostri esempi, spesso si è trattato del frammento n. 0, ma non sempre (cfr. Esempio-22). Chiederemo quindi all’attività figlia (riga 30) di indicarci quale sia questa prima vista;
  • riga 25: qui abbiamo fattorizzato tutto il possibile. Ora, la classe figlia deve eseguire le proprie inizializzazioni (riga 29);

2.5.7.10. Gestione dell’immagine di attesa

Nella classe [AbstractActivity], l’immagine di attesa è gestita dai due metodi seguenti:


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

  public void beginWaiting() {
    if (loadingPanel != null) {
      loadingPanel.setVisibility(View.VISIBLE);
    }
}

2.5.7.11. Implementazione dell’interfaccia [IDao]

Nella classe [AbstractActivity], l'interfaccia [IDao] (cfr. paragrafo 2.5.5) è implementata come segue:


public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
  // livello [DAO]
  private IDao dao;
...
  // interfaccia IDao -----------------------------------------------------
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    dao.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    dao.setBasicAuthentification(isBasicAuthentificationNeeded);
  }

  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    dao.setDebugMode(isDebugEnabled);
  }

  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
}
  • riga 3: si ricorda che il valore di questo campo è stato fornito dall’attività figlia nel metodo [onCreate];

2.5.7.12. Implementazione del gestore dei frammenti

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


...
  // gestore dei frammenti --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // costruttore
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // frammenti della classe figlia
      fragments = getFragments();
    }

    // deve restituire il frammento n. posizione
    @Override
    public AbstractFragment getItem(int position) {
      // si visualizza il frammento
      return fragments[position];
    }

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

    // restituisce il titolo del frammento n. posizione
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // classi figlie
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • riga 5: l'array dei frammenti associati all'attività. Tutti i frammenti saranno derivati dalla classe [AbstractFragment];
  • righe 8-12: il costruttore che inizializza l’array dei frammenti. Richiede i frammenti alla classe figlia dell’attività (riga 35);
  • righe 28-31: i titoli dei frammenti possono essere utilizzati in un'applicazione in cui il numero di schede corrisponde a quello dei frammenti. In questo caso, è possibile assegnare alla scheda il titolo del frammento. Qui, tali titoli vengono richiesti alla classe figlia (riga 37);

2.5.7.13. Il metodo [onResume]

Il metodo [onResume] viene eseguito poco prima che la vista associata all'attività diventi visibile. Qui viene utilizzato per selezionare una scheda dopo un salvataggio/ripristino:


  @Override
  public void onResume() {
    // classe padre
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // in caso di ripristino, occorre ripristinare l'ultima scheda selezionata
    if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
      tabLayout.getTabAt(session.getPreviousTab()).select();
    }
}
  • riga 10: selezione della scheda che era selezionata prima del processo di salvataggio/ripristino. Va ricordato che nel metodo [onCreate], che nel ciclo di vita dell’attività viene eseguito prima del metodo [onResume], la navigazione in seguito alla selezione di una scheda è stata disabilitata. Pertanto, in questo caso, avviene la selezione di una scheda ma non il cambio di frammento;

2.5.7.14. Résumé

La classe astratta [AbstractActivity] sarà la classe padre dell'unica attività dell'applicazione.

L'attività figlia dovrà implementare i seguenti sei metodi:


  // classi figlie
  protected abstract void onCreateActivity();

  protected abstract IDao getDao();

  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);

  protected abstract void navigateOnTabSelected(int position);

protected abstract int getFirstView();

L’attività figlia ha inoltre accesso ai seguenti elementi protetti della sua classe padre:


  // la sessione
  protected ISession session;
  // il contenitore dei frammenti
  protected MyPager mViewPager;
  // barra delle schede
  protected CustomTabLayout tabLayout;
  // nome della classe
protected String className;

2.5.8. L'attività [MainActivity]

  

La classe [MainActivity] può avere un nome diverso. L'unico requisito è che implementi l'interfaccia [IMainActivity]. La classe di base fornita è la seguente:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

  // metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessione
    this.session = (Session) super.session;
    // da fare: si proseguono le inizializzazioni avviate dalla classe padre
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // da fare: definire qui i frammenti
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // da fare: definire qui i titoli dei frammenti
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // da fare: navigazione tramite schede - definire la vista da visualizzare
  }

  @Override
  protected int getFirstView() {
    // da fare: navigazione tramite schede - definire la prima vista da visualizzare
    return 0;
  }
}
  • riga 14: affinché la notazione AA [@Bean] della riga 19 sia comprensibile, è necessario che l’attività abbia la notazione AA [@EActivity];
  • riga 15: l'attività è associata al menu XML [menu_main]. Attualmente questo menu è vuoto. Lo sviluppatore dovrà completarlo se necessario;
  • riga 16: la classe estende la classe [AbstractActivity];
  • righe 19-20: un riferimento al livello [DAO]. Questo verrà istanziato dalla libreria AA prima che questo campo venga inizializzato. Ciò implica che il bean AA [Dao] debba esistere. Questo vale sempre per l’applicazione scheletro che forniamo. Anche in un’applicazione priva del livello [DAO], è possibile lasciare che il pacchetto [dao] esista. Ciò non comporta complicazioni;
  • riga 22: la sessione come istanza del tipo [Session]. La sessione esiste nella classe padre [AbstractActivity] ma come istanza dell’interfaccia [ISession] (riga 32);
  • righe 24-63: i sei metodi imposti dalla classe padre [AbstractActivity];
  • righe 36-39: il metodo [getDao] restituisce un riferimento al livello [DAO]. In questo caso, tale riferimento non è mai null. Tuttavia, nella classe padre [AbstractActivity], è stato previsto il caso in cui la classe figlia restituisse un riferimento null per indicare che non esisteva il livello [DAO]. Se si desidera avvalersi di questa possibilità (a mio avviso non molto utile), è qui che occorre restituire il puntatore null;

2.6. Il livello [DAO]

Image

  

2.6.1. L'interfaccia IDao

È stata presentata al paragrafo 2.5.5:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // utente
  void setUser(String user, String mdp);

  // timeout del client
  void setTimeout(int timeout);

  // autenticazione di base
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  // da fare: dichiarare qui la propria interfaccia
}

Lo sviluppatore aggiungerà i metodi del proprio livello [DAO] a partire dalla riga 24.

2.6.2. L'interfaccia [WebClient]

  

L’interfaccia [WebClient] è la seguente:


package client.android.dao.service;

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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // da fare: dichiarare qui i URL da raggiungere
}

Lo sviluppatore aggiungerà i metodi che comunicano con URL esposti dal server jSON a partire dalla riga 17.

2.6.3. L'intercettatore di autenticazione [MyAuthInterceptor]

  

La classe [MyAuthInterceptor] è la seguente:


package client.android.dao.service;

import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;

import java.io.IOException;

@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {

  // utente
  private String user;
  // password
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // intestazioni HTTP della richiesta HTTP intercettata
    HttpHeaders headers = request.getHeaders();
    // l'intestazione HTTP di autenticazione di base
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // aggiunta alle intestazioni HTTP
    headers.setAuthorization(auth);
    // si prosegue il ciclo di vita della richiesta HTTP
    return execution.execute(request, body);
  }

  // elementi dell'autenticazione
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Questa classe genera la seguente intestazione di autenticazione HTTP:

Authorization: Basic code

dove [code] è il codice Base64 della stringa 'user:mp'. Questa classe è utile solo se il server jSON richiede questa forma di autenticazione. Ne esistono altre.

Nota: l'utilizzo di questa classe è illustrato nel paragrafo 3.6.3.1.

2.6.4. La classe [AbstractDao]

  

La classe [AbstractDao] è la seguente:


package client.android.dao.service;

import android.util.Log;
import client.android.architecture.core.Utils;
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();
  // modalità debug
  protected boolean isDebugEnabled;
  // nome della classe
  protected String className;
  // tempo di attesa prima dell'esecuzione della richiesta
  private int delay;

  // costruttore
  public AbstractDao() {
    // nome della classe
    className = getClass().getName();
    Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
  }

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

  // richiesta generica a un servizio web / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // esecuzione del servizio - si attende una sola risposta
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // esecuzione del servizio
        try {
          // in attesa?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // si sta eseguendo la richiesta sincrona
          T response = request.getResponse();
          // log
          if (isDebugEnabled) {
            String log;
            if (response instanceof String) {
              log = (String) response;
            } else {
              log = mapper.writeValueAsString(response);
            }
            Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
          }
          // si invia la risposta all'osservatore
          subscriber.onNext(response);
          // si segnala la fine dell'osservabile
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // log
          if (isDebugEnabled) {
            try {
              Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
            } catch (JsonProcessingException e1) {
              Log.d(className, String.format("Erreur jSON imprévue"));
            }
          }
          // viene generata un'eccezione
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // modalità debug
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • righe 35-81: il metodo [getResponse] utilizza la libreria RxAndroid per rappresentare un tipo [Observable<T>]. A differenza di alcuni esempi visti in precedenza, non viene renderizzato un tipo [Response<T>], che è un tipo proprietario, ma un tipo T qualsiasi;
  • riga 35: il metodo [getResponse] riceve come parametro un'istanza del tipo [IRequest<T>] delle righe 30-32, dal quale il metodo [IRequest.getReponse()] ottiene il tipo T tramite un'operazione sincrona HTTP;
  • righe 48-50: artificialmente si attende [delay] millisecondi. In produzione si imposterà [delay=0]. In fase di debug si imposterà [delay=qqs secondes] per dare all'utente la possibilità di annullare l'operazione asincrona e vedere così come si comporta il codice;
  • riga 52: la risposta attesa viene richiesta con una richiesta sincrona;
  • riga 64: una volta ricevuta la risposta, questa viene passata all’osservatore;
  • riga 66: si indica che non ci saranno ulteriori emissioni. Ci troviamo qui nel caso particolare di un'azione asincrona che restituisce un solo elemento;
  • righe 67-78: in caso di eccezione, l’eccezione viene segnalata all’osservatore (riga 77);

2.6.5. La classe [Dao]

  

La classe [Dao] è la seguente:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client del servizio web
  @RestService
  protected WebClient webClient;
  // sicurezza
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // il RestTemplate
  private RestTemplate restTemplate;
  // factory di RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // si costruisce il restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // si installa il convertitore jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // si imposta il restTemplate del client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // si imposta l'URL del servizio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // si registra l'utente nell'intercettatore
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configurazione predefinita
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercettatore di autenticazione?
    if (isBasicAuthentificationNeeded) {
      // si aggiunge l'intercettatore di autenticazione
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // metodi privati -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // da fare: implementazione IDao
}
  • righe 21-22: iniezione del bean AA [WebClient] che gestirà gli scambi con il server web / jSON;
  • righe 24-25: iniezione dell’intercettatore di autenticazione;
  • righe 31-42: metodo eseguito dopo l'inserimento dei campi delle righe 21-25;
  • riga 37: l'oggetto [RestTemplate], che gestisce gli scambi client/server, viene creato a partire da un factory. Non è indispensabile, ma è tramite il factory che è possibile configurare i timeout degli scambi. Per questo motivo non utilizziamo il costruttore senza parametri [RestTemplate()];
  • riga 39: si aggiunge un convertitore jSON ai convertitori del [RestTemplate]. Sarà l’unico convertitore. Inoltre, quando un metodo del client [WebClient] riceverà una stringa jSON dal server, questa verrà automaticamente deserializzata nell’oggetto che il metodo deve restituire;
  • riga 41: l’oggetto [RestTemplate] così configurato viene passato al client web, che grazie ad esso gestirà gli scambi client/server;
  • righe 44-48: si imposta la radice del server web URL / jSON. Tutte le URL dichiarate nella classe [WebClient] sono URL relative a questa URL radice;
  • righe 50-54: questo metodo consente di specificare il proprietario della connessione quando questa è controllata da un'autorizzazione di tipo base (cfr. paragrafo 2.6.3);
  • righe 56-64: definiscono i timeouts degli scambi client/server. Ciò avviene tramite il factory dell'oggetto [RestTemplate] che regola gli scambi;
  • righe 66-78: questo metodo consente di indicare che il server è protetto da un'autenticazione di tipo base;
  • righe 72-77: se viene richiesta un'autenticazione di tipo basic, l'intercettatore di autenticazione inserito alla riga 25 viene aggiunto agli intercettatori dell'oggetto [RestTemplate]. Questo intercettatore aggiungerà automaticamente a tutte le richieste del client web la riga HTTP di autenticazione di base richiesta dal server;
  • lo sviluppatore implementerà l’interfaccia [IDao] a partire dalla riga 87;

2.7. I frammenti

  

2.7.1. La classe [MenuItemState]

La classe [MenuItemState] incapsula lo stato di un'opzione di menu:


package client.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
...
}

2.7.2. La classe [Utils]

La classe [Utils] raggruppa metodi statici di utilità:


package client.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 getMessageForAlert(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();
  }

  // elenco dei messaggi di un'eccezione - versione 3
  static public String getMessageForAlert(List<String> messages) {
    // si crea il testo da visualizzare
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // risultato
    return texte.toString();
  }
}

2.7.3. La classe padre [AbstractFragment]

La classe [AbstractFragment] raggruppa gli elementi comuni a tutti i frammenti dell'applicazione. Come nella classe [AbstractActivity], il suo codice è complesso. Anche in questo caso lo analizzeremo passo dopo passo.

2.7.3.1. La struttura di base


package client.android.architecture.core;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;

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

public abstract class AbstractFragment extends Fragment {

  // dati privati ------------------------------------------------------------
  // gli abbonamenti agli osservabili
  private List<Subscription> abonnements = new ArrayList<>();
  // menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // stato del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // attività asincrone
  private boolean runningTasksHaveBeenCanceled;

  // dati  accessibili alle classi figlie ---------------------------------------
  // modalità debug
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nome della classe
  protected String className;
  // attività asincrone
  protected int numberOfRunningTasks;
  // attività
  protected IMainActivity mainActivity;
  protected Activity activity;
  // sessione
  protected Session session;


  // aggiornamento del frammento ----------------------------------------------------------------------------------
 ...

  // gestione del menu ------------------------------------------
  ...

  // gestione dell'attesa -------------------------------------------------------------
...

  // gestione delle operazioni asincrone --------------------------------------------------------------------
...

  // gestione delle eccezioni -------------------------------------------------------------------
....

  // gestione del ciclo di vita del frammento --------------------------------------------------------
...

  // classi figlie -----------------------------------------------------
  public abstract CoreState saveFragment();

  protected abstract int getNumView();

  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();

  protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);

}
  • righe 28-45: i dati privati della classe;
  • righe 47-58: i dati protetti accessibili dalle classi figlie;
  • righe 61-62: codice che aggiorna il frammento che verrà visualizzato;
  • righe 64-65: codice di utilità per gestire l'eventuale menu;
  • righe 67-68: codice di utilità per gestire l'attesa durante un'operazione asincrona;
  • righe 70-71: codice per facilitare la comunicazione del frammento con il livello [DAO];
  • righe 73-74: codice di utilità per gestire qualsiasi eccezione in modo standard;
  • righe 76-77: codice che gestisce il ciclo di vita del frammento;
  • righe 80-94: la classe padre impone 8 metodi alle sue classi figlie;

2.7.3.2. Il costruttore

Il costruttore della classe è il seguente:


  // nome della classe
  protected String className;
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
...
  // costruttore ----------------------
  public AbstractFragment() {
    // inizializzazione
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // log
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • riga 9: si nota il nome della classe figlia che viene qui istanziata. Questo nome viene utilizzato in tutti i log della classe padre;
  • riga 10: si registra che il frammento è in fase di costruzione. Questa informazione verrà utilizzata quando al frammento figlio verrà richiesto di aggiornarsi;

2.7.3.3. Gestione del menu

Nella nostra architettura, ogni frammento deve avere un menu, anche se vuoto. I log hanno infatti dimostrato che quando viene eseguito il metodo [onCreateOptionsMenu] – che viene chiamato quando il frammento ha un menu – il frammento è già stato associato alla sua attività, alla sua vista e al suo menu e sta per diventare visibile. Si tratta quindi di un momento in cui è possibile aggiornare l’interfaccia visiva e il menu. È all’interno di questo metodo [onCreateOptionsMenu] che richiediamo al frammento figlio di aggiornarsi.

La gestione del menu raggruppa metodi di utilità che consentono al frammento figlio di visualizzare o meno gli elementi del menu:


  // menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // gestione del menu ------------------------------------------
  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);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // risultato
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // si recuperano gli identificatori delle opzioni del menu
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // si trasferiscono le opzioni del menu in un array
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // identificativo opzione
      int id = menuOptionsIds.get(i);
      // stato opzione
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // risultato
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // stati delle opzioni di menu
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // stato
      MenuItemState state = this.menuOptionsStates[i];
      // ID del menu
      int id = state.getMenuItemId();
      // inizializzazione stato
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // risultato
    return menuOptionsStates;
  }

  // visualizzazione delle opzioni di menu -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // si aggiornano tutte le opzioni del menu
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // si aggiornano alcune opzioni del menu
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • righe 6-18: questo metodo consente di ottenere gli identificatori numerici di tutte le opzioni del menu;
  • riga 6: il metodo [getMenuOptions] riceve due parametri:
    • [Menu menu]: il menu del frammento;
    • [List<Integer> menuOptionsIds]: l'elenco degli identificatori Android delle opzioni del menu. Inizialmente questo elenco è vuoto. Viene poi popolato tramite una scansione ricorsiva (riga 15) dell'albero del menu;
  • righe 20-40: a partire dal menu, costruisce l’array degli stati (identificativo, visibilità) delle opzioni del menu. Questo array viene memorizzato alla riga 3. La classe [MenuItemState] è stata descritta al paragrafo 2.7.1;
  • righe 43-55: una variante del metodo precedente. Esegue la stessa operazione, ma invece di ricalcolare gli identificatori di tutte le opzioni del menu, cosa che è già stata fatta, utilizza gli identificatori della tabella degli stati della riga 3;
  • righe 58-63: il metodo [setAllMenuOptionsStates] consente di nascondere o mostrare tutte le opzioni del menu del frammento;
  • righe 65-69: il metodo [setMenuOptionsStates] consente, in modo selettivo, di visualizzare o nascondere alcune delle opzioni del menu;
  • i metodi [getMenuOptions, getMenuOptionsStates] sono dichiarati privati poiché utilizzati esclusivamente all’interno di [AbstractFragment]. I metodi [setAllMenuOptionsStates] (riga 58) e [setMenuOptionsStates] (riga 65) sono dichiarati protetti per renderli disponibili alle classi figlie;

2.7.3.4. Gestione dell'attesa della fine di un'operazione asincrona


   // gli abbonamenti agli osservabili
  private List<Subscription> abonnements = new ArrayList<>();
// attività asincrone
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // gestione dell'attesa del completamento di un'operazione asincrona -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // si registra il numero di attività che verranno eseguite
    this.numberOfRunningTasks = numberOfRunningTasks;
    // si imposta l'immagine di attesa
    mainActivity.beginWaiting();
    // si svuota l'elenco degli abbonamenti
    abonnements.clear();
    // ancora nessuna cancellazione
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // si nasconde l'immagine di attesa
    mainActivity.cancelWaiting();
  }

  • righe 9-18: per avviare una o più operazioni asincrone, il frammento figlio chiamerà il metodo padre [beginRunningTasks]. Il parametro di questo metodo è il numero di attività asincrone che il frammento figlio avvierà;
  • riga 11: si memorizza il parametro del metodo;
  • riga 13: viene visualizzata l'immagine di attesa;
  • riga 15: si azzera l'elenco delle sottoscrizioni alle operazioni asincrone. Queste non sono ancora state create dal frammento figlio;
  • riga 17: si gestisce un valore booleano per segnalare che le attività asincrone richieste dal frammento figlio sono state annullate. Inizialmente il valore booleano ha il valore false;
  • righe 20-25: il frammento figlio chiama il metodo padre [cancelWaitingTasks] per indicare che desidera annullare le attività che ha avviato;
  • riga 22: l'immagine di attesa viene nascosta;

2.7.3.5. Gestione delle eccezioni


  // gestione delle eccezioni -------------------------------------------------------------------

  // visualizzazione avviso di eccezione
  protected void showAlert(Throwable th) {
    // visualizzazione dei messaggi della pila di eccezioni del Throwable th
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // visualizzazione dell'elenco dei messaggi
  protected void showAlert(List<String> messages) {
    // visualizza l'elenco dei messaggi
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
  • righe 4-7: il metodo [showAlert(Throwable)] consente a un frammento figlio di visualizzare in una finestra i messaggi della pila delle eccezioni del Throwable passato come parametro;
  • righe 10-13: il metodo [showAlert(List<String>] consente a un frammento figlio di visualizzare in una finestra l'elenco dei messaggi passato come parametro;
  • la classe [Utils] utilizzata alle righe 6 e 12 è stata descritta nel paragrafo 2.7.2;

2.7.3.6. Gestione delle operazioni asincrone


...
  // Abbonamenti agli osservabili
  private List<Subscription> abonnements = new ArrayList<>();
  // attività asincrone
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // esecuzione di un'attività asincrona con RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // process: l'osservabile da eseguire/osservare
    // consumeResult: il metodo che elabora la risposta ottenuta
    // 
    // si creano nuovi abbonamenti solo se non c'è stata alcuna cancellazione
    if (!runningTasksHaveBeenCanceled) {
      // esecuzione sul thread di I/O e osservazione sul thread dell'interfaccia utente
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // si esegue l'osservabile
      try {
        abonnements.add(process.subscribe(
          // consumo del risultato
          consumeResult,
          // consumo dell'eccezione
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // fine attività
          new Action0() {

            @Override
            public void call() {
              endOfTask();
            }
          }));
      } catch (Throwable th) {
        consumeThrowable(th);
      }
    }
  }

  private void endOfTask() {
...
  }

  // un'operazione asincrona ha generato un'eccezione
  // oppure si è verificata un'eccezione durante l'esecuzione di un'operazione asincrona
  private void consumeThrowable(Throwable th) {
...
  }

  • righe 9-41: eseguono un'attività asincrona;
  • riga 9: il metodo [executeInBackground] richiede due parametri:
    • [Observable<T> process]: il processo asincrono da eseguire;
    • [Action1<T> consumeResult]: il metodo del frammento figlio da chiamare per trasmettergli gli elementi emessi dal processo. Nei nostri esempi precedenti, i processi hanno sempre emesso un solo elemento. Il tipo T di [Action1<T>] è il tipo T del risultato restituito dal processo osservato;
  • riga 14: l’attività asincrona viene avviata solo se non si è già verificata un’annullamento da parte dell’utente o del programma (a causa di un’eccezione);
  • riga 16: il processo è configurato per essere eseguito su un thread di I/O e osservato sul thread dell’interfaccia utente;
  • riga 16: l'istruzione [process.subscribe] avvia l'esecuzione del processo nel thread di I/O. All'interno di questo thread, le operazioni vengono eseguite in modo sincrono poiché utilizziamo una libreria HTTP che è sincrona;
  • riga 19: il metodo [process.subscribe] ha tre parametri:
    • riga 21: [consumeResult]: il metodo del frammento figlio che consumerà gli elementi emessi dal processo;
    • righe 22-28: il metodo eseguito quando si verifica un'eccezione durante l'elaborazione dell'attività asincrona. L'elaborazione viene delegata al metodo [consumeThrowable] della riga 49;
    • righe 29-36: il metodo eseguito quando l'attività invia la notifica di fine trasmissione. L'elaborazione viene delegata al metodo [endOfTask] alla riga 43;
  • riga 19: l’attività asincrona appena avviata viene registrata nel campo [abonnements], che tiene traccia di tutte le attività asincrone avviate. Ciò consentirà di annullarle se necessario;
  • righe 37-39: metodo eseguito quando si è verificata un’eccezione durante l’elaborazione dell’attività asincrona. L’elaborazione viene delegata al metodo [consumeThrowable] alla riga 49;

Il metodo [endOfTask] è il seguente:


  // attività asincrone
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // un'attività in meno da attendere
    numberOfRunningTasks--;
    // finito?
    if (numberOfRunningTasks == 0) {
      // fine attesa
      cancelWaitingTasks();
      // si segnala la fine delle attività alla classe figlia
      notifyEndOfTasks(false);
    }
  }
...
  // classi figlie -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • riga 6: un'attività asincrona è appena terminata. Si decrementa il contatore delle attività attive;
  • riga 8: se non ci sono più attività attive, allora il frammento figlio ha ottenuto tutte le sue risposte;
  • riga 10: si annulla l'attesa;
  • riga 12: si segnala al frammento figlio che tutte le attività da esso avviate sono terminate chiamando il suo metodo [notifyEndOfTasks]. Il parametro di questo metodo indica come si sono concluse le attività: normalmente oppure a seguito di un’annullamento da parte dell’utente o del codice a causa di un’eccezione. Alla riga 12 si segnala una conclusione normale. Si noti che il frammento figlio non deve preoccuparsi di tenere traccia delle attività ancora attive. La sua classe padre lo fa per lui;

Il metodo [consumeThrowable] è il seguente:


  // attività asincrone
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // un'operazione asincrona ha generato un'eccezione
  // oppure si è verificata un'eccezione durante l'esecuzione di un'operazione asincrona
  private void consumeThrowable(Throwable th) {
    // th: l'eccezione da gestire
    // 
    // log
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // si annullano le attività già avviate
    cancelRunningTasks();
    // vengono visualizzati i messaggi di errore
    showAlert(th);
  }

  // annullamento delle attività
  protected void cancelRunningTasks() {
    // log
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // si annullano tutte le attività asincrone registrate
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // si registra l'annullamento
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // fine dell'attesa
    cancelWaitingTasks();
    // si segnala l'annullamento delle attività al frammento figlio
    notifyEndOfTasks(true);
}

...
  // classi figlie -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • riga 3: il metodo [consumeThrowable] riceve l’eccezione che si è verificata;
  • riga 15: tutte le attività ancora attive vengono annullate;
  • riga 17: viene visualizzato il testo dell’eccezione;
  • righe 21-37: annullamento di tutte le attività;
  • righe 27-29: tutti gli abbonamenti vengono annullati;
  • riga 31: si registra che è avvenuta l'annullamento;
  • riga 32: il contatore delle attività viene azzerato;
  • riga 34: l'attesa viene annullata;
  • riga 36: si segnala al frammento figlio la fine delle attività a seguito dell'annullamento;

2.7.3.7. Gestione del ciclo di vita del frammento


  // ciclo di vita --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // genitore
    super.onDestroyView();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // genitore
    super.onDestroy();
    // log
    if (isDebugEnabled) {
      Log.d(className, "onDestroy");
    }
  }

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

  private void saveState() {
...
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
...
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
...
}
  • righe 2-20: i metodi [onDestroyView, onDestroy] servono solo per i log. Questi consentono allo sviluppatore di comprendere meglio il ciclo di vita dei frammenti;

Il salvataggio del frammento durante la rotazione del dispositivo viene effettuato dai seguenti metodi [setUserVisibleHint, onSaveInstanceState, saveState]:


  // ciclo di vita del frammento
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // genitore
    super.setUserVisibleHint(isVisibleToUser);
    // salvataggio?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // il frammento sta per essere nascosto - lo si salva
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // memoria
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // genitore
    super.onSaveInstanceState(outState);
    // salvataggio del frammento solo se è visibile
    if (isVisibleToUser) {
      // forse il salvataggio è già stato effettuato
      if (!saveFragmentDone) {
        saveState();
      }
      // ripristino da eseguire in ogni caso
      session.setAction(ISession.Action.RESTORE);
    }
}
  • righe 6-19: il salvataggio del frammento avviene se questo passa dallo stato visualizzato a quello nascosto (riga 11). È il metodo [setUserVisibleHint] che ci fornisce questa informazione;
  • riga 14: il salvataggio viene effettuato dal metodo privato delle righe 21-23;
  • righe 25-41: in caso di rotazione del dispositivo, verrà chiamato il metodo [onSaveInstanceState]. Il frammento viene salvato a due condizioni:
    • è visibile (riga 34);
    • non è stato ancora salvato (riga 36). È possibile che i metodi [setUserVisibleHint, onSaveInstanceState] non possano essere eseguiti entrambi quando il frammento è visibile e che, di conseguenza, la gestione del valore booleano [saveFragmentDone] sia superflua. Nel dubbio, ho preferito utilizzare quest’ultimo;
  • riga 40: dopo il salvataggio seguirà il ripristino. Si noti che, la prossima volta che il frammento dovrà aggiornarsi, dovrà farlo tramite un'operazione [RESTORE];

Si notino i due momenti in cui viene richiesto un salvataggio del frammento:

  1. quando il frammento passa dallo stato visibile a quello nascosto;
  2. quando si verifica una rotazione del dispositivo;

Il metodo privato [saveState] è il seguente:


...
  private void saveState() {
    // attività da annullare?
    if (numberOfRunningTasks != 0) {
      // si annullano le attività
      cancelRunningTasks();
    }
    // si salva lo stato del frammento
    CoreState currentState = saveFragment();
    // il frammento è stato visitato
    currentState.setHasBeenVisited(true);
    // salvataggio dello stato del menu
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // accesso
    session.setCoreState(getNumView(), currentState);
    // salvataggio effettuato
    saveFragmentDone = true;
    // registro
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // classi figlie -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • righe 4-7: la rotazione del dispositivo può avvenire mentre sono in corso operazioni asincrone. In questo caso si decide di annullarle tutte. Non è una buona decisione per l’utente, che dovrà effettuare una nuova richiesta potenzialmente lunga solo perché ha spostato il proprio telefono o tablet oppure ha ricevuto una telefonata. È possibile mantenere le connessioni di rete durante un ciclo di backup/ripristino. Tuttavia, le soluzioni non sono ovvie e ho deciso di non affrontarle in questo corso per principianti. La strada da seguire consiste nell’effettuare queste connessioni di rete tramite un frammento senza interfaccia visiva associata e che non venga distrutto durante il ciclo di salvataggio/ripristino. A tal fine è sufficiente utilizzare l’istruzione [Fragment.setRetainInstance(true)];
  • riga 9: si richiede al frammento figlio di salvare il proprio stato in un tipo derivato da [CoreState] (riga 31);
  • riga 11: si registra che il frammento è stato visitato. Questa informazione è utile. Quando un frammento viene visitato per la prima volta, il suo aggiornamento può essere diverso da quelli successivi poiché in quel momento non ha uno stato precedente nella sessione;
  • riga 13: si salva lo stato del menu, il che ci consentirà di ripristinarlo automaticamente;
  • riga 15: questo stato corrente viene salvato nella sessione. In essa, gli stati sono raggruppati per vista/frammento, ciascuno dei quali ha uno stato. Il numero della vista è fornito dal frammento figlio (riga 33);
  • riga 17: si registra che il salvataggio del frammento è stato effettuato. Questo perché due metodi potrebbero chiamare il metodo [saveState] ed è inutile effettuare due salvataggi;

La rigenerazione della vista associata al frammento è garantita dal seguente metodo:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // classe padre
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // la vista deve essere ripristinata
    viewHasToBeInitialized = true;
}

Nel ciclo di vita, il metodo [onActivityCreated] viene eseguito subito dopo il metodo [onCreateView]. La chiamata di quest’ultimo metodo indica che la vista associata al frammento deve essere ricostruita. Ci limitiamo a prenderne nota alla riga 10.

2.7.3.8. Aggiornamento del frammento

L'aggiornamento del frammento è l'ultima operazione eseguita sul frammento prima che diventi visibile e rimanga in attesa delle azioni dell'utente. È garantito dal seguente codice:


  // menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // stati del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // aggiornamento del frammento ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // registro
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // memoria
    this.menu = menu;
    // si recuperano le # opzioni del menu se non è già stato fatto
    if (fragmentHasToBeInitialized) {
      // si recuperano le # opzioni del menu
      getMenuOptionsStates(menu);
      // attività
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // si recupera lo stato precedente del frammento (la primissima volta, solo il valore booleano hasBeenVisited ha un significato)
    previousState = session.getCoreState(getNumView());
    // aggiornamento del frammento figlio in più fasi
    // fase 1 - è la prima visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // non è la prima visita
      // fase 2: il frammento deve essere inizializzato?
      ...
      // Fase 3: la vista deve essere inizializzata?
      ...
    }
    // Fase 4: un invio, una navigazione, un ripristino?
    ...

    // Fase 5: aggiornamenti terminali ----------------------
...
  }
...
  // classi figlie -----------------------------------------------------
  protected abstract void initFragment(CoreState previousState);

  protected abstract void initView(CoreState previousState);

  protected abstract void updateOnSubmit(CoreState previousState);

  protected abstract void updateOnRestore(CoreState previousState);

  protected abstract void notifyEndOfUpdates();
  • riga 19: viene utilizzato il metodo [onCreateOptionsMenu] per aggiornare il frammento. Per questo motivo, il frammento deve disporre di un menu, anche vuoto se necessario. Quando questo metodo viene eseguito, il frammento è già stato associato alla propria vista e alla propria attività ed è inoltre visibile;
  • riga 25: si memorizza il menu che è stato passato come parametro (riga 22) al metodo;
  • righe 27-34: se il frammento deve essere inizializzato:
    • riga 29: gli stati delle opzioni del menu vengono inseriti nell’array [menuOptionsStates] della riga 3;
    • riga 31: l'attività viene memorizzata come istanza del tipo Android [Activity];
    • riga 32: l'attività viene memorizzata come istanza dell'interfaccia [IMainActivity];
    • riga 33: la sessione viene memorizzata. Il cambio di tipo è necessario, poiché il metodo [mainActivity.getSession()] restituisce un tipo [ISession];
  • riga 36: si recupera dalla sessione lo stato precedente del frammento. Se si tratta della prima visita al frammento, solo il valore booleano [previousState.hasBeenVisited] ha un significato;
  • righe 39-44: codice eseguito quando si tratta della prima visita al frammento. In questo caso, il suo stato precedente non è significativo;
  • righe 44-50: codice eseguito quando non si tratta della prima visita al frammento;
  • righe 46-47: codice eseguito se è stato chiamato il costruttore del frammento (fragmentHasToBeInitialized==true);
  • righe 48-49: codice eseguito se la vista associata al frammento è stata ricostruita (viewHasToBeInitialized==true);
  • righe 51-52: codice eseguito a seconda dell'azione (SUBMIT, NAVIGATION, RESTORE) in corso;
  • righe 54-55: codice sempre in esecuzione;

Le cinque fasi dell'aggiornamento sono le seguenti:

fase 1


  // menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // stati del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // si recupera lo stato precedente del frammento (la primissima volta, solo il valore booleano hasBeenVisited ha un significato)
    previousState = session.getCoreState(getNumView());
    // aggiornamento del frammento figlio in più fasi
    // fase 1 - è la prima visita?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // inizializzazione del frammento e della vista
      initFragment(null);
      initView(null);
      // azzeramento di previousState per il seguito
      previousState = null;
    } else {
      // Non è la prima visita
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • riga 19: lo stato precedente del frammento viene recuperato dalla sessione;
  • righe 22-31: codice eseguito se il frammento non è mai stato visitato;
  • riga 27: si richiede alla classe figlia di inizializzare il frammento. Il parametro del metodo [initFragment] alla riga 35 è lo stato precedente del frammento. In questo caso, si passa null per indicare al frammento figlio che si tratta della prima visita;
  • riga 28: si richiede alla classe figlia di inizializzare la vista associata al frammento. Il parametro del metodo [initView] alla riga 37 è lo stato precedente del frammento. Qui si passa null per indicare al frammento figlio che si tratta della prima visita;
  • riga 30: si imposta lo stato precedente su null per le fasi successive;

fasi 2 e 3


// menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // stati del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // si recupera lo stato precedente del frammento (la primissima volta, solo il valore booleano hasBeenVisited ha un significato)
    previousState = session.getCoreState(getNumView());
    // aggiornamento del frammento figlio in più fasi
    // fase 1 - è la prima visita?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // non è la prima visita
      // fase 2: il frammento deve essere inizializzato?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // frammento figlio
        initFragment(previousState);
      }
      // Fase 3: la vista deve essere inizializzata?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // frammento figlio
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • righe 24-42: eseguite quando non si tratta della prima visita al frammento;
  • righe 27-33: se il frammento è stato appena ricostruito, lo si reinizializza chiamando il metodo [initFragment] della classe figlia (righe 32, 46). Gli si passa lo stato precedente del frammento;
  • righe 35-51: se la vista associata al frammento deve essere inizializzata o reinizializzata, si chiede al frammento figlio di farlo (righe 40, 48). Anche in questo caso, gli viene passato l’ultimo stato noto del frammento;

fase 4


// menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Stati del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // si recupera lo stato precedente del frammento (la primissima volta, solo il valore booleano hasBeenVisited ha un significato)
    previousState = session.getCoreState(getNumView());
    // aggiornamento del frammento figlio in più fasi
 ...

    // fase 4: un invio, una navigazione, un ripristino?
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
        Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // azione in corso
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // frammento figlio
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // ripristino del menu
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // frammento figlio
          updateOnRestore(previousState);
        } else {
          // si tratta di una prima visita - non c'è nulla da fare
        }
        break;
      case RESTORE:
        // ripristino
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // ripristino menu (previousState non può essere nullo)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // frammento secondario
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • righe 34-66: si elabora l’azione in corso, che può essere una delle tre seguenti:
    • RESTORE: si sta eseguendo un ripristino del frammento dopo una rotazione del dispositivo;
    • NAVIGATION: si ritorna al frammento con l’intenzione di ritrovarlo nello stato in cui è stato lasciato l’ultima volta che è stato utilizzato;
    • SUBMIT: tutti gli altri casi;
  • riga 34: si recupera l'azione in corso;
  • righe 36-42: per un'azione di tipo SUBMIT, si chiama il metodo [updateOnSubmit] del frammento figlio (righe 41, 68) passandole l'ultimo stato noto del frammento;
  • righe 43-55: per un'azione di tipo NAVIGATION;
  • righe 47-54: vogliamo riportare il frammento al suo ultimo stato conosciuto. L'operazione NAVIGATION può verificarsi in occasione di una prima visita. Ciò avverrebbe, ad esempio, in un'applicazione a schede: se passo dalla scheda 1 alla scheda 4:
    • devo inizializzare il frammento della scheda 4 se si tratta della prima visita;
    • ripristinare il frammento della scheda 4 al suo stato precedente se non si tratta della prima visita;
  • righe 52-54: non si fa nulla se si tratta di una prima visita. Sarà il metodo figlio [initView(CoreState previousState)] a occuparsi di questa inizializzazione. La prima visita è caratterizzata dalla condizione [previousState==null];
  • riga 49: se non è la prima visita del frammento, gli viene restituito il suo menu;
  • riga 51: si chiede alla classe figlia di aggiornarsi richiamando il metodo della riga 70. Le viene passato lo stato precedente del frammento affinché possa svolgere il proprio compito;
  • righe 56-66: nel caso di un'operazione di ripristino del frammento, si procede come nel caso di una navigazione al di fuori della prima visita;

fase 5


// menu del frammento
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // ciclo di vita del frammento
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // stati del frammento
  private CoreState previousState;
  // mappatore jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // ciclo di vita del frammento
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // fase 5: aggiornamenti finali ----------------------
    // si è cambiata la vista
    session.setPreviousView(getNumView());
    // nessuna azione in corso
    session.setAction(ISession.Action.NONE);
    // quando si uscirà da questo frammento, dovrà essere salvato
    saveFragmentDone = false;
    // finché il frammento non viene ricostruito, non deve essere inizializzato
    fragmentHasToBeInitialized = false;
    // finché la vista non viene ricostruita, non è necessario inizializzarla
    viewHasToBeInitialized = false;
    // si torna al normale funzionamento della selezione delle schede
    session.setNavigationOnTabSelectionNeeded(true);

    // si segnala al frammento che la vista è pronta
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • righe 18-30: quando si arriva qui, il frammento è stato inizializzato ed è pronto per essere visualizzato. Si riportano quindi tutti gli indicatori utilizzati nella gestione del ciclo di vita del frammento allo stato iniziale;
  • riga 20: si è cambiata la vista: lo si annota nella sessione;
  • riga 22: non ci sono più azioni in corso;
  • riga 24: quando si uscirà dal frammento attualmente visualizzato, sarà necessario salvarlo al momento dell’uscita;
  • riga 26: il frammento non deve più essere ricostruito. Questo indicatore verrà reimpostato a vrai quando il costruttore del frammento verrà nuovamente eseguito;
  • riga 28: la vista associata al frammento non deve più essere inizializzata. Questo indicatore verrà reimpostato a vrai quando il metodo [onActivityCreated] verrà nuovamente eseguito;
  • riga 30: il frammento viene visualizzato, ad esempio, in un'applicazione a schede. In questo caso, quando l'utente clicca su una di esse, deve avvenire un cambio di frammento;
  • riga 36: si comunica alla classe figlia che il frammento è pronto. Quest’ultima può inserire nel metodo [notifyEndOfUpdates] gli aggiornamenti da effettuare in ogni caso, avviare un’operazione asincrona per ottenere nuovi dati, ...

2.7.4. Un esempio di frammento

  

Nel progetto [client-android-skel] è stato inserito un esempio di frammento per mostrare al lettore la struttura tipica di un frammento di un'applicazione basata su questo progetto.

La classe [DummyFragment] è la seguente:


package client.android.fragments.behavior;

import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;

public class DummyFragment extends AbstractFragment {

  // campi ereditati dalla classe padre -------------------------------------------------------

  // modalità debug
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // nome della classe
  //-- stringa protetta className;
  // attività asincrone
  //-- int protetto numberOfRunningTasks;
  // attività
  //-- protected IMainActivity mainActivity;
  //-- protetta Attività;
  // sessione
  //-- protected Session session;

  // metodi ereditati dalla classe padre -------------------------------------------------------

  // visualizzazione delle opzioni di menu
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // gestione dell'attesa del completamento di una serie di attività asincrone
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // esecuzione di un'attività asincrona con RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // annullamento delle attività
  //-- protected void cancelRunningTasks() {
  // visualizzazione di un avviso in caso di eccezione
  //-- protected void showAlert(Throwable th) {
  // visualizzazione dell'elenco dei messaggi
  //-- protected void showAlert(List<String> messages) {

  // metodi imposti dalla classe padre -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // è necessario salvare il frammento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senon c'è nulla da salvare, eseguire [return new CoreState();] ed eliminare la classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // è necessario restituire il numero del frammento nella tabella dei frammenti gestiti dall'attività (cfr. MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // il frammento diventa visibile ed è stato creato in questa fase o in una fase precedente
    // ciò avviene all’avvio dell’applicazione e ad ogni rotazione del dispositivo Android
    // è necessariamente seguita dall'esecuzione di [initView]
    // è necessario inizializzare i campi del frammento che è stato ricostruito
    // previousState è l'ultimo salvataggio del frammento - assume il valore null se si tratta della prima visita al frammento
  }

  @Override
  protected void initView(CoreState previousState) {
    // il frammento diventa visibile e la vista associata è stata ricostruita in questa fase o in una fase precedente
    // ciò avviene ogni volta che viene eseguita [initFragment] e ogni volta che il frammento esce dall’adiacenza del frammento visualizzato
    // è necessario inizializzare i componenti della vista che è stata ricostruita
    // previousState è l'ultimo salvataggio del frammento - ha valore null se si tratta della prima visita al frammento

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // viene eseguito dopo [initFragment, initView] se questi metodi vengono eseguiti
    // la vista verrà visualizzata dopo un'operazione di tipo SUBMIT
    // in genere è necessario inizializzare il frammento e la vista associata a partire dalla sessione
    // previousState è l'ultimo salvataggio del frammento - ha valore null se si tratta della prima visita al frammento
    // non c'è nulla da fare se non è possibile arrivare al frammento tramite un'operazione SUBMIT
    // se è possibile raggiungere il frammento tramite operazioni SUBMIT a partire da frammenti diversi, è possibile conoscere la vista precedente tramite [session.getPreviousView]
    // se è possibile arrivare al frammento tramite diverse operazioni SUBMIT a partire dallo stesso frammento, allora è necessario impostare un indicatore per distinguere i diversi tipi di SUBMIT a partire da quel frammento
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // viene eseguito dopo [initFragment, initView] se questi metodi vengono eseguiti
    //: la vista verrà visualizzata dopo un'operazione di tipo RESTORE o NAVIGATION
    // previousState è l'ultimo salvataggio del frammento - non è mai nullo
    // è necessario riportare la vista allo stato precedente

  }

  @Override
  protected void notifyEndOfUpdates() {
    // viene eseguito dopo i metodi [updateOnSubmit, updateOnRestore]
    // a questo punto, la vista è stata costruita e inizializzata
    // spesso qui non c'è nulla da fare, ma è anche possibile raggruppare qui le azioni da eseguire indipendentemente dal modo in cui si arriva a questa vista
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // chiamata quando le attività asincrone avviate dal frammento sono state completate o annullate
    // Questi due casi possono essere distinti grazie al parametro runningTasksHaveBeenCanceled
    // in genere è necessario riportare la vista in uno stato diverso da quello in cui si trovava mentre era in attesa delle risposte delle attività asincrone

  }
}

La classe [DummyFragment] potrebbe non avere uno stato. In questo caso ne è stato inserito uno per ricordare cosa ci si aspetta al suo interno:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // stato del frammento [DummyFragment]
  // inserire solo campi serializzabili in jSON
  // inserire l'annotazione @JsonIgnore sugli altri, ma non è chiaro a cosa potrebbero servire
  // non dimenticare i getter/setter: servono per la serializzazione/deserializzazione
}

Per illustrare l'utilizzo del progetto [client-android-skel], utilizzeremo innanzitutto alcuni esempi semplici prima di passare a un caso di studio più completo.

2.8. Esercizi illustrativi

Inizieremo rifattorizzando alcuni esempi già scritti.

2.8.1. Esempio 17B

Riprendiamo l’esempio 17 esaminato nel paragrafo 1.18. Si tratta di un’applicazione con un unico frammento, senza attività asincrone e senza schede. La esaminiamo per vedere come si comporta durante la rotazione del dispositivo. Effettuiamo le seguenti immissioni:

Image

Poi, in [1], ruotiamo il dispositivo due volte. La nuova vista è quindi la seguente:

Image

Se confrontiamo le visualizzazioni, tutto è stato conservato tranne l’elenco [2], che ora è vuoto.

Inoltre, se si fa clic sul pulsante [Valider], viene visualizzata una finestra di dialogo che mostra i dati inseriti nel modulo. Se in quel momento si ruota il dispositivo, la finestra di dialogo scompare.

Sarà quindi necessario, in caso di rotazione, rigenerare:

  • l'elenco a discesa e l'elemento selezionato;
  • la finestra di dialogo, se era visualizzata al momento della rotazione;

2.8.1.1. Il progetto [Exemple-17B]

Duplichiamo il progetto [client-android-skel] in esempi/Esempio-17B. Quindi carichiamo il nuovo progetto [1]:

  • nel file [2-3], nella cartella [behavior], incolliamo il frammento [Vue1Fragment] del progetto [Exemple-17];
  • in [4-5], nella cartella [layout] di [Exemple-17B], si incolla la vista [vue1.xml] da [Exemple-17]. Questa è la vista associata al frammento;
  • in [6], la cartella [values] di [Exemple-17B] viene sostituita dalla cartella [values] di [Exemple-17];

Modificheremo il margine superiore della vista [vue1.xml] a 80 dp:


    <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="80dp"
      android:text="@string/titre_vue1"
android:textSize="30sp"/>

A questo punto, è possibile eseguire una prima compilazione per individuare gli errori. I primi errori segnalati provengono da imports e riguardano pacchetti che hanno cambiato posizione. Li si corregge (Ctrl-Shift-O). Altri errori derivano dal fatto che la vista [Vue1Fragment] non implementa tutti i metodi richiesti dalla sua classe padre [AbstractParent]:

Image

Si generano i metodi mancanti (Alt-Invio).

Un altro errore di compilazione segnalato è il seguente:

Image

Si corregge questo errore nel file [build.gradle] del modulo (riga 20 qui sotto):

 

A questo punto, è possibile ricompilare per verificare la presenza di eventuali errori residui. L'unico errore segnalato riguarda il metodo [Vue1Fragment.updateFragment]:

 

È necessario rimuovere l'annotazione [@Override] dalla riga 135. Ora non ci sono più errori. Partiremo da qui per modificare il progetto.

2.8.1.2. Stato del frammento [Vue1Fragment]

Il frammento [Vue1Fragment] deve salvare alcune informazioni durante la rotazione del dispositivo, in modo da poter essere ripristinato completamente. A tal fine creiamo una classe [Vue1FragmentState]:

  

Per il momento, questa classe è vuota:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {
  
}

2.8.1.3. Personalizzazione del progetto

  

Nella cartella [custom] si trovano gli elementi dell'architettura personalizzabili dallo sviluppatore.

Le costanti dell'interfaccia [IMainActivity] saranno le seguenti:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 0;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = false;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = false;

  // numero di frammenti dell'applicazione
  int FRAGMENTS_COUNT = 1;

}
  • righe 24-31: l'applicazione non utilizza qui il proprio livello [DAO]. Queste costanti non verranno utilizzate;
  • riga 34: un’adiacenza di frammenti pari a 1, che è il valore predefinito. Poiché l’applicazione ha un solo frammento (riga 43), questo valore non ha alcuna rilevanza;
  • righe 39-40: poiché non vi sono operazioni con il livello [DAO], è superfluo avere un'immagine di attesa;
  • riga 37: non si tratta di un'applicazione a schede;
  • riga 43: c'è un solo frammento;

La classe [Session] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore

}

È vuota. Infatti, poiché c'è un solo frammento, non è necessario prevedere una comunicazione tra frammenti con una sessione.

Infine, la classe [CoreState] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • righe 11-13: dobbiamo inserire tutte le classi derivate da [CoreState] che memorizzano lo stato dei diversi frammenti. In questo caso, ce n’è solo una (riga 12);

2.8.1.4. L'attività [MainActivity]

L'attività [MainActivity] è attualmente la seguente:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

  // metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessione
    this.session = (Session) super.session;
    // da fare: continuiamo le inizializzazioni avviate dalla classe padre
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // da fare: definire qui i frammenti
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // da fare: definire qui i titoli dei frammenti
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // da fare: navigazione tramite schede - definire la vista da visualizzare quando è selezionata la scheda n. [position]
  }

  @Override
  protected int getFirstView() {
    // da fare: definire il numero della prima vista (frammento) da visualizzare
    return 0;
  }
}

I commenti [//todo] indicano cosa deve fare lo sviluppatore. La classe [MainActivity] si evolve come segue:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

  // metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessione
    this.session = (Session) super.session;
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new Vue1Fragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {

  }

  @Override
  protected int getFirstView() {
    return 0;
  }
}

Deve essere modificato solo il metodo delle righe 41-44. Deve restituire l'array dei frammenti dell'applicazione. Alla riga 43, non bisogna dimenticare di inserire il trattino basso dopo il nome del frammento.

2.8.1.5. Stato del frammento [FragmentState]

A seguito dei test di rotazione effettuati sul progetto [Exemple-17], si decide di memorizzare i seguenti elementi del frammento:

  • l'elenco dei valori del menu a tendina;
  • la posizione dell’elemento selezionato in tale elenco;
  • il messaggio visualizzato dalla finestra di dialogo, se presente al momento della rotazione;

La classe [Vue1FragmentState] sarà la seguente:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

import java.util.List;

public class Vue1FragmentState extends CoreState {

  // i valori dell'elenco a discesa
  private List<String> list;
  // elemento selezionato nell'elenco a discesa
  private int listSelectedPosition;
  // il messaggio visualizzato nella finestra di dialogo
  private String message;

  // getter e setter
...
}

2.8.1.6. Il frammento [AbstractFragment]

Attualmente il ciclo di vita del frammento è gestito da due metodi (righe 6 e 32):


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

Il codice di questi due metodi verrà trasferito nei metodi definiti dalla classe [AbstractFragment] nel modo seguente:


// gestione del ciclo di vita del frammento ---------------------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    Vue1FragmentState state = new Vue1FragmentState();
    state.setList(list);
    state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
    state.setMessage(message);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Prima visita?
    if (previousState == null) {
      // si creano i valori dell'elenco a discesa
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // vengono restituiti i valori dell'elenco a discesa
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // e il messaggio della finestra di dialogo
      message = state.getMessage();
    }
    // 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);
  }

  @Override
  protected void initView(CoreState previousState) {
    // 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));
      }
    });
    // inizializzazione dell'adattatore dell'elenco a discesa
    dropDownList.setAdapter(dataAdapter);
    // Prima visita?
    if (previousState == null) {
      // si seleziona il primo pulsante
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // valore della barra di scorrimento
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // elemento selezionato nell'elenco a discesa
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // finestra di dialogo visibile?
    if (message != null) {
      // viene visualizzata
      showMessage();
    }
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

}
  • righe 2-9: il metodo [saveFragment] deve inserire gli elementi del frammento da memorizzare in una classe derivata da [CoreState] e restituire l'istanza di quest'ultima;
  • righe 11-14: il metodo [getNumView] deve restituire il numero del frammento. In questo caso, c’è un solo frammento il cui numero è 0;
  • righe 16-34: il metodo [initFragment] deve inizializzare i campi del frammento. Riceve lo stato precedente del frammento. Se [previousState] è uguale a null, allora si tratta della prima visita;
  • righe 19-25: alla prima visita, vengono creati i valori dell’elenco a discesa;
  • righe 26-30: se non si tratta della prima visita, i campi [list, message] del frammento vengono ripristinati dallo stato precedente;
  • righe 33-34: inizializzazione del campo [dataAdapter] del frammento. Si tratta della fonte dati dell’elenco a discesa;
  • righe 37-62: il metodo [initView] serve a inizializzare i componenti dell'interfaccia visiva. Riceve come parametro lo stato precedente [previousState]. Se [previousState==null], allora si tratta della prima visita;
  • qui si ritrova ciò che era presente in precedenza nel metodo [@AfterViews];
  • righe 57-61: alla prima visita, ci si assicura che sia selezionato il primo pulsante di opzione;
  • righe 64-67: il metodo [updateOnSubmit] viene eseguito quando l’azione in corso è [SUBMIT]. Qui non c’è navigazione tra frammenti e quindi non c’è un’azione in corso;
  • righe 69-81: il metodo [updateOnRestore] viene eseguito quando l'azione in corso è [NAVIGATION] o [RESTORE]. Qui non c'è navigazione tra frammenti e quindi non è possibile l'azione [NAVIGATION];
  • riga 72: si ricalcola (non si ripristina) il valore di TextView seekBarValue. Infatti, durante le rotazioni, a volte se ne perdeva il valore;
  • righe 74-75: si posiziona l’elenco sull’elemento che era selezionato prima della rotazione. In caso contrario, l’elenco si posizionava sul primo elemento;
  • righe 76-80: si visualizza nuovamente la finestra di dialogo se il messaggio dello stato precedente non è null. Torneremo sul metodo [showMessage] (riga 79);
  • righe 83-86: il metodo [notifyEndOfUpdates] è l’ultimo metodo chiamato dalla classe padre prima di lasciare il frammento figlio in pace. Qui non c’è nulla da fare;
  • righe 88-91: il metodo [notifyEndOfTasks] segnala la fine delle attività asincrone avviate dal frammento. In questo caso non ce ne sono;

Il ripristino della finestra di dialogo avviene nel modo seguente:


  // il messaggio della finestra di dialogo
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // elenco dei messaggi da visualizzare
    List<String> messages = new ArrayList<>();
    ...
    // visualizzazione
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // si costruisce il testo da visualizzare
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // si memorizza il messaggio
    message = texte.toString();
    // visualizzazione
    showMessage();
  }

  private void showMessage() {
    // visualizzazione
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // reset del messaggio
        message = null;
      }
    }).show();
}

Quando l'utente conferma il modulo, il metodo [doValider] (riga 5) crea un elenco di messaggi che poi visualizza (riga 10) nella finestra di dialogo.

  • righe 14-20: l’elenco dei messaggi viene concatenato in un unico messaggio che viene memorizzato nella riga 2;
  • righe 25-33: è questo il messaggio visualizzato nella finestra di dialogo ed è lo stesso messaggio che il metodo [updateOnRestore] fa visualizzare;
  • riga 27: il secondo parametro del metodo [setNeutralButton] è il metodo eseguito quando l’utente fa clic sul pulsante [Fermer] della finestra di dialogo;
  • riga 31: alla chiusura della finestra di dialogo, si restituisce il messaggio a null per indicare che la finestra di dialogo non è più presente;

2.8.1.7. Tests

Si invita il lettore a testare questo progetto e a verificare che il frammento sia stato correttamente conservato dopo una o più rotazioni successive.

2.8.2. Esempio 23: client meteo

Alcuni siti consentono di ottenere informazioni meteorologiche sotto forma di stringhe jSON. Ecco un esempio:

Image

Il codice URL ha la forma: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} dove:

  • city: la città di cui si desidera conoscere le previsioni meteorologiche, in questo caso Angers;
  • country: il paese della città, in questo caso la Francia (fr);
  • APPID: una chiave ottenuta registrandosi sul sito [https://home.openweathermap.org/users/sign_up];

2.8.2.1. Il progetto

  

Il progetto è stato sviluppato a partire dal progetto [client-android-skel]. Presenta le seguenti caratteristiche:

  • ha un solo frammento di cui non è necessario conservare lo stato;
  • effettua richieste asincrone;

2.8.2.2. Personalizzazione del progetto

  

L'interfaccia [IMainActivity] consente di specificare alcune caratteristiche del progetto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 5000;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = false;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = true;

  // numero di frammenti dell'applicazione
  int FRAGMENTS_COUNT = 1;

}
  • righe 25, 28, 31, 40: caratteristiche del livello [DAO]. Riga 31: non è necessaria l'autenticazione di base;
  • riga 34: adiacenza dei frammenti. In questo caso la costante non ha importanza poiché è presente un solo frammento;
  • riga 37: non si tratta di un'applicazione a schede;
  • riga 43: c'è un solo frammento;

La classe [CoreState] che memorizza lo stato dei frammenti sarà la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// da fare: aggiungere qui le sottoclassi di [CoreState]
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • righe 10-13: non c'è nulla da dichiarare poiché in questa applicazione c'è un solo frammento di cui non si conserva lo stato;

La classe [Session] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
}

È vuota poiché in questa applicazione non vi è alcuna comunicazione tra frammenti.

2.8.2.3. Il livello [DAO]

  

Nel livello [DAO], devono essere personalizzate tre classi:

  • l'interfaccia IDao;
  • la sua implementazione Dao;
  • l'interfaccia WebClient per la comunicazione con il server web / jSON;

L'interfaccia [WebClient] sarà la seguente:


package client.android.dao.service;

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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // servizio meteo
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • righe 18-19: l'URL del servizio meteorologico. Si ricorda che questa si riferisce all'URL radice (RestClientRootUrl, riga 12) del client. In questo caso, tale URL radice sarà [http://api.openweathermap.org/];

L'interfaccia [IDao] sarà la seguente:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // utente
  void setUser(String user, String mdp);

  // timeout del client
  void setTimeout(int timeout);

  // autenticazione di base
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  //  servizio meteo
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • si ricorda che i metodi delle righe 6-22 sono presenti di default nell’interfaccia IDao del progetto [client-android-skel];
  • riga 25: il metodo [getWeatherForecast] consente di ottenere la stringa jSON relativa alle previsioni meteorologiche della città [city] del paese [country]. Il terzo parametro è la chiave ottenuta dal sito [https://home.openweathermap.org/users/sign_up];

L'interfaccia [IDao] è implementata dalla seguente classe [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client del servizio web
  @RestService
  protected WebClient webClient;
  // sicurezza
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // il RestTemplate
  private RestTemplate restTemplate;
  // factory di RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // timeout
  private int timeout;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // si costruisce il restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // si imposta il convertitore jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // si imposta il restTemplate del client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // si imposta l'URL del servizio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // si registra l'utente nell'intercettatore
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // memoria
    this.timeout = timeout;
    // configurazione predefinita
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercettatore di autenticazione?
    if (isBasicAuthentificationNeeded) {
      // si aggiunge l'intercettatore di autenticazione
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


  // metodi privati -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // servizio meteo ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
    }
    // risultato
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • si ricorda che le righe 17-90 sono presenti di default nella classe [Dao] del progetto [client-android-skel]. È sufficiente aggiungere i metodi di implementazione dell’interfaccia [IDao], specifici per l’applicazione (riga 92);
  • righe 93-105: implementazione del metodo [getWeatherForecast]. Si tratta di un’implementazione molto semplice, realizzata in 6 righe, dalle righe 100 alla 105;
  • riga 100: il metodo [getResponse] è un metodo della classe padre [AbstractDao]. Esso richiede un parametro di tipo [IRequest<T>], dove T è il tipo della risposta attesa dal server, in questo caso un String poiché si attende una stringa jSON. Il tipo T di [IRequest<T>] deve essere il tipo T del metodo [Observable<T> getWeatherForecast];
  • l’interfaccia [IRequest<T>] ha un solo metodo: getResponse. Il suo ruolo è quello di fornire la risposta di tipo T che deve restituire il metodo [Observable<T> getWeatherForecast];
  • riga 103: è l’interfaccia [WebClient] a fornire questa risposta. Le vengono passati i tre parametri ricevuti alla riga 94. Per questo motivo, essi devono avere l’attributo «final»;

2.8.2.4. L'attività [MainActivity]

  

L'attività [MainActivity] è la seguente:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // metodi classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new MeteoFragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interfaccia IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • si ricorda che le righe 15-55 sono presenti di default nel progetto [client-android-skel]. È sufficiente personalizzarle;
  • righe 37-40: la tabella dei frammenti. Qui ce n'è solo uno;
  • righe 43-46: non sono necessari titoli dei frammenti;
  • righe 48-50: qui non ci sono schede;
  • righe 52-55: la prima vista da visualizzare è la vista n. 0, quella di [MeteoFragment];
  • righe 58-61: implementazione dell’interfaccia [IDao]. Qui non c’è altro da fare che delegare il lavoro al livello [DAO] della riga 21;

2.8.2.5. Il frammento [MeteoFragment]

  

Il frammento [MeteoFragment] interroga il servizio web / jSON relativo alle previsioni meteorologiche. La sua struttura è la seguente:


package client.android.fragments;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
  • riga 14: la vista [res / layout / meteo_fragment.xml] è la 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="Construisez votre interface visuelle"
    android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
    android:layout_marginTop="120dp"/>
</RelativeLayout>

La vista mostra solo il testo della riga 10;

  • riga 15: il menu [res / menu / menu_meteo.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=".activity.MainActivity">
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionMeteo"
        android:title="@string/actionMeteo"/>
      <item
        android:id="@+id/actionAnnuler"
        android:title="@string/actionAnnuler"/>
      <item
        android:id="@+id/actionTerminer"
        android:title="@string/actionTerminer"/>
    </menu>
  </item>
</menu>
  • righe 10-12: questa opzione di menu serve a richiedere le previsioni meteo di una città;
  • righe 14-15: questa opzione di menu serve per annullare la richiesta se è in corso;
  • righe 16-18: questa opzione di menu chiude l'applicazione;

Il codice completo del frammento è il seguente:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;

@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {

  // dati locali
  private int nbReponsesRecues;

  // gestione degli eventi ---------------------------------------------------------------------------------------
  // città di cui si desidera conoscere le previsioni meteo
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // il proprio paese
    String country = "fr";
    // ottenere un ID API creando un account [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL del servizio web / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // inizio dell'attesa delle attività asincrone [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // numero di risposte ricevute
    nbReponsesRecues = 0;
    // le chiamate asincrone vengono eseguite in parallelo
    for (String city : paysDeLoire) {
      // meteo
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // elaborazione della risposta
          consumeResponse(response);
          // una risposta positiva
          nbReponsesRecues++;
        }
      });
    }
  }

  // elaborazione della risposta del server
  private void consumeResponse(String response) {
    // registro
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // inizio dell'attesa
  protected void beginWaiting(int numberOfRunningTasks) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // genitore
    beginRunningTasks(numberOfRunningTasks);
    // viene visualizzata l'opzione [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // menu
    initMenu();
    // visualizzazione dei risultati
    String message;
    switch (nbReponsesRecues) {
      case 0:
        message = "Aucune réponse n'a été reçue";
        break;
      case 1:
        message = "Une réponse a été reçue. Consultez vos logs...";
        break;
      default:
        message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
        break;
    }
    Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
  }

  // metodi privati -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // menu
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // gestione del ciclo di vita ---------------------------------------------------------------------------------------
...
}
  • righe 25-50: gestione del clic sull'opzione di menu [Météo];
  • riga 32: creazione dell’URL del servizio web / jSON del servizio meteo. Questa viene poi passata al livello [DAO] tramite l’attività;
  • riga 34: si avvia l'attesa. Si passa il numero di attività che verranno avviate, in modo che la classe padre possa segnalarne il completamento. In questo caso, le attività sono cinque poiché si richiederanno le previsioni meteorologiche delle cinque città della riga 23;
  • riga 16: si conta il numero di risposte ricevute per poterlo visualizzare;
  • righe 38-50: si esegue un ciclo sulle città di cui si desidera conoscere le previsioni meteorologiche;
  • riga 40: si effettueranno 5 richieste HTTP in parallelo;
  • riga 40: si chiede alla classe padre [AbstractParent] di interrogare il servizio web /jSON;
  • righe 40-48: il metodo [executeInBackground] richiede due parametri:
    • riga 40: il processo da monitorare ed eseguire è fornito dal metodo [mainActivity.getWeatherForecast];
    • righe 40-48: l'istanza [Action1] da eseguire quando si riceve la risposta dal servizio asincrono. Il tipo T di [Action1<T>] deve essere il tipo T del risultato del metodo [getWeatherForecast];
  • riga 44: è stata ricevuta una risposta. La si passa al metodo [consumeResponse] della riga 53;
  • riga 46: si incrementa il contatore delle risposte ricevute;
  • righe 53-56: elaborazione di una risposta jSON del servizio meteorologico;
  • riga 55: ci si limita a registrare la stringa jSON;
  • righe 59-72: codice eseguito prima dell’avvio delle attività asincrone;
  • riga 65: si passa il numero di attività da eseguire alla classe padre [AbstractParent]. Questo permette alla classe padre di avvisarci quando tutte le attività saranno terminate;
  • righe 67-70: preparazione del menu per l'attesa. Si mantiene solo l'opzione [Actions/Annuler] che consentirà all'utente di annullare le attività avviate;
  • righe 74-92: codice eseguito quando la classe padre ci avvisa che tutte le attività avviate sono terminate;
  • riga 77: si riporta il menu allo stato iniziale. Il metodo [initMenu] (righe 95-102) visualizza il menu con tutte le sue opzioni tranne l'opzione [Actions/Annuler], che viene nascosta;
  • righe 80-91: viene visualizzato il numero di risposte ricevute;

Il clic sull’opzione di menu [Annuler] è gestito dal codice seguente:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // si annullano le attività asincrone
    cancelRunningTasks();
}
  • riga 7: si richiede alla classe padre di annullare le attività ancora attive;

Il clic sull'opzione di menu [Terminer] è gestito dal codice seguente:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // si arresta tutto
    System.exit(0);
}

La gestione del ciclo di vita del frammento è assicurata dai seguenti metodi:


  // gestione del ciclo di vita ---------------------------------------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // Prima visita?
    if (previousState == null) {
      initMenu();
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

}
  • righe 3-6: servono a memorizzare lo stato del frammento in una classe derivata da [CoreState]. Se il frammento non ha uno stato da memorizzare, come in questo caso, ci si limita a restituire un'istanza di [CoreState]. Non bisogna restituire null perché ciò causerebbe successivamente un crash;
  • righe 8-11: devono restituire il numero della vista. In questo caso il frammento [MeteoFragment] ha il numero 0;
  • righe 13-16: servono a inizializzare il frammento una volta che è stato costruito (previousState==null) o ricostruito (previousState!=null). Qui non c'è nulla da fare. L'unico campo che può essere inizializzato è il seguente:

  // città di cui si desidera conoscere le previsioni meteo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

ma si inizializza automaticamente;

  • righe 18-24: servono a inizializzare la vista associata al frammento una volta che è stata costruita (previousState==null) o ricostruita (previousState!=null);
  • righe 21-23: se si tratta della prima visita al frammento, si inizializza il suo menu per nascondere l’opzione [Annuler];
  • righe 27-30: vengono chiamate se, per arrivare al frammento, è avvenuta una navigazione con un'azione di tipo [SUBMIT]. In questo caso, non vi è navigazione tra frammenti poiché esiste un solo frammento;
  • righe 32-35: vengono chiamate durante un ciclo di salvataggio/ripristino dovuto alla rotazione del dispositivo o ad altro motivo. In questo caso, poiché non è stato salvato alcuno stato, non c'è nulla da fare;
  • righe 37-40: vengono chiamate quando tutti gli aggiornamenti precedenti sono stati completati. In questo caso, non c'è nulla da fare;

2.8.2.6. Tests

Ora eseguiamo l’esempio:

Image

Image

I log sono quindi i seguenti:


07-23 13:24:30.899 2642-2642/client.android D/MainActivity_: constructeur
07-23 13:24:30.945 2642-2642/client.android D/AbstractDao: constructeur, thread=main
07-23 13:24:32.861 2642-2642/client.android D/client.android.dao.service.Dao_: afterInject
07-23 13:24:32.950 2642-2642/client.android D/MainActivity_: onCreate
07-23 13:24:32.951 2642-2642/client.android D/client.android.dao.service.Dao_: setTimeout thread=main, timeout=1000
07-23 13:24:32.952 2642-2642/client.android D/client.android.dao.service.Dao_: setBasicAuthentification thread=main, isBasicAuthentificationNeeded=false
07-23 13:24:33.041 2642-2642/client.android D/MainActivity_: adding loadingPanel
07-23 13:24:33.043 2642-2642/client.android D/MeteoFragment_: constructeur
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: navigation vers vue 0 sur action NONE
07-23 13:24:33.044 2642-2642/client.android D/MainActivity_: onCreateActivity
07-23 13:24:33.080 2642-2642/client.android D/MainActivity_: onResume
07-23 13:24:33.325 2642-2642/client.android D/MeteoFragment_: onActivityCreated
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: onCreateOptionsMenu
07-23 13:24:33.518 2642-2642/client.android D/MeteoFragment_: getMenuOptionsStates(Menu)
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: Nombre d'options de menu=4
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initFragment initView updateForFirstVisit
07-23 13:24:33.519 2642-2642/client.android D/MeteoFragment_: initMenu
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: session={"action":"NONE","coreStates":[{"@type":"CoreState","hasBeenVisited":false,"menuOptionsState":null}],"previousTab":0,"previousView":0}
07-23 13:24:33.557 2642-2642/client.android D/MeteoFragment_: état précédent=null
07-23 13:24:33.558 2642-2642/client.android D/MeteoFragment_: notifyEndOfUpdates
07-23 13:24:39.766 2642-2642/client.android D/MeteoFragment_: beginWaiting
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=angers, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.831 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=le mans, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.882 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=nantes, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.885 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=laval, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.886 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: getWeatherForecast city=la roche sur yon, country=fr, APIID=aa6bb491c9a16810c4f0881f17e888c7, thread=main, timeout=1000
07-23 13:24:39.887 2642-2642/client.android D/client.android.dao.service.Dao_: delay=5000
07-23 13:24:45.035 2642-2961/client.android D/client.android.dao.service.Dao_: response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200} sur thread [RxIoScheduler-4]
07-23 13:24:45.035 2642-2963/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-6]
07-23 13:24:45.035 2642-2959/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-2]
07-23 13:24:45.035 2642-2962/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-5]
07-23 13:24:45.036 2642-2960/client.android D/client.android.dao.service.Dao_: response={} sur thread [RxIoScheduler-3]
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={"coord":{"lon":-1.55,"lat":47.22},"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01d"}],"base":"cmc stations","main":{"temp":298.05,"pressure":1022,"humidity":47,"temp_min":297.15,"temp_max":299.15},"wind":{"speed":2.6,"deg":310},"clouds":{"all":0},"dt":1469277000,"sys":{"type":1,"id":5641,"message":0.0032,"country":"FR","sunrise":1469248505,"sunset":1469303378},"id":2990969,"name":"Nantes","cod":200}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: thread=main, response={}
07-23 13:24:45.039 2642-2642/client.android D/MeteoFragment_: initMenu
  • righe 32-36: le risposte jSON vengono ottenute sui thread di I/O
  • righe 37-41: il frammento recupera le 5 risposte sul thread dell'interfaccia utente;

Ora si esegue la richiesta con un identificativo API errato:


    String APIID = "";

Image

I log sono quindi i seguenti:


07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
  • righe 3-6, 10: le 5 chiamate a HTTP hanno generato 5 eccezioni;
  • riga 7: il frammento [MeteoFragment] riceve la prima eccezione. A questo punto annullerà tutte le attività;

Ora impostiamo un tempo di attesa di 5 secondi [IMainActivity.DELAY] e annulliamo l’operazione. I log sono quindi i seguenti:


07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
  • riga 3: richiesta di annullamento;
  • riga 4: l'attesa viene annullata perché è avvenuta un'annullamento;
  • righe 6-10: l'annullamento delle attività provoca un'eccezione su ciascuno dei thread delle cinque attività. Il tipo di eccezione dipende dalle applicazioni. L'eccezione in questo caso è [java.lang.InterruptedException] perché i task sono stati interrotti mentre stavano eseguendo l'istruzione [Thread.sleep(delay)], che li fa attendere artificialmente per [delay] millisecondi;

2.8.3. Esempio 16B

Qui rifattorizziamo l’esempio 16 del paragrafo 1.17. Esso presenta un frammento che effettua chiamate asincrone a un server di numeri casuali. Vediamo come si comporta durante una rotazione del dispositivo:

Image

  • in [1], si ruota il dispositivo due volte;

Image

Si nota che sono andati persi tutti i messaggi di errore. Cercheremo di migliorare questo aspetto.

2.8.3.1. Il progetto Esempio-16B

Copiamo il progetto [client-android-skel] nel progetto [exemples/Exemple-16B], quindi carichiamo il nuovo progetto:

  

Dal progetto iniziale [Exemple-16], copiamo nel progetto [Exemple-16B] i seguenti elementi:

  • il file [res/layout/vue1.xml], la cartella [res/values]:
  

Modificheremo il margine superiore della vista [vue1.xml] impostandolo a 80 dp:


  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="80dp"
    android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
  • il frammento [Vue1Fragment]:
  
  • la classe [dao / service / Response]:
  

A questo punto, si può tentare una prima compilazione:

  • un primo tipo di errori riguarda le classi imports. Alcune classi sono state spostate in un altro pacchetto durante la migrazione verso [Exemple-16B]. Si inizia correggendo questo tipo di errori;
  • viene segnalato un secondo tipo di errore sulla classe [Vue1Fragment] perché non implementa i metodi richiesti dalla classe padre [AbstractParent]. Si procede alla generazione automatica di questi ultimi;

Si tenta una seconda compilazione:

  • tutti gli errori rimanenti sono ora concentrati sulla classe [Vue1Fragment], la classe che subirà il maggior numero di modifiche;

2.8.3.2. Creazione di uno stato per il frammento [Vue1Fragment]

Abbiamo visto che alcune informazioni del frammento dovranno essere salvate durante una rotazione per ripristinare il frammento così com’era prima della rotazione. Creiamo quindi uno stato [Vue1FragmentState], per il momento vuoto:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Personalizzazione del progetto

  

L’interfaccia [IMainActivity] consente di specificare alcune caratteristiche del progetto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di visualizzazione
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // costanti dell'applicazione -------------------------------------

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 5000;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = false;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = true;

  // numero di frammenti dell'applicazione
  int FRAGMENTS_COUNT = 1;

}
  • righe 25, 28, 31, 40: caratteristiche del livello [DAO]. Non è necessaria l'autenticazione di base;
  • riga 34: adiacenza dei frammenti. In questo caso questa costante non ha importanza poiché c'è un solo frammento;
  • riga 37: non si tratta di un'applicazione a schede;
  • riga 43: c'è un solo frammento;

La classe [CoreState] che memorizza lo stato dei frammenti sarà la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • riga 12: dichiariamo la classe dello stato del frammento [Vue1Fragment];

La classe [Session] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
}

È vuota poiché in questa applicazione non vi è alcuna comunicazione tra i frammenti.

2.8.3.4. Il livello [DAO]

  

Nel livello [DAO], devono essere personalizzate tre classi:

  • l'interfaccia IDao;
  • la sua implementazione Dao;
  • l'interfaccia WebClient per la comunicazione con il server web / jSON;

La classe [Response] proviene dal progetto [Exemple-16] che la utilizza:


package client.android.dao.service;

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

L'interfaccia [WebClient] sarà la seguente:


package client.android.dao.service;

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.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

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

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // 1 numero casuale nell'intervallo [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • righe 18-19: l'URL del servizio dei numeri casuali. Si ricorda che questa si riferisce al URL radice (RestClientRootUrl, riga 12) del cliente. In questo caso, tale URL radice sarà [http://localhost:8080];

L’interfaccia [IDao] sarà la seguente:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL del servizio web
  void setUrlServiceWebJson(String url);

  // utente
  void setUser(String user, String mdp);

  // timeout del client
  void setTimeout(int timeout);

  // autenticazione di base
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

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

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

  // servizio di numeri casuali
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • si ricorda che i metodi delle righe 6-22 sono presenti di default nell’interfaccia IDao del progetto [client-android-skel];
  • riga 25: il metodo [getAlea] consente di ottenere un numero casuale nell’intervallo [a,b]. Tale numero viene restituito in una risposta di tipo [Response<Integer>], in cui il numero casuale si trova nel campo [body] di tale tipo;

L'interfaccia [IDao] è implementata dalla seguente classe [Dao]:


package client.android.dao.service;

import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

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

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // client del servizio web
  @RestService
  protected WebClient webClient;
  // sicurezza
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // il RestTemplate
  private RestTemplate restTemplate;
  // factory di RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // si costruisce il restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // si installa il convertitore jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // si imposta il restTemplate del client web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // si imposta l'URL del servizio web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // si registra l'utente nell'intercettatore
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configurazione predefinita
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // intercettatore di autenticazione?
    if (isBasicAuthentificationNeeded) {
      // si aggiunge l'intercettatore di autenticazione
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // metodi privati -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // servizio di numeri casuali
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // esecuzione client web
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • si ricorda che le righe 17-85 sono presenti di default nella classe [Dao] del progetto [client-android-skel]. È sufficiente aggiungere i metodi di implementazione dell'interfaccia [IDao];
  • righe 88-97: implementazione del metodo [getAlea]. Si tratta di un'implementazione molto semplice, realizzata in 6 righe, dalle righe 91 alla 96;
  • riga 91: il metodo [getResponse] è un metodo della classe padre [AbstractDao]. Esso richiede un parametro di tipo [IRequest<T>], dove T è il tipo della risposta prevista, in questo caso un tipo Response<Integer>. Il tipo T di [IRequest<T>] (riga 91) deve corrispondere al tipo T del metodo [Observable<T> getAlea] (riga 89);
  • L'interfaccia [IRequest<T>] ha un solo metodo: getResponse. Il suo ruolo è quello di fornire la risposta di tipo T che deve restituire il metodo [Observable<T> getAlea];
  • riga 94: è l'interfaccia [WebClient] a fornire questa risposta. Le vengono passati i due parametri ricevuti alla riga 89. Per questo motivo, essi devono avere l'attributo «final»;

2.8.3.5. L'attività [MainActivity]

  

L'attività [MainActivity] è la seguente:


package client.android.activity;

import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;

  // metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // si proseguono le inizializzazioni avviate dalla classe padre
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // definire qui i frammenti
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // definire qui i titoli dei frammenti
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // navigazione tramite schede - definire la vista da visualizzare
  }

  @Override
  protected int getFirstView() {
    return 0;
  }

  // interfaccia IDao ------------------------------------------
  @Override
  public Observable<Response<Integer>> getAlea(int a, int b) {
    return dao.getAlea(a, b);
  }

}
  • si ricorda che le righe 15-61 sono presenti di default nel progetto [client-android-skel]. È sufficiente personalizzarle;
  • righe 40-44: la tabella dei frammenti. Qui ce n'è solo uno;
  • righe 47-51: non sono necessari titoli dei frammenti;
  • righe 53-56: qui non ci sono schede;
  • righe 58-61: la prima vista da visualizzare è la vista n. 0, quella di [Vue1Fragment];
  • righe 64-67: implementazione dell’interfaccia [IDao]. Qui non c’è altro da fare che delegare il lavoro al livello [DAO] della riga 23;

2.8.3.6. Lo stato del frammento [Vue1Fragment]

  

La classe [Vue1FragmentState] sarà la seguente:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

public class Vue1FragmentState extends CoreState {

  // stato del frammento ------------------------
  // elenco delle risposte
  private List<String> reponses = new ArrayList<>();
  // stato della vista ------------------------
  // messaggio di errore relativo al numero di numeri casuali richiesti
  private boolean txtErrorAleasVisible = false;
  // messaggio di errore sull'intervallo di generazione [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // messaggio di errore relativo a URL del servizio web
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // messaggio di errore relativo al tempo di attesa
  private boolean textViewErreurDelayVisible = false;
  // Stato visibile o non visibile del pulsante Esegui
  private boolean btnExecuterVisible = true;

  // getter e setter
...
}

Per individuare le informazioni da memorizzare nel frammento, abbiamo sottoposto il dispositivo a rotazioni in diverse situazioni e abbiamo osservato cosa fosse andato perso durante il ripristino. Siamo giunti alla conclusione che fosse necessario memorizzare le informazioni delle righe 10-23.

2.8.3.7. Il frammento [Vue1Fragment]

  

Attualmente la vista [Vue1Fragment] presenta diversi errori dovuti al fatto che la classe padre [AbstractFragment] da cui deriva è cambiata. Anziché descrivere una per una le modifiche da apportare, commenteremo direttamente la versione finale.

La struttura del frammento è la seguente:


package client.android.fragments.behavior;

import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

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

...
}
  • alla riga 26 si ricorda che ogni frammento deve avere un menu, anche se vuoto. È il caso qui.

2.8.3.7.1. Gestione del clic sul pulsante [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // si verificano i dati inseriti
    if (!isPageValid()) {
      return;
    }
    // si cancellano le risposte precedenti
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // si azzera il contatore delle risposte
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // inizializzazione attività
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // si prepara l'attività casuale
    beginWaiting(1);
    // si richiedono i numeri casuali
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // si crea il processo da osservare
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // richiesta dei numeri casuali
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // si elabora la risposta
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // log
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // una risposta di +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // si analizza la risposta
    // errore?
    if (response.getStatus() != 0) {
      // visualizzazione
      showAlert(response.getMessages());
      // annullamento
      doAnnuler();
      // ritorno all'interfaccia utente
      return;
    }
    // si aggiunge l'informazione all'elenco delle risposte
    reponses.add(0, String.valueOf(response.getBody()));
    // si aggiornano le risposte
    dataAdapterReponses.notifyDataSetChanged();
  }

  // annullamento ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // si annullano le attività asincrone
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // si attiva la clessidra
    beginRunningTasks(nbRunningTasks);
    // il pulsante [Annuler] sostituisce il pulsante [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • righe 4-6: si verifica innanzitutto che i dati inseriti siano validi. Potrebbero quindi comparire dei messaggi di errore;
  • righe 8-9: l'elenco delle risposte viene svuotato. Questa modifica viene applicata al ListView che le visualizza;
  • righe 11-12: il numero di risposte ricevute viene azzerato;
  • riga 14: si imposta il URL del servizio dei numeri casuali. Questa informazione verrà trasmessa al livello [DAO];
  • riga 15: si imposta il tempo di attesa prima di inviare la richiesta al servizio dei numeri casuali. Questa informazione verrà trasmessa al livello [DAO];
  • riga 17: ci si prepara a lanciare 1 attività asincrona (e non N, vedremo perché);
  • righe 24-27: delle N attività asincrone, se ne esegue una alla volta tramite una sequenza di operazioni [merge];
  • righe 29-36: si richiede alla classe padre [AbstractParent] di interrogare il servizio web / jSON per ottenere numeri casuali;
  • righe 29-36: il metodo [executeInBackground] richiede due parametri:
    • riga 29: il processo da osservare ed eseguire è quello calcolato nelle righe precedenti;
    • righe 29-36: l'istanza [Action1] da eseguire quando si riceve la risposta dal servizio asincrono. Il tipo T di [Action1<T>] deve essere il tipo T del risultato del metodo [getAlea], ovvero un tipo [Response<Integer>];
  • riga 34: quando arriva una risposta (un numero casuale), questa viene elaborata nel metodo della riga 39;
  • righe 49-50: si registra e si segnala di aver ricevuto una nuova risposta;
  • righe 53-60: il tipo [Response<T>] ha un campo [status] che rappresenta un codice di errore. Se questo codice è diverso da zero, significa che il server ha riscontrato un problema;
  • riga 55: viene visualizzato un messaggio di errore. Il metodo [showAlert] appartiene alla classe padre;
  • riga 57: viene chiamato il metodo delle righe 68-75. Esso annullerà le attività ancora attive (riga 74);
  • riga 62: la risposta viene aggiunta all’elenco delle risposte, che costituisce la fonte di dati di ListView;
  • riga 64: il ListView viene aggiornato;
  • righe 77-83: il metodo [beginWaiting(int nbRunningTasks)] prepara la vista per l'attesa (righe 81-82) e comunica alla classe padre che le attività [nbRunningTasks] stanno per essere eseguite (riga 79);

2.8.3.7.2. Il ciclo di vita del frammento

Il ciclo di vita del frammento è gestito dai seguenti metodi:


  // dati locali
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // gestione del ciclo di vita ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // stato attuale della vista
    Vue1FragmentState state = new Vue1FragmentState();
    state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
    state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
    state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
    state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
    state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
    state.setReponses(reponses);
    return state;
  }

  @Override
  protected int getNumView() {
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Prima visita?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // fonte dati di listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // numero di risposte
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // collegamento listview / adattatore
    listReponses.setAdapter(dataAdapterReponses);
    // Prima visita?
    if (previousState == null) {
      // si nascondono i messaggi di errore
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // i pulsanti
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Stato precedente della vista
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // Mostra/nascondi i messaggi di errore
    txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
    txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
    textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
    // pulsanti
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // numero di risposte
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // il pulsante [Exécuter] sostituisce il pulsante [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • righe 7-18: garantiscono il salvataggio del frammento quando la classe padre lo richiede;
  • riga 11: visualizzazione del messaggio di errore relativo al tempo di attesa;
  • riga 12: visualizzazione del messaggio di errore relativo al numero di numeri casuali richiesti;
  • riga 13: visualizzazione del messaggio di errore relativo all’URL del servizio web / jSON;
  • riga 14: visibilità del messaggio di errore relativo all'intervallo [a,b] di generazione dei numeri casuali;
  • riga 15: visibilità del pulsante [Exécuter];
  • riga 16: l'elenco delle risposte ricevute;
  • righe 20-23: devono restituire il numero della vista. Il numero del frammento è qui 0 poiché ce n’è solo uno;
  • righe 25-38: inizializzazione dei campi del frammento, sia in caso di prima visita (previousState==null), sia in caso di visita successiva;
    • righe 29-30: se non si tratta della prima visita, il campo [reponses] viene ripristinato dallo stato precedente del frammento;
    • righe 31-33: se si tratta della prima visita, il campo [reponses] viene inizializzato con un elenco vuoto;
    • righe 34-37: a partire dal campo [reponses] è possibile ricavare la fonte dei dati del campo ListView del frammento (riga 35) e il numero di risposte (riga 37);
  • righe 40-55: eseguite per inizializzare la vista associata al frammento, sia alla prima visita (previousState==null), sia in occasione di una visita successiva;
    • riga 43: si associa il ListView del frammento alla fonte dati appena costruita nel metodo [initFragment];
    • righe 45-54: se si tratta della prima visita, si prepara la vista per la sua prima visualizzazione;
  • righe 57-60: eseguite durante una navigazione tra frammenti associata a un'azione di tipo [SUBMIT]. In questo caso, c'è un solo frammento e quindi non vi è navigazione tra frammenti;
  • righe 63-76: eseguite durante una navigazione tra frammenti associata a un'azione di tipo [NAVIGATION] oppure durante un ciclo di salvataggio/ripristino dovuto alla rotazione del dispositivo o ad altro motivo. In questo caso, può verificarsi solo quest’ultimo scenario. È importante ricordare che, in ogni caso, [previousState] è sempre diverso da null;
  • riga 65: si converte lo stato precedente nel tipo dello stato del frammento;
  • righe 66-75: si utilizza il contenuto dello stato precedente per ripristinare la vista;
  • righe 78-81: vengono chiamate quando tutti gli aggiornamenti precedenti sono stati effettuati. Qui non c’è nulla da fare;
  • righe 83-89: vengono eseguite quando tutte le attività asincrone sono terminate. Qui si nasconde il pulsante [Annuler] per sostituirlo con il pulsante [Exécuter];

2.8.3.8. I test

Si invita il lettore a eseguire i seguenti test:

  • generare degli errori e far funzionare il dispositivo: i messaggi di errore devono rimanere visualizzati;
  • ottenere numeri casuali e far funzionare il dispositivo: i numeri casuali ottenuti devono rimanere visualizzati;
  • impostare un'attesa di diversi secondi e far funzionare il dispositivo durante l'attesa: le attività devono essere state annullate (come si può vedere nei log);

2.8.4. Esempio-22B

Riprendiamo qui l’esempio 22 per rifattorizzarlo secondo il modello del progetto [client-android-skel]. Ricordiamo che il progetto [Exemple-22] gestisce correttamente il ciclo di salvataggio/ripristino dei frammenti durante una rotazione e che è proprio questo progetto ad aver fatto da base al progetto [client-android-skel].

Duplichiamo il progetto [client-android-skel] in [exemples/Exemple-22B] e carichiamo quest'ultimo progetto:

  

Quindi copiamo vari elementi dal progetto [Exemple-22] nel progetto [Exemple-22B].

Per prima cosa, copiamo alcuni elementi dalla cartella [res]:

  • [layout/fragment_main.xml, layout/vue1.xml, menu/menu_fragment.xml, menu/menu_main.xml, la cartella [values];
  

Modificheremo il margine superiore delle due viste a 120 dp:

[vue1.xml]:


  <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="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


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

Successivamente copiamo gli elementi [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

A questo punto, possiamo tentare una prima compilazione. Compare un primo tipo di errore: quello relativo ai imports non corretti perché alcune classi sono state spostate in un altro pacchetto. Si correggono questi imports. Un secondo tipo di errore è dovuto al fatto che i frammenti non implementano tutti i metodi della loro classe padre [AbstractFragment]. Si corregge premendo (Alt+Invio).

Gli errori rimanenti derivano dalle differenze esistenti tra la vecchia e la nuova classe [AbstractFragment]. Per il momento, li ignoriamo.

2.8.4.1. Personalizzazione del progetto

  

Nella cartella [custom] si trovano gli elementi dell’architettura personalizzabili dallo sviluppatore.

L’interfaccia [IMainActivity] consente di specificare alcune caratteristiche del progetto:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // accesso alla sessione
  ISession getSession();

  // cambio di vista
  void navigateToView(int position, ISession.Action action);

  // gestione dell'attesa
  void beginWaiting();

  void cancelWaiting();

  // modalità debug
  boolean IS_DEBUG_ENABLED = true;

  // tempo massimo di attesa per la risposta del server
  int TIMEOUT = 1000;

  // tempo di attesa prima dell'esecuzione della richiesta del client
  int DELAY = 0;

  // autenticazione di base
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adiacenza dei frammenti
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra delle schede
  boolean ARE_TABS_NEEDED = true;

  // immagine di attesa
  boolean IS_WAITING_ICON_NEEDED = false;

  // numero di frammenti
  int FRAGMENTS_COUNT = 5;

}
  • righe 23, 26, 29, 38: caratteristiche del livello [DAO]. Qui non ce ne sono;
  • riga 41: qui ci sono cinque frammenti;
  • riga 32: adiacenza dei frammenti. Questa costante può assumere qui un valore in [1,4]. Si invita il lettore a variare questo valore per verificare se l’applicazione continua a funzionare;
  • riga 35: si tratta di un'applicazione a schede;

La classe [CoreState] che memorizza lo stato dei frammenti sarà la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
  // frammento visitato o meno
  protected boolean hasBeenVisited = false;
  // stato dell'eventuale menu del frammento
  protected MenuItemState[] menuOptionsState;

  // getter e setter
...
}
  • riga 12: dichiariamo la classe dello stato del frammento [PlaceHolderFragment]. Il frammento [Vue1Fragment] non ha invece alcuno stato;

La classe [Session] è la seguente:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // dati da condividere tra i frammenti stessi e tra i frammenti e l'attività
  // gli elementi che non possono essere serializzati in jSON devono avere l'annotazione @JsonIgnore
  // non dimenticare i getter e i setter necessari per la serializzazione/deserializzazione in jSON

  // numero di frammenti visitati
  private int numVisit;
  // numero del frammento di tipo [PlaceholderFragment] visualizzato nella seconda scheda
  private int numFragment = -1;

  // getter e setter
...
}

Si tratta della sessione del progetto [Exemple-22].

2.8.4.2. L'attività [MainActivity]

  

L'attività [MainActivity] è la seguente:


package client.android.activity;

import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // livello [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessione
  private Session session;

  // gestione del menu-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

  private void showFragment(int i) {
...
  }

  // implementazione dei metodi della classe padre ---------------------------------------------------
  ...
}

In questo caso, la classe [MainActivity] è più articolata rispetto a quella degli esempi precedenti per due motivi:

  • ci sono delle schede da gestire;
  • c'è un menu da gestire;

2.8.4.2.1. Implementazione dei metodi della classe padre

// metodi della classe padre -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // si proseguono le inizializzazioni avviate dalla classe padre
    // sessione
    this.session = (Session) super.session;
    ...
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    // numero del frammento
    final String ARG_SECTION_NUMBER = "section_number";
    // inizializzazione dell'array dei frammenti
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (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[i] = new Vue1Fragment_();
    // risultato
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // qui non ci sono titoli
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
...
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }
  • righe 2-12: il metodo [onCreateActivity] viene chiamato dalla classe padre [AbstractActivity] quando l'attività viene creata per la prima volta o ricreata durante un ciclo di salvataggio/ripristino. Quando questo metodo viene chiamato, la classe padre ha già ripristinato la sessione;
  • riga 10: si recupera un riferimento locale alla sessione. Il cambio di tipo è dovuto al fatto che la sessione della classe padre è di tipo [AbstractSession];
  • righe 19-38: il metodo [getFragments] deve restituire alla classe padre l’array dei frammenti gestiti dall’applicazione. In questo caso ce ne sono [FRAGMENTS_COUNT], numero definito in [IMainActivity]. I primi [FRAGMENTS_COUNT-1] frammenti sono di tipo [PlaceHolderFragment] e l'ultimo di tipo [Vue1Fragment];
  • righe 41-45: il metodo [getFragmentTitle] deve restituire i titoli dei frammenti quando tale informazione può essere utile. In questo caso non è così;
  • righe 47-50: questo metodo viene chiamato dalla classe padre quando l'utente fa clic su una scheda. Ne riparleremo nel paragrafo successivo;
  • righe 52-55: restituisce il numero della prima vista da visualizzare all’avvio dell’applicazione. In questo caso è il frammento [Vue1Fragment] che deve essere visualizzato per primo. Il metodo [getFirstView] potrebbe essere vantaggiosamente sostituito da una costante in [IMainActivity];

2.8.4.2.2. Gestione delle schede

Le schede sono gestite dai seguenti metodi:


@Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // si proseguono le inizializzazioni avviate dalla classe padre
    // sessione
    this.session = (Session) super.session;
    // 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);
    }
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // numero del frammento da visualizzare
    int numFragment;
    switch (position) {
      case 0:
        // numero del frammento [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // numero del frammento [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // visualizzazione frammento
    if (numFragment != mViewPager.getCurrentItem()) {
      navigateToView(numFragment, ISession.Action.SUBMIT);
    }
  }
}
  • righe 1-20: il metodo [onCreateActivity] viene chiamato dalla classe padre [AbstractActivity] quando l’attività viene creata per la prima volta o ricreata durante un ciclo di salvataggio/ripristino. Quando questo metodo viene chiamato, la classe padre ha già ripristinato la sessione;
  • riga 9: si recupera un riferimento locale alla sessione. Il cambio di tipo è dovuto al fatto che la sessione della classe padre è di tipo [AbstractSession];
  • righe 11-13: si crea la prima scheda;
  • righe 15-20: si crea la seconda scheda se nella sessione è registrato un numero di frammento (riga 15). Questo numero è inizialmente pari a -1 al momento della prima creazione dell’attività;
  • righe 23-39: questo metodo viene chiamato dalla classe padre quando l’utente fa clic su una scheda;
  • righe 28-31: se viene cliccata la scheda 0, allora si deve visualizzare [Vue1Fragment]. Si sa che questa è la prima vista visualizzata all'avvio dell'applicazione;
  • righe 32-35: se viene cliccata la scheda 1, allora si deve visualizzare il frammento il cui numero è memorizzato nella sessione;
  • righe 37-39: si naviga verso il frammento scelto. L’azione associata è [SUBMIT]. Avrebbe potuto essere [NAVIGATION]? In questo documento, si utilizza [NAVIGATION] solo quando la visualizzazione del nuovo frammento richiede di conoscere esclusivamente il suo stato precedente. In questo caso, non è così poiché la visualizzazione del frammento deve cambiare rispetto al suo stato precedente per mostrare una visita in più;

2.8.4.2.3. Gestione del menu

L’attività è associata al seguente menu [menu_main.xml]:


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

che visualizza quanto segue:

  

La gestione del menu è assicurata dai seguenti metodi:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // registro
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // elaborazione delle opzioni di menu
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d(className, "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;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // nessuna navigazione in seguito alla selezione software di una scheda
      session.setNavigationOnTabSelectionNeeded(false);
      // si ricreano le due schede per una questione relativa al carattere dei titoli
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // il numero del frammento da visualizzare viene impostato nella sessione
      session.setNumFragment(i);
      // si seleziona la scheda n. 2 con navigazione
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • righe 16-31: gestione del clic su un'opzione di menu di tipo [Fragmenti];
  • righe 37-50: visualizzano il frammento n. i (si tratta dei frammenti di tipo PlaceHolderFragment) nella scheda n. 1 (seconda scheda);
  • righe 42-44: si decide di eliminare le schede esistenti per ricrearne due nuove. Questa decisione è stata presa per aggirare il seguente problema: quando ci si limita a visualizzare il frammento nella scheda 1 esistente (senza quindi eliminarla), curiosamente il suo titolo ha un aspetto (carattere, dimensione) diverso da quello del titolo della scheda 0;
  • righe 43-44: le due schede vengono create ma non selezionate (ultimo parametro in false);
  • riga 40: le operazioni delle righe 42-44 potrebbero eseguire operazioni [select] sulle schede, il che richiamerà il gestore [onTabSelected]. Se non si interviene, si passerà quindi a un frammento. Si evita ciò impostando il valore booleano [navigationOnTabSelectionNeeded] su faux nella sessione. Questo valore booleano viene automaticamente reimpostato su vrai dalla classe [AbstractFragment] quando un frammento diventa visibile;
  • riga 46: si registra il numero del frammento da visualizzare nella sessione;
  • righe 48-50: si seleziona la scheda n. 2 con navigazione (riga 48). Ciò attiverà la procedura [onTabSelected] che:
    • visualizzerà il frammento il cui numero è stato inserito nella sessione;
    • memorizzerà nella sessione il numero della scheda selezionata;

2.8.4.3. Il frammento [Vue1Fragment]

Di seguito riportiamo la versione finale del frammento:


package client.android.fragments.behavior;

import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

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

  // gli 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(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // ciclo di vita del frammento -----------------------------------------------
  private void initFragment() {
    // nulla da fare
  }

  // salvataggio dello stato del frammento
  @Override
  public CoreState saveFragment() {
    // stato della vista - nulla da salvare
    return new CoreState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.FRAGMENTS_COUNT - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // nulla da fare
  }

  @Override
  protected void initView(CoreState previousState) {
    // Prima visita?
    if (previousState == null) {
      // viene visualizzato il numero della visita
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // viene visualizzato il numero della visita
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // metodi privati -------------------------------------
  // visualizzazione del numero di visita
  private void showNumVisit() {
    // incrementa il numero di visita
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // viene visualizzato il numero della visita
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

La classe è quasi vuota.

  • righe 35-39: chiamate dalla classe padre quando il frammento deve salvare il proprio stato. Il frammento [Vue1Fragment] non ha uno stato da salvare. Si restituisce semplicemente un'istanza della classe base [CoreState] (promemoria: non si deve restituire null);
  • righe 41-44: devono restituire il numero del frammento. Il frammento [Vue1Fragment] ha, per definizione, il numero [FRAGMENTS_COUNT-1];
  • righe 51-59: vengono chiamate dalla classe padre quando il frammento viene costruito per la prima volta (previousState==null) o nelle volte successive (previousState!=null);
    • righe 54-57: se si tratta della prima visita, si incrementa il numero di visita e lo si visualizza (righe 85-92);
  • righe 61-65: vengono chiamate quando il frammento sta per essere visualizzato in associazione a un'azione [SUBMIT]. Si incrementa il numero di visita e lo si visualizza. In questo caso, non è possibile che il numero di visita venga incrementato due volte nel ciclo di vita. Infatti, la prima visita al frammento [Vue1Fragment] avviene all'avvio dell'applicazione quando l'azione è impostata su [NONE] per impostazione predefinita nella sessione. Ciò garantisce che il metodo [updateOnSubmit] non venga chiamato. Successivamente, non si tratterà più della prima visita e il metodo [initView] non farà nulla;
  • righe 68-71: chiamate in un ciclo di salvataggio/ripristino. Poiché il frammento non ha uno stato, qui non c'è nulla da ripristinare;
  • righe 73-76: vengono chiamate quando tutti gli aggiornamenti precedenti sono stati effettuati. Qui non c’è più nulla da fare;
  • righe 78-81: vengono chiamate quando tutte le attività asincrone avviate sono terminate. In questo caso non ci sono attività asincrone;

2.8.4.4. Lo stato [PlaceHolderFragmentState]

Lo stato del frammento [PlaceHolderFragment] sarà il seguente:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class PlaceHolderFragmentState extends CoreState {
  // testo
  private String text;

  // costruttori
  public PlaceHolderFragmentState() {

  }

  public PlaceHolderFragmentState(String text) {
    super();
    this.text = text;
  }

  // getter e setter
 ...
}
  • quando sarà necessario salvare lo stato del frammento, verrà salvato il testo che visualizzava (riga 7);

2.8.4.5. Il frammento [PlaceHolderFragment]

Il frammento [PlaceHolderFragment] sarà il seguente:


package client.android.fragments.behavior;

import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {

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

  // dati
  private String text;

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

  // implementazione dei metodi della classe padre ----------------------------
  @Override
  public CoreState saveFragment() {
    // si salva lo stato del frammento
    PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
    placeHolderFragmentState.setText(textViewInfo.getText().toString());
    return placeHolderFragmentState;
  }

  @Override
  protected int getNumView() {
    return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // testo originale
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // si aggiorna il testo visualizzato
    // incremento del numero di visite
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // testo modificato
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // registro
    if (isDebugEnabled) {
      Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
    }
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // si ripristina il testo visualizzato
    PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
    textViewInfo.setText(state.getText());

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

}
  • righe 30-36: quando la classe padre richiede al frammento di salvare il proprio stato, si salva il testo visualizzato dal frammento (riga 34);
  • righe 38-41: restituiscono il numero del frammento. Questo dipende dal numero di sezione che gli è stato passato come argomento al momento della sua creazione;
  • righe 43-47: vengono chiamate durante la prima costruzione del frammento (previousState==null) o durante quelle successive (previousState !=null);
    • riga 46: in questo caso non si utilizza lo stato precedente. Il testo iniziale [text] (riga 24), visualizzato alla prima visita, viene ricalcolato ogni volta. Ciò è discutibile. Si sarebbe potuto scegliere di inserire anche questa informazione nello stato del frammento;
  • righe 49-51: vengono chiamate durante la prima costruzione della vista associata al frammento (previousState==null) o durante quelle successive (previousState!=null). Non c’è nulla da fare;
  • righe 53-56: chiamate quando il frammento sta per essere visualizzato in associazione a un'azione [SUBMIT]. Questo avviene sempre, tranne che nel ciclo di salvataggio/ripristino, in cui l'azione è [RESTORE]. Si incrementa quindi il numero della visita e lo si visualizza;
  • righe 68-74: vengono chiamate in un ciclo di salvataggio/ripristino. Si ripristina il testo che era stato salvato nello stato del frammento;
  • righe 76-79: vengono chiamate quando tutti gli aggiornamenti precedenti sono stati effettuati. In questo caso, non c’è altro da fare;
  • righe 82-83: vengono chiamate quando tutte le attività asincrone avviate sono terminate. In questo caso non ci sono attività asincrone;

2.8.4.6. Tests

Si invita il lettore a testare l’applicazione ruotando il dispositivo per verificare che il frammento visualizzato non perda il proprio stato. Si esamineranno inoltre i log.

2.9. Conclusion

Al termine di questo capitolo, disponiamo di un progetto modello [client-android-skel] di client Android che comunica con un servizio web / jSON con le seguenti caratteristiche:

  • la comunicazione asincrona con il server web / jSON avviene tramite la libreria RxJava;
  • il ciclo di vita di un frammento (aggiornamento, salvataggio, ripristino) è gestito dalla sua classe padre [AbstractFragment], che in momenti specifici richiama determinati metodi delle sue classi figlie. Il frammento figlio non deve quindi occuparsi delle fasi del ciclo di vita, ma solo di implementare alcuni metodi imposti dalla sua classe padre;
  • il ciclo di vita dell’attività (salvataggio / ripristino) è gestito da una classe astratta [AbstractActivity], che a sua volta impone all’attività figlia di implementare determinati metodi;
  • la classe [AbstractActivity] è in grado di gestire un’applicazione con o senza schede, con o senza immagine di attesa, con o senza autenticazione di base presso il server web / jSON. La presenza o meno di questi elementi viene configurata;

Presenteremo ora un caso di studio più complesso rispetto agli esempi precedenti. La nuova applicazione si baserà sul progetto modello [client-android-skel].