Skip to content

2. Grundgerüst eines Android-Clients, der mit einem Webdienst / JSON kommuniziert

Wir stellen nun ein Gerüst für eine Android-Anwendung bereit, die mit einem oder mehreren Webdiensten / JSON kommuniziert. Dies ist das Projekt [client-android-skel], das sich im Ordner [architecture] der Beispiele befindet:

  

Die Betrachtung dieser Skelettanwendung bietet die Gelegenheit, bestimmte Punkte zu wiederholen, auf die wir in den vorherigen Beispielen gestoßen sind. Diese Anwendung dient als Grundlage für alle zukünftigen Anwendungen. Sie ist das Ergebnis zahlreicher Überarbeitungen. Ihr Ziel ist es, so viele Elemente wie möglich aus den Anwendungen, die wir in Kürze erstellen werden, in abstrakte Klassen auszulagern, um zu vermeiden, dass wir immer wieder denselben Code schreiben müssen, der sich nur in Details unterscheidet. Sie weist folgende Merkmale auf:

  • Die asynchrone Kommunikation mit dem Webserver/JSON wird mithilfe der RxJava-Bibliothek abgewickelt;
  • Der Lebenszyklus eines Fragments (Aktualisieren, Speichern, Wiederherstellen) wird von seiner übergeordneten Klasse [AbstractFragment] verwaltet, die zu bestimmten Zeitpunkten bestimmte Methoden ihrer untergeordneten Klassen aufruft. Die untergeordnete Klasse muss sich somit nicht um die Lebenszyklusphasen kümmern, sondern lediglich bestimmte Methoden implementieren, die von ihrer übergeordneten Klasse gefordert werden;
  • Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ebenfalls verlangt, dass die untergeordnete Aktivität bestimmte Methoden implementiert;
  • Die Klasse [AbstractActivity] ist in der Lage, eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild und mit oder ohne Basisauthentifizierung gegenüber dem Webserver/JSON zu verwalten. Das Vorhandensein oder Fehlen dieser Elemente wird durch die Konfiguration bestimmt;

Dieses Skelett wurde für alle nachfolgenden Beispiele verwendet. Aufgrund ihrer Vielfalt funktionierte das, was für ein Beispiel galt, möglicherweise nicht für das nächste. Da das Skelett für insgesamt sieben Beispiele verwendet wurde, fanden zahlreiche Iterationen statt. Würden wir es für ein achtes Beispiel verwenden, ist es möglich, dass wir erneut feststellen würden, dass die Besonderheiten dieses neuen Beispiels neue Fehler verursachen. Dennoch wird die Verwendung dieses Skeletts das Schreiben zukünftiger Beispiele erheblich vereinfachen. Tatsächlich ist die Verwaltung des Lebenszyklus eines Fragments (Aktualisieren, Speichern, Wiederherstellen) in Verbindung mit dem Konzept der Fragment-Nachbarschaft besonders komplex. Hier ist dies vollständig in der Klasse [AbstractFragment] verborgen.

2.1. Android-Client-Architektur

Der vorgeschlagene Android-Client basiert auf der folgenden Architektur:

  • Die [DAO]-Schicht implementiert eine [IDao]-Schnittstelle. Sie ist für die Kommunikation mit dem Web-/JSON-Server zuständig;
  • es gibt nur eine Aktivität, die ebenfalls die [IDao]-Schnittstelle implementiert. Die Ansichten greifen darauf zu, um auf den Server zuzugreifen;
  • Die Ansichten werden durch Fragmente implementiert;

Das Android-Projekt spiegelt diese Architektur wider:

  

Wir werden die verschiedenen Elemente dieses Projekts nacheinander vorstellen.

2.2. Die Gradle-Konfiguration

 

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'
  }
}
  • Alle Versionsnummern können sich ändern. Sie können jedoch mit den aktuellen Nummern beginnen, wenn Sie Android Studio so konfigurieren, dass diese Versionen der Android-Tools (Zeilen 15–16, 47–48) vorhanden sind (siehe Abschnitt 6.11);

2.3. Das Anwendungsmanifest

 

<?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>
  • Zeile 3: Ändern Sie das Anwendungspaket;
  • Zeilen 10, 15: Wir werden den Wert des Elements [app_name] in der Datei [res/values/strings.xml] festlegen. Derzeit lautet er wie folgt:

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
  <!-- application name -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. Die Organisation von Java-Code

  
  • [Architektur] fasst die Hauptelemente der Code-Organisation zusammen;
  • [activity] enthält die einzelne Aktivität der Anwendung;
  • [fragments] fasst die Fragmente oder Ansichten der Anwendung zusammen;
  • [dao] fasst die Elemente für die Kommunikation mit dem Webserver / JSON zusammen;

2.5. Aktivitätselemente

 

Image

2.5.1. Die mit der Aktivität verknüpfte Ansicht

Die mit der Aktivität verknüpfte Ansicht [activity_main.xml] sieht wie folgt aus:


<?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>
  • Zeile 29: Es wird ein bestimmter Fragment-Container verwendet;

Die Aktivität verfügt außerdem über ein Menü [res/menu/menu_main.xml] für ihre Ansicht:


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

Derzeit ist er leer. Der Entwickler wird ihn nach Bedarf ausfüllen.

2.5.2. Der Fragment-Container [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;
  }
}

Diese Klasse erweitert die Standard-Android-Klasse [ViewPager] ausschließlich, um das Wischen (Zeile 11) und Scrollen (Zeile 13) zwischen Ansichten zu ermöglichen.

  • Zeilen 26–43: Methoden, die das Wischen deaktivieren, falls es ausgeschaltet wurde;
  • Zeilen 46–49: Neudefinition der Methode [setCurrentItem], die zum Ändern der angezeigten Ansicht verwendet wird. Wenn das Scrollen deaktiviert wurde, wechselt die Ansicht ohne Scrollen. Beachten Sie, dass der Entwickler dieses Verhalten durch die Verwendung der Methode [setCurrentItem(int position, boolean smoothScrolling)] überschreiben kann, wodurch er das gewünschte Scrollverhalten festlegen kann;

2.5.3. Die Klasse [CoreState]

  

Die Klasse [CoreState] ist die Oberklasse für die Zustände der verschiedenen Fragmente:


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
...
}
  • Zeile 16: Jedes Fragment verfügt in seinem Status über einen booleschen Wert [hasBeenVisited], der angibt, ob es bereits aufgerufen wurde oder nicht. Dies ist notwendig, da manchmal, wenn ein Fragment zum ersten Mal angezeigt wird, bestimmte Aktionen durchgeführt werden müssen;
  • Zeile 18: Das Projekt [client-android-skel] speichert und stellt Fragmentmenüs automatisch wieder her, sofern vorhanden. Im Array MenuItemState[] menuOptionsState speichern wir den sichtbaren oder ausgeblendeten Status aller Menüoptionen;
  • Zeilen 10–13: Wie in [Beispiel-22] wird der Zustand der Aktivität und ihrer Fragmente in der Sitzung gespeichert, die wiederum als JSON-Zeichenkette gespeichert wird. Wir werden sehen, dass die Sitzung ein Array von Elementen des Typs [CoreState] speichert. Wenn wir nichts unternehmen, wird die JSON-Zeichenkette vom Typ [CoreState] gespeichert. Wir möchten jedoch die Zustände der Fragmente speichern, die von [CoreState] abgeleitet sind. Um sicherzustellen, dass die JSON-Zeichenkette des abgeleiteten Typs und nicht die des übergeordneten Typs generiert wird, müssen die abgeleiteten Typen wie in den Zeilen 10–13 gezeigt deklariert werden. Die Klasse [CoreState] ist eine der Architekturklassen, die der Entwickler für jede neue Anwendung anpassen muss (Zeilen 10–13);

2.5.4. Die Schnittstelle [IMainActivity]

  

Die Schnittstelle [IMainActivity] definiert, welche Anforderungen Fragmente in der folgenden Architektur an die Aktivität stellen können:

Image


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
}
  • Zeile 6: Die Schnittstelle [IMainActivity] erweitert die Schnittstelle [IDao] der [DAO]-Schicht;
  • Zeile 9: Dies ist die Aktivität, die Zugriff auf die Sitzung in Form einer Instanz der Schnittstelle [ISession] bietet;
  • Zeile 12: Dies ist die Aktivität, die zum Wechseln zwischen Ansichten verwendet wird. Der zweite Parameter ist die Aktion, die diesen Ansichtswechsel auslöst, einer der Werte SUBMIT, NAVIGATION oder RESTORE;
  • Zeilen 15–17: Dies ist die Aktivität, die den Ladebildschirm verwaltet;
  • Zeile 22: zum Debuggen der Anwendung;
  • Zeile 25: um zu vermeiden, dass zu lange gewartet wird, wenn der Server nicht mehr reagiert;
  • Zeile 28: Setzen Sie diesen Wert während des Debuggens auf einige Sekunden, um Zeit zu haben, den Vorgang mit dem Server abzubrechen und zu sehen, was passiert;
  • Zeile 31: Setzen Sie diesen Wert auf „true“, wenn der JSON-Dienst eine Basisauthentifizierung erfordert;
  • Zeile 34: Fragment-Adjazenz;
  • Zeile 37: Setze auf „true“, wenn die Anwendung Registerkarten enthält;
  • Zeile 39: Setze auf „true“, wenn die Anwendung mit einem Web-/JSON-Server kommuniziert und du während des Datenaustauschs ein Lade-Symbol anzeigen möchtest;
  • Zeile 43: Die Anzahl der von der Anwendung verwalteten Fragmente;

Die Schnittstelle [IMainActivity] ist das zweite Element der Architektur, das der Entwickler implementieren muss (Zeile 45).

2.5.5. Die [IDao]-Schnittstelle

Die Schnittstelle [IMainActivity] erweitert die folgende Schnittstelle [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
}
  • Zeile 24: Der Entwickler wird die Schnittstelle hier vervollständigen;

2.5.6. Die Sitzung

  

Die Klasse [Session] kapselt Elemente, die von der Aktivität und den Fragmenten gemeinsam genutzt werden. Sie implementiert die folgende Schnittstelle [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);
}

Wir führen die Schnittstelle [ISession] ein, um das Vorhandensein bestimmter Methoden in der Sitzung vorzuschreiben:

  • Zeilen 7–10: die Nummer der zuletzt angezeigten Ansicht (Fragment);
  • Zeilen 12–15: der Status einer bestimmten Ansicht;
  • Zeilen 17–24: Wir führen das Konzept einer laufenden Aktion ein. Es gibt vier (Zeile 17):
    • RESTORE: Ein Speichervorgang ist im Gange. Es findet kein Ansichtswechsel statt;
    • NAVIGATION: Die Navigation ist im Gange. Hier definieren wir Navigation als einen Ansichtswechsel, bei dem die neue Ansicht aus ihrem letzten, während der Sitzung gespeicherten Zustand wiederhergestellt werden kann;
    • SUBMIT: Wir weisen einer ausstehenden Aktion den Typ [SUBMIT] zu, wenn eine Ansichtsänderung vorliegt und die neue Ansicht vom Gesamtzustand der Aktivität abhängt, nicht nur von ihrem eigenen Zustand. Manchmal ist es schwierig, zwischen NAVIGATION und SUBMIT zu unterscheiden. In solchen Fällen verwenden wir den allgemeineren Fall von SUBMIT;
    • NONE: Der Wert der Aktion, wenn sie noch keinen ersten Wert erhalten hat;
  • Zeilen 26–30: Die Zustände der Aktivität und der Fragmente werden in einem CoreState[]-Array gespeichert. Um sicherzustellen, dass dies bei der JSON-Serialisierung/Deserialisierung korrekt gehandhabt wird, muss es über einen Getter und einen Setter verfügen;
  • Zeilen 32–35: Nummer der zuletzt ausgewählten Registerkarte. Wird während des Speicher-/Wiederherstellungszyklus verwendet, um die Registerkarte wieder auszuwählen, die vor dem Drehen des Geräts ausgewählt war;
  • Zeilen 37–40: Verwaltet einen booleschen Wert, der angibt, ob die Auswahl einer Registerkarte mit einem Fragmentwechsel einhergehen soll;

Die Schnittstelle [ISession] wird durch die folgende abstrakte Klasse [AbstractSession] implementiert:


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;
  }
}
  • Zeile 9: Die ID der Ansicht, die vor der aktuell angezeigten Ansicht angezeigt wurde. Diese Information ist nützlich, wenn eine Ansicht von mehreren Stellen aus aufgerufen werden kann. Dies ist typischerweise bei der tabbasierten Navigation der Fall. Die angezeigte Ansicht kann dann feststellen, welche Ansicht zuvor angezeigt wurde;
  • Zeile 12: Das Array der Zustände für alle von der Aktivität angezeigten Fragmente;
  • Zeile 18: Die ID der zuvor ausgewählten Registerkarte. Spielt eine ähnliche Rolle wie die ID der vorherigen Ansicht in Zeile 9. Diese Information ist nützlich, wenn das Gerät gedreht wird und man zu der Registerkarte zurückkehren muss, die vor der Drehung ausgewählt war;
  • Zeile 22: Ein boolescher Wert, der angibt, ob die Auswahl eines Tabs zu einer Änderung des angezeigten Fragments führen soll. Beachten Sie, dass das Projekt [client-android-skel] Tabs und Fragmente separat verwaltet, sodass es in Fällen verwendet werden kann, in denen die Anzahl der Tabs geringer ist als die Anzahl der Fragmente. Es gibt zwei Arten der Auswahl:
    • eine Auswahl, die der Benutzer durch Klicken auf eine Registerkarte vornimmt. In diesem Fall muss das angezeigte Fragment in der Regel geändert werden;
    • eine softwaregesteuerte Auswahl über die Methode [Tablayout.Tab.select()]. In diesem Fall ist eine Änderung des angezeigten Fragments nicht immer wünschenswert. Hier sind zwei Beispiele:
      • Wenn das Gerät gedreht wird, wird die Aktivität neu erstellt, ebenso wie die Registerkarten. Beim Erstellen der ersten Registerkarte wird jedoch automatisch ein softwaregesteuerter [select]-Vorgang ausgeführt. Es ist daher nicht wünschenswert, das angezeigte Fragment zu ändern, da wir uns in einer Phase der Neuerstellung der Aktivität befinden, in der das letztendlich angezeigte Fragment nicht unbedingt dasjenige ist, das der ersten Registerkarte zugeordnet ist;
      • Da die Tab-Verwaltung von der Fragmentverwaltung getrennt ist, möchten Sie möglicherweise die Tabs aktualisieren (löschen, hinzufügen), ohne die zugehörigen Fragmente zu beeinträchtigen. Einige dieser Vorgänge können jedoch wiederum eine implizite [select]-Softwareoperation auf einem der Tabs auslösen. Diese Auswahl führt nicht zwangsläufig zur Navigation zum zugehörigen Fragment;
  • Zeile 21: Das Feld [navigationOnTabSelectionNeeded] soll bei Speicheroperationen für die Aktivität und ihre Fragmente nicht gespeichert werden. Die Annotation [@JsonIgnore] bewirkt, dass das Feld bei der JSON-Serialisierung/Deserialisierung ignoriert wird;
  • Zeilen 25–31: Der Konstruktor initialisiert das Array der Zustände für die [FRAGMENTS_COUNT] Fragmente der Anwendung. Die Elemente dieses Arrays werden mit dem Feld [hasBeenVisited=false] initialisiert. Diese Information wird verwendet, um festzustellen, ob dies der erste Besuch des Fragments ist oder nicht;

Die Klasse [Session] sieht wie folgt aus:


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
}
  • Zeile 5: Die Klasse [Session] erweitert die soeben vorgestellte Klasse [AbstractSession]. Der Entwickler platziert hier die Elemente, die zwischen den Fragmenten untereinander sowie zwischen den Fragmenten und der Aktivität gemeinsam genutzt werden sollen. Beachten Sie, dass die Klasse [Session] nicht mehr mit der Annotation [@EBean] versehen ist. Sie ist zu einer normalen Klasse geworden;

2.5.7. Die abstrakte Klasse [AbstractActivity]

  

2.5.7.1. Grundgerüst

Die Klasse [AbstractActivity] ist eine Klasse mit über 300 Zeilen. Wir werden sie Schritt für Schritt untersuchen. Ihr Grundgerüst sieht wie folgt aus:


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

Die Klasse [AbstractActivity]:

  • implementiert die Schnittstelle [IMainActivity] (Zeilen 21, 55);
  • übernimmt das Speichern und Wiederherstellen der Aktivität und ihrer Fragmente, wenn das Gerät gedreht wird (Zeile 58);
  • übernimmt die Verwaltung des Ladebildschirms während der Kommunikation mit dem Webserver / JSON (Zeile 61);
  • implementiert die IDao-Schnittstelle der [DAO]-Schicht (Zeile 64);
  • implementiert den Fragment-Manager (Zeile 67);
  • verlangt von seinen untergeordneten Klassen, dass sie sechs Methoden enthalten (Zeilen 71–81);

2.5.7.2. Implementierung der [IMainActivity]-Schnittstelle

Die Implementierung der [IMainActivity]-Schnittstelle (siehe Abschnitt 2.5.4) sieht wie folgt aus:


  // 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. Speichern des Zustands der Aktivität und ihrer Fragmente

Der Zustand der Aktivität und ihrer Fragmente ist vollständig in der Sitzung enthalten. Daher müssen wir die Sitzung speichern. Hier verwenden wir das wieder, was im Projekt [Beispiel-22] gemacht wurde (siehe Abschnitt 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. Wiederherstellen des Zustands der Aktivität und ihrer Fragmente

Dies beinhaltet das Wiederherstellen der Sitzung. Wir gehen dabei wie in [Beispiel-22] gezeigt vor:


@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();
    }
...
  • Zeilen 10–26: Wenn der Parameter [Bundle savedInstanceState] in Zeile 2 nicht null ist, wird die Sitzung wiederhergestellt (Zeilen 12–17);
  • Zeilen 26–29: Wenn der Parameter [Bundle savedInstanceState] in Zeile 2 null ist, entspricht dies dem ersten Start der Aktivität. Es wird dann eine leere Sitzung erstellt;

2.5.7.5. Initialisierung der [DAO]-Schicht


@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();
....
}
  • Zeile 11: Von der untergeordneten Aktivität wird eine Referenz auf die [DAO]-Schicht angefordert (Zeile 21);
  • Zeilen 14–17: Wenn die [DAO]-Schicht vorhanden ist, wird sie anhand der in der [IMainActivity]-Schnittstelle enthaltenen Informationen konfiguriert;

2.5.7.6. Initialisierung der mit der Aktivität verbundenen Ansicht

Die mit der Aktivität verbundene Ansicht wurde in Abschnitt 2.5.1 vorgestellt:


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

Diese Ansicht wird mit dem folgenden Code initialisiert:


  @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);
    }
...
  • Zeile 11: Die XML-Ansicht [activity_main] wird der Aktivität zugeordnet;
  • Zeilen 14–15: Die Symbolleiste ist integriert und wird unterstützt;
  • Zeilen 17–27: optionales Hinzufügen eines Ladesymbols: wenn der boolesche Wert [IS_WAITING_ICON_NEEDED] in der Schnittstelle [IMainActivity] wahr ist;
  • Zeile 23: Erstellung des Ladesymbols vom Typ [ProgressBar], auf das das Feld [loadingPanel] verweist;
  • Zeile 24: Dieses Bild ist zunächst ausgeblendet;
  • Zeile 26: Es wird zur Symbolleiste hinzugefügt;

2.5.7.7. Registerkartenverwaltung

Die Schnittstelle [IMainActivity] kann eine Registerkartenleiste anfordern. Diese wird wie folgt hinzugefügt und verwaltet:


// 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);
...
  • Zeilen 12–48: Hinzufügen und Verwalten einer Tab-Leiste;
  • Zeile 6: Die Registerkartenleiste wird hinzugefügt, wenn die Konstante [ARE_TABS_NEEDED] in der Schnittstelle [IMainActivity] auf „true“ gesetzt ist;
  • Zeile 12: Beim Erstellen der Tab-Leiste können implizite [Tablayout.Tab.select]-Operationen auftreten (diese werden nicht vom Benutzer ausgelöst). Wir setzen den booleschen Wert [session.navigationOnTabSelectionNeeded] auf „false“, um jegliche Navigation während dieser falschen Auswahlen zu verhindern. Es obliegt dem Entwickler, das anzuzeigende Fragment mithilfe der Methode [navigateToView] auszuwählen. Der boolesche Wert [session.navigationOnTabSelectionNeeded] wird wieder auf „true“ gesetzt, wenn dieses Fragment angezeigt wird (siehe Klasse AbstractFragment);
  • Zeile 14: Erstellung einer Tab-Leiste, auf die das Feld [tabLayout] verweist. Wir verwenden eine benutzerdefinierte Tab-Leiste [CustomTabLayout], auf die wir später noch eingehen werden;
  • Zeile 15: Wir legen die Farben der Tab-Titel fest. Diese finden sich in der folgenden Datei [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>
    • Zeile (c): die Farbe des Registerkartentitels, wenn die Registerkarte ausgewählt ist;
    • Zeile (d): die Farbe des Registerkartentitels, wenn die Registerkarte nicht ausgewählt ist;

Diese Datei ist natürlich editierbar. Die hexadezimalen Farbcodes finden Sie beispielsweise hier.

  • Zeilen 17–18: Hinzufügen dieser Registerkartenleiste zur Anwendungsleiste in der XML-Ansicht [activity_main];
  • Zeilen 20–47: Ereignisbehandler für die Registerleiste;
  • Zeilen 22–36: Es wird nur das [onTabSelected]-Ereignis behandelt. Es entspricht einem Klick auf die Registerkarte [Tab], die als Parameter an die Methode übergeben wird, oder einem Softwarebefehl [TabLayout.Tab.select];
  • Zeile 30: Position der ausgewählten Registerkarte;
  • Zeile 32: Diese Position wird in der Sitzung gespeichert;
  • Zeile 34: Das mit dieser Registerkarte verknüpfte Fragment muss nun angezeigt werden. Nur die Unterklasse (Zeile 52) kann diese Verknüpfung herstellen. Beachten Sie, dass wir die Registerkartenleiste nicht mit dem Fragment-Container [mViewPager] verknüpfen, wie es in einigen der untersuchten Beispiele der Fall war. Hier trennen wir die Verwaltung der Registerkartenleiste vollständig von der der Fragmente. Deshalb müssen wir beim Klicken auf eine Registerkarte angeben, welche Ansicht angezeigt werden soll;
  • Zeile 28: Wir unterscheiden zwischen der Auswahl einer Registerkarte mit oder ohne Navigation. Im Allgemeinen wird erwartet, dass eine Navigation erfolgt, wenn der Benutzer auf eine Registerkarte klickt, während dies bei einer programmgesteuerten Auswahl nicht der Fall ist. Der Entwickler unterscheidet diese beiden Fälle mithilfe des Elements [session.navigationOnTabSelectionNeeded]. Wenn keine Navigation durchgeführt wird, wird die Nummer der zuletzt ausgewählten Registerkarte nicht in der Sitzung gespeichert. Es obliegt dem Entwickler, dies zu tun;

2.5.7.8. Der Tab-Manager [CustomTabLayout]

  

Wir verwenden einen benutzerdefinierten Tab-Manager, um Tab-Titel in verschiedenen Schriftarten anzuzeigen. Die Klasse [CustomTabLayout] sieht wie folgt aus:


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);
      }
    }
  }
 
}
  • Die Schriftart für die Registerkartentitel wird in den Zeilen 30 und 44 angepasst;

Der Ordner [fonts] sieht wie folgt aus:

  

Quellen:

2.5.7.9. Neueste Initialisierungen


  @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();
...
  • Zeilen 10–19: Dieser Code kommt häufig in den Beispielen vor, die wir behandelt haben;
  • Zeilen 21–23: Anzeige der allerersten Ansicht. Es gibt zweifellos mehrere Möglichkeiten, diesen Fall zu unterscheiden. Hier haben wir die Tatsache genutzt, dass für die allererste Ansicht der Wert der Aktion, die den Ansichtswechsel auslöst, NONE ist;
  • Zeile 22: Wir treffen keine Annahmen darüber, welches Fragment als erstes angezeigt werden soll. In unseren Beispielen war dies oft Fragment Nr. 0, aber nicht immer (siehe Beispiel 22). Wir werden daher die untergeordnete Aktivität (Zeile 30) bitten, uns mitzuteilen, um welche Ansicht es sich handelt;
  • Zeile 25: Wir haben hier alles herausgearbeitet, was wir konnten. Nun muss die untergeordnete Klasse ihre eigenen Initialisierungen durchführen (Zeile 29);

2.5.7.10. Behandlung des Ladebildes

In der Klasse [AbstractActivity] wird das Platzhalterbild durch die folgenden zwei Methoden verwaltet:


  // 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. Implementierung der Schnittstelle [IDao]

In der Klasse [AbstractActivity] wird die Schnittstelle [IDao] (siehe Abschnitt 2.5.5) wie folgt implementiert:


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);
}
  • Zeile 3: Erinnern Sie sich daran, dass der Wert dieses Feldes von der untergeordneten Aktivität in der Methode [onCreate] bereitgestellt wurde;

2.5.7.12. Implementierung des Fragment-Managers

In der Klasse [AbstractActivity] wird der Fragment-Manager wie folgt implementiert:


...
  // 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);
...
}
  • Zeile 5: das Array der mit der Aktivität verbundenen Fragmente. Alle Fragmente werden von der Klasse [AbstractFragment] abgeleitet;
  • Zeilen 8–12: Dies ist der Konstruktor, der das Array der Fragmente initialisiert. Er fordert diese von der Unterklasse der Aktivität an (Zeile 35);
  • Zeilen 28–31: Fragmenttitel können in einer Anwendung verwendet werden, in der es genauso viele Registerkarten wie Fragmente gibt. In diesem Fall kann der Registerkarte der Titel des Fragments zugewiesen werden. Hier werden diese Titel von der Unterklasse angefordert (Zeile 37);

2.5.7.13. Die Methode [onResume]

Die Methode [onResume] wird kurz bevor die mit der Aktivität verbundene Ansicht sichtbar wird, ausgeführt. Sie wird hier verwendet, um nach einem Speichern/Wiederherstellen-Vorgang eine Registerkarte auszuwählen:


  @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();
    }
}
  • Zeile 10: Auswahl der Registerkarte, die vor dem Speichern/Wiederherstellen ausgewählt war. Es ist wichtig zu beachten, dass in der Methode [onCreate] – die im Lebenszyklus der Aktivität vor der Methode [onResume] ausgeführt wird – die Navigation bei der Auswahl einer Registerkarte deaktiviert wurde. Daher wird hier zwar eine Registerkarte ausgewählt, es findet jedoch kein Fragmentwechsel statt;

2.5.7.14. Zusammenfassung

Die abstrakte Klasse [AbstractActivity] ist die übergeordnete Klasse der einzigen Aktivität der Anwendung.

Die untergeordnete Aktivität muss die folgenden sechs Methoden implementieren:


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

Die untergeordnete Aktivität hat außerdem Zugriff auf die folgenden geschützten Mitglieder ihrer übergeordneten Klasse:


  // the session
  protected ISession session;
  // the fragment container
  protected MyPager mViewPager;
  // tab bar
  protected CustomTabLayout tabLayout;
  // class name
protected String className;

2.5.8. Die [MainActivity]-Aktivität

  

Die Klasse [MainActivity] kann einen anderen Namen haben. Die einzige Voraussetzung ist, dass sie die Schnittstelle [IMainActivity] implementiert. Die bereitgestellte Standardklasse lautet wie folgt:


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;
  }
}
  • Zeile 14: Damit die AA-Annotation [@Bean] in Zeile 19 gültig ist, muss die Aktivität die AA-Annotation [@EActivity] aufweisen;
  • Zeile 15: Die Aktivität ist mit dem XML-Menü [menu_main] verknüpft. Derzeit ist dieses Menü leer. Der Entwickler muss es bei Bedarf ausfüllen;
  • Zeile 16: Die Klasse erweitert die Klasse [AbstractActivity];
  • Zeilen 19–20: Ein Verweis auf die [DAO]-Schicht. Diese wird von der AA-Bibliothek instanziiert, bevor dieses Feld initialisiert wird. Das bedeutet, dass die AA-Bean [Dao] vorhanden sein muss. Dies ist bei der von uns bereitgestellten Skeleton-Anwendung immer der Fall. Selbst in einer Anwendung ohne [DAO]-Schicht können Sie das [dao]-Paket an Ort und Stelle belassen. Dies verursacht keine Komplikationen;
  • Zeile 22: Die Session als Instanz vom Typ [Session]. Die Session existiert in der übergeordneten Klasse [AbstractActivity], jedoch als Instanz der Schnittstelle [ISession] (Zeile 32);
  • Zeilen 24–63: die sechs Methoden, die von der übergeordneten Klasse [AbstractActivity] benötigt werden;
  • Zeilen 36–39: Die Methode [getDao] gibt eine Referenz auf die [DAO]-Schicht zurück. Hier ist diese Referenz niemals null. In der übergeordneten Klasse [AbstractActivity] haben wir jedoch den Fall vorgesehen, dass die untergeordnete Klasse eine null-Referenz zurückgibt, um anzuzeigen, dass keine [DAO]-Schicht vorhanden ist. Wenn Sie diese Option nutzen möchten (meiner Meinung nach nicht sehr nützlich), müssen Sie den Zeiger an dieser Stelle auf null setzen;

2.6. Die [DAO]-Schicht

Image

  

2.6.1. Die IDao-Schnittstelle

Es wurde in Abschnitt 2.5.5 vorgestellt:


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
}

Der Entwickler fügt die Methoden für seine [DAO]-Schicht ab Zeile 24 hinzu.

2.6.2. Die [WebClient]-Schnittstelle

  

Die [WebClient]-Schnittstelle sieht wie folgt aus:


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
}

Der Entwickler fügt ab Zeile 17 die Methoden hinzu, die mit den vom JSON-Server bereitgestellten URLs kommunizieren.

2.6.3. Der Authentifizierungs-Interceptor [MyAuthInterceptor]

  

Die Klasse [MyAuthInterceptor] sieht wie folgt aus:


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

Diese Klasse generiert den folgenden HTTP-Authentifizierungsheader:

Authorization: Basic code

wobei [code] die Base64-kodierte Zeichenfolge 'user:mp' ist. Diese Klasse wird nur verwendet, wenn der JSON-Server diese Form der Authentifizierung erwartet. Es gibt auch andere Formen.

Hinweis: Die Verwendung dieser Klasse wird in Abschnitt 3.6.3.1 veranschaulicht.

2.6.4. Die Klasse [AbstractDao]

  

Die Klasse [AbstractDao] sieht wie folgt aus:


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;
  }
}
  • Zeilen 35–81: Die Methode [getResponse] verwendet die RxAndroid-Bibliothek, um einen Typ [Observable<T>] zurückzugeben. Im Gegensatz zu einigen zuvor gesehenen Beispielen gibt sie keinen Typ [Response<T>] zurück – bei dem es sich um einen proprietären Typ handelt –, sondern einen beliebigen Typ T;
  • Zeile 35: Die Methode [getResponse] nimmt als Parameter eine Instanz vom Typ [IRequest<T>] aus den Zeilen 30–32 entgegen, deren Methode [IRequest.getResponse()] den Typ T über eine synchrone HTTP-Operation abruft;
  • Zeilen 48–50: Künstlich warten wir [delay] Millisekunden. In der Produktion setzen wir [delay=0]. Während der Fehlersuche setzen wir [delay=einige Sekunden], um dem Benutzer die Möglichkeit zu geben, den asynchronen Vorgang abzubrechen und so zu sehen, wie sich der Code in diesem Fall verhält;
  • Zeile 52: Die erwartete Antwort wird mit einer synchronen Anfrage angefordert;
  • Zeile 64: Sobald die Antwort empfangen wurde, wird sie an den Beobachter weitergeleitet;
  • Zeile 66: Wir geben an, dass keine weiteren Emissionen erfolgen werden. Dies ist der Sonderfall einer asynchronen Aktion, die nur ein Element zurückgibt;
  • Zeilen 67–78: Im Falle einer Ausnahme wird die Ausnahme an den Beobachter weitergeleitet (Zeile 77);

2.6.5. Die [Dao]-Klasse

  

Die Klasse [Dao] sieht wie folgt aus:


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
}
  • Zeilen 21–22: Einbindung der AA-Bean [WebClient], die die Kommunikation mit dem Webserver / JSON übernimmt;
  • Zeilen 24–25: Einbindung des Authentifizierungs-Interceptors;
  • Zeilen 31–42: Methode, die nach der Injektion der Felder aus den Zeilen 21–25 ausgeführt wird;
  • Zeile 37: Das [RestTemplate]-Objekt, das die Client-Server-Kommunikation übernimmt, wird über eine Factory erstellt. Dies ist nicht zwingend erforderlich, aber die Factory ermöglicht es uns, Zeitüberschreitungen bei der Kommunikation zu konfigurieren. Deshalb verwenden wir nicht den parameterlosen Konstruktor [RestTemplate()];
  • Zeile 39: Wir fügen den Konvertern des [RestTemplate] einen JSON-Konverter hinzu. Dies wird der einzige Konverter sein. Wenn also eine Methode des [WebClient] eine JSON-Zeichenkette vom Server erhält, wird diese automatisch in das Objekt deserialisiert, das die Methode zurückgeben soll;
  • Zeile 41: Das auf diese Weise konfigurierte [RestTemplate]-Objekt wird an den Web-Client übergeben, der damit die Client-Server-Kommunikation abwickelt;
  • Zeilen 44–48: Wir legen die Stamm-URL des Web-/JSON-Servers fest. Alle in der [WebClient]-Klasse deklarierten URLs beziehen sich relativ auf diese Stamm-URL;
  • Zeilen 50–54: Mit dieser Methode können Sie den Verbindungsinhaber angeben, wenn die Verbindung durch eine Basisauthentifizierung gesteuert wird (siehe Abschnitt 2.6.3);
  • Zeilen 56–64: Legen Sie die Zeitüberschreitungen für den Austausch zwischen Client und Server fest. Dies geschieht über die Factory des [RestTemplate]-Objekts, die den Austausch steuert;
  • Zeilen 66–78: Diese Methode legt fest, dass der Server durch die Basisauthentifizierung geschützt ist;
  • Zeilen 72–77: Wenn eine Basisauthentifizierung erforderlich ist, wird der in Zeile 25 eingefügte Authentifizierungs-Interceptor zu den Interceptoren des [RestTemplate]-Objekts hinzugefügt. Dieser Interceptor fügt automatisch den vom Server erwarteten HTTP-Header für die Basisauthentifizierung zu allen Web-Client-Anfragen hinzu;
  • Der Entwickler implementiert die [IDao]-Schnittstelle ab Zeile 87;

2.7. Fragmente

  

2.7.1. Die Klasse [MenuItemState]

Die Klasse [MenuItemState] kapselt den Status einer Menüoption:


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. Die Klasse [Utils]

Die Klasse [Utils] enthält statische Hilfsmethoden:


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. Die übergeordnete Klasse [AbstractFragment]

Die Klasse [AbstractFragment] enthält die Elemente, die allen Fragmenten in der Anwendung gemeinsam sind. Wie bei der Klasse [AbstractActivity] ist ihr Code komplex. Wir werden ihn ebenfalls Schritt für Schritt analysieren.

2.7.3.1. Das Grundgerüst


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);
 
}
  • Zeilen 28–45: die privaten Daten der Klasse;
  • Zeilen 47–58: geschützte Daten, auf die Unterklassen zugreifen können;
  • Zeilen 61–62: Code, der das anzuzeigende Fragment aktualisiert;
  • Zeilen 64–65: Hilfscode zur Verwaltung des Menüs, falls vorhanden;
  • Zeilen 67–68: Hilfscode zur Abwicklung der Wartezeit während einer asynchronen Operation;
  • Zeilen 70–71: Code zur Erleichterung der Kommunikation zwischen dem Fragment und der [DAO]-Schicht;
  • Zeilen 73–74: Hilfscode zur standardmäßigen Behandlung von Ausnahmen;
  • Zeilen 76–77: Code zur Verwaltung des Lebenszyklus des Fragments;
  • Zeilen 80–94: Die übergeordnete Klasse schreibt ihren untergeordneten Klassen 8 Methoden vor;

2.7.3.2. Der Konstruktor

Der Klassenkonstruktor sieht wie folgt aus:


  // 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");
    }
}
  • Zeile 9: Der Name der hier instanziierten untergeordneten Klasse wird vermerkt. Dieser Name wird in allen Protokollen der übergeordneten Klasse verwendet;
  • Zeile 10: Wir vermerken, dass das Fragment erstellt wird. Diese Information wird verwendet, wenn das untergeordnete Fragment aufgefordert wird, sich selbst zu aktualisieren;

2.7.3.3. Menüverwaltung

In unserer Architektur muss jedes Fragment über ein Menü verfügen, auch wenn dieses leer ist. Die Protokolle haben tatsächlich gezeigt, dass bei Ausführung der Methode [onCreateOptionsMenu] – die ausgeführt wird, wenn das Fragment über ein Menü verfügt – das Fragment bereits mit seiner Aktivität, Ansicht und seinem Menü verknüpft ist und kurz davor steht, sichtbar zu werden. Dies ist daher der Moment, in dem die visuelle Oberfläche und das Menü aktualisiert werden können. Innerhalb dieser [onCreateOptionsMenu]-Methode weisen wir das untergeordnete Fragment an, sich selbst zu aktualisieren.

Die Menüverwaltung umfasst Hilfsmethoden, mit denen das untergeordnete Fragment Menüelemente ein- oder ausblenden kann:


  // 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());
    }
}
  • Zeilen 6–18: Diese Methode ruft die numerischen Kennungen aller Menüoptionen ab;
  • Zeile 6: Die Methode [getMenuOptions] benötigt zwei Parameter:
    • [Menu menu]: das Menü des Fragments;
    • [List<Integer> menuOptionsIds]: die Liste der Android-IDs für die Menüoptionen. Zu Beginn ist diese Liste leer. Sie wird dann durch eine rekursive Durchquerung (Zeile 15) des Menübaums gefüllt;
  • Zeilen 20–40: Erstellt auf Basis des Menüs das Array mit den Zuständen (ID, Sichtbarkeit) für die Menüoptionen. Dieses Array wird in Zeile 3 gespeichert. Die Klasse [MenuItemState] wurde in Abschnitt 2.7.1 beschrieben;
  • Zeilen 43–55: eine Variante der vorherigen Methode. Sie führt dasselbe aus, verwendet jedoch anstelle einer Neuberechnung der Identifikatoren für alle Menüoptionen – was bereits geschehen ist – die Identifikatoren aus dem Zustandsarray in Zeile 3;
  • Zeilen 58–63: Mit der Methode [setAllMenuOptionsStates] können Sie alle Menüoptionen des Fragments ausblenden oder einblenden;
  • Zeilen 65–69: Mit der Methode [setMenuOptionsStates] können Sie bestimmte Menüoptionen selektiv ein- oder ausblenden;
  • Die Methoden [getMenuOptions, getMenuOptionsStates] sind als privat deklariert, da sie nur innerhalb von [AbstractFragment] verwendet werden. Die Methoden [setAllMenuOptionsStates] (Zeile 58) und [setMenuOptionsStates] (Zeile 65) sind als geschützt deklariert, damit sie für untergeordnete Klassen verfügbar sind;

2.7.3.4. Behandlung des Wartens auf den Abschluss einer asynchronen Aufgabe


   // 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();
  }
 
  • Zeilen 9–18: Um eine oder mehrere asynchrone Operationen zu starten, ruft das untergeordnete Fragment die übergeordnete Methode [beginRunningTasks] auf. Der Parameter dieser Methode ist die Anzahl der asynchronen Aufgaben, die das untergeordnete Fragment starten wird;
  • Zeile 11: Wir speichern den Parameter der Methode;
  • Zeile 13: Der Ladebildschirm wird eingeblendet;
  • Zeile 15: Die Liste der Abonnements für asynchrone Operationen wird gelöscht. Diese wurden vom untergeordneten Fragment noch nicht erstellt;
  • Zeile 17: Es wird ein boolescher Wert verwaltet, um anzuzeigen, dass die vom untergeordneten Fragment angeforderten asynchronen Aufgaben abgebrochen wurden. Zu Beginn hat der boolesche Wert den Wert „false“;
  • Zeilen 20–25: Das untergeordnete Fragment ruft die übergeordnete Methode [cancelWaitingTasks] auf, um anzuzeigen, dass es die von ihm gestarteten Aufgaben abbrechen möchte;
  • Zeile 22: Das wartende Bild wird ausgeblendet;

2.7.3.5. Ausnahmebehandlung


  // 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();
}
  • Zeilen 4–7: Die Methode [showAlert(Throwable)] ermöglicht es einem untergeordneten Fragment, die Meldungen aus dem Ausnahmestapel des als Parameter übergebenen Throwable in einem Fenster anzuzeigen;
  • Zeilen 10–13: Die Methode [showAlert(List<String>)] ermöglicht es einem untergeordneten Fragment, die als Parameter übergebene Liste von Meldungen in einem Fenster anzuzeigen;
  • Die in den Zeilen 6 und 12 verwendete Klasse [Utils] wurde in Abschnitt 2.7.2 beschrieben;

2.7.3.6. Behandlung asynchroner Operationen


...
  // 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) {
...
  }
 
  • Zeilen 9–41: Führen Sie eine asynchrone Aufgabe aus;
  • Zeile 9: Die Methode [executeInBackground] erwartet zwei Parameter:
    • [Observable<T> process]: der auszuführende asynchrone Prozess;
    • [Action1<T> consumeResult]: die Methode des untergeordneten Fragments, die aufgerufen werden soll, um ihr die vom Prozess ausgegebenen Elemente zu übergeben. In unseren vorherigen Beispielen haben die Prozesse immer nur ein Element ausgegeben. Der Typ T von [Action1<T>] ist der Typ T des vom beobachteten Prozess zurückgegebenen Ergebnisses;
  • Zeile 14: Die asynchrone Aufgabe wird nur gestartet, wenn sie nicht bereits vom Benutzer oder vom Programm (aufgrund einer Ausnahme) abgebrochen wurde;
  • Zeile 16: Der Prozess ist so konfiguriert, dass er in einem E/A-Thread ausgeführt und im UI-Thread beobachtet wird;
  • Zeile 16: Die Anweisung [process.subscribe] startet den Prozess im I/O-Thread. Innerhalb dieses Threads werden Operationen synchron ausgeführt, da wir eine synchrone HTTP-Bibliothek verwenden;
  • Zeile 19: Die Methode [process.subscribe] hat drei Parameter:
    • Zeile 21: [consumeResult]: die Methode des untergeordneten Fragments, die die vom Prozess ausgegebenen Elemente verarbeitet;
    • Zeilen 22–28: Die Methode, die ausgeführt wird, wenn während der Verarbeitung der asynchronen Aufgabe eine Ausnahme auftritt. Die Behandlung wird an die Methode [consumeThrowable] in Zeile 49 delegiert;
    • Zeilen 29–36: Die Methode, die ausgeführt wird, wenn die Aufgabe die Benachrichtigung über das Ende der Ausgabe sendet. Die Verarbeitung wird an die Methode [endOfTask] in Zeile 43 delegiert;
  • Zeile 19: Die soeben gestartete asynchrone Aufgabe wird im Feld [subscriptions] erfasst, das alle gestarteten asynchronen Aufgaben nachverfolgt. Dadurch können diese bei Bedarf abgebrochen werden;
  • Zeilen 37–39: Methode, die ausgeführt wird, wenn während der Verarbeitung der asynchronen Aufgabe eine Ausnahme auftritt. Die Behandlung wird an die Methode [consumeThrowable] in Zeile 49 delegiert;

Die Methode [endOfTask] lautet wie folgt:


  // 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);
  • Zeile 6: Eine asynchrone Aufgabe wurde gerade abgeschlossen. Der Zähler für aktive Aufgaben wird dekrementiert;
  • Zeile 8: Wenn keine aktiven Aufgaben mehr vorhanden sind, hat der untergeordnete Thread alle seine Antworten erhalten;
  • Zeile 10: Das Warten wird abgebrochen;
  • Zeile 12: Wir benachrichtigen das untergeordnete Fragment darüber, dass alle von ihm gestarteten Aufgaben beendet sind, indem wir seine Methode [notifyEndOfTasks] aufrufen. Der Parameter dieser Methode gibt an, wie die Aufgaben beendet wurden – normal oder aufgrund einer Abbruch durch den Benutzer oder den Code, weil eine Ausnahme aufgetreten ist. In Zeile 12 signalisieren wir ein normales Ende. Beachten Sie, dass das untergeordnete Fragment nicht nachverfolgen muss, welche Aufgaben noch aktiv sind. Das übernimmt seine übergeordnete Klasse für es;

Die Methode [consumeThrowable] sieht wie folgt aus:


  // 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);
  • Zeile 3: Die Methode [consumeThrowable] fängt die aufgetretene Ausnahme ab;
  • Zeile 15: Alle noch aktiven Aufgaben werden abgebrochen;
  • Zeile 17: Der Ausnahmetext wird angezeigt;
  • Zeilen 21–37: Alle Aufgaben werden abgebrochen;
  • Zeilen 27–29: Alle Abonnements werden abgebrochen;
  • Zeile 31: Es wird vermerkt, dass eine Abbruchaktion stattgefunden hat;
  • Zeile 32: Der Aufgabenzähler wird auf Null zurückgesetzt;
  • Zeile 34: Die Wartezeit wird abgebrochen;
  • Zeile 36: Das untergeordnete Fragment wird darüber informiert, dass die Aufgaben nach der Stornierung beendet wurden;

2.7.3.7. Verwaltung des Fragment-Lebenszyklus


  // 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) {
...
}
  • Zeilen 2–20: Die Methoden [onDestroyView, onDestroy] dienen ausschließlich zu Protokollierungszwecken. Sie ermöglichen es dem Entwickler, den Lebenszyklus des Fragments besser zu verstehen;

Das Speichern des Fragments bei einer Drehung des Geräts wird durch die folgenden Methoden abgewickelt: [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);
    }
}
  • Zeilen 6–19: Das Fragment wird gespeichert, wenn es vom sichtbaren in den ausgeblendeten Zustand wechselt (Zeile 11). Die Methode [setUserVisibleHint] liefert diese Information;
  • Zeile 14: Das Speichern erfolgt durch die private Methode in den Zeilen 21–23;
  • Zeilen 25–41: Wenn das Gerät gedreht wird, wird die Methode [onSaveInstanceState] aufgerufen. Das Fragment wird unter zwei Bedingungen gespeichert:
    • es ist sichtbar (Zeile 34);
    • es wurde noch nicht gespeichert (Zeile 36). Es ist möglich, dass die Methoden [setUserVisibleHint] und [onSaveInstanceState] nicht beide ausgeführt werden, wenn das Fragment sichtbar ist, und dass daher die Verwaltung des booleschen Werts [saveFragmentDone] unnötig ist. Im Zweifelsfall habe ich mich dafür entschieden, ihn zu verwenden;
  • Zeile 40: Nach dem Speichern folgt das Wiederherstellen. Beachten Sie, dass das Fragment sich beim nächsten Mal, wenn es aktualisiert werden muss, über einen [RESTORE]-Vorgang aktualisiert;

Beachten Sie die beiden Momente, in denen das Speichern eines Fragments angefordert wird:

  1. wenn es vom sichtbaren in den ausgeblendeten Zustand wechselt;
  2. wenn das Gerät gedreht wird;

Die private Methode [saveState] lautet wie folgt:


...
  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();
  • Zeilen 4–7: Während asynchrone Vorgänge laufen, kann es zu einer Drehung des Geräts kommen. Hier wird entschieden, alle Vorgänge abzubrechen. Dies ist keine gute Entscheidung für den Nutzer, der eine neue, möglicherweise zeitaufwändige Anfrage stellen muss, nur weil er sein Smartphone oder Tablet bewegt hat oder einen Anruf erhalten hat. Es ist möglich, Netzwerkverbindungen durch einen Speicher-/Wiederherstellungszyklus aufrechtzuerhalten. Die Lösungen sind jedoch nicht ganz einfach, und ich habe beschlossen, sie in diesem Anfängerkurs nicht zu behandeln. Der Weg nach vorne besteht darin, diese Netzwerkverbindungen über ein Fragment herzustellen, das keine UI hat und während des Speichern/Wiederherstellen-Zyklus nicht zerstört wird. Verwenden Sie dazu einfach die Anweisung [Fragment.setRetainInstance(true)];
  • Zeile 9: Wir weisen das untergeordnete Fragment an, seinen Zustand in einem von [CoreState] abgeleiteten Typ zu speichern (Zeile 31);
  • Zeile 11: Wir vermerken, dass das Fragment aufgerufen wurde. Diese Information ist nützlich. Wenn ein Fragment zum ersten Mal aufgerufen wird, kann sich seine Aktualisierung von den nachfolgenden unterscheiden, da es keinen vorherigen Zustand in der Sitzung hat;
  • Zeile 13: Wir speichern den Zustand des Menüs, wodurch wir es automatisch wiederherstellen können;
  • Zeile 15: Dieser aktuelle Zustand wird in der Sitzung gespeichert. In der Sitzung werden Zustände nach Ansicht/Fragment gruppiert, wobei jedes einen Zustand hat. Die Ansichtsnummer wird vom untergeordneten Fragment bereitgestellt (Zeile 33);
  • Zeile 17: Wir vermerken, dass das Fragment gespeichert wurde. Dies geschieht, da zwei Methoden die Methode [saveState] aufrufen könnten und es unnötig ist, zwei Speichervorgänge durchzuführen;

Die mit dem Fragment verbundene Ansicht wird durch die folgende Methode neu generiert:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the view must be restored
    viewHasToBeInitialized = true;
}

Im Lebenszyklus wird die Methode [onActivityCreated] unmittelbar nach der Methode [onCreateView] ausgeführt. Der Aufruf der letzteren Methode bedeutet, dass die mit dem Fragment verbundene Ansicht neu erstellt werden muss. Wir vermerken dies einfach in Zeile 10.

2.7.3.8. Aktualisieren des Fragments

Das Aktualisieren des Fragments ist der letzte Vorgang, der am Fragment durchgeführt wird, bevor es sichtbar wird und auf Benutzereingaben wartet. Dies wird durch den folgenden Code abgewickelt:


  // 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();
  • Zeile 19: Die Methode [onCreateOptionsMenu] wird verwendet, um das Fragment zu aktualisieren. Aus diesem Grund muss das Fragment über ein Menü verfügen, auch wenn dieses leer ist. Wenn diese Methode ausgeführt wird, ist das Fragment bereits mit seiner Ansicht und seiner Aktivität verknüpft und zudem sichtbar;
  • Zeile 25: Das als Parameter (Zeile 22) an die Methode übergebene Menü wird gespeichert;
  • Zeilen 27–34: Falls das Fragment initialisiert werden muss:
    • Zeile 29: Die Zustände der Menüoptionen werden im Array [menuOptionsStates] aus Zeile 3 gespeichert;
    • Zeile 31: Die Aktivität wird als Instanz des Android-Typs [Activity] gespeichert;
    • Zeile 32: Die Aktivität wird als Instanz der Schnittstelle [IMainActivity] gespeichert;
    • Zeile 33: Die Sitzung wird gespeichert. Der Typumwandlung ist notwendig, da die Methode [mainActivity.getSession()] einen Typ [ISession] zurückgibt;
  • Zeile 36: Der vorherige Zustand des Fragments wird aus der Sitzung abgerufen. Wenn dies der erste Besuch des Fragments ist, ist nur der boolesche Wert [previousState.hasBeenVisited] relevant;
  • Zeilen 39–44: Code, der ausgeführt wird, wenn dies der erste Besuch des Fragments ist. In diesem Fall ist sein vorheriger Zustand nicht relevant;
  • Zeilen 44–50: Code, der ausgeführt wird, wenn dies nicht der erste Besuch des Fragments ist;
  • Zeilen 46–47: Code, der ausgeführt wird, wenn der Konstruktor des Fragments aufgerufen wurde (fragmentHasToBeInitialized == true);
  • Zeilen 48–49: Code, der ausgeführt wird, wenn die mit dem Fragment verknüpfte Ansicht neu aufgebaut wurde (viewHasToBeInitialized == true);
  • Zeilen 51–52: Code, der je nach aktueller Aktion (SUBMIT, NAVIGATION, RESTORE) ausgeführt wird;
  • Zeilen 54–55: Code, der immer ausgeführt wird;

Die fünf Schritte der Aktualisierung sind wie folgt:

Schritt 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);
  • Zeile 19: Der vorherige Zustand des Fragments wird aus der Sitzung abgerufen;
  • Zeilen 22–31: Code, der ausgeführt wird, wenn das Fragment noch nie aufgerufen wurde;
  • Zeile 27: Die untergeordnete Klasse wird aufgefordert, das Fragment zu initialisieren. Der Parameter der Methode [initFragment] in Zeile 35 ist der vorherige Zustand des Fragments. Hier wird null übergeben, um dem untergeordneten Fragment mitzuteilen, dass dies der erste Aufruf ist;
  • Zeile 28: Die untergeordnete Klasse wird aufgefordert, die mit dem Fragment verbundene Ansicht zu initialisieren. Der Parameter der Methode [initView] in Zeile 37 ist der vorherige Zustand des Fragments. Hier wird null übergeben, um dem untergeordneten Fragment anzuzeigen, dass dies der erste Besuch ist;
  • Zeile 30: Wir setzen den vorherigen Zustand für die folgenden Schritte auf null;

Schritte 2 und 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);
  • Zeilen 24–42: wird ausgeführt, wenn dies nicht der erste Besuch des Fragments ist;
  • Zeilen 27–33: Wenn das Fragment gerade neu aufgebaut wurde, wird es durch Aufruf der Methode [initFragment] der untergeordneten Klasse neu initialisiert (Zeilen 32, 46). Der vorherige Zustand des Fragments wird an diese Methode übergeben;
  • Zeilen 35–51: Wenn die mit dem Fragment verbundene Ansicht initialisiert oder zurückgesetzt werden muss, wird das untergeordnete Fragment dazu aufgefordert (Zeilen 40, 48). Auch hier wird der letzte bekannte Zustand des Fragments an das Fragment übergeben;

Schritt 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);
  • Zeilen 34–66: Wir verarbeiten die aktuelle Aktion, die eine der folgenden drei sein kann:
    • RESTORE: Wir stellen das Fragment nach einer Geräte-Drehung wieder her;
    • NAVIGATION: Wir kehren zu dem Fragment zurück, um es in dem Zustand vorzufinden, in dem wir es bei der letzten Verwendung verlassen haben;
    • SUBMIT: alle anderen Fälle;
  • Zeile 34: Abrufen der aktuellen Aktion;
  • Zeilen 36–42: Bei einer Aktion vom Typ SUBMIT rufen wir die Methode [updateOnSubmit] des untergeordneten Fragments auf (Zeilen 41, 68) und übergeben ihr den letzten bekannten Zustand des Fragments;
  • Zeilen 43–55: für eine Aktion vom Typ NAVIGATION;
  • Zeilen 47–54: Wir möchten das Fragment in seinen letzten bekannten Zustand zurücksetzen. Die NAVIGATION-Operation kann mit einem ersten Besuch zusammenfallen. Dies wäre beispielsweise in einer Anwendung mit Registerkarten der Fall: Wenn ich von Registerkarte 1 zu Registerkarte 4 wechsle:
    • muss ich das Fragment für Registerkarte 4 initialisieren, wenn dies der erste Besuch ist;
    • das Fragment von Registerkarte 4 in seinen vorherigen Zustand zurücksetzen, wenn es sich nicht um den ersten Besuch handelt;
  • Zeilen 52–54: nichts tun, wenn es sich um den ersten Besuch handelt. Die untergeordnete Methode [initView(CoreState previousState)] übernimmt diese Initialisierung. Der erste Besuch wird durch die Bedingung [previousState == null] identifiziert;
  • Zeile 49: Wenn dies nicht der erste Besuch des Fragments ist, stelle sein Menü wieder her;
  • Zeile 51: Wir fordern die untergeordnete Klasse auf, sich selbst zu aktualisieren, indem wir die Methode in Zeile 70 aufrufen. Wir übergeben ihr den vorherigen Zustand des Fragments, damit sie ihre Aufgabe erfüllen kann;
  • Zeilen 56–66: Im Falle einer Fragment-Wiederherstellung gehen wir genauso vor wie bei der Navigation außerhalb des ersten Besuchs;

Schritt 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();
  • Zeilen 18–30: Wenn wir diesen Punkt erreichen, ist das Fragment initialisiert und bereit zur Anzeige. Anschließend setzen wir alle Indikatoren, die im Lebenszyklusmanagement des Fragments verwendet werden, auf ihren Ausgangszustand zurück;
  • Zeile 20: Die Ansicht hat sich geändert; dies wird in der Sitzung vermerkt;
  • Zeile 22: Es sind keine Aktionen mehr im Gange;
  • Zeile 24: Wenn wir das aktuell angezeigte Fragment verlassen, müssen wir es beim Beenden speichern;
  • Zeile 26: Das Fragment muss nicht mehr neu aufgebaut werden. Dieses Flag wird auf „true“ gesetzt, wenn der Konstruktor des Fragments erneut ausgeführt wird;
  • Zeile 28: Die mit dem Fragment verbundene Ansicht muss nicht mehr initialisiert werden. Dieses Flag wird wieder auf „true“ gesetzt, wenn die Methode [onActivityCreated] erneut ausgeführt wird;
  • Zeile 30: Das Fragment kann in einer Anwendung mit Registerkarten angezeigt werden. In diesem Fall muss ein Fragmentwechsel erfolgen, wenn der Benutzer auf eine der Registerkarten klickt;
  • Zeile 36: Die untergeordnete Klasse wird darüber informiert, dass das Fragment bereit ist. Sie kann die Methode [notifyEndOfUpdates] verwenden, um Aktualisierungen durchzuführen, die ohnehin erforderlich wären, eine asynchrone Operation zum Abrufen neuer Daten zu starten usw.

2.7.4. Ein Beispiel für ein Fragment

  

Wir haben im Projekt [client-android-skel] ein Beispiel für ein Fragment eingefügt, um dem Leser die typische Struktur eines Fragments in einer auf diesem Projekt basierenden Anwendung zu veranschaulichen.

Die Klasse [DummyFragment] sieht wie folgt aus:


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

Die Klasse [DummyFragment] darf keinen Status haben. Hier haben wir einen hinzugefügt, um uns daran zu erinnern, was darin erwartet wird:


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
}

Um die Verwendung des Projekts [client-android-skel] zu veranschaulichen, werden wir zunächst einfache Beispiele verwenden, bevor wir zu einer umfassenderen Fallstudie übergehen.

2.8. Illustrative Übungen

Wir beginnen mit der Umgestaltung bereits geschriebener Beispiele.

2.8.1. Beispiel-17B

Wir greifen Beispiel 17 aus Abschnitt 1.18 wieder auf. Dies ist eine App mit einem einzigen Fragment, ohne asynchrone Aufgaben und ohne Registerkarten. Wir werden untersuchen, wie sie sich verhält, wenn das Gerät gedreht wird. Wir geben Folgendes ein:

Image

Dann drehen wir in [1] das Gerät zweimal. Die neue Ansicht sieht wie folgt aus:

Image

Wenn wir die Ansichten vergleichen, ist alles erhalten geblieben, mit Ausnahme der Liste [2], die nun leer ist.

Wenn Sie außerdem auf die Schaltfläche [Absenden] klicken, erscheint ein Dialogfeld, das die im Formular vorgenommenen Eingaben anzeigt. Wenn Sie das Gerät in diesem Moment drehen, verschwindet das Dialogfeld.

Daher müssen wir während einer Drehung Folgendes neu generieren:

  • die Dropdown-Liste und den darin ausgewählten Eintrag;
  • das Dialogfeld, falls es während der Drehung angezeigt wurde;

2.8.1.1. Das Projekt [Beispiel-17B]

Wir duplizieren das Projekt [client-android-skel] in examples/Example-17B. Anschließend laden wir das neue Projekt [1]:

  • In [2-3], im Ordner [behavior], fügen wir das Fragment [Vue1Fragment] aus dem Projekt [Example-17] ein;
  • in [4-5] fügen wir im Ordner [layout] von [Example-17B] die Ansicht [vue1.xml] aus [Example-17] ein. Dies ist die Ansicht, die mit dem Fragment verknüpft ist;
  • in [6] wird der Ordner [values] aus [Beispiel-17B] durch den Ordner [values] aus [Beispiel-17] ersetzt;

Wir ändern den oberen Rand der Ansicht [vue1.xml] auf 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"/>

An dieser Stelle können wir eine erste Kompilierung durchführen, um nach Fehlern zu suchen. Die ersten gemeldeten Fehler stammen von Paketimporten, die verschoben wurden. Wir beheben sie (Strg-Umschalt-O). Andere Fehler, wie z. B. , treten auf, weil die Ansicht [Vue1Fragment] nicht alle von ihrer übergeordneten Klasse [AbstractParent] geforderten Methoden implementiert:

Image

Generieren Sie die fehlenden Methoden (Alt-Enter).

Ein weiterer gemeldeter Kompilierungsfehler lautet wie folgt:

Image

Wir beheben dies in der Datei [build.gradle] des Moduls (Zeile 20 unten):

 

An dieser Stelle können wir neu kompilieren, um die verbleibenden Fehler anzuzeigen. Der einzige gemeldete Fehler betrifft die Methode [Vue1Fragment.updateFragment]:

 

Sie müssen die Annotation [@Override] aus Zeile 135 entfernen. Es gibt nun keine Fehler mehr. Wir werden dies als Ausgangspunkt für die Änderung des Projekts verwenden.

2.8.1.2. Der Zustand des [Vue1Fragment]-Fragments

Das [Vue1Fragment]-Fragment muss Informationen speichern, wenn das Gerät gedreht wird, damit es vollständig wiederhergestellt werden kann. Dazu erstellen wir eine [Vue1FragmentState]-Klasse:

  

Derzeit ist diese Klasse leer:


package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

2.8.1.3. Projektanpassung

  

Der Ordner [custom] enthält Architekturelemente, die vom Entwickler angepasst werden können.

Die Konstanten für die Schnittstelle [IMainActivity] lauten wie folgt:


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;
 
}
  • Zeilen 24–31: Die Anwendung nutzt hier ihre [DAO]-Schicht nicht. Diese Konstanten werden nicht verwendet;
  • Zeile 34: eine Fragment-Nachbarschaft von 1, was dem Standardwert entspricht. Da die Anwendung nur ein Fragment hat (Zeile 43), ist dieser Wert irrelevant;
  • Zeilen 39–40: Da keine Operationen mit der [DAO]-Ebene stattfinden, ist kein Platzhalterbild erforderlich;
  • Zeile 37: Dies ist keine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die Klasse [Session] sieht wie folgt aus:


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
 
}

Es ist leer. Da es nur ein Fragment gibt, ist es tatsächlich nicht erforderlich, die Kommunikation zwischen Fragmenten über eine Sitzung zu gewährleisten.

Schließlich sieht die Klasse [CoreState] wie folgt aus:


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
...
}
  • Zeilen 11–13: Wir müssen alle von [CoreState] abgeleiteten Klassen auflisten, die den Status der verschiedenen Fragmente speichern. Hier gibt es nur eine (Zeile 12);

2.8.1.4. Die [MainActivity]

Die [MainActivity] sieht derzeit wie folgt aus:


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

Die Kommentare [//todo] geben an, was der Entwickler tun muss. Die Klasse [MainActivity] entwickelt sich wie folgt:


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

Nur die Methode in den Zeilen 41–44 muss geändert werden. Sie muss das Array der Fragmente der App zurückgeben. Vergessen Sie in Zeile 43 nicht, den Unterstrich nach dem Fragmentnamen hinzuzufügen.

2.8.1.5. Der Fragmentstatus [FragmentState]

Nach den im Projekt [Beispiel-17] durchgeführten Rotationstests haben wir uns entschieden, die folgenden Elemente des Fragments zu speichern:

  • die Liste der Werte in der Dropdown-Liste;
  • die Position des ausgewählten Elements in dieser Liste;
  • die vom Dialogfeld angezeigte Meldung, falls diese zum Zeitpunkt der Drehung vorhanden ist;

Die Klasse [Vue1FragmentState] sieht wie folgt aus:

  

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. Das [AbstractFragment]-Fragment

Derzeit wird der Lebenszyklus des Fragments durch zwei Methoden verwaltet (Zeilen 6 und 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);
  }

Der Code für diese beiden Methoden wird wie folgt in die von der Klasse [AbstractFragment] definierten Methoden verschoben:


// 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) {
 
}
  • Zeilen 2–9: Die Methode [saveFragment] muss die zu speichernden Elemente des Fragments in eine von [CoreState] abgeleitete Klasse einfügen und eine Instanz dieser Klasse zurückgeben;
  • Zeilen 11–14: Die Methode [getNumView] muss die Fragmentnummer zurückgeben. Hier gibt es nur ein Fragment, dessen Nummer 0 ist;
  • Zeilen 16–34: Die Methode [initFragment] muss die Felder des Fragments initialisieren. Sie erhält den vorherigen Zustand des Fragments. Ist [previousState] null, handelt es sich um den ersten Aufruf;
  • Zeilen 19–25: Beim ersten Besuch werden die Werte für die Dropdown-Liste erstellt;
  • Zeilen 26–30: Wenn dies nicht der erste Besuch ist, werden die Felder [list, message] des Fragments aus dem vorherigen Zustand wiederhergestellt;
  • Zeilen 33–34: Initialisierung des Feldes [dataAdapter] des Fragments. Dies ist die Datenquelle für die Dropdown-Liste;
  • Zeilen 37–62: Die Methode [initView] dient zur Initialisierung der Komponenten der Benutzeroberfläche. Sie erhält den vorherigen Zustand [previousState] als Parameter. Ist [previousState == null], handelt es sich um den ersten Aufruf;
  • Hier sehen wir, was zuvor in der Methode [@AfterViews] stand;
  • Zeilen 57–61: Beim ersten Besuch stellen wir sicher, dass das erste Optionsfeld ausgewählt ist;
  • Zeilen 64–67: Die Methode [updateOnSubmit] wird ausgeführt, wenn die aktuelle Aktion [SUBMIT] ist. Hier findet keine fragmentübergreifende Navigation statt und es gibt daher keine aktuelle Aktion;
  • Zeilen 69–81: Die Methode [updateOnRestore] wird ausgeführt, wenn die aktuelle Aktion [NAVIGATION] oder [RESTORE] ist. Hier findet keine Navigation zwischen Fragmenten statt und daher ist keine [NAVIGATION]-Aktion möglich;
  • Zeile 72: Wir berechnen den Wert von TextView seekBarValue neu (und stellen ihn nicht wieder her). Der Grund dafür ist, dass sein Wert während der Drehungen manchmal verloren ging;
  • Zeilen 74–75: Die Liste wird auf das Element positioniert, das vor der Drehung ausgewählt war. Ohne diese Maßnahme würde die Liste standardmäßig zum ersten Element springen;
  • Zeilen 76–80: Das Dialogfeld wird erneut angezeigt, wenn die Meldung aus dem vorherigen Zustand nicht null ist. Wir kehren zur Methode [showMessage] (Zeile 79) zurück;
  • Zeilen 83–86: Die Methode [notifyEndOfUpdates] ist die letzte Methode, die von der übergeordneten Klasse aufgerufen wird, bevor das untergeordnete Fragment allein gelassen wird. Hier gibt es nichts zu tun;
  • Zeilen 88–91: Die Methode [notifyEndOfTasks] signalisiert das Ende der vom Fragment gestarteten asynchronen Aufgaben. Hier gibt es keine;

Das Dialogfeld wird wie folgt wiederhergestellt:


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

Wenn der Benutzer das Formular absendet, erstellt die Methode [doValider] (Zeile 5) eine Liste mit Meldungen, die sie anschließend (Zeile 10) im Dialogfeld anzeigt.

  • Zeilen 14–20: Die Liste der Meldungen wird zu einer einzigen Meldung verkettet, die in Zeile 2 gespeichert wird;
  • Zeilen 25–33: Dies ist die vom Dialogfeld angezeigte Meldung, und es handelt sich um dieselbe Meldung, die die Methode [updateOnRestore] anzeigt;
  • Zeile 27: Der zweite Parameter der Methode [setNeutralButton] ist die Methode, die ausgeführt wird, wenn der Benutzer im Dialogfeld auf die Schaltfläche [Schließen] klickt;
  • Zeile 31: Wenn das Dialogfeld geschlossen wird, wird die Meldung auf null gesetzt, um anzuzeigen, dass das Dialogfeld nicht mehr vorhanden ist;

2.8.1.7. Tests

Leser sind eingeladen, dieses Projekt zu testen und zu überprüfen, ob das Fragment nach einer oder mehreren aufeinanderfolgenden Drehungen erhalten bleibt.

2.8.2. Beispiel 23: Wetter-Client

Einige Websites stellen Wetterinformationen in Form von JSON-Strings bereit. Hier ist ein Beispiel:

Image

Die URL hat folgende Form: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, wobei:

  • city: die Stadt, für die Sie das Wetter wünschen, hier Angers;
  • country: das Land der Stadt, in diesem Fall Frankreich (fr);
  • APPID: ein Schlüssel, den Sie durch Registrierung auf der Website [https://home.openweathermap.org/users/sign_up] erhalten;

2.8.2.1. Das Projekt

  

Das Projekt wurde auf der Grundlage des [client-android-skel]-Projekts erstellt. Es weist folgende Merkmale auf:

  • Es verfügt über nur ein Fragment, dessen Zustand nicht verwaltet werden muss;
  • es führt asynchrone Anfragen durch;

2.8.2.2. Projektanpassung

  

Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:


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;
 
}
  • Zeilen 25, 28, 31, 40: Eigenschaften der [DAO]-Schicht. Zeile 31: Eine Basisauthentifizierung ist nicht erforderlich;
  • Zeile 34: Fragment-Adjazenz. Hier ist diese Konstante irrelevant, da es nur ein Fragment gibt;
  • Zeile 37: Dies ist keine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die [CoreState]-Klasse, die den Status der Fragmente speichert, sieht wie folgt aus:


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
...
}
  • Zeilen 10–13: Es gibt nichts zu deklarieren, da diese Anwendung nur ein Fragment hat, dessen Zustand nicht gespeichert wird;

Die Klasse [Session] sieht wie folgt aus:


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
}

Es ist leer, da in dieser Anwendung keine Kommunikation zwischen Fragmenten stattfindet.

2.8.2.3. Die [DAO]-Schicht

  

In der [DAO]-Schicht müssen drei Klassen angepasst werden:

  • die IDao-Schnittstelle;
  • die Dao-Implementierung;
  • die WebClient-Schnittstelle für die Kommunikation mit dem Webserver / JSON;

Die [WebClient]-Schnittstelle sieht wie folgt aus:


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);
}
  • Zeilen 18–19: Die URL des Wetterdienstes. Beachten Sie, dass diese relativ zur Stamm-URL des Clients (RestClientRootUrl, Zeile 12) ist. Hier lautet diese Stamm-URL [http://api.openweathermap.org/];

Die [IDao]-Schnittstelle sieht wie folgt aus:


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);
}
  • Beachten Sie, dass die Methoden in den Zeilen 6–22 standardmäßig in der IDao-Schnittstelle des Projekts [client-android-skel] enthalten sind;
  • Zeile 25: Die Methode [getWeatherForecast] ruft die JSON-Zeichenkette für das Wetter in der Stadt [city] des Landes [country] ab. Der dritte Parameter ist der Schlüssel, der von der Website [https://home.openweathermap.org/users/sign_up] bezogen wird;

Die [IDao]-Schnittstelle wird durch die folgende [Dao]-Klasse implementiert:


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);
      }
    });
  }
}
  • Beachten Sie, dass die Zeilen 17–90 standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] enthalten sind. Sie müssen lediglich die für die Anwendung spezifischen Implementierungsmethoden für die Schnittstelle [IDao] hinzufügen (Zeile 92);
  • Zeilen 93–105: Implementierung der Methode [getWeatherForecast]. Diese ist sehr einfach und nimmt 6 Zeilen ein, Zeilen 100–105;
  • Zeile 100: Die Methode [getResponse] ist eine Methode der übergeordneten Klasse [AbstractDao]. Sie erwartet einen Parameter vom Typ [IRequest<T>], wobei T der Typ der erwarteten Antwort vom Server ist; hier ist es ein String, da wir einen JSON-String erwarten. Der Typ T von [IRequest<T>] muss der Typ T der Methode [Observable<T> getWeatherForecast] sein;
  • die Schnittstelle [IRequest<T>] hat nur eine Methode: getResponse. Ihre Aufgabe ist es, die Antwort vom Typ T bereitzustellen, die die Methode [Observable<T> getWeatherForecast] zurückgeben muss;
  • Zeile 103: Es ist die Schnittstelle [WebClient], die diese Antwort bereitstellt. Wir übergeben ihr die drei in Zeile 94 empfangenen Parameter. Aus diesem Grund müssen diese das Attribut final aufweisen;

2.8.2.4. Die [MainActivity]

  

Die [MainActivity]-Aktivität sieht wie folgt aus:


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);
  }
}
  • Beachte, dass die Zeilen 15–55 standardmäßig im Projekt [client-android-skel] enthalten sind. Du musst sie lediglich anpassen;
  • Zeilen 37–40: das Fragment-Array. Hier gibt es nur eines;
  • Zeilen 43–46: Es sind keine Fragmenttitel erforderlich;
  • Zeilen 48–50: Hier gibt es keine Registerkarten;
  • Zeilen 52–55: Die erste anzuzeigende Ansicht ist Ansicht Nr. 0, die von [MeteoFragment];
  • Zeilen 58–61: Implementierung der [IDao]-Schnittstelle. Hier gibt es nichts weiter zu tun, als die Arbeit in Zeile 21 an die [DAO]-Schicht zu delegieren;

2.8.2.5. Das [MeteoFragment]-Fragment

  

Das [MeteoFragment] fragt den Wetter-Webdienst / JSON ab. Sein Grundgerüst sieht wie folgt aus:


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 {
...
}
  • Zeile 14: Die Ansicht [res/layout/meteo_fragment.xml] sieht wie folgt aus:

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

Die Ansicht zeigt nur den Text ab Zeile 10 an;

  • Zeile 15: Das Menü [res / menu / menu_meteo.xml] sieht wie folgt aus:

<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>
  • Zeilen 10–12: Diese Menüoption dient dazu, die Wettervorhersage für eine Stadt abzufragen;
  • Zeilen 14–15: Diese Menüoption dient dazu, die Anfrage abzubrechen, falls sie gerade läuft;
  • Zeilen 16–18: Diese Menüoption schließt die Anwendung;

Der vollständige Code für das Fragment lautet wie folgt:


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 ---------------------------------------------------------------------------------------
...
}
  • Zeilen 25–50: Verarbeitung des Klicks auf die Menüoption [Wetter];
  • Zeile 32: Erstellung der Webservice-URL / JSON für den Wetterdienst. Diese wird dann über die Aktivität an die [DAO]-Schicht übergeben;
  • Zeile 34: Wir starten die Wartezeit. Wir übergeben die Anzahl der zu startenden Aufgaben, damit die übergeordnete Klasse uns benachrichtigen kann, wenn diese abgeschlossen sind. Hier gibt es fünf Aufgaben, da wir das Wetter für die fünf in Zeile 23 aufgeführten Städte abfragen;
  • Zeile 16: Wir zählen die Anzahl der empfangenen Antworten, damit wir sie anzeigen können;
  • Zeilen 38–50: Wir durchlaufen die Städte, für die wir das Wetter abfragen möchten;
  • Zeile 40: Wir stellen 5 HTTP-Anfragen parallel;
  • Zeile 40: Wir bitten die übergeordnete Klasse [AbstractParent], den Webservice / JSON abzufragen;
  • Zeilen 40–48: Die Methode [executeInBackground] erwartet zwei Parameter:
    • Zeile 40: Der zu beobachtende und auszuführende Prozess wird von der Methode [mainActivity.getWeatherForecast] bereitgestellt;
    • Zeilen 40–48: die Instanz [Action1], die ausgeführt werden soll, wenn die Antwort vom asynchronen Dienst empfangen wird. Der Typ T von [Action1<T>] muss der Typ T des Ergebnisses der Methode [getWeatherForecast] sein;
  • Zeile 44: Eine Antwort wurde empfangen. Sie wird an die Methode [consumeResponse] in Zeile 53 übergeben;
  • Zeile 46: Der Zähler für empfangene Antworten wird erhöht;
  • Zeilen 53–56: Verarbeiten einer JSON-Antwort vom Wetterdienst;
  • Zeile 55: Wir protokollieren einfach die JSON-Zeichenkette;
  • Zeilen 59–72: Code, der vor dem Start der asynchronen Aufgaben ausgeführt wird;
  • Zeile 65: Wir übergeben die Anzahl der auszuführenden Aufgaben an die übergeordnete Klasse [AbstractParent]. Dadurch kann diese uns benachrichtigen, wenn alle Aufgaben abgeschlossen sind;
  • Zeilen 67–70: Vorbereitung des Menüs für eine Wartephase. Wir behalten nur die Option [Actions/Cancel] bei, mit der der Benutzer die gestarteten Aufgaben abbrechen kann;
  • Zeilen 74–92: Code, der ausgeführt wird, wenn die übergeordnete Klasse uns mitteilt, dass alle gestarteten Aufgaben abgeschlossen sind;
  • Zeile 77: Wir setzen das Menü auf seinen Ausgangszustand zurück. Die Methode [initMenu] (Zeilen 95–102) zeigt das Menü mit allen Optionen an, mit Ausnahme der Option [Actions/Cancel], die ausgeblendet ist;
  • Zeilen 80–91: Die Anzahl der empfangenen Antworten wird angezeigt;

Das Klicken auf die Menüoption [Abbrechen] wird durch den folgenden Code verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchronous tasks are cancelled
    cancelRunningTasks();
}
  • Zeile 7: Wir bitten die übergeordnete Klasse, die noch aktiven Aufgaben abzubrechen;

Das Klicken auf die Menüoption [Fertigstellen] wird durch den folgenden Code verarbeitet:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // we stop everything
    System.exit(0);
}

Der Lebenszyklus des Fragments wird durch die folgenden Methoden verwaltet:


  // 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() {
 
}
  • Zeilen 3–6: Dienen dazu, den Status des Fragments in einer von [CoreState] abgeleiteten Klasse zu speichern. Wenn das Fragment keinen Status zu speichern hat, wie in diesem Fall, geben wir einfach eine Instanz von [CoreState] zurück. Geben Sie nicht null zurück, da dies letztendlich zu einem Absturz führen würde;
  • Zeilen 8–11: Hier muss die View-ID zurückgegeben werden. In diesem Fall hat das [MeteoFragment] die ID 0;
  • Zeilen 13–16: dienen dazu, das Fragment zu initialisieren, sobald es erstellt (previousState == null) oder neu erstellt (previousState != null) wurde. Hier gibt es nichts zu tun. Das einzige Feld, das initialisiert werden kann, ist das folgende:

  // villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

aber es initialisiert sich selbst;

  • Zeilen 18–24: dienen dazu, die mit dem Fragment verbundene Ansicht zu initialisieren, sobald es erstellt (previousState == null) oder neu erstellt (previousState != null) wurde;
  • Zeilen 21–23: Wenn dies der erste Besuch des Fragments ist, wird dessen Menü so initialisiert, dass die Option [Abbrechen] ausgeblendet wird;
  • Zeilen 27–30: werden aufgerufen, wenn die Navigation zum Fragment eine [SUBMIT]-Aktion beinhaltete. Hier findet keine Navigation zwischen Fragmenten statt, da es nur ein Fragment gibt;
  • Zeilen 32–35: Wird während eines Speicher-/Wiederherstellungszyklus aufgrund einer Geräte-Drehung oder aus einem anderen Grund aufgerufen. Da hier kein Status gespeichert wurde, gibt es nichts zu tun;
  • Zeilen 37–40: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts zu tun;

2.8.2.6. Tests

Wir führen nun das Beispiel aus:

Image

Image

Die Protokolle lauten wie folgt:


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
  • Zeilen 32–36: JSON-Antworten werden auf I/O-Threads abgerufen
  • Zeilen 37–41: Das Fragment ruft die 5 Antworten im UI-Thread ab;

Nun senden wir die Anfrage mit einer falschen API-ID:


    String APIID = "";

Image

Die Protokolle sehen dann wie folgt aus:


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"]]
  • Zeilen 3–6, 10: Die 5 HTTP-Aufrufe haben 5 Ausnahmen generiert;
  • Zeile 7: Das Fragment [MeteoFragment] empfängt die erste Ausnahme. Es bricht daraufhin alle Aufgaben ab;

Legen wir nun ein 5-Sekunden-Timeout [IMainActivity.DELAY] fest und brechen den Vorgang ab. Die Protokolle sehen dann wie folgt aus:


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]]
  • Zeile 3: Abbruchanforderung;
  • Zeile 4: Das Warten wird abgebrochen, da eine Stornierung stattgefunden hat;
  • Zeilen 6–10: Das Abbrechen der Aufgaben löst in jedem der fünf Aufgabenthreads eine Ausnahme aus. Der Ausnahmetyp hängt von den Anwendungen ab. Die Ausnahme hier ist [java.lang.InterruptedException], da die Aufgaben während der Ausführung der Anweisung [Thread.sleep(delay)] unterbrochen wurden, wodurch sie künstlich für [delay] Millisekunden warten;

2.8.3. Beispiel 16B

Hier refaktorisieren wir Beispiel 16 aus Abschnitt 1.17. Es zeigt einen Ausschnitt, der asynchrone Aufrufe an einen Zufallszahlenserver durchführt. Schauen wir uns an, wie er sich bei einer Geräte-Drehung verhält:

Image

  • In [1] wird das Gerät zweimal gedreht;

Image

Wir sehen, dass alle Fehlermeldungen verloren gegangen sind. Wir werden versuchen, dies zu verbessern.

2.8.3.1. Das Projekt „Example-16B“

Wir kopieren das Projekt [client-android-skel] in das Projekt [examples/Example-16B] und laden dann das neue Projekt:

  

Aus dem ursprünglichen Projekt [Beispiel-16] kopieren wir die folgenden Elemente in [Beispiel-16B]:

  • die Datei [res/layout/vue1.xml], den Ordner [res/values]:
  

Wir ändern den oberen Rand der Ansicht [vue1.xml] auf 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" />
  • das Fragment [View1Fragment]:
  
  • die Klasse [DAO / Service / Response]:
  

An dieser Stelle können wir einen ersten Kompilierungsversuch unternehmen:

  • Die erste Art von Fehlern betrifft Importe. Einige Klassen wurden während der Migration zu [Beispiel-16B] in andere Pakete verschoben. Wir beginnen damit, diese Fehler zu beheben;
  • Eine zweite Art von Fehler wird für die Klasse [Vue1Fragment] gemeldet, da sie die von der übergeordneten Klasse [AbstractParent] geforderten Methoden nicht implementiert. Wir generieren diese Methoden automatisch;

Wir versuchen eine zweite Kompilierung:

  • Alle verbleibenden Fehler konzentrieren sich nun auf die Klasse [Vue1Fragment], die die meisten Änderungen erfahren wird;

2.8.3.2. Erstellen eines Zustands für das Fragment [Vue1Fragment]

Wir haben gesehen, dass bestimmte Informationen aus dem Fragment während einer Drehung gespeichert werden müssen, um das Fragment in seinen Zustand vor der Drehung zurückzusetzen. Wir erstellen daher einen [Vue1FragmentState]-Zustand, der vorerst leer ist:

  

package client.android.fragments.state;
 
import client.android.architecture.custom.CoreState;
 
public class Vue1FragmentState extends CoreState {
 
}

2.8.3.3. Projektanpassung

  

Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:


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;
 
}
  • Zeilen 25, 28, 31, 40: Eigenschaften der [DAO]-Schicht. Eine Basisauthentifizierung ist nicht erforderlich;
  • Zeile 34: Fragment-Nachbarschaft. Hier ist diese Konstante irrelevant, da es nur ein Fragment gibt;
  • Zeile 37: Dies ist keine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die [CoreState]-Klasse, die den Status der Fragmente speichert, sieht wie folgt aus:


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
...
}
  • Zeile 12: Wir deklarieren die Fragment-State-Klasse [Vue1Fragment];

Die Klasse [Session] sieht wie folgt aus:


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
}

Sie ist leer, da es in dieser Anwendung keine Kommunikation zwischen Fragmenten gibt.

2.8.3.4. Die [DAO]-Schicht

  

In der [DAO]-Schicht müssen drei Klassen angepasst werden:

  • die IDao-Schnittstelle;
  • die Dao-Implementierung;
  • die WebClient-Schnittstelle für die Kommunikation mit dem Webserver / JSON;

Die Klasse [Response] stammt aus dem Projekt [Example-16], das sie verwendet:


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

Die [WebClient]-Schnittstelle sieht wie folgt aus:


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);
 
}
  • Zeilen 18–19: Die URL für den Zufallszahlendienst. Beachten Sie, dass diese URL relativ zur Stamm-URL des Clients (RestClientRootUrl, Zeile 12) ist. Hier lautet die Stamm-URL [http://localhost:8080];

Die [IDao]-Schnittstelle sieht wie folgt aus:


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);
 
}
  • Beachten Sie, dass die Methoden in den Zeilen 6–22 standardmäßig in der IDao-Schnittstelle des Projekts [client-android-skel] vorhanden sind;
  • Zeile 25: Die Methode [getAlea] gibt eine Zufallszahl im Bereich [a,b] zurück. Diese Zahl wird in einer [Response<Integer>]-Antwort zurückgegeben, wobei die Zufallszahl im Feld [body] dieses Typs enthalten ist;

Die [IDao]-Schnittstelle wird durch die folgende [Dao]-Klasse implementiert:


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);
      }
    });
  }
 
}
  • Beachten Sie, dass die Zeilen 17–85 standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] enthalten sind. Sie müssen lediglich die Methoden hinzufügen, um die Schnittstelle [IDao] zu implementieren;
  • Zeilen 88–97: Implementierung der Methode [getAlea]. Dies ist sehr einfach und nimmt 6 Zeilen ein, Zeilen 91–96;
  • Zeile 91: Die Methode [getResponse] ist eine Methode der übergeordneten Klasse [AbstractDao]. Sie erwartet einen Parameter vom Typ [IRequest<T>], wobei T der Typ der erwarteten Antwort ist, in diesem Fall ein Response<Integer>-Typ. Der Typ T von [IRequest<T>] (Zeile 91) muss der Typ T der Methode [Observable<T> getAlea] (Zeile 89) sein;
  • Die Schnittstelle [IRequest<T>] hat nur eine Methode: getResponse. Ihre Aufgabe ist es, die Antwort vom Typ T bereitzustellen, die die Methode [Observable<T> getAlea] zurückgeben muss;
  • Zeile 94: Es ist die Schnittstelle [WebClient], die diese Antwort bereitstellt. Ihr werden die beiden in Zeile 89 empfangenen Parameter übergeben. Aus diesem Grund müssen diese das Attribut final aufweisen;

2.8.3.5. Die [MainActivity]

  

Die [MainActivity]-Aktivität sieht wie folgt aus:


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);
  }
 
}
  • Beachte, dass die Zeilen 15–61 standardmäßig im Projekt [client-android-skel] enthalten sind. Du musst sie lediglich anpassen;
  • Zeilen 40–44: das Fragment-Array. Hier gibt es nur eines;
  • Zeilen 47–51: Es sind keine Fragmenttitel erforderlich;
  • Zeilen 53–56: Hier gibt es keine Registerkarten;
  • Zeilen 58–61: Die erste anzuzeigende Ansicht ist Ansicht Nr. 0, die von [Vue1Fragment];
  • Zeilen 64–67: Implementierung der [IDao]-Schnittstelle. Hier gibt es nichts weiter zu tun, als die Arbeit in Zeile 23 an die [DAO]-Schicht zu delegieren;

2.8.3.6. Der Zustand des Fragments [Vue1Fragment]

  

Die Klasse [Vue1FragmentState] sieht wie folgt aus:


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

Um festzustellen, was im Fragment gespeichert werden musste, drehten wir das Gerät in verschiedenen Situationen und beobachteten, was bei der Wiederherstellung verloren ging. Wir kamen zu dem Schluss, dass die Informationen in den Zeilen 10–23 gespeichert werden mussten.

2.8.3.7. Das Fragment [View1Fragment]

  

Derzeit enthält die Ansicht [Vue1Fragment] verschiedene Fehler, da sich die übergeordnete Klasse [AbstractFragment], von der sie abgeleitet ist, geändert hat. Anstatt die vorzunehmenden Änderungen einzeln zu beschreiben, werden wir direkt auf die endgültige Version eingehen.

Das Grundgerüst des Fragments sieht wie folgt aus:


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 {
 
...
}
  • Zeile 26: Beachten Sie, dass jedes Fragment ein Menü haben muss, auch wenn es leer ist. Dies ist hier der Fall.

2.8.3.7.1. Behandlung des Klicks auf die Schaltfläche [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);
  }
  • Zeilen 4–6: Zunächst prüfen wir, ob die Einträge gültig sind. Es können dann Fehlermeldungen erscheinen;
  • Zeilen 8–9: Die Liste der Antworten wird gelöscht. Diese Änderung wird in der ListView übernommen, die sie anzeigt;
  • Zeilen 11–12: Die Anzahl der empfangenen Antworten wird auf Null zurückgesetzt;
  • Zeile 14: Wir legen die URL für den Zufallszahlendienst fest. Diese Information wird an die [DAO]-Schicht weitergeleitet;
  • Zeile 15: Die Zeitüberschreitung vor dem Senden der Anfrage an den Zufallszahlendienst wird festgelegt. Diese Information wird an die [DAO]-Schicht weitergeleitet;
  • Zeile 17: Wir bereiten den Start einer asynchronen Aufgabe vor (nicht N; wir werden sehen, warum);
  • Zeilen 24–27: Wir fassen die N asynchronen Aufgaben zu einer einzigen Abfolge von Operationen zusammen [merge];
  • Zeilen 29–36: Wir weisen die übergeordnete Klasse [AbstractParent] an, den Zufallszahlen-Webdienst / JSON abzufragen;
  • Zeilen 29–36: Die Methode [executeInBackground] erwartet zwei Parameter:
    • Zeile 29: Der zu beobachtende und auszuführende Prozess ist der in den vorangegangenen Zeilen berechnete;
    • Zeilen 29–36: die Instanz [Action1], die ausgeführt werden soll, wenn die Antwort vom asynchronen Dienst empfangen wird. Der Typ T von [Action1<T>] muss der Typ T des Ergebnisses der Methode [getAlea] sein, d. h. ein Typ [Response<Integer>];
  • Zeile 34: Wenn eine Antwort eintrifft (eine Zufallszahl), wird sie in der Methode in Zeile 39 verarbeitet;
  • Zeilen 49–50: Wir protokollieren und melden, dass eine neue Antwort empfangen wurde;
  • Zeilen 53–60: Der Typ [Response<T>] verfügt über ein Feld [status], das einen Fehlercode enthält. Ist dieser Code ungleich Null, ist beim Server ein Problem aufgetreten;
  • Zeile 55: Es wird eine Fehlermeldung angezeigt. Die Methode [showAlert] gehört zur übergeordneten Klasse;
  • Zeile 57: Die Methode in den Zeilen 68–75 wird aufgerufen. Sie bricht alle noch aktiven Aufgaben ab (Zeile 74);
  • Zeile 62: Die Antwort wird der Liste der Antworten hinzugefügt, die die Datenquelle für die ListView darstellt;
  • Zeile 64: Die ListView wird aktualisiert;
  • Zeilen 77–83: Die Methode [beginWaiting(int nbRunningTasks)] bereitet die Ansicht auf das Warten vor (Zeilen 81–82) und benachrichtigt die übergeordnete Klasse, dass [nbRunningTasks] Aufgaben bald ausgeführt werden (Zeile 79);

2.8.3.7.2. Der Lebenszyklus des Fragments

Der Lebenszyklus des Fragments wird durch die folgenden Methoden verwaltet:


  // 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);
 
}
  • Zeilen 7–18: Stellen sicher, dass das Fragment gespeichert wird, wenn die übergeordnete Klasse dies anfordert;
  • Zeile 11: Zeigt die Fehlermeldung bezüglich des Timeouts an;
  • Zeile 12: Anzeige der Fehlermeldung bezüglich der Anzahl der angeforderten Zufallszahlen;
  • Zeile 13: Anzeige der Fehlermeldung bezüglich der Webservice-URL / JSON;
  • Zeile 14: Zeigt die Fehlermeldung bezüglich des Bereichs [a,b] für die Zufallszahlengenerierung an;
  • Zeile 15: Sichtbarkeit der Schaltfläche [Ausführen];
  • Zeile 16: die Liste der empfangenen Antworten;
  • Zeilen 20–23: muss die View-ID zurückgeben. Die Fragment-ID ist hier 0, da es nur eine gibt;
  • Zeilen 25–38: Initialisierung der Felder des Fragments, entweder beim ersten Besuch (previousState == null) oder bei einem späteren Besuch;
    • Zeilen 29–30: Wenn dies nicht der erste Besuch ist, wird das Feld [reponses] aus dem vorherigen Zustand des Fragments wiederhergestellt;
    • Zeilen 31–33: Wenn dies der erste Besuch ist, wird das Feld [reponses] mit einer leeren Liste initialisiert;
    • Zeilen 34–37: Mithilfe des Felds [reponses] können wir die Datenquelle für die ListView des Fragments (Zeile 35) sowie die Anzahl der Antworten (Zeile 37) erstellen;
  • Zeilen 40–55: werden ausgeführt, um die mit dem Fragment verbundene Ansicht zu initialisieren, entweder beim ersten Besuch (previousState == null) oder bei einem späteren Besuch;
    • Zeile 43: Die ListView des Fragments wird an die Datenquelle gebunden, die gerade in der Methode [initFragment] erstellt wurde;
    • Zeilen 45–54: Wenn dies der erste Besuch ist, wird die Ansicht für ihre erste Anzeige vorbereitet;
  • Zeilen 57–60: werden während der fragmentübergreifenden Navigation im Zusammenhang mit einer [SUBMIT]-Aktion ausgeführt. Hier gibt es nur ein Fragment und daher keine fragmentübergreifende Navigation;
  • Zeilen 63–76: Wird während der Navigation zwischen Fragmenten im Zusammenhang mit einer [NAVIGATION]-Aktion oder während eines Speicher-/Wiederherstellungszyklus aufgrund einer Geräte-Drehung oder aus einem anderen Grund ausgeführt. Hier kann nur der letztere Fall eintreten. Beachten Sie, dass hier in allen Fällen [previousState] immer nicht null ist;
  • Zeile 65: Der vorherige Zustand wird in den Fragment-Zustandstyp umgewandelt;
  • Zeilen 66–75: Der Inhalt des vorherigen Zustands wird zum Wiederherstellen der Ansicht verwendet;
  • Zeilen 78–81: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts zu tun;
  • Zeilen 83–89: Wird ausgeführt, wenn alle asynchronen Aufgaben abgeschlossen sind. Hier wird die Schaltfläche [Abbrechen] ausgeblendet und durch die Schaltfläche [Ausführen] ersetzt;

2.8.3.8. Tests

Der Leser ist eingeladen, die folgenden Tests durchzuführen:

  • Erzeugen Sie Fehler und führen Sie das Gerät aus: Die Fehlermeldungen müssen weiterhin angezeigt werden;
  • Zufallszahlen generieren und das Gerät ausführen: Die generierten Zufallszahlen müssen weiterhin angezeigt werden;
  • Stellen Sie eine Wartezeit von mehreren Sekunden ein und lassen Sie das Gerät während der Wartezeit laufen: Die Aufgaben müssen abgebrochen worden sein (dies ist in den Protokollen ersichtlich);

2.8.4. Beispiel-22B

Hier greifen wir Beispiel 22 wieder auf, um es gemäß dem [client-android-skel]-Projektmodell umzugestalten. Erinnern Sie sich daran, dass das [Beispiel-22]-Projekt den Fragment-Speicher-/Wiederherstellungszyklus während der Drehung korrekt handhabt und dass es als Grundlage für das [client-android-skel]-Projekt diente.

Wir duplizieren das Projekt [client-android-skel] in [examples/Example-22B] und laden das letztere Projekt:

  

Anschließend kopieren wir verschiedene Elemente aus dem Projekt [Beispiel-22] in das Projekt [Beispiel-22B].

Zunächst kopieren wir Elemente aus dem Ordner [res]:

  • [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, den Ordner [values];
  

Wir ändern den oberen Rand beider Ansichten auf 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"/>

Als Nächstes kopieren wir die Elemente [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

An dieser Stelle können wir einen ersten Kompilierungsversuch unternehmen. Es tritt eine erste Fehlerart auf: falsche Importe, da sich die Pakete der Klassen geändert haben. Wir korrigieren diese Importe. Eine zweite Fehlerart ist darauf zurückzuführen, dass die Fragmente nicht alle Methoden ihrer übergeordneten Klasse [AbstractFragment] implementieren. Wir beheben dies durch Drücken von (Alt+Enter).

Die verbleibenden Fehler resultieren aus Unterschieden zwischen der alten und der neuen Klasse [AbstractFragment]. Diese ignorieren wir vorerst.

2.8.4.1. Projektanpassung

  

Der Ordner [custom] enthält Architekturelemente, die vom Entwickler angepasst werden können.

Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:


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;
 
}
  • Zeilen 23, 26, 29, 38: Merkmale der [DAO]-Schicht. Hier gibt es keine;
  • Zeile 41: Hier gibt es fünf Fragmente;
  • Zeile 32: Fragment-Nachbarschaft. Diese Konstante kann hier einen Wert zwischen [1,4] annehmen. Der Leser wird dazu ermutigt, diesen Wert zu variieren, um zu sehen, ob die Anwendung weiterhin funktioniert;
  • Zeile 35: Dies ist eine Anwendung mit Registerkarten;

Die Klasse [CoreState], die den Zustand der Fragmente speichert, sieht wie folgt aus:


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
...
}
  • Zeile 12: Wir deklarieren die Fragment-State-Klasse [PlaceHolderFragment]. Das Fragment [Vue1Fragment] selbst hat keinen State;

Die Klasse [Session] sieht wie folgt aus:


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

Dies ist die Sitzung für das Projekt [Beispiel-22].

2.8.4.2. Die [MainActivity]

  

Die [MainActivity]-Aktivität sieht wie folgt aus:


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

Hier ist die Klasse [MainActivity] aus zwei Gründen umfangreicher als in den vorherigen Beispielen:

  • Es müssen Registerkarten verwaltet werden;
  • es gibt ein Menü zu verwalten;

2.8.4.2.1. Implementierung der Methoden der übergeordneten Klasse

// 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;
  }
  • Zeilen 2–12: Die Methode [onCreateActivity] wird von der übergeordneten Klasse [AbstractActivity] aufgerufen, wenn die Aktivität zum ersten Mal erstellt oder während eines Speicher-/Wiederherstellungszyklus neu erstellt wird. Wenn diese Methode aufgerufen wird, hat die übergeordnete Klasse die Sitzung bereits wiederhergestellt;
  • Zeile 10: Es wird eine lokale Referenz auf die Sitzung abgerufen. Der Typumwandlung ist notwendig, da die Sitzung der übergeordneten Klasse vom Typ [AbstractSession] ist;
  • Zeilen 19–38: Die Methode [getFragments] muss der übergeordneten Klasse das Array der von der Anwendung verwalteten Fragmente zurückgeben. Hier gibt es [FRAGMENTS_COUNT] Fragmente, eine in [IMainActivity] definierte Zahl. Die ersten [FRAGMENTS_COUNT-1] Fragmente sind vom Typ [PlaceHolderFragment] und das letzte ist vom Typ [Vue1Fragment];
  • Zeilen 41–45: Die Methode [getFragmentTitle] muss die Fragmenttitel zurückgeben, wenn diese Informationen nützlich sind. Dies ist hier nicht der Fall;
  • Zeilen 47–50: Diese Methode wird von der übergeordneten Klasse aufgerufen, wenn der Benutzer auf eine Registerkarte klickt. Wir werden im nächsten Abschnitt darauf zurückkommen;
  • Zeilen 52–55: Gibt die Nummer der ersten Ansicht zurück, die beim Start der Anwendung angezeigt werden soll. Hier muss das Fragment [Vue1Fragment] zuerst angezeigt werden. Die Methode [getFirstView] könnte vorteilhaft durch eine Konstante in [IMainActivity] ersetzt werden;

2.8.4.2.2. Tab-Verwaltung

Registerkarten werden mit den folgenden Methoden verwaltet:


@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);
    }
  }
}
  • Zeilen 1–20: Die Methode [onCreateActivity] wird von der übergeordneten Klasse [AbstractActivity] aufgerufen, wenn die Aktivität zum ersten Mal erstellt oder während eines Speicher-/Wiederherstellungszyklus neu erstellt wird. Wenn diese Methode aufgerufen wird, hat die übergeordnete Klasse die Sitzung bereits wiederhergestellt;
  • Zeile 9: Es wird eine lokale Referenz auf die Sitzung abgerufen. Der Typumwandlung ist notwendig, da die Sitzung der übergeordneten Klasse vom Typ [AbstractSession] ist;
  • Zeilen 11–13: Die erste Registerkarte wird erstellt;
  • Zeilen 15–20: Die zweite Registerkarte wird erstellt, wenn eine Fragment-ID in der Sitzung gespeichert ist (Zeile 15). Diese ID wird beim ersten Aufbau der Aktivität zunächst auf -1 gesetzt;
  • Zeilen 23–39: Diese Methode wird von der übergeordneten Klasse aufgerufen, wenn der Benutzer auf eine Registerkarte klickt;
  • Zeilen 28–31: Wenn auf Registerkarte 0 geklickt wird, muss [Vue1Fragment] angezeigt werden. Wir wissen, dass dies die erste Ansicht ist, die beim Start der Anwendung angezeigt wurde;
  • Zeilen 32–35: Wenn auf Registerkarte 1 geklickt wird, muss das Fragment angezeigt werden, dessen Nummer in der Sitzung gespeichert ist;
  • Zeilen 37–39: Wir navigieren zum ausgewählten Fragment. Die zugehörige Aktion ist [SUBMIT]. Hätte es auch [NAVIGATION] sein können? In diesem Dokument verwenden wir [NAVIGATION] nur dann, wenn die Anzeige des neuen Fragments lediglich die Kenntnis seines vorherigen Zustands erfordert. Hier ist das nicht der Fall, da sich die Anzeige des Fragments gegenüber dem vorherigen Zustand ändern muss, um einen weiteren Besuch anzuzeigen;

2.8.4.2.3. Menüverwaltung

Die Aktivität ist mit dem folgenden Menü [menu_main.xml] verknüpft:


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

was Folgendes anzeigt:

  

Das Menü wird über die folgenden Methoden verwaltet:


@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();
    }
  }
  • Zeilen 16–31: Verarbeiten eines Klicks auf eine Menüoption [Fragmenti];
  • Zeilen 37–50: Anzeigen von Fragment #i (es handelt sich um Fragmente vom Typ „PlaceHolderFragment“) in Registerkarte #1 (der zweiten Registerkarte);
  • Zeilen 42–44: Wir entscheiden uns, die vorhandenen Registerkarten zu entfernen, um zwei neue zu erstellen. Diese Entscheidung wurde getroffen, um das folgende Problem zu umgehen: Wenn wir das Fragment einfach in der vorhandenen Registerkarte 1 anzeigen (ohne sie zu löschen), erscheint dessen Titel seltsamerweise (Schriftart, Größe) anders als der Titel von Registerkarte 0;
  • Zeilen 43–44: Die beiden Registerkarten werden erstellt, aber nicht ausgewählt (letzter Parameter auf „false“ gesetzt);
  • Zeile 40: Die Vorgänge in den Zeilen 42–44 können [select]-Operationen auf den Registerkarten auslösen, wodurch der [onTabSelected]-Handler aufgerufen wird. Wenn keine Aktion durchgeführt wird, führt dies zur Navigation zu einem Fragment. Wir verhindern dies, indem wir den booleschen Wert [navigationOnTabSelectionNeeded] in der Sitzung auf „false“ setzen. Dieser boolesche Wert wird von der Klasse [AbstractFragment] automatisch auf „true“ zurückgesetzt, sobald ein Fragment sichtbar wird;
  • Zeile 46: Wir speichern die Nummer des anzuzeigenden Fragments in der Sitzung;
  • Zeilen 48–50: Wählen Sie Registerkarte Nr. 2 mit Navigation aus (Zeile 48). Dies löst die Prozedur [onTabSelected] aus, die Folgendes bewirkt:
    • das Fragment anzeigt, dessen Nummer in der Sitzung gespeichert wurde;
    • die Nummer der ausgewählten Registerkarte in der Sitzung speichert;

2.8.4.3. Das Fragment [Vue1Fragment]

Hier ist die endgültige Version des Fragments:


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

Die Klasse ist fast leer.

  • Zeilen 35–39: Wird von der übergeordneten Klasse aufgerufen, wenn das Fragment seinen Zustand speichern muss. Das Fragment [Vue1Fragment] hat keinen Zustand, der gespeichert werden muss. Wir geben einfach eine Instanz der Basisklasse [CoreState] zurück (zur Erinnerung: Wir dürfen nicht null zurückgeben);
  • Zeilen 41–44: Muss die Fragment-ID zurückgeben. Das Fragment [Vue1Fragment] hat standardmäßig die ID [FRAGMENTS_COUNT-1];
  • Zeilen 51–59: Wird von der übergeordneten Klasse aufgerufen, wenn das Fragment zum ersten Mal erstellt wird (previousState == null) oder bei nachfolgenden Aufrufen (previousState != null);
    • Zeilen 54–57: Wenn dies der erste Besuch ist, wird die Besuchsanzahl erhöht und angezeigt (Zeilen 85–92);
  • Zeilen 61–65: Wird aufgerufen, wenn das Fragment im Zusammenhang mit einer [SUBMIT]-Aktion angezeigt werden soll. Die Besuchsanzahl wird erhöht und angezeigt. Hier ist es nicht möglich, dass die Besuchsanzahl während des Lebenszyklus zweimal erhöht wird. Tatsächlich erfolgt der erste Besuch des Fragments [Vue1Fragment] beim Start der Anwendung, wenn die Aktion in der Sitzung designmäßig auf [NONE] gesetzt ist. Dies stellt sicher, dass die Methode [updateOnSubmit] nicht aufgerufen wird. Danach wird es nie wieder der erste Besuch sein, und die Methode [initView] führt keine Aktion aus;
  • Zeilen 68–71: Wird während eines Speicher-/Wiederherstellungszyklus aufgerufen. Da das Fragment keinen Status hat, gibt es hier nichts wiederherzustellen;
  • Zeilen 73–76: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts mehr zu tun;
  • Zeilen 78–81: Wird aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;

2.8.4.4. Der Zustand [PlaceHolderFragmentState]

Der Zustand des [PlaceHolderFragment]-Fragments ist wie folgt:


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
 ...
}
  • Wenn wir den Status des Fragments speichern müssen, speichern wir den angezeigten Text (Zeile 7);

2.8.4.5. Das [PlaceHolderFragment]-Fragment

Das [PlaceHolderFragment]-Fragment sieht wie folgt aus:


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) {
 
  }
 
}
  • Zeilen 30–36: Wenn die übergeordnete Klasse das Fragment auffordert, seinen Zustand zu speichern, wird der vom Fragment angezeigte Text gespeichert (Zeile 34);
  • Zeilen 38–41: Gibt die ID des Fragments zurück. Diese hängt von der Abschnitts-ID ab, die bei der Erstellung des Fragments als Argument übergeben wurde;
  • Zeilen 43–47: Wird beim ersten Aufbau des Fragments (previousState == null) oder bei nachfolgenden Aufbauten (previousState != null) aufgerufen;
    • Zeile 46: Hier wird der vorherige Zustand nicht verwendet. Der beim ersten Aufruf angezeigte Ausgangstext [text] (Zeile 24) wird jedes Mal neu berechnet. Dies ist umstritten. Wir hätten uns dafür entscheiden können, diese Information ebenfalls in den Zustand des Fragments aufzunehmen;
  • Zeilen 49–51: Wird beim ersten Rendern der mit dem Fragment verbundenen Ansicht (previousState == null) oder bei nachfolgenden Renderings (previousState != null) aufgerufen. Es gibt nichts zu tun;
  • Zeilen 53–56: Wird aufgerufen, wenn das Fragment im Zusammenhang mit einer [SUBMIT]-Aktion angezeigt werden soll. Dies ist immer der Fall, außer während des Speicher-/Wiederherstellungszyklus, bei dem die Aktion [RESTORE] lautet. Wir erhöhen daher die Besuchsnummer und zeigen sie an;
  • Zeilen 68–74: Wird während eines Speicher-/Wiederherstellungszyklus aufgerufen. Wir stellen den Text wieder her, der im Status des Fragments gespeichert wurde;
  • Zeilen 76–79: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts weiter zu tun;
  • Zeilen 82–83: Wird aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;

2.8.4.6. Tests

Der Leser ist eingeladen, die Anwendung zu testen, indem er das Gerät dreht, um zu überprüfen, ob das angezeigte Fragment seinen Zustand beibehält. Wir werden auch die Protokolle untersuchen.

2.9. Fazit

Am Ende dieses Kapitels verfügen wir über ein Beispielprojekt [client-android-skel] für einen Android-Client, der mit einem Webservice / JSON kommuniziert und folgende Funktionen aufweist:

  • Die asynchrone Kommunikation mit dem Web-/JSON-Server wird mithilfe der RxJava-Bibliothek abgewickelt;
  • Der Lebenszyklus des Fragments (Aktualisieren, Speichern, Wiederherstellen) wird von seiner übergeordneten Klasse [AbstractFragment] verwaltet, die zu genau festgelegten Zeitpunkten bestimmte Methoden ihrer untergeordneten Klassen aufruft. Das untergeordnete Fragment muss sich somit nicht um die Lebenszyklusphasen kümmern, sondern lediglich bestimmte Methoden implementieren, die von seiner übergeordneten Klasse gefordert werden;
  • Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ebenfalls verlangt, dass die untergeordnete Aktivität bestimmte Methoden implementiert;
  • Die Klasse [AbstractActivity] kann eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild und mit oder ohne Basisauthentifizierung gegenüber dem Webserver / JSON verarbeiten. Das Vorhandensein oder Fehlen dieser Elemente wird durch die Konfiguration bestimmt;

Wir stellen nun eine Fallstudie vor, die komplexer ist als die vorherigen Beispiele. Die neue Anwendung basiert auf dem Vorlagenprojekt [client-android-skel].