2. Scheletro di un client Android che comunica con un servizio web / JSON
Forniamo ora uno scheletro per un'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:
![]() |
Lo studio di questa applicazione di base ci offrirà l'opportunità di ripassare alcuni punti affrontati negli esempi precedenti. Questa applicazione fungerà da modello per tutte le applicazioni future. È stata realizzata dopo numerose iterazioni. Il suo obiettivo è quello di estrapolare il maggior numero possibile di elementi dalle applicazioni che realizzeremo prossimamente e di raggrupparli in classi astratte, in modo da evitare di dover scrivere più volte 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 è gestita utilizzando la libreria RxJava;
- il ciclo di vita di un frammento (aggiornamento, salvataggio, ripristino) è gestito dalla sua classe padre [AbstractFragment], che chiama determinati metodi delle sue classi figlie in momenti specifici. La classe figlia non deve quindi preoccuparsi delle fasi del ciclo di vita, ma deve solo implementare determinati metodi richiesti dalla sua classe padre;
- il ciclo di vita dell'attività (salvataggio / ripristino) è gestito da una classe astratta [AbstractActivity], che richiede anche 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 caricamento e con o senza autenticazione di base sul server web/JSON. La presenza o l'assenza di questi elementi è determinata dalla configurazione;
Questo scheletro è stato utilizzato per tutti gli esempi successivi. A causa della loro diversità, ciò che funzionava per un esempio potrebbe non funzionare per quello successivo. Poiché lo scheletro è stato utilizzato per un totale di sette esempi, si sono verificate numerose iterazioni. Se dovessimo utilizzarlo per un ottavo esempio, è possibile che ci rendessimo nuovamente conto che la natura specifica di questo nuovo esempio genera nuovi errori. Tuttavia, l'uso di questo scheletro semplificherà notevolmente la scrittura di esempi futuri. Infatti, la gestione del ciclo di vita di un frammento (aggiornamento, salvataggio, ripristino) combinata con il concetto di adiacenza dei frammenti è particolarmente complessa. Qui, è completamente nascosta all'interno della classe [AbstractFragment].
2.1. Architettura del client Android
Il client Android proposto si basa sulla seguente architettura:
![]() |
- il livello [DAO] implementa un'interfaccia [IDao]. È responsabile della comunicazione con il server web/JSON;
- c'è una sola attività che implementa anch'essa l'interfaccia [IDao]. Le viste la chiamano per accedere al server;
- le viste sono implementate da frammenti;
Il progetto Android riflette questa architettura:
![]() |
Presenteremo uno per uno i vari elementi di questo progetto.
2.2. La configurazione di Gradle
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// options de packaging nécessaires pour être capable de produire l'APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile '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. Tuttavia, è possibile iniziare con i numeri attuali se si configura Android Studio in modo da garantire la presenza di queste versioni degli strumenti Android (righe 15–16, 47–48) (vedere la sezione 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: Modifica il pacchetto dell'applicazione;
- Righe 10, 15: imposteremo il valore della voce [app_name] nel file [res/values/strings.xml]. Per ora, è il seguente:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- application name -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. L'organizzazione del codice Java
![]() |
- [architettura] raggruppa gli elementi principali dell'organizzazione del codice;
- [attività] contiene la singola attività dell'applicazione;
- [fragments] raggruppa i frammenti o le viste dell'applicazione;
- [dao] raggruppa gli elementi per la comunicazione con il server web / JSON;
2.5. Elementi dell'attività
![]() | ![]() |

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>
<!-- fragment container -->
<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: viene utilizzato un contenitore di frammenti specifico;
L'attività dispone anche di un menu [res/menu/menu_main.xml] per la sua 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 ora è vuoto. Lo sviluppatore lo riempirà secondo necessità.
2.5.2. Il contenitore dei 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 {
// swipe control
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to be redefined to manage swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// scrolling control
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
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 scorrimento (riga 11) e lo scorrimento verticale (riga 13) tra le viste.
- righe 26–43: metodi che disabilitano lo scorrimento se è stato disattivato;
- righe 46–49: ridefinizione del metodo [setCurrentItem], utilizzato per cambiare la vista visualizzata. Se lo scorrimento è stato disabilitato, la vista cambierà senza scorrimento. Si noti che lo sviluppatore può sovrascrivere questo comportamento utilizzando il metodo [setCurrentItem(int position, boolean smoothScrolling)], che consente di specificare il comportamento di scorrimento desiderato;
2.5.3. La classe [CoreState]
![]() |
La classe [CoreState] è la classe padre degli stati dei vari 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)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- riga 16: Ogni frammento ha un valore booleano [hasBeenVisited] nel proprio stato che indica se è già stato visitato o meno. Ciò è necessario perché a volte, quando un frammento viene visualizzato per la prima volta, è necessario eseguire azioni specifiche;
- riga 18: il progetto [client-android-skel] salva e ripristina automaticamente i menu dei frammenti, se presenti. Nell'array MenuItemState[] menuOptionsState, memorizziamo lo stato visibile o nascosto di tutte le opzioni di menu;
- Righe 10–13: Come fatto in [Esempio-22], lo stato dell'attività e dei suoi frammenti verrà salvato nella sessione, che a sua volta verrà salvata come stringa JSON. Vedremo che la sessione memorizza un array di elementi di tipo [CoreState]. Se non facciamo nulla, verrà salvata la stringa JSON di tipo [CoreState]. Tuttavia, vogliamo salvare gli stati dei frammenti, che derivano da [CoreState]. Per garantire che venga generata la stringa JSON del tipo derivato anziché quella del tipo padre, i tipi derivati devono essere dichiarati come mostrato nelle righe 10–13. La classe [CoreState] è una delle classi di 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:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants (to be modified) -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 0;
// todo add your constants and other methods here
}
- riga 6: l'interfaccia [IMainActivity] estende l'interfaccia [IDao] del livello [DAO];
- Riga 9: questa è l'attività che fornisce l'accesso alla sessione sotto forma di un'istanza dell'interfaccia [ISession];
- riga 12: questa è l'attività utilizzata per cambiare vista. Il secondo parametro è l'azione che innesca questo cambio di vista, uno dei valori SUBMIT, NAVIGATION o RESTORE;
- righe 15–17: questa è l'attività che gestisce la schermata di caricamento;
- riga 22: per il debug dell'applicazione;
- riga 25: per evitare un'attesa troppo lunga se il server smette di rispondere;
- riga 28: durante il debug, impostare questo valore su pochi secondi per consentire di annullare l'operazione con il server e vedere cosa succede;
- riga 31: impostare su true se il servizio JSON richiede l'autenticazione di base;
- riga 34: adiacenza dei frammenti;
- riga 37: impostare su true se l'applicazione ha delle schede;
- riga 39: impostare su true se l'applicazione comunica con un server web/JSON e si desidera visualizzare un'immagine di caricamento durante gli scambi;
- riga 43: il numero di frammenti gestiti dall'applicazione;
L'interfaccia [IMainActivity] è il secondo elemento dell'architettura che lo sviluppatore deve implementare (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 {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
- riga 24: lo sviluppatore completerà l'interfaccia qui;
2.5.6. La sessione
![]() |
La classe [Session] incapsula gli elementi condivisi dall'attività e dai frammenti. Implementa la seguente interfaccia [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// number of last view displayed
int getPreviousView();
void setPreviousView(int numView);
// last state of a view
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// action in progress
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// status of all views -
// not used by code but required for serialization / deserialization jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// last selected tab number
int getPreviousTab();
void setPreviousTab(int position);
// tab selection navigation
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Introduciamo l'interfaccia [ISession] per richiedere 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 un'azione in corso. Ce ne sono quattro (riga 17):
- RESTORE: è in corso un'operazione di salvataggio/ripristino. Non vi è alcun cambiamento di vista;
- NAVIGATION: la navigazione è in corso. Qui, definiamo la navigazione come un cambiamento di vista in cui la nuova vista può essere ripristinata dal suo ultimo stato salvato durante la sessione;
- SUBMIT: assegniamo il tipo [SUBMIT] a un'azione in sospeso quando c'è un cambiamento di vista e la nuova vista dipende dallo stato complessivo dell'attività, non solo dal proprio stato. A volte è difficile distinguere tra NAVIGATION e SUBMIT. In questi casi, useremo il caso più generale di SUBMIT;
- NONE: il valore dell'azione quando non ha ancora ricevuto il suo primo valore;
- righe 26–30: gli stati dell'attività e dei frammenti saranno memorizzati in un array CoreState[]. Per garantire che ciò venga gestito correttamente durante la serializzazione/deserializzazione JSON, deve avere un getter e un setter;
- righe 32-35: numero dell'ultima scheda selezionata. Utilizzato durante il ciclo di salvataggio/ripristino per riselezionare la scheda che era stata selezionata prima della rotazione del dispositivo;
- righe 37–40: gestisce 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 {
// previous view no
private int preViousView;
// view status
private CoreState[] coreStates = new CoreState[0];
// action in progress
private Action action = Action.NONE;
// previously selected tab
private int previousTab;
// tab selection navigation
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// manufacturer
public AbstractSession() {
// initialize the fragment status table
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// interface 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: l'ID della vista che era visualizzata prima di quella attualmente visualizzata. Questa informazione è utile quando una vista è raggiungibile da più posizioni. Questo è tipicamente il caso della navigazione basata su schede. La vista visualizzata può quindi determinare quale vista era visualizzata in precedenza;
- riga 12: l'array degli stati per tutti i frammenti visualizzati dall'attività;
- riga 18: l'ID della scheda selezionata in precedenza. Svolge un ruolo simile a quello dell'ID della vista precedente alla riga 9. Questa informazione è utile quando il dispositivo viene ruotato ed è necessario tornare alla scheda che era stata selezionata prima della rotazione;
- riga 22: un valore booleano che indica se la selezione di una scheda deve comportare una modifica al frammento visualizzato. Si noti che il progetto [client-android-skel] gestisce le schede e i frammenti separatamente, in modo da 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 fa clic su una scheda. In questo caso, il frammento visualizzato deve generalmente cambiare;
- una selezione guidata dal software tramite il metodo [Tablayout.Tab.select()]. In questo caso, la modifica del frammento visualizzato non è sempre auspicabile. Ecco due esempi:
- quando il dispositivo viene ruotato, l'attività viene ricreata, così come le schede. Tuttavia, quando viene creata la prima scheda, essa subisce automaticamente un'operazione software [select]. Non è quindi auspicabile cambiare il frammento visualizzato perché ci troviamo 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, potresti voler aggiornare le schede (eliminare, aggiungere) senza interferire con i frammenti associati. Tuttavia, alcune di queste operazioni possono nuovamente innescare un'operazione software [select] implicita su una delle schede. Questa selezione non comporta necessariamente la 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 la serializzazione/deserializzazione JSON;
- righe 25–31: il costruttore inizializza l'array degli stati per i [FRAGMENTS_COUNT] frammenti dell'applicazione. Gli elementi di questo array vengono inizializzati con il campo [hasBeenVisited=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 {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
}
- riga 5: la classe [Session] estende la classe [AbstractSession] che abbiamo appena visto. Lo sviluppatore inserirà qui gli elementi da condividere tra i frammenti stessi e tra i frammenti e l'attività. Si noti che la classe [Session] non è più annotata con l'annotazione [@EBean]. È diventata una classe normale;
2.5.7. La classe astratta [AbstractActivity]
![]() |
2.5.7.1. Struttura
La classe [AbstractActivity] è una classe di oltre 300 righe. La esamineremo passo dopo passo. La sua struttura è 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 {
// layer [DAO]
private IDao dao;
// the session
protected Session session;
// the fragment container
protected MyPager mViewPager;
// the toolbar
private Toolbar toolbar;
// the waiting image
private ProgressBar loadingPanel;
// tab bar
protected TabLayout tabLayout;
// the fragment or section manager
private FragmentPagerAdapter mSectionsPagerAdapter;
// class name
protected String className;
// mapper jSON
private ObjectMapper jsonMapper;
// manufacturer
public AbstractActivity() {
// class name
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implémentation IMainActivity --------------------------------------------------------------------
...
// life cycle - backup / restore ------------------------------------
...
// hold image management ---------------------------------
...
// interface IDao -----------------------------------------------------
...
// the fragment manager --------------------------------
...
// girls' classes
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 quando il dispositivo ruota (riga 58);
- gestisce la schermata di caricamento durante la comunicazione con il server web / JSON (riga 61);
- implementa l'interfaccia IDao del livello [DAO] (riga 64);
- implementa il gestore dei frammenti (riga 67);
- richiede che le sue classi figlie abbiano sei metodi (righe 71–81);
2.5.7.2. Implementazione dell'interfaccia [IMainActivity]
L'implementazione dell'interfaccia [IMainActivity] (vedere la sezione 2.5.4) è la seguente:
// implémentation 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));
}
// display new fragment
mViewPager.setCurrentItem(position);
// we note the action in progress when the view changes
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. Pertanto, dobbiamo salvare la sessione. Qui riutilizziamo ciò che è stato fatto nel progetto [Esempio-22] (vedi sezione 1.23):
// backup / restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// save session as jSON string
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
Ciò comporta il ripristino della sessione. Procediamo come mostrato in [Esempio-22]:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// something to restore?
if (savedInstanceState != null) {
// session recovery
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// session
session = new Session();
}
...
- righe 10–26: se il parametro [Bundle savedInstanceState] alla riga 2 non è nullo, la sessione viene ripristinata (righe 12–17);
- righe 26–29: se il parametro [Bundle savedInstanceState] alla riga 2 è nullo, ciò 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) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// layer [DAO]
dao = getDao();
if (dao != null) {
// layer configuration [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// girls' classes
protected abstract IDao getDao();
....
}
- riga 11: viene richiesto un riferimento al livello [DAO] dall'attività figlia (riga 21);
- righe 14–17: se il livello [DAO] esiste, viene configurato utilizzando le informazioni contenute nell'interfaccia [IMainActivity];
2.5.7.6. Inizializzazione della vista associata all'attività
La vista associata all'attività è stata presentata nella Sezione 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>
<!-- fragment container -->
<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) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// associated view
setContentView(R.layout.activity_main);
// view components ---------------------
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// waiting image?
if (IS_WAITING_ICON_NEEDED) {
// we add the waiting image
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// creation ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// added ProgressBar to toolbar
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: aggiunta facoltativa di un'icona di caricamento: se il valore booleano [IS_WAITING_ICON_NEEDED] è vero nell'interfaccia [IMainActivity];
- riga 23: creazione dell'immagine di caricamento 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 come segue:
// tab bar
protected TabLayout tabLayout;
...
// tab bar?
if (ARE_TABS_NEEDED) {
// add the tab bar
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// no selection navigation until a fragment is displayed
session.setNavigationOnTabSelectionNeeded(false);
// tab bar creation
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// tab bar added to application bar
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// tab bar event manager
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// a tab has been selected
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()) {
// miter position
int position = tab.getPosition();
// memory
session.setPreviousTab(position);
// associated fragment display?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// girls' classes
protected abstract void navigateOnTabSelected(int position);
...
- righe 12–48: aggiunta e gestione di una barra delle schede;
- riga 6: la barra delle schede viene aggiunta se la costante [ARE_TABS_NEEDED] è impostata su true nell'interfaccia [IMainActivity];
- riga 12: durante la creazione della barra delle schede, possono verificarsi operazioni implicite [Tablayout.Tab.select] (queste non sono attivate dall'utente). Impostiamo il valore booleano [session.navigationOnTabSelectionNeeded] su false per impedire qualsiasi navigazione durante queste false selezioni. Spetterà allo sviluppatore selezionare il frammento da visualizzare utilizzando il metodo [navigateToView]. Il valore booleano [session.navigationOnTabSelectionNeeded] verrà reimpostato su true quando questo frammento verrà visualizzato (vedere la classe AbstractFragment);
- riga 14: creazione di una barra delle schede a cui fa riferimento il campo [tabLayout]. Utilizziamo una barra delle schede personalizzata [CustomTabLayout], di cui parleremo più avanti;
- riga 15: impostiamo i colori dei titoli delle schede. Questi si trovano 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 la scheda è selezionata;
- riga (d): il colore del titolo della scheda quando non è selezionata;
Questo file è, ovviamente, modificabile. Puoi trovare i codici esadecimali dei colori qui, ad esempio.
- righe 17-18: aggiunta di questa barra delle schede alla barra dell'applicazione nella vista XML [activity_main];
- righe 20–47: gestore di eventi per la barra delle schede;
- righe 22–36: viene gestito solo l'evento [onTabSelected]. Corrisponde a un clic sulla [scheda Tab] passata come parametro al metodo o a un'operazione software [TabLayout.Tab.select];
- riga 30: posizione della scheda selezionata;
- riga 32: questa posizione viene memorizzata nella sessione;
- riga 34: il frammento associato a questa scheda deve ora essere visualizzato. Solo la classe figlia (riga 52) può effettuare questa associazione. Si noti che non associamo la barra delle schede al contenitore dei frammenti [mViewPager] come è stato fatto in alcuni degli esempi studiati. Qui, separiamo completamente la gestione della barra delle schede da quella dei frammenti. Questo è il motivo per cui, quando si clicca su una scheda, dobbiamo specificare quale vista vogliamo vedere visualizzata;
- riga 28: distinguiamo tra selezione della scheda con o senza navigazione. Generalmente, quando l'utente clicca su una scheda, ci si aspetta una navigazione, mentre durante una selezione programmatica non è così. Lo sviluppatore distingue tra questi due casi utilizzando l'elemento [session.navigationOnTabSelectionNeeded]. Quando la navigazione non viene eseguita, il numero dell'ultima scheda selezionata non viene salvato nella sessione. Spetta allo sviluppatore farlo;
2.5.7.8. Il gestore delle schede [CustomTabLayout]
![]() |
Utilizziamo un gestore di schede personalizzato per visualizzare i titoli delle schede con caratteri diversi. 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);
}
}
}
}
- Il carattere dei titoli delle schede è personalizzato alle righe 30 e 44;
La cartella [fonts] è la seguente:
![]() |
Fonti:
- Il codice della classe [CustomTabLayout] è stato trovato all'URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout];
- I font sono stati trovati all'URL [https://www.fontsquirrel.com/fonts/roboto];
2.5.7.9. Ultime inizializzazioni
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// fragment manager instantiation
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment adjacency
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// display 1st view
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// we hand over to the daughter activity
onCreateActivity();
}
...
// girls' classes
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- righe 10–19: Questo codice si trova comunemente negli esempi che abbiamo studiato;
- righe 21–23: Visualizzazione della primissima vista. Esistono indubbiamente diversi modi per distinguere questo caso. Qui abbiamo sfruttato il fatto che, per la primissima vista, il valore dell’azione che innesca il cambio di vista è NONE;
- riga 22: non facciamo ipotesi sul primo frammento da visualizzare. Nei nostri esempi, questo è stato spesso il frammento n. 0, ma non sempre (vedi Esempio-22). Chiederemo quindi all'attività figlia (riga 30) di dirci di quale vista si tratta;
- riga 25: abbiamo estrapolato tutto ciò che potevamo qui. Ora, la classe figlia ha le proprie inizializzazioni da eseguire (riga 29);
2.5.7.10. Gestione dell'immagine di caricamento
Nella classe [AbstractActivity], l'immagine segnaposto è gestita dai seguenti due metodi:
// hold image management ---------------------------------
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] (vedere la sezione 2.5.5) è implementata come segue:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// layer [DAO]
private IDao dao;
...
// interface 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: Ricordiamo 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:
...
// the fragment manager --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// daughter class fragments
fragments = getFragments();
}
// must render fragment no. position
@Override
public AbstractFragment getItem(int position) {
// we return the fragment
return fragments[position];
}
// makes the number of fragments to manage
@Override
public int getCount() {
return fragments.length;
}
// makes the title of fragment no. position
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// girls' classes
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- riga 5: l'array dei frammenti associati all'attività. Tutti i frammenti deriveranno dalla classe [AbstractFragment];
- righe 8–12: questo è il costruttore che inizializza l'array di frammenti. Li richiede 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, alla scheda può essere assegnato il titolo del frammento. Qui, questi 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'operazione di salvataggio/ripristino:
@Override
public void onResume() {
// parent
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// if restore, then restore last selected tab
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. È importante notare qui che nel metodo [onCreate] — che, nel ciclo di vita dell'attività, viene eseguito prima del metodo [onResume] — la navigazione alla selezione di una scheda è stata disabilitata. Pertanto, qui, viene selezionata una scheda ma non vi è alcun cambio di frammento;
2.5.7.14. Riepilogo
La classe astratta [AbstractActivity] sarà la classe padre dell'unica attività dell'applicazione.
L'attività figlia deve implementare i seguenti sei metodi:
// girls' classes
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 membri protetti della sua classe padre:
// the session
protected ISession session;
// the fragment container
protected MyPager mViewPager;
// tab bar
protected CustomTabLayout tabLayout;
// class name
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 predefinita 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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
// todo: tabbed browsing - define the first view to be displayed
return 0;
}
}
- riga 14: affinché l'annotazione AA [@Bean] alla riga 19 sia valida, l'attività deve avere l'annotazione AA [@EActivity];
- riga 15: l'attività è associata al menu XML [menu_main]. Attualmente, questo menu è vuoto. Lo sviluppatore dovrà compilarlo 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ò significa che il bean AA [Dao] deve esistere. Questo è sempre il caso con l'applicazione scheletro che forniamo. Anche in un'applicazione senza un livello [DAO], è possibile lasciare il pacchetto [dao] al suo posto. Ciò non causa alcuna complicazione;
- riga 22: la sessione come istanza di tipo [Session]. La sessione esiste nella classe padre [AbstractActivity] ma come istanza dell'interfaccia [ISession] (riga 32);
- righe 24–63: i sei metodi richiesti dalla classe padre [AbstractActivity];
- Righe 36–39: il metodo [getDao] restituisce un riferimento al livello [DAO]. Qui, questo riferimento non è mai nullo. Tuttavia, nella classe padre [AbstractActivity], abbiamo previsto il caso in cui la classe figlia restituisca un riferimento nullo per indicare che non esiste un livello [DAO]. Se si desidera utilizzare questa opzione (a mio avviso non molto utile), è qui che si deve impostare il puntatore su null;
2.6. Il livello [DAO]

![]() |
2.6.1. L'interfaccia IDao
È stata introdotta nella sezione 2.5.5:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
Lo sviluppatore aggiungerà i metodi per il 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);
// todo : declare here the URL to be reached
}
Lo sviluppatore aggiungerà i metodi che comunicano con gli 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 {
// user
private String user;
// password
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// headers HTTP of the HTTP request intercepted
HttpHeaders headers = request.getHeaders();
// the HTTP basic authentication header
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// added to HTTP headers
headers.setAuthorization(auth);
// continue the query life cycle HTTP
return execution.execute(request, body);
}
// authentication elements
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Questa classe genera la seguente intestazione di autenticazione HTTP:
dove [code] è la stringa 'user:mp' codificata in Base64. Questa classe viene utilizzata solo se il server JSON richiede questa forma di autenticazione. Esistono altre forme.
Nota: L'uso di questa classe è illustrato nella sezione 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 {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// debug mode
protected boolean isDebugEnabled;
// class name
protected String className;
// timeout before request execution
private int delay;
// manufacturer
public AbstractDao() {
// class name
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// méthodes protégées ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
T getResponse();
}
// generic request to a web service / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// service execution - a single response is expected
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// waiting?
if (delay > 0) {
Thread.sleep(delay);
}
// execute the synchronous request
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()));
}
// the response is sent to the observer
subscriber.onNext(response);
// we signal the end of the observable
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"));
}
}
// an exception is issued
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// debug mode
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 restituire un tipo [Observable<T>]. A differenza di alcuni esempi visti in precedenza, non restituisce un tipo [Response<T>] — che è un tipo proprietario — ma piuttosto qualsiasi tipo T;
- riga 35: il metodo [getResponse] accetta come parametro un'istanza di tipo [IRequest<T>] dalle righe 30–32, il cui metodo [IRequest.getResponse()] ottiene il tipo T tramite un'operazione HTTP sincrona;
- righe 48–50: artificialmente, attendiamo [delay] millisecondi. In produzione, imposteremo [delay=0]. Durante il debug, imposteremo [delay=alcuni secondi] per dare all'utente la possibilità di annullare l'operazione asincrona e vedere così come si comporta il codice in quel caso;
- riga 52: la risposta prevista viene richiesta con una richiesta sincrona;
- riga 64: una volta ricevuta la risposta, questa viene passata all'osservatore;
- riga 66: indichiamo che non ci saranno ulteriori emissioni. Questo è il caso specifico di un'azione asincrona che restituisce un solo elemento;
- righe 67–78: in caso di eccezione, l'eccezione viene propagata 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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
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));
}
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// todo: implementation IDao
}
- righe 21-22: iniezione del bean AA [WebClient], che gestirà la comunicazione con il server web / JSON;
- righe 24-25: iniezione dell'intercettatore di autenticazione;
- righe 31-42: metodo eseguito dopo l'iniezione dei campi delle righe 21-25;
- riga 37: l'oggetto [RestTemplate], che gestisce la comunicazione client/server, viene creato da una factory. Questo non è strettamente necessario, ma la factory ci permette di configurare i timeout di comunicazione. Ecco perché non usiamo il costruttore senza parametri [RestTemplate()];
- riga 39: aggiungiamo un convertitore JSON ai convertitori di [RestTemplate]. Questo sarà l'unico convertitore. Pertanto, quando un metodo di [WebClient] riceve una stringa JSON dal server, questa verrà automaticamente deserializzata nell'oggetto che il metodo dovrebbe restituire;
- riga 41: l'oggetto [RestTemplate] configurato in questo modo viene passato al client web, che gestirà la comunicazione client/server utilizzandolo;
- Righe 44–48: impostiamo l'URL radice del server web/JSON. Tutti gli URL dichiarati nella classe [WebClient] sono relativi a questo URL radice;
- righe 50–54: questo metodo consente di specificare il proprietario della connessione quando la connessione è controllata dall'autenticazione di base (vedere la sezione 2.6.3);
- righe 56–64: impostano i timeout per gli scambi client/server. Ciò avviene tramite la factory dell'oggetto [RestTemplate], che governa gli scambi;
- righe 66–78: questo metodo specifica che il server è protetto dall'autenticazione di base;
- righe 72–77: se è richiesta l'autenticazione di base, l'intercettatore di autenticazione iniettato alla riga 25 viene aggiunto agli intercettatori dell'oggetto [RestTemplate]. Questo intercettatore aggiungerà automaticamente l'intestazione HTTP di autenticazione di base richiesta dal server a tutte le richieste del client web;
- Lo sviluppatore implementerà l'interfaccia [IDao] a partire dalla riga 87;
2.7. 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 {
// menu option identify
private int menuItemId;
// option visibility
private boolean isVisible;
// manufacturers
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
2.7.2. La classe [Utils]
La classe [Utils] contiene metodi di utilità statici:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// list of exception messages - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list of error msgs from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// exception message list - version 2
static public String getMessageForAlert(Throwable th) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
// list of exception messages - version 3
static public String getMessageForAlert(List<String> messages) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
}
2.7.3. La classe padre [AbstractFragment]
La classe [AbstractFragment] contiene gli elementi comuni a tutti i frammenti dell'applicazione. Come nella classe [AbstractActivity], il suo codice è complesso. Lo analizzeremo passo dopo passo.
2.7.3.1. Lo scheletro
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 {
// données privées ------------------------------------------------------------
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment status
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
// data accessible to daughter classes ---------------------------------------
// debug mode
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
protected String className;
// asynchronous tasks
protected int numberOfRunningTasks;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// update Fragment ----------------------------------------------------------------------------------
...
// menu management ------------------------------------------
...
// wait management -------------------------------------------------------------
...
// asynchronous operation management --------------------------------------------------------------------
...
// gestion exception -------------------------------------------------------------------
....
// fragment lifecycle management --------------------------------------------------------
...
// classes filles -----------------------------------------------------
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: dati protetti accessibili alle classi figlie;
- righe 61–62: codice che aggiorna il frammento da visualizzare;
- righe 64-65: codice di utilità per gestire il menu, se presente;
- righe 67-68: codice di utilità per gestire l'attesa durante un'operazione asincrona;
- righe 70–71: codice per facilitare la comunicazione tra il frammento e il livello [DAO];
- righe 73-74: codice di utilità per gestire eventuali eccezioni 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:
// class name
protected String className;
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
...
// constructeur ----------------------
public AbstractFragment() {
// init
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- Riga 9: viene annotato il nome della classe figlia che viene istanziata qui. Questo nome viene utilizzato in tutti i log della classe padre;
- riga 10: si annota che il frammento è in fase di costruzione. Questa informazione verrà utilizzata quando al frammento figlio verrà richiesto di aggiornarsi;
2.7.3.3. Gestione dei 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 eseguito 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. Questo è quindi il momento in cui l'interfaccia visiva e il menu possono essere aggiornati. È all'interno di questo metodo [onCreateOptionsMenu] che istruiamo il frammento figlio ad aggiornarsi.
La gestione dei menu comprende metodi di utilità che consentono al frammento figlio di visualizzare o nascondere le voci di menu:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// menu management ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// scroll through all menu items
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item n° i is a sub-menu, then start again
if (menuItem.hasSubMenu()) {
// recursivity
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// result
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// we retrieve the identifiers of the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer menu options to a table
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identify option
int id = menuOptionsIds.get(i);
// status option
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// menu option status
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// status
MenuItemState state = this.menuOptionsStates[i];
// menu id
int id = state.getMenuItemId();
// initialization status
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
return menuOptionsStates;
}
// display menu options -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// update all menu options
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- righe 6–18: questo metodo recupera gli identificatori numerici di tutte le opzioni del menu;
- riga 6: il metodo [getMenuOptions] accetta due parametri:
- [Menu menu]: il menu del frammento;
- [List<Integer> menuOptionsIds]: l'elenco degli ID Android delle opzioni di menu. Inizialmente, questo elenco è vuoto. Viene poi popolato tramite una traversata ricorsiva (riga 15) dell'albero del menu;
- righe 20–40: in base al menu, costruisce l'array degli stati (ID, visibilità) per le opzioni del menu. Questo array è memorizzato nella riga 3. La classe [MenuItemState] è stata descritta nella sezione 2.7.1;
- righe 43–55: una variante del metodo precedente. Esegue la stessa operazione, ma invece di ricalcolare gli identificatori per tutte le opzioni di menu — cosa che è già stata fatta — utilizza gli identificatori dall’array degli stati alla riga 3;
- righe 58–63: il metodo [setAllMenuOptionsStates] consente di nascondere o mostrare tutte le opzioni di menu del frammento;
- righe 65–69: il metodo [setMenuOptionsStates] consente di mostrare o nascondere in modo selettivo determinate opzioni di menu;
- I metodi [getMenuOptions, getMenuOptionsStates] sono dichiarati privati perché vengono utilizzati solo all'interno di [AbstractFragment]. I metodi [setAllMenuOptionsStates] (riga 58) e [setMenuOptionsStates] (riga 65) sono dichiarati protetti in modo da essere disponibili per le classi figlie;
2.7.3.4. Gestione dell'attesa del completamento di un'attività asincrona
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// management of waiting for the end of an asynchronous operation -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// the number of tasks to be executed is noted
this.numberOfRunningTasks = numberOfRunningTasks;
// we put the image on hold
mainActivity.beginWaiting();
// empty the subscription list
abonnements.clear();
// no cancellations yet
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// we hide the waiting image
mainActivity.cancelWaiting();
}
- righe 9–18: Per avviare una o più operazioni asincrone, il frammento figlio chiamerà il metodo del frammento padre [beginRunningTasks]. Il parametro di questo metodo è il numero di attività asincrone che il frammento figlio avvierà;
- riga 11: memorizziamo il parametro del metodo;
- riga 13: viene visualizzata la schermata di caricamento;
- riga 15: l'elenco delle sottoscrizioni alle operazioni asincrone viene azzerato. Queste non sono ancora state create dal frammento figlio;
- riga 17: viene mantenuto un valore booleano per indicare che le attività asincrone richieste dal frammento figlio sono state annullate. Inizialmente, il valore booleano è 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 in attesa viene nascosta;
2.7.3.5. Gestione delle eccezioni
// gestion exception -------------------------------------------------------------------
// exception alert display
protected void showAlert(Throwable th) {
// display messages from the Throwable th exception stack
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// message list display
protected void showAlert(List<String> messages) {
// the message list is displayed
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 provenienti dallo stack delle eccezioni del Throwable passato come parametro;
- righe 10–13: il metodo [showAlert(List<String>)] consente a un frammento figlio di visualizzare l'elenco dei messaggi passati come parametro in una finestra;
- La classe [Utils] utilizzata nelle righe 6 e 12 è stata descritta nella sezione 2.7.2;
2.7.3.6. Gestione delle operazioni asincrone
...
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// asynchronous task execution with RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// process: the observable to be executed/observed
// consumeResult: the method that uses the response obtained
//
// new subscriptions are only created if they have not been cancelled
if (!runningTasksHaveBeenCanceled) {
// execution on I/O thread and observation on Ui thread
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// the observable is executed
try {
abonnements.add(process.subscribe(
// consumption result
consumeResult,
// consumption exception
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// end of task
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
...
}
- righe 9–41: esegue 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 passargli 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 è già stata annullata dall'utente o dal programma (a causa di un'eccezione);
- riga 16: il processo è configurato per essere eseguito su un thread I/O e osservato sul thread dell'interfaccia utente;
- riga 16: l'istruzione [process.subscribe] avvia il processo nel thread I/O. All'interno di questo thread, le operazioni vengono eseguite in modo sincrono perché stiamo utilizzando una libreria HTTP 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. La gestione è delegata al metodo [consumeThrowable] alla riga 49;
- righe 29–36: il metodo eseguito quando l'attività emette la notifica di fine emissione. La gestione è delegata al metodo [endOfTask] alla riga 43;
- riga 19: il task asincrono appena avviato viene registrato nel campo [subscriptions], che tiene traccia di tutti i task asincroni avviati. Ciò consentirà di annullarli se necessario;
- righe 37–39: metodo eseguito quando si verifica un'eccezione durante l'elaborazione del task asincrono. La gestione è delegata al metodo [consumeThrowable] alla riga 49;
Il metodo [endOfTask] è il seguente:
// asynchronous tasks
protected int numberOfRunningTasks;
...
private void endOfTask() {
// one less job to wait for
numberOfRunningTasks--;
// finished?
if (numberOfRunningTasks == 0) {
// end waiting
cancelWaitingTasks();
// the end of tasks is signalled to the daughter class
notifyEndOfTasks(false);
}
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- riga 6: un'attività asincrona è appena terminata. Il contatore delle attività attive viene decrementato;
- riga 8: se non ci sono più attività attive, allora il thread figlio ha ricevuto tutte le sue risposte;
- riga 10: l'attesa viene annullata;
- riga 12: notifichiamo al frammento figlio che tutte le attività da esso avviate sono terminate chiamando il suo metodo [notifyEndOfTasks]. Il parametro di questo metodo indica come le attività sono terminate: normalmente, oppure a causa di una cancellazione da parte dell'utente o del codice a seguito di un'eccezione. Alla riga 12, segnaliamo una fine normale. Si noti che il frammento figlio non ha bisogno di tenere traccia di quali attività siano ancora attive. La sua classe padre lo fa per lui;
Il metodo [consumeThrowable] è il seguente:
// asynchronous tasks
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
// th: the exception to be dealt with
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// cancel tasks already started
cancelRunningTasks();
// error messages are displayed
showAlert(th);
}
// cancel tasks
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// cancel all registered asynchronous tasks
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// we note the cancellation
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// end of wait
cancelWaitingTasks();
// the cancellation of tasks is reported to the daughter fragment
notifyEndOfTasks(true);
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- Riga 3: il metodo [consumeThrowable] intercetta l'eccezione verificatasi;
- riga 15: tutte le attività ancora attive vengono annullate;
- riga 17: viene visualizzato il testo dell'eccezione;
- righe 21–37: tutte le attività vengono annullate;
- righe 27–29: tutte le sottoscrizioni vengono annullate;
- riga 31: viene registrata una nota che indica che si è verificata una cancellazione;
- riga 32: il contatore delle attività viene azzerato;
- riga 34: l'attesa viene annullata;
- riga 36: al frammento figlio viene notificato che le attività sono terminate a seguito della cancellazione;
2.7.3.7. Gestione del ciclo di vita dei frammenti
// life cycle --------------------------------------------------------
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// parent
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] sono inclusi esclusivamente a scopo di registrazione. Questi consentono allo sviluppatore di comprendere meglio il ciclo di vita del frammento;
Il salvataggio del frammento quando il dispositivo ruota è gestito dai seguenti metodi: [setUserVisibleHint, onSaveInstanceState, saveState]:
// fragment life cycle
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser) {
// the fragment will be hidden - save it
if (!saveFragmentDone) {
saveState();
}
}
// memory
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));
}
// parent
super.onSaveInstanceState(outState);
// save fragment only if visible
if (isVisibleToUser) {
// perhaps the backup has already been made
if (!saveFragmentDone) {
saveState();
}
// restoration to be carried out in all cases
session.setAction(ISession.Action.RESTORE);
}
}
- righe 6-19: il frammento viene salvato se passa dallo stato visibile a quello nascosto (riga 11). Il metodo [setUserVisibleHint] fornisce questa informazione;
- riga 14: il salvataggio viene eseguito dal metodo privato nelle righe 21–23;
- righe 25–41: quando il dispositivo ruota, viene chiamato il metodo [onSaveInstanceState]. Il frammento viene salvato in due condizioni:
- è visibile (riga 34);
- non è stato ancora salvato (riga 36). È possibile che i metodi [setUserVisibleHint] e [onSaveInstanceState] non vengano entrambi eseguiti quando il frammento è visibile e che, pertanto, la gestione del booleano [saveFragmentDone] non sia necessaria. In caso di dubbio, ho scelto di utilizzarlo;
- riga 40: dopo il salvataggio viene il ripristino. Si noti che la prossima volta che il frammento dovrà aggiornarsi, lo farà tramite un'operazione [RESTORE];
Si notino i due momenti in cui viene richiesto il salvataggio di un frammento:
- quando passa dallo stato visibile a quello nascosto;
- quando il dispositivo ruota;
Il metodo privato [saveState] è il seguente:
...
private void saveState() {
// tasks to cancel?
if (numberOfRunningTasks != 0) {
// cancel tasks
cancelRunningTasks();
}
// save fragment state
CoreState currentState = saveFragment();
// the fragment has been visited
currentState.setHasBeenVisited(true);
// save menu status
currentState.setMenuOptionsState(getMenuOptionsStates());
// session setting
session.setCoreState(getNumView(), currentState);
// backup done
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- righe 4-7: La rotazione del dispositivo può verificarsi mentre sono in corso operazioni asincrone. Qui, si decide di annullarle tutte. Questa non è una buona decisione per l'utente, che dovrà effettuare una nuova richiesta, potenzialmente dispendiosa in termini di tempo, semplicemente perché ha spostato il proprio telefono o tablet o ha ricevuto una telefonata. È possibile mantenere le connessioni di rete attraverso un ciclo di salvataggio/ripristino. Tuttavia, le soluzioni non sono semplici e ho deciso di non trattarle in questo corso per principianti. La strada da seguire è quella di stabilire queste connessioni di rete tramite un frammento che non abbia un'interfaccia utente associata e che non venga distrutto durante il ciclo di salvataggio/ripristino. Per farlo, basta usare l'istruzione [Fragment.setRetainInstance(true)];
- riga 9: chiediamo al frammento figlio di salvare il proprio stato in un tipo derivato da [CoreState] (riga 31);
- riga 11: registriamo che il frammento è stato visitato. Questa informazione è utile. Quando un frammento viene visitato per la prima volta, il suo aggiornamento potrebbe differire da quelli successivi poiché non ha uno stato precedente nella sessione;
- riga 13: salviamo lo stato del menu, il che ci consentirà di ripristinarlo automaticamente;
- riga 15: lo stato attuale viene salvato nella sessione. Nella sessione, gli stati sono raggruppati per vista/frammento, ciascuno con uno stato. Il numero della vista è fornito dal frammento figlio (riga 33);
- riga 17: segnaliamo che il frammento è stato salvato. Questo perché due metodi potrebbero chiamare il metodo [saveState], e non è necessario eseguire due salvataggi;
La vista associata al frammento viene rigenerata dal seguente metodo:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the view must be restored
viewHasToBeInitialized = true;
}
Nel ciclo di vita, il metodo [onActivityCreated] viene eseguito immediatamente dopo il metodo [onCreateView]. La chiamata a quest'ultimo metodo indica che la vista associata al frammento deve essere ricostruita. Lo annotiamo semplicemente 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 attenda l'input dell'utente. Viene gestito dal seguente codice:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// update Fragment ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// log
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memory
this.menu = menu;
// retrieve # menu options if not already done
if (fragmentHasToBeInitialized) {
// retrieve the # menu options
getMenuOptionsStates(menu);
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// this is not the 1st visit
// step 2: does the fragment need to be initialized?
...
// step 3: should the view be initialized?
...
}
// step 4: a submit, a browse, a restore?
...
// step 5: terminal updates ----------------------
...
}
...
// classes filles -----------------------------------------------------
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: il metodo [onCreateOptionsMenu] viene utilizzato per aggiornare il frammento. Per questo motivo, il frammento deve avere un menu, anche se vuoto. Quando questo metodo viene eseguito, il frammento è stato associato alla sua vista e attività ed è anche visibile;
- riga 25: viene memorizzato il menu 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 memorizzati 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 type cast è necessario perché il metodo [mainActivity.getSession()] restituisce un tipo [ISession];
- riga 36: lo stato precedente del frammento viene recuperato dalla sessione. Se questa è la prima visita al frammento, è rilevante solo il valore booleano [previousState.hasBeenVisited];
- righe 39–44: codice eseguito quando questa è la prima visita al frammento. In questo caso, il suo stato precedente non è rilevante;
- righe 44–50: codice eseguito quando questa non è la prima visita al frammento;
- righe 46–47: codice eseguito se il costruttore del frammento è stato chiamato (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 corrente (SUBMIT, NAVIGATION, RESTORE);
- righe 54-55: codice sempre eseguito;
I cinque passaggi dell'aggiornamento sono i seguenti:
fase 1
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// fragment and view initialization
initFragment(null);
initView(null);
// raz previousState for the suite
previousState = null;
} else {
// this is not the 1st visit
...
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: alla classe figlia viene richiesto di inizializzare il frammento. Il parametro del metodo [initFragment] alla riga 35 è lo stato precedente del frammento. Qui viene passato null per indicare al frammento figlio che si tratta della prima visita;
- riga 28: alla classe figlia viene chiesto di inizializzare la vista associata al frammento. Il parametro del metodo [initView] alla riga 37 è lo stato precedente del frammento. Qui viene passato null per indicare al frammento figlio che si tratta della prima visita;
- riga 30: impostiamo lo stato precedente su null per i passaggi che seguono;
Passaggi 2 e 3
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// we retrieve the previous fragment state (the very 1st time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
...
} else {
// ce n'is not the 1st visit
// step 2: does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// girl fragment
initFragment(previousState);
}
// step 3: should the view be initialized?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// girl fragment
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- righe 24–42: eseguite quando questa non è la prima visita al frammento;
- righe 27–33: se il frammento è stato appena ricostruito, viene reinizializzato chiamando il metodo [initFragment] della classe figlia (righe 32, 46). Lo stato precedente del frammento viene passato a quest'ultimo;
- righe 35–51: se la vista associata al frammento deve essere inizializzata o reimpostata, viene chiesto al frammento figlio di farlo (righe 40, 48). Anche in questo caso, gli viene passato l'ultimo stato conosciuto del frammento;
Fase 4
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
...
// step 4: a submit, a browse, a restore?
// 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();
}
}
// action in progress
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// girl fragment
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// catering menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
} else {
// 1st visit - nothing to do
}
break;
case RESTORE:
// restoration
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// menu restoration (previousState cannot be null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- righe 34–66: elaboriamo l'azione corrente, che può essere una delle tre seguenti:
- RESTORE: stiamo ripristinando il frammento dopo una rotazione del dispositivo;
- NAVIGATION: stiamo tornando al frammento, con l'intenzione di trovarlo nello stato in cui l'abbiamo lasciato l'ultima volta che l'abbiamo utilizzato;
- SUBMIT: tutti gli altri casi;
- riga 34: recuperiamo l'azione corrente;
- righe 36–42: per un'azione di tipo SUBMIT, chiamiamo il metodo [updateOnSubmit] del frammento figlio (righe 41, 68), passandole l'ultimo stato conosciuto del frammento;
- righe 43–55: per un'azione di tipo NAVIGATION;
- righe 47–54: vogliamo ripristinare il frammento al suo ultimo stato conosciuto. L'operazione NAVIGATION può coincidere con una prima visita. Questo sarebbe il caso, ad esempio, in un'applicazione a schede: se passo dalla scheda 1 alla scheda 4:
- devo inizializzare il frammento per la scheda 4 se questa è la prima visita;
- ripristinare il frammento della scheda 4 al suo stato precedente se non è la prima visita;
- righe 52–54: non fare nulla se si tratta della prima visita. Il metodo figlio [initView(CoreState previousState)] gestirà questa inizializzazione. La prima visita è identificata dalla condizione [previousState == null];
- riga 49: se questa non è la prima visita al frammento, ripristina il suo menu;
- riga 51: chiediamo alla classe figlia di aggiornarsi chiamando il metodo alla riga 70. Le passiamo lo stato precedente del frammento in modo che possa svolgere il suo compito;
- righe 56–66: nel caso di un'operazione di ripristino del frammento, facciamo la stessa cosa che nel caso della navigazione al di fuori della prima visita;
Passaggio 5
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// step 5: terminal updates ----------------------
// we've changed our view
session.setPreviousView(getNumView());
// more action in progress
session.setAction(ISession.Action.NONE);
// when leaving this fragment, it must be saved
saveFragmentDone = false;
// as long as the fragment has not been rebuilt, it does not need to be initialized
fragmentHasToBeInitialized = false;
// as long as the view has not been rebuilt, it does not need to be initialized
viewHasToBeInitialized = false;
// returns to normal tab selection operation
session.setNavigationOnTabSelectionNeeded(true);
// the fragment is notified that the view is ready
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- righe 18–30: quando si raggiunge questo punto, il frammento è stato inizializzato ed è pronto per essere visualizzato. Si reimpostano quindi tutti gli indicatori utilizzati nella gestione del ciclo di vita del frammento al loro stato iniziale;
- riga 20: la vista è cambiata; questo viene annotato nella sessione;
- riga 22: non ci sono più azioni in corso;
- riga 24: quando usciamo dal frammento attualmente visualizzato, dovremo salvarlo all'uscita;
- riga 26: il frammento non deve più essere ricostruito. Questo flag verrà impostato su true quando il costruttore del frammento verrà eseguito nuovamente;
- riga 28: la vista associata al frammento non deve più essere inizializzata. Questo flag verrà impostato nuovamente su true quando il metodo [onActivityCreated] verrà eseguito di nuovo;
- riga 30: il frammento può essere visualizzato in un'applicazione a schede. In questo caso, quando l'utente fa clic su una delle schede, deve verificarsi un cambio di frammento;
- riga 36: la classe figlia viene informata che il frammento è pronto. Può utilizzare il metodo [notifyEndOfUpdates] per eseguire aggiornamenti che dovrebbero essere effettuati in ogni caso, avviare un'operazione asincrona per recuperare nuovi dati, ecc.
2.7.4. Un esempio di frammento
![]() |
Abbiamo incluso un esempio di frammento nel progetto [client-android-skel] per mostrare al lettore la struttura tipica di un frammento in 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 {
// fields inherited from parent class -------------------------------------------------------
// debug mode
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
//-- protected String className;
// asynchronous tasks
//-- protected int numberOfRunningTasks;
// activity
//-- protected IMainActivity mainActivity;
//-- protected Activity activity;
// session
//-- protected Session session;
// methods inherited from the parent class -------------------------------------------------------
// display menu options
//-- protected void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// management of waiting for the end of a series of asynchronous tasks
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// asynchronous task execution with RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// cancel tasks
//-- protected void cancelRunningTasks() {
// exception alert display
//-- protected void showAlert(Throwable th) {
// message list display
//-- protected void showAlert(List<String> messages) {
// methods imposed by the parent class -------------------------------------------------------
@Override
public CoreState saveFragment() {
// save the fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
@Override
protected int getNumView() {
// return the fragment number in the table of fragments managed by the activity (cf MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// the fragment becomes visible and has undergone construction in this or a previous stage
// this happens on application startup and every time the Android device is rotated
// is necessarily followed by the execution of [initView]
// the fields of the fragment that has been rebuilt must be initialized
// previousState is the fragment's last save - is null if this is the fragment's 1st visit
}
@Override
protected void initView(CoreState previousState) {
// the fragment becomes visible and the associated view has been reconstructed in this or a previous step
// this happens every time [initFragment] is executed and every time the fragment leaves the adjacency of the displayed fragment
// initialize the components of the view that has been rebuilt
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type SUBMIT
// the fragment and the associated view usually have to be initialized from the session
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
// there's nothing to be done if the fragment can't be reached by a SUBMIT operation
// if the fragment can be reached by SUBMIT operations from different fragments, the previous view can be known by [session.getPreviousView]
// if the fragment can be reached by several SUBMIT operations from the same fragment, then a flag must be set to differentiate between the different types of SUBMIT from this fragment
}
@Override
protected void updateOnRestore(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type RESTORE or NAVIGATION
// previousState is the fragment's last backup - never null
// restore the view to its previous state
}
@Override
protected void notifyEndOfUpdates() {
// comes after methods [updateOnSubmit, updateOnRestore]
// when we're there, the view has been built and initialized
// there's often nothing to do here, but you can also factor in actions that need to be done no matter how you arrive at this view
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called when asynchronous tasks launched by the fragment are either completed or cancelled
// these two cases can be differentiated using parameter runningTasksHaveBeenCanceled
// the view generally needs to be reset to a different state from the one it had while waiting for responses from asynchronous tasks
}
}
La classe [DummyFragment] potrebbe non avere uno stato. Qui ne abbiamo incluso uno per ricordarci cosa ci si aspetta al suo interno:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// fragment status [DummyFragment]
// set only serializable fields to jSON
// put the annotation @JsonIgnore on the others, but it's hard to see what use they could be
// don't forget the getters/setters - they are used for serialization/deserialization
}
Per illustrare l'uso del progetto [client-android-skel], utilizzeremo prima alcuni semplici esempi prima di passare a un caso di studio più completo.
2.8. Esercizi illustrativi
Inizieremo rifattorizzando esempi già scritti.
2.8.1. Esempio-17B
Riprenderemo l'Esempio 17 della Sezione 1.18. Si tratta di un'app con un unico frammento, senza attività asincrone e senza schede. La esamineremo per vedere come si comporta quando il dispositivo viene ruotato. Inseriremo quanto segue:

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

Se confrontiamo le visualizzazioni, tutto è rimasto invariato tranne l'elenco [2], che ora è vuoto.
Inoltre, se si fa clic sul pulsante [Invia], appare una finestra di dialogo che mostra i dati inseriti nel modulo. Se in quel momento si ruota il dispositivo, la finestra di dialogo scompare.
Pertanto, durante una rotazione, dovremo rigenerare:
- l'elenco a discesa e la voce selezionata;
- la finestra di dialogo, se era visualizzata durante la rotazione;
2.8.1.1. Il progetto [Example-17B]
Duplichiamo il progetto [client-android-skel] in examples/Example-17B. Quindi carichiamo il nuovo progetto [1]:
![]() | ![]() | ![]() |
- in [2-3], nella cartella [behavior], incolliamo il frammento [Vue1Fragment] dal progetto [Example-17];
![]() | ![]() | ![]() |
- in [4-5], nella cartella [layout] di [Example-17B], incolliamo la vista [vue1.xml] da [Example-17]. Questa è la vista associata al frammento;
- in [6], la cartella [values] di [Esempio-17B] viene sostituita dalla cartella [values] di [Esempio-17];
Modificheremo il margine superiore della vista [vue1.xml] impostandolo 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, possiamo provare a eseguire una compilazione iniziale per verificare la presenza di errori. I primi errori segnalati derivano dalle importazioni di pacchetti che sono state spostate. Li correggiamo (Ctrl-Shift-O). Altri errori, come , sorgono perché la vista [Vue1Fragment] non implementa tutti i metodi richiesti dalla sua classe padre [AbstractParent]:

Generiamo i metodi mancanti (Alt-Invio).
Un altro errore di compilazione segnalato è il seguente:

Risolviamo questo problema nel file [build.gradle] del modulo (riga 20 di seguito):
![]() |
A questo punto, possiamo ricompilare per vedere gli errori rimanenti. L'unico errore segnalato riguarda il metodo [Vue1Fragment.updateFragment]:
![]() |
È necessario rimuovere l'annotazione [@Override] dalla riga 135. Ora non ci sono più errori. Useremo questo come punto di partenza per modificare il progetto.
2.8.1.2. Lo stato del frammento [Vue1Fragment]
Il frammento [Vue1Fragment] deve salvare le informazioni quando il dispositivo ruota, in modo da poterle ripristinare completamente. A tal fine creiamo una classe [Vue1FragmentState]:
![]() |
Per ora, 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
![]() |
La cartella [custom] contiene elementi dell'architettura che possono essere personalizzati dallo sviluppatore.
Le costanti per l'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 {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- righe 24–31: L'applicazione non utilizza il suo livello [DAO] in questo punto. Queste costanti non verranno utilizzate;
- riga 34: un'adiacenza dei frammenti pari a 1, che è il valore predefinito. Poiché l'applicazione ha un solo frammento (riga 43), questo valore è irrilevante;
- righe 39-40: poiché non ci sono operazioni con il livello [DAO], non c'è bisogno di un'immagine segnaposto;
- riga 37: questa non è 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 {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
È vuoto. Infatti, poiché c'è un solo frammento, non è necessario prevedere la comunicazione tra frammenti tramite 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 {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Righe 11–13: Dobbiamo elencare tutte le classi derivate da [CoreState] che memorizzano lo stato dei vari frammenti. In questo caso, ce n'è solo una (riga 12);
2.8.1.4. La [MainActivity]
L'attività [MainActivity] attualmente si presenta così:
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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed navigation - define the view to be displayed when tab no. [position] is selected
}
@Override
protected int getFirstView() {
// todo: define the number of the first view (fragment) to be displayed
return 0;
}
}
I commenti [//todo] indicano ciò che lo sviluppatore deve fare. 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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
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;
}
}
È necessario modificare solo il metodo alle righe 41–44. Deve restituire l'array dei frammenti dell'app. Alla riga 43, non dimenticare di aggiungere il trattino basso dopo il nome del frammento.
2.8.1.5. Lo stato del frammento [FragmentState]
A seguito dei test di rotazione eseguiti sul progetto [Esempio-17], decidiamo di memorizzare i seguenti elementi del frammento:
- l'elenco dei valori presenti nel menu a tendina;
- la posizione della voce selezionata in questo 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 {
// drop-down list values
private List<String> list;
// the item selected from the drop-down list
private int listSelectedPosition;
// the message displayed in the
private String message;
// getters and setters
...
}
2.8.1.6. Il frammento [AbstractFragment]
Attualmente, il ciclo di vita del frammento è gestito da due metodi (righe 6 e 32):
// drop-down list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// on seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// the drop-down list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
Il codice relativo a questi due metodi verrà spostato nei metodi definiti dalla classe [AbstractFragment] come segue:
// fragment lifecycle management ---------------------------------------------------------------------
@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) {
// 1st visit?
if (previousState == null) {
// create drop-down list values
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// returns values from the drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// and the
message = state.getMessage();
}
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// the calendar
datePicker1.setCalendarViewShown(false);
// on seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// initialize drop-down list adapter
dropDownList.setAdapter(dataAdapter);
// 1st visit?
if (previousState == null) {
// check the first button
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// seekbar value
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// item selected from drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// visible dialogue?
if (message != null) {
// we display it
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- righe 2–9: il metodo [saveFragment] deve inserire gli elementi del frammento da salvare in una classe derivata da [CoreState] e restituire un'istanza di quella classe;
- righe 11–14: il metodo [getNumView] deve restituire il numero del frammento. Qui 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] è nullo, allora questa è la prima visita;
- righe 19–25: alla prima visita, vengono creati i valori per l'elenco a discesa;
- righe 26–30: se questa non è la prima visita, i campi [list, message] del frammento vengono ripristinati dallo stato precedente;
- righe 33-34: inizializzazione del campo [dataAdapter] del frammento. Questa è l'origine dati per l'elenco a discesa;
- righe 37–62: il metodo [initView] viene utilizzato per inizializzare i componenti dell'interfaccia visiva. Riceve come parametro lo stato precedente [previousState]. Se [previousState == null], significa che si tratta della prima visita;
- Qui vediamo ciò che era precedentemente nel metodo [@AfterViews];
- righe 57–61: alla prima visita, ci assicuriamo che il primo pulsante di opzione sia selezionato;
- righe 64–67: il metodo [updateOnSubmit] viene eseguito quando l'azione corrente è [SUBMIT]. Qui non c'è navigazione tra i frammenti e quindi nessuna azione corrente;
- righe 69–81: il metodo [updateOnRestore] viene eseguito quando l'azione corrente è [NAVIGATION] o [RESTORE]. Qui non c'è navigazione tra i frammenti e quindi nessuna azione [NAVIGATION] possibile;
- riga 72: ricalcoliamo (non ripristiniamo) il valore di TextView seekBarValue. Questo perché, durante le rotazioni, il suo valore a volte andava perso;
- righe 74–75: l'elenco viene posizionato sull'elemento selezionato prima della rotazione. Senza questo, l'elenco tornerebbe di default al suo primo elemento;
- righe 76-80: la finestra di dialogo viene visualizzata nuovamente se il messaggio dallo stato precedente non è nullo. Torneremo al metodo [showMessage] (riga 79);
- righe 83–86: il metodo [notifyEndOfUpdates] è l'ultimo metodo chiamato dalla classe padre prima di lasciare il frammento figlio da solo. Qui non c'è nulla da fare;
- righe 88–91: il metodo [notifyEndOfTasks] segnala la fine delle attività asincrone avviate dal frammento. Qui non ce ne sono;
La finestra di dialogo viene ripristinata come segue:
// dialog box message
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// list of messages to display
List<String> messages = new ArrayList<>();
...
// display
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// poster text is created
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// the message is memorized
message = texte.toString();
// we display it
showMessage();
}
private void showMessage() {
// we display it
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// message reset
message = null;
}
}).show();
}
Quando l'utente invia 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 dalla finestra di dialogo ed è lo stesso messaggio visualizzato dal metodo [updateOnRestore];
- riga 27: il secondo parametro del metodo [setNeutralButton] è il metodo eseguito quando l'utente fa clic sul pulsante [Chiudi] nella finestra di dialogo;
- riga 31: quando la finestra di dialogo si chiude, il messaggio viene impostato su null per indicare che la finestra di dialogo non è più presente;
2.8.1.7. Test
I lettori sono invitati a testare questo progetto e a verificare che il frammento sia conservato dopo una o più rotazioni successive.
2.8.2. Esempio-23: Client meteo
Alcuni siti web forniscono informazioni meteorologiche sotto forma di stringhe JSON. Ecco un esempio:

L'URL ha il formato: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID} dove:
- città: la città per la quale si desidera conoscere le condizioni meteorologiche, in questo caso Angers;
- country: il paese della città, in questo caso 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 realizzato sulla base del progetto [client-android-skel]. Presenta le seguenti caratteristiche:
- ha un solo frammento il cui stato non deve essere mantenuto;
- 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 {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Righe 25, 28, 31, 40: caratteristiche del livello [DAO]. Riga 31: l'autenticazione di base non è richiesta;
- riga 34: adiacenza dei frammenti. In questo caso, questa costante è irrilevante poiché c'è un solo frammento;
- riga 37: questa non è 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)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- righe 10–13: non c'è nulla da dichiarare poiché questa applicazione ha un solo frammento il cui stato non viene salvato;
La classe [Session] è la seguente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
È vuoto perché in questa applicazione non vi è alcuna comunicazione tra frammenti.
2.8.2.3. Il livello [DAO]
![]() |
Nel livello [DAO], è necessario personalizzare 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);
// weather service
@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 meteo. Si noti che questo è relativo all'URL radice del client (RestClientRootUrl, riga 12). In questo caso, l'URL radice sarà [http://api.openweathermap.org/];
L'interfaccia [IDao] sarà la seguente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// weather service
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Si noti che i metodi nelle righe 6–22 sono inclusi per impostazione predefinita nell'interfaccia IDao del progetto [client-android-skel];
- Riga 25: Il metodo [getWeatherForecast] recupera la stringa JSON relativa al meteo nella città [city] del paese [country]. Il terzo parametro è la chiave ottenuta dal sito web [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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
// timeout
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
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));
}
// memory
this.timeout = timeout;
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// service météo ---------------------------------------------------------
@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));
}
// result
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Si noti che le righe 17–90 sono incluse di default nella classe [Dao] del progetto [client-android-skel]. È sufficiente aggiungere i metodi di implementazione per l'interfaccia [IDao], specifici per l'applicazione (riga 92);
- righe 93–105: implementazione del metodo [getWeatherForecast]. È molto semplice e occupa 6 righe, le righe 100–105;
- riga 100: il metodo [getResponse] è un metodo della classe padre [AbstractDao]. Si aspetta un parametro di tipo [IRequest<T>], dove T è il tipo della risposta prevista dal server; in questo caso, è una String poiché ci aspettiamo 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 il metodo [Observable<T> getWeatherForecast] deve restituire;
- riga 103: è l'interfaccia [WebClient] che fornisce questa risposta. Le passiamo i tre parametri ricevuti alla riga 94. Per questo motivo, questi devono avere l'attributo final;
2.8.2.4. La [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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@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;
}
// interface IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- Si noti che le righe 15–55 sono incluse di default nel progetto [client-android-skel]. È sufficiente personalizzarle;
- Righe 37–40: l'array dei frammenti. Qui ce n'è solo uno;
- Righe 43–46: non sono richiesti 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] alla riga 21;
2.8.2.5. Il frammento [MeteoFragment]
![]() |
Il [MeteoFragment] interroga il servizio web meteo / JSON. 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 dalla 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 per richiedere le previsioni del tempo per 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 {
// local data
private int nbReponsesRecues;
// gestion des événements ---------------------------------------------------------------------------------------
// cities whose weather we want
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// his country
String country = "fr";
// get a API login by creating an account [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL web service / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// start waiting for [paysDeLoire.length] asynchronous tasks
beginWaiting(paysDeLoire.length);
// number of responses received
nbReponsesRecues = 0;
// asynchronous calls are made in parallel
for (String city : paysDeLoire) {
// weather
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// exploiting the answer
consumeResponse(response);
// a + response
nbReponsesRecues++;
}
});
}
}
// exploitation server response
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// start of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// parent
beginRunningTasks(numberOfRunningTasks);
// the [Cancel] option is displayed
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();
// displaying results
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();
}
// private methods -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// life cycle management ---------------------------------------------------------------------------------------
...
}
- righe 25-50: gestione del clic sull'opzione di menu [Meteo];
- riga 32: costruzione dell'URL del servizio web / JSON per il servizio meteo. Questo viene poi passato al livello [DAO] tramite l'attività;
- riga 34: iniziamo l'attesa. Passiamo il numero di attività da avviare in modo che la classe padre possa avvisarci quando sono completate. Qui ci sono cinque attività perché stiamo richiedendo il meteo per le cinque città elencate alla riga 23;
- riga 16: contiamo il numero di risposte ricevute in modo da poterlo visualizzare;
- righe 38–50: eseguiamo un ciclo sulle città per le quali vogliamo le previsioni meteo;
- riga 40: effettueremo 5 richieste HTTP in parallelo;
- riga 40: chiediamo alla classe padre [AbstractParent] di interrogare il servizio web / JSON;
- righe 40–48: il metodo [executeInBackground] richiede due parametri:
- riga 40: il processo da osservare ed eseguire è fornito dal metodo [mainActivity.getWeatherForecast];
- righe 40–48: l'istanza [Action1] da eseguire quando viene ricevuta 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. Viene passata al metodo [consumeResponse] alla riga 53;
- riga 46: il contatore delle risposte ricevute viene incrementato;
- righe 53–56: consumo di una risposta JSON dal servizio meteo;
- riga 55: registriamo semplicemente la stringa JSON;
- righe 59–72: codice eseguito prima di avviare le attività asincrone;
- riga 65: passiamo il numero di attività da eseguire alla classe padre [AbstractParent]. Questo le permette di avvisarci quando sono tutte terminate;
- righe 67–70: preparazione del menu per l'attesa. Manteniamo solo l'opzione [Actions/Cancel], che consentirà all'utente di annullare le attività avviate;
- righe 74–92: codice eseguito quando la classe padre ci notifica che tutte le attività avviate sono state completate;
- riga 77: riportiamo il menu allo stato iniziale. Il metodo [initMenu] (righe 95-102) visualizza il menu con tutte le sue opzioni tranne l'opzione [Actions/Cancel], che rimane nascosta;
- righe 80–91: viene visualizzato il numero di risposte ricevute;
Il clic sull'opzione di menu [Annulla] viene gestito dal seguente codice:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- riga 7: chiediamo alla classe padre di annullare le attività ancora attive;
Il clic sull'opzione di menu [Fine] viene gestito dal seguente codice:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// we stop everything
System.exit(0);
}
Il ciclo di vita del frammento è gestito dai seguenti metodi:
// life cycle management ---------------------------------------------------------------------------------------
@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) {
// 1st visit?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- righe 3-6: utilizzate per memorizzare lo stato del frammento in una classe derivata da [CoreState]. Se il frammento non ha uno stato da memorizzare, come in questo caso, restituiamo semplicemente un'istanza di [CoreState]. Non restituire null, poiché ciò causerebbe un crash;
- righe 8-11: devono restituire l'ID della vista. Qui, [MeteoFragment] ha ID 0;
- righe 13–16: utilizzate per 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:
// villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
ma si inizializza da solo;
- righe 18–24: vengono utilizzate per inizializzare la vista associata al frammento una volta che è stato costruito (previousState == null) o ricostruito (previousState != null);
- righe 21-23: se questa è la prima visita al frammento, il suo menu viene inizializzato per nascondere l'opzione [Annulla];
- righe 27–30: chiamate se la navigazione verso il frammento ha comportato un'azione [INVIA]. In questo caso, non c'è navigazione tra frammenti poiché c'è un solo frammento;
- righe 32-35: chiamate durante un ciclo di salvataggio/ripristino dovuto alla rotazione del dispositivo o ad altro motivo. Qui, 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. Test
Ora eseguiamo l'esempio:


I log sono 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 inviamo la richiesta utilizzando un ID API errato:
String APIID = "";

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 HTTP hanno generato 5 eccezioni;
- riga 7: il frammento [MeteoFragment] riceve la prima eccezione. A quel punto annullerà tutte le attività;
Ora impostiamo un timeout di 5 secondi [IMainActivity.DELAY] e annulliamo l'operazione. I log risultano 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é si è verificata una cancellazione;
- righe 6–10: l'annullamento delle attività causa un'eccezione su ciascuno dei cinque thread delle attività. Il tipo di eccezione dipende dalle applicazioni. L'eccezione in questo caso è [java.lang.InterruptedException] perché le attività sono state interrotte durante l'esecuzione dell'istruzione [Thread.sleep(delay)], che le induce ad attendere artificialmente per [delay] millisecondi;
2.8.3. Esempio 16B
Qui rifattoriamo l'Esempio 16 della Sezione 1.17. Presenta un frammento che effettua chiamate asincrone a un server di numeri casuali. Vediamo come si comporta durante la rotazione del dispositivo:

- In [1], il dispositivo viene ruotato due volte;

Possiamo notare che abbiamo perso tutti i messaggi di errore. Cercheremo di migliorare questo aspetto.
2.8.3.1. Il progetto Example-16B
Copiamo il progetto [client-android-skel] nel progetto [examples/Example-16B], quindi carichiamo il nuovo progetto:
![]() |
Dal progetto iniziale [Esempio-16], copiamo i seguenti elementi in [Esempio-16B]:
- 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 [View1Fragment]:
![]() |
- la classe [DAO / servizio / Risposta]:
![]() |
A questo punto, possiamo provare a eseguire una prima compilazione:
- Il primo tipo di errore riguarda le importazioni. Alcune classi sono state spostate in pacchetti diversi durante la migrazione a [Example-16B]. Iniziamo correggendo questi errori;
- Viene segnalato un secondo tipo di errore sulla classe [Vue1Fragment] poiché non implementa i metodi richiesti dalla classe padre [AbstractParent]. Generiamo automaticamente questi metodi;
Proviamo a eseguire una seconda compilazione:
- tutti gli errori rimanenti sono ora concentrati nella 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 allo stato precedente alla rotazione. Creiamo quindi uno stato [Vue1FragmentState], che per ora è 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 {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- righe 25, 28, 31, 40: caratteristiche del livello [DAO]. L'autenticazione di base non è richiesta;
- riga 34: adiacenza dei frammenti. In questo caso, questa costante è irrilevante poiché c'è un solo frammento;
- riga 37: questa non è 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 {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Riga 12: dichiariamo la classe di 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 {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
È vuoto perché in questa applicazione non vi è alcuna comunicazione tra frammenti.
2.8.3.4. Il livello [DAO]
![]() |
Nel livello [DAO], è necessario personalizzare 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 [Example-16], che la utilizza:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
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 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- Righe 18–19: L'URL del servizio di generazione di numeri casuali. Si noti che questo URL è relativo all'URL radice del client (RestClientRootUrl, riga 12). In questo caso, l'URL radice è [http://localhost:8080];
L'interfaccia [IDao] sarà la seguente:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// random number service
Observable<Response<Integer>> getAlea(int a, int b);
}
- Si noti che i metodi nelle righe 6–22 sono presenti per impostazione predefinita nell'interfaccia IDao del progetto [client-android-skel];
- Riga 25: Il metodo [getAlea] restituisce un numero casuale nell'intervallo [a,b]. Questo numero viene restituito in una risposta [Response<Integer>], dove il numero casuale è contenuto nel campo [body] di quel 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 {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
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));
}
// factory configuration
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));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// random number service
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// web client execution
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- Si noti che le righe 17–85 sono incluse di default nella classe [Dao] del progetto [client-android-skel]. È sufficiente aggiungere i metodi per implementare l'interfaccia [IDao];
- righe 88–97: implementazione del metodo [getAlea]. È molto semplice e occupa 6 righe, le righe 91–96;
- riga 91: il metodo [getResponse] è un metodo della classe padre [AbstractDao]. Si aspetta 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 essere il 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 il metodo [Observable<T> getAlea] deve restituire;
- riga 94: è l'interfaccia [WebClient] che fornisce questa risposta. Le vengono passati i due parametri ricevuti alla riga 89. Per questo motivo, questi devono avere l'attributo final;
2.8.3.5. La [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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// define fragments here
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Si noti che le righe 15–61 sono incluse di default nel progetto [client-android-skel]. È sufficiente personalizzarle;
- righe 40–44: l'array 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] alla 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 {
// fragment status ------------------------
// list of answers
private List<String> reponses = new ArrayList<>();
// condition view ------------------------
// error msg on the number of random numbers requested
private boolean txtErrorAleasVisible = false;
// error msg on generation interval [a,b]
private boolean txtErrorIntervalleVisible = false;
// error msg on the URL of the web service
private boolean txtMsgErreurUrlServiceWebVisible = false;
// waiting time error msg
private boolean textViewErreurDelayVisible = false;
// whether or not the Execute button is visible
private boolean btnExecuterVisible = true;
// getters and setters
...
}
Per determinare cosa fosse necessario salvare nel frammento, abbiamo ruotato il dispositivo in varie situazioni e osservato cosa andava perso al ripristino. Abbiamo concluso che fosse necessario salvare le informazioni contenute nelle righe 10–23.
2.8.3.7. Il frammento [View1Fragment]
![]() |
Attualmente, la vista [Vue1Fragment] contiene diversi errori dovuti al fatto che la classe padre [AbstractFragment] da cui deriva è stata modificata. Anziché descrivere una per una le modifiche da apportare, commenteremo direttamente la versione finale.
Lo scheletro del frammento è il 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 {
...
}
- Riga 26: Si noti che ogni frammento deve avere un menu, anche se è vuoto. È il caso qui.
2.8.3.7.1. Gestione del clic sul pulsante [Execute]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// check the data entered
if (!isPageValid()) {
return;
}
// delete previous answers
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// reset the response counter to 0
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepare the random task
beginWaiting(1);
// we ask for the random numbers
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// create the process to be observed
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// we ask for the random numbers
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// we consume the answer
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();
}
}
// a + response
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// we analyze the response
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// cancellation
doAnnuler();
// back to Ui
return;
}
// we add the information to the list of answers
reponses.add(0, String.valueOf(response.getBody()));
// refreshing the answers
dataAdapterReponses.notifyDataSetChanged();
}
// cancellation ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// we set the hourglass
beginRunningTasks(nbRunningTasks);
// the [Cancel] button replaces the [Execute] button
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- righe 4-6: Innanzitutto, verifichiamo che le voci siano valide. Potrebbero quindi apparire dei messaggi di errore;
- righe 8-9: l'elenco delle risposte viene cancellato. Questa modifica si riflette nella ListView che le visualizza;
- righe 11-12: il numero di risposte ricevute viene azzerato;
- riga 14: Impostiamo l'URL per il servizio di numeri casuali. Questa informazione verrà passata al livello [DAO];
- riga 15: viene impostato il periodo di timeout prima dell'invio della richiesta al servizio di numeri casuali. Questa informazione verrà trasmessa al livello [DAO];
- riga 17: ci prepariamo a lanciare 1 attività asincrona (non N; vedremo perché);
- righe 24–27: Combiniamo le N attività asincrone in un'unica sequenza di operazioni [merge];
- righe 29–36: chiediamo alla classe padre [AbstractParent] di interrogare il servizio web dei numeri casuali / JSON;
- 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 utilizzata nel metodo alla riga 39;
- righe 49–50: registriamo e segnaliamo che è stata ricevuta una nuova risposta;
- righe 53–60: il tipo [Response<T>] ha un campo [status] che è 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 alle righe 68–75. Esso annullerà tutte le attività ancora attive (riga 74);
- riga 62: la risposta viene aggiunta all'elenco delle risposte, che costituisce l'origine dati per ListView;
- riga 64: la ListView viene aggiornata;
- righe 77–83: il metodo [beginWaiting(int nbRunningTasks)] prepara la vista all'attesa (righe 81–82) e notifica alla classe padre che [nbRunningTasks] attività saranno presto eseguite (riga 79);
2.8.3.7.2. Il ciclo di vita del frammento
Il ciclo di vita del frammento è gestito dai seguenti metodi:
// local data
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// life cycle management ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// current view status
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) {
// 1st visit?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// listView data source
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// nre of responses
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter link
listReponses.setAdapter(dataAdapterReponses);
// 1st visit?
if (previousState == null) {
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// buttons
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// previous view status
Vue1FragmentState state = (Vue1FragmentState) previousState;
// show / hide error msg
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);
// buttons
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// no. of responses
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// the [Execute] button replaces the [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- righe 7–18: assicurano che il frammento venga salvato quando la classe padre lo richiede;
- riga 11: visualizza il messaggio di errore relativo al timeout;
- riga 12: visualizza il messaggio di errore relativo al numero di numeri casuali richiesti;
- riga 13: visualizza il messaggio di errore relativo all'URL del servizio web / JSON;
- riga 14: visualizza il messaggio di errore relativo all'intervallo [a,b] per la generazione di numeri casuali;
- riga 15: visibilità del pulsante [Esegui];
- riga 16: l'elenco delle risposte ricevute;
- righe 20–23: devono restituire l'ID della vista. L'ID del frammento qui è 0 poiché ce n'è solo uno;
- righe 25–38: inizializzazione dei campi del frammento, sia alla prima visita (previousState == null) che a una visita successiva;
- righe 29–30: se questa non è la prima visita, il campo [reponses] viene ripristinato dallo stato precedente del frammento;
- righe 31–33: se questa è la prima visita, il campo [reponses] viene inizializzato con un elenco vuoto;
- righe 34-37: utilizzando il campo [reponses], possiamo costruire l'origine dati per la 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 a una visita successiva;
- riga 43: la ListView del frammento viene associata alla fonte dati appena costruita nel metodo [initFragment];
- righe 45–54: se questa è la prima visita, la vista viene preparata per la sua prima visualizzazione;
- righe 57–60: eseguite durante la navigazione tra frammenti associata a un'azione [SUBMIT]. Qui c'è un solo frammento e quindi non c'è navigazione tra frammenti;
- righe 63–76: eseguite durante la navigazione tra frammenti associata a un'azione [NAVIGATION] o durante un ciclo di salvataggio/ripristino dovuto alla rotazione del dispositivo o ad altro motivo. Qui può verificarsi solo il secondo caso. Ricordate che qui, in tutti i casi, [previousState] è sempre diverso da null;
- riga 65: lo stato precedente viene convertito nel tipo di stato del frammento;
- righe 66–75: il contenuto dello stato precedente viene utilizzato per ripristinare la vista;
- righe 78–81: chiamate quando tutti gli aggiornamenti precedenti sono stati completati. Qui non c'è nulla da fare;
- righe 83–89: eseguite quando tutte le attività asincrone sono state completate. Qui, il pulsante [Cancel] viene nascosto e sostituito con il pulsante [Execute];
2.8.3.8. Test
Il lettore è invitato a eseguire i seguenti test:
- creare degli errori ed eseguire il dispositivo: i messaggi di errore devono rimanere visualizzati;
- Generare numeri casuali ed eseguire il dispositivo: i numeri casuali generati devono rimanere visualizzati;
- impostare un'attesa di alcuni secondi ed eseguire il dispositivo durante l'attesa: le attività devono essere state annullate (questo è visibile nei log);
2.8.4. Esempio-22B
Qui riprendiamo l'Esempio 22 per rifattorizzarlo secondo il modello di progetto [client-android-skel]. Ricordiamo che il progetto [Esempio-22] gestisce correttamente il ciclo di salvataggio/ripristino dei frammenti durante la rotazione e che è servito come base per il progetto [client-android-skel].
Duplichiamo il progetto [client-android-skel] in [examples/Example-22B] e carichiamo quest'ultimo progetto:
![]() |
Quindi copiamo vari elementi dal progetto [Esempio-22] nel progetto [Esempio-22B].
Per prima cosa, copiamo gli elementi dalla cartella [res]:
- [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, la cartella [values];
![]() |
Modificheremo il margine superiore di entrambe le viste impostandolo a 120 dp:
[view1.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 [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
A questo punto, possiamo provare a eseguire una prima compilazione. Compare un primo tipo di errore: importazioni errate perché le classi hanno cambiato pacchetto. Correggiamo queste importazioni. Un secondo tipo di errore è dovuto al fatto che i frammenti non implementano tutti i metodi della loro classe padre [AbstractFragment]. Correggiamo questo errore premendo (Alt+Invio).
Gli errori rimanenti derivano dalle differenze tra la vecchia e la nuova classe [AbstractFragment]. Per ora, li ignoriamo.
2.8.4.1. Personalizzazione del progetto
![]() |
La cartella [custom] contiene elementi dell'architettura che possono essere personalizzati dallo sviluppatore.
L'interfaccia [IMainActivity] consente di specificare determinate 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 {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = true;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of fragments
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 un valore compreso tra [1,4] qui. Si invita il lettore a variare questo valore per verificare se l'applicazione continua a funzionare;
- riga 35: questa è 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 {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Riga 12: dichiariamo la classe di stato del frammento [PlaceHolderFragment]. Il frammento [Vue1Fragment] in sé non ha uno stato;
La classe [Session] è la seguente:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment = -1;
// getters and setters
...
}
Questa è la sessione relativa al progetto [Esempio-22].
2.8.4.2. La [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 {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// menu management-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// implementation of parent class methods ---------------------------------------------------
...
}
Qui, la classe [MainActivity] è più grande rispetto agli 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
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// fragment no
final String ARG_SECTION_NUMBER = "section_number";
// initialization of fragment table
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[i] = new Vue1Fragment_();
// result
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// no titles here
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: viene recuperato un riferimento locale alla sessione. Il type cast è necessario perché la sessione della classe padre è di tipo [AbstractSession];
- righe 19–38: il metodo [getFragments] deve restituire alla classe padre l'array di frammenti gestiti dall'applicazione. Qui ci sono [FRAGMENTS_COUNT] frammenti, un 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 questa informazione è utile. Qui non è il caso;
- righe 47–50: questo metodo viene chiamato dalla classe padre quando l'utente fa clic su una scheda. Torneremo su questo argomento nella sezione successiva;
- righe 52–55: restituisce il numero della prima vista da visualizzare all'avvio dell'applicazione. Qui, il frammento [Vue1Fragment] 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 vengono gestite utilizzando i seguenti metodi:
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2nd tab ?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// fragment number to display
int numFragment;
switch (position) {
case 0:
// fragment no. [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// fragment no. [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment display
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: viene recuperato un riferimento locale alla sessione. Il type cast è necessario perché la sessione della classe padre è di tipo [AbstractSession];
- righe 11–13: viene creata la prima scheda;
- righe 15–20: viene creata la seconda scheda se nella sessione è memorizzato un ID frammento (riga 15). Questo ID è inizialmente impostato su -1 quando l'attività viene costruita per la prima volta;
- righe 23–39: questo metodo viene chiamato dalla classe padre quando l'utente fa clic su una scheda;
- righe 28-31: se si fa clic sulla scheda 0, deve essere visualizzato [Vue1Fragment]. Sappiamo che questa è la prima vista visualizzata all'avvio dell'applicazione;
- righe 32–35: se si clicca sulla scheda 1, deve essere visualizzato il frammento il cui numero è memorizzato nella sessione;
- righe 37–39: navighiamo verso il frammento selezionato. L'azione associata è [SUBMIT]. Avrebbe potuto essere [NAVIGATION]? In questo documento, usiamo [NAVIGATION] solo quando la visualizzazione del nuovo frammento richiede di conoscere solo il suo stato precedente. Qui non è così, poiché la visualizzazione del frammento deve cambiare rispetto allo 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:
![]() |
Il menu è gestito tramite i seguenti metodi:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// processing menu options
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;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// no navigation on software tab selection
session.setNavigationOnTabSelectionNeeded(false);
// we recreate the two tabs for a title font issue
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// the fragment number to be displayed is set in session
session.setNumFragment(i);
// select tab 2 with navigation
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- righe 16-31: gestisce un clic su un'opzione del menu [Fragmenti];
- righe 37–50: visualizza il frammento #i (si tratta di frammenti di tipo PlaceHolderFragment) nella scheda #1 (la seconda scheda);
- righe 42-44: decidiamo di rimuovere le schede esistenti per crearne due nuove. Questa decisione è stata presa per ovviare al seguente problema: quando visualizziamo semplicemente il frammento nella scheda 1 esistente (senza eliminarla), curiosamente il suo titolo appare (carattere, dimensione) diverso da quello della scheda 0;
- righe 43–44: le due schede vengono create ma non selezionate (ultimo parametro impostato su false);
- Riga 40: Le operazioni alle righe 42–44 potrebbero innescare operazioni [select] sulle schede, che richiameranno il gestore [onTabSelected]. Se non viene intrapresa alcuna azione, ciò comporterà la navigazione verso un frammento. Lo impediamo impostando il booleano [navigationOnTabSelectionNeeded] su false nella sessione. Questo booleano viene automaticamente reimpostato su true dalla classe [AbstractFragment] quando un frammento diventa visibile;
- riga 46: memorizziamo il numero del frammento da visualizzare nella sessione;
- righe 48–50: selezioniamo la scheda n. 2 con navigazione (riga 48). Ciò attiverà la procedura [onTabSelected], che:
- visualizzerà il frammento il cui numero è stato memorizzato nella sessione;
- memorizzerà il numero della scheda selezionata nella sessione;
2.8.4.3. Il frammento [Vue1Fragment]
Ecco 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 {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment life cycle -----------------------------------------------
private void initFragment() {
// nothing to do
}
// save fragment status
@Override
public CoreState saveFragment() {
// view status - nothing to save
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nothing to do
}
@Override
protected void initView(CoreState previousState) {
// 1st visit?
if (previousState == null) {
// the visit number is displayed
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// the visit number is displayed
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// private methods -------------------------------------
// display visit no
private void showNumVisit() {
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
La classe è quasi vuota.
- righe 35-39: chiamato dalla classe padre quando il frammento deve salvare il proprio stato. Il frammento [Vue1Fragment] non ha uno stato da salvare. Restituiamo semplicemente un'istanza della classe base [CoreState] (promemoria: non dobbiamo restituire null);
- righe 41-44: deve restituire l'ID del frammento. Per impostazione predefinita, il frammento [Vue1Fragment] ha l'ID [FRAGMENTS_COUNT-1];
- righe 51-59: chiamato dalla classe padre quando il frammento viene costruito per la prima volta (previousState == null) o nelle visite successive (previousState != null);
- righe 54-57: se questa è la prima visita, incrementare il conteggio delle visite e visualizzarlo (righe 85-92);
- righe 61-65: chiamate quando il frammento sta per essere visualizzato in associazione con un'azione [SUBMIT]. Il conteggio delle visite viene incrementato e visualizzato. In questo caso, non è possibile che il conteggio delle visite venga incrementato due volte durante il 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. Dopodiché, non sarà mai più la prima visita e il metodo [initView] non farà nulla;
- righe 68–71: chiamato durante un ciclo di salvataggio/ripristino. Poiché il frammento non ha uno stato, qui non c'è nulla da ripristinare;
- righe 73–76: chiamate quando tutti gli aggiornamenti precedenti sono stati completati. Qui non c'è più nulla da fare;
- righe 78–81: chiamate quando tutte le attività asincrone avviate sono state completate. Qui 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 {
// text
private String text;
// manufacturers
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters and setters
...
}
- Quando dovremo salvare lo stato del frammento, salveremo il testo che stava visualizzando (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 {
// visual interface components
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// data
private String text;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// implementation of parent class methods ----------------------------
@Override
public CoreState saveFragment() {
// save fragment state
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) {
// original text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// update the text displayed
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// log
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) {
// restore displayed text
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 chiede al frammento di salvare il proprio stato, viene salvato il testo visualizzato dal frammento (riga 34);
- righe 38–41: restituiscono l'ID del frammento. Questo dipende dall'ID della sezione passato come argomento al momento della creazione del frammento;
- righe 43–47: chiamate durante la prima costruzione del frammento (previousState == null) o durante le costruzioni successive (previousState != null);
- riga 46: qui, lo stato precedente non viene utilizzato. Il testo iniziale [text] (riga 24) visualizzato alla prima visita viene ricalcolato ogni volta. Questo è discutibile. Avremmo potuto scegliere di includere anche questa informazione nello stato del frammento;
- righe 49–51: chiamate durante il primo rendering della vista associata al frammento (previousState == null) o durante i rendering successivi (previousState != null). Non c'è nulla da fare;
- righe 53–56: chiamate quando il frammento sta per essere visualizzato in associazione con un'azione [SUBMIT]. Questo avviene sempre tranne che durante il ciclo di salvataggio/ripristino, dove l'azione è [RESTORE]. Incrementiamo quindi il numero di visite e lo visualizziamo;
- righe 68–74: chiamate durante un ciclo di salvataggio/ripristino. Ripristiniamo il testo che era stato salvato nello stato del frammento;
- righe 76–79: chiamate quando tutti gli aggiornamenti precedenti sono stati completati. Qui non c'è altro da fare;
- righe 82-83: chiamate quando tutte le attività asincrone avviate sono state completate. Qui non ci sono attività asincrone;
2.8.4.6. Test
Il lettore è invitato a testare l'applicazione ruotando il dispositivo per verificare che il frammento visualizzato non perda il suo stato. Esamineremo anche i log.
2.9. Conclusione
Alla fine di questo capitolo, abbiamo un progetto di esempio [client-android-skel] per un client Android che comunica con un servizio web / JSON con le seguenti caratteristiche:
- La comunicazione asincrona con il server web/JSON è gestita utilizzando la libreria RxJava;
- il ciclo di vita del frammento (aggiornamento, salvataggio, ripristino) è gestito dalla sua classe padre [AbstractFragment], che chiama metodi specifici delle sue classi figlie in momenti precisi. Il frammento figlio non deve quindi preoccuparsi delle fasi del ciclo di vita, ma solo di implementare determinati metodi richiesti dalla sua classe padre;
- il ciclo di vita dell'attività (salvataggio / ripristino) è gestito da una classe astratta [AbstractActivity], che richiede anche all'attività figlia di implementare determinati metodi;
- La classe [AbstractActivity] può gestire un'applicazione con o senza schede, con o senza immagine di caricamento e con o senza autenticazione di base sul server web / JSON. La presenza o l'assenza di questi elementi è determinata dalla configurazione;
Presenteremo ora un caso di studio più complesso rispetto agli esempi precedenti. La nuova applicazione sarà basata sul progetto modello [client-android-skel].



















































