Skip to content

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

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

  

Die Untersuchung dieser Anwendungsvorlage bietet die Gelegenheit, einige Punkte zu wiederholen, die wir in den vorherigen Beispielen behandelt haben. Diese Anwendung dient als Grundgerüst für alle zukünftigen Anwendungen. Sie wurde nach zahlreichen Iterationen erstellt. Ihr Ziel ist es, möglichst viele Elemente der Anwendungen, die wir in Kürze erstellen werden, in abstrakten Klassen zu kapseln, um zu vermeiden, dass immer wieder derselbe Code geschrieben werden muss, der sich nur in Details unterscheidet. Sie weist folgende Merkmale auf:

  • Die asynchrone Kommunikation mit dem Webserver / jSON erfolgt über die Bibliothek RxJava;
  • Der Lebenszyklus eines Fragments (Update, Save, Restore) 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 Schritte des Lebenszyklus kümmern, sondern lediglich bestimmte Methoden implementieren, die von ihrer übergeordneten Klasse vorgegeben werden;
  • Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ihrerseits der untergeordneten Aktivität vorschreibt, bestimmte Methoden zu implementieren;
  • Die Klasse [AbstractActivity] ist in der Lage, eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild sowie mit oder ohne Basisauthentifizierung beim Webserver / jSON zu verwalten. Das Vorhandensein oder Fehlen dieser Elemente wird über die Konfiguration festgelegt;

Dieses Grundgerüst wurde für alle folgenden Beispiele verwendet. Aufgrund ihrer Vielfalt funktionierte das, was für ein Beispiel galt, möglicherweise nicht für das nächste. Da das Grundgerüst für insgesamt sieben Beispiele verwendet wurde, fanden zahlreiche Iterationen statt. Würde man es für ein achtes Beispiel verwenden, ist es möglich, dass die Besonderheiten dieses neuen Beispiels erneut zu Fehlern führen würden. Dennoch wird die Verwendung dieses Skeletts das Schreiben der kommenden Beispiele erheblich vereinfachen. Tatsächlich ist die Verwaltung des Lebenszyklus eines Fragments (update, save, restore) in Verbindung mit dem Konzept der Nachbarschaft von Fragmenten besonders komplex. Hier ist sie vollständig in der Klasse [AbstractFragment] verborgen.

2.1. Architektur des Android-Clients

Der vorgeschlagene Android-Client basiert auf folgender Architektur:

  • Die Schicht [DAO] implementiert eine Schnittstelle [IDao]. Sie kommuniziert mit dem Webserver / jSON;
  • es gibt nur eine Aktivität, die ebenfalls die Schnittstelle [IDao] implementiert. Die Ansichten wenden sich an sie, um den Server zu erreichen;
  • 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 {
    // Seit Android-Gradle-Plugin 0.11 muss android-apt >= 1.3 verwendet werden
    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'
    }
  }

  // Erforderliche Paketierungsoptionen, um das APK erstellen zu können
  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. Man kann jedoch von den aktuellen Nummern ausgehen, wenn man Android Studio so konfiguriert, 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: Das Anwendungspaket wird geändert;
  • Zeilen 10, 15: Der Wert des Eintrags [app_name] in der Datei [res / values / strings.xml] wird festgelegt. Derzeit lautet dieser wie folgt:

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

  <!-- Name der Anwendung -->
  <string name="app_name">[Donnez un nom à votre application]</string>
</resources>

2.4. Die Struktur des Java-Codes

  
  • [architecture] fasst die wichtigsten Elemente der Code-Struktur zusammen;
  • [activity] enthält die einzige 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. Elemente der Aktivität

 

Image

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

Die der Aktivität zugeordnete Ansicht [activity_main.xml] lautet wie folgt:


<?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 spezifischer 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 es leer. Der Entwickler wird es bei Bedarf vervollständigen.

2.5.2. Der Fragmentcontainer [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 {

  // steuert das Wischen
  private boolean isSwipeEnabled;
  // Steuert das Scrollen
  private boolean isScrollingEnabled;

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

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

  // Methoden, die zur Steuerung des Wischens neu definiert werden müssen
  @Override
  public boolean onInterceptTouchEvent(MotionEvent event) {
    // Wischen erlaubt?
    if (isSwipeEnabled) {
      return super.onInterceptTouchEvent(event);
    } else {
      return false;
    }
  }

  @Override
  public boolean onTouchEvent(MotionEvent event) {
    // Wischen erlaubt?
    if (isSwipeEnabled) {
      return super.onTouchEvent(event);
    } else {
      return false;
    }
  }

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

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

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

Diese Klasse erweitert die Android-Standardklasse [ViewPager] ausschließlich zur Steuerung des Wischens (Zeile 11) und des Scrollens (Zeile 13) zwischen Ansichten.

  • Zeilen 26–43: Methoden, die das Wischen unterbinden, wenn dieses deaktiviert wurde;
  • Zeilen 46–49: Neudefinition der Methode [setCurrentItem], die zum Wechseln der angezeigten Ansicht dient. Wenn das Scrollen unterbunden wurde, erfolgt der Ansichtswechsel ohne Scrollen. Es ist zu beachten, dass der Entwickler diese Funktionsweise umgehen kann, indem er die Methode [setCurrentItem(int position, boolean smoothScrolling)] verwendet, mit der er das gewünschte Scrollen festlegen kann;

2.5.3. Die Klasse [CoreState]

  

Die Klasse [CoreState] ist die übergeordnete Klasse der 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)
// To-Do: Hier die Unterklassen von [CoreState] hinzufügen
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // Fragment besucht oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeile 16: Jedes Fragment enthält in seinem Status einen booleschen Wert [hasBeenVisited], der angibt, ob es bereits aufgerufen wurde oder nicht. Dies ist notwendig, da bei der ersten Anzeige eines Fragments manchmal bestimmte Maßnahmen erforderlich sind;
  • Zeile 18: Das Projekt [client-android-skel] speichert und stellt die Menüs der Fragmente automatisch wieder her, sofern diese über ein Menü verfügen. In der Tabelle MenuItemState[] menuOptionsState wird der Status (sichtbar oder nicht) aller Menüoptionen gespeichert;
  • Zeilen 10–13: Wie bereits in [Exemple-22] erfolgt, wird der Status der Aktivität und ihrer Fragmente in der Sitzung gespeichert, die wiederum in Form einer Zeichenkette jSON gespeichert wird. Wir werden sehen, dass die Sitzung ein Array von Elementen vom Typ [CoreState] speichert. Wenn nichts unternommen wird, wird die Zeichenkette jSON vom Typ [CoreState] gespeichert. Wir möchten jedoch die Zustände der Fragmente speichern, also die von [CoreState] abgeleiteten Zustände. Damit die Zeichenfolge jSON des abgeleiteten Typs und nicht die des übergeordneten Typs erzeugt wird, müssen die abgeleiteten Typen wie in den Zeilen 10–13 angegeben deklariert werden. Die Klasse [CoreState] ist eine der Klassen der Architektur, die der Entwickler für jede neue Anwendung anpassen muss (Zeilen 10–13);

2.5.4. Die Schnittstelle [IMainActivity]

  

Die Schnittstelle [IMainActivity] legt fest, welche Anforderungen die Fragmente an die Aktivität in der folgenden Architektur 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 {

  // Zugriff auf die Sitzung
  ISession getSession();

  // Wechsel der Ansicht
  void navigateToView(int position, ISession.Action action);

  // Wartungsverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Anwendungskonstanten (zu ändern) -------------------------------------

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 0;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // Aneinandergrenzung der Fragmente
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = false;

  // Ladebild
  boolean IS_WAITING_ICON_NEEDED = false;

  // Anzahl der Anwendungsfragmente
  int FRAGMENTS_COUNT = 0;

  // To-Do: Fügen Sie hier Ihre Konstanten und andere Methoden hinzu
}
  • Zeile 6: Die Schnittstelle [IMainActivity] erweitert die Schnittstelle [IDao] der Schicht [DAO];
  • Zeile 9: Diese Aktivität ermöglicht den Zugriff auf die Sitzung in Form einer Instanz der Schnittstelle [ISession];
  • Zeile 12: Über diese Aktivität wird die Ansicht gewechselt. Der zweite Parameter ist die Aktion, die diesen Ansichtswechsel auslöst, einer der Werte SUBMIT, NAVIGATION, RESTORE;
  • Zeilen 15–17: Diese Aktivität verwaltet das Ladebild;
  • Zeile 22: zum Debuggen der Anwendung;
  • Zeile 25: Damit nicht zu lange gewartet wird, wenn der Server nicht mehr antwortet;
  • Zeile 28: Im Debugging-Modus wird ein Wert von einigen Sekunden festgelegt, um Zeit zu haben, den Vorgang mit dem Server abzubrechen und zu sehen, was passiert;
  • Zeile 31: auf true, falls der Dienst jSON eine Basic-Authentifizierung verlangt;
  • Zeile 34: Fragment-Adjazenz;
  • Zeile 37: auf vrai, wenn die Anwendung Registerkarten enthält;
  • Zeile 39: zu vrai, wenn die Anwendung mit einem Webserver / jSON kommuniziert und während des Datenaustauschs ein Ladebild angezeigt werden soll;
  • Zeile 43: die Anzahl der von der Anwendung verwalteten Fragmente;

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

2.5.5. Die Schnittstelle [IDao]

Die Schnittstelle [IMainActivity] erweitert die folgende Schnittstelle [IDao]:

  

package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

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

  // Client-Timeout
  void setTimeout(int timeout);

  // Basis-Authentifizierung
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);

  // Wartezeit des Clients in Millisekunden vor der Anfrage
  void setDelay(int delay);

  // To-Do: Deklarieren Sie hier Ihre Schnittstelle
}
  • Zeile 24: Der Entwickler ergänzt die Schnittstelle an dieser Stelle;

2.5.6. Die Sitzung

  

Die Klasse [Session] kapselt die 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 {

  // Nummer der zuletzt angezeigten Ansicht
  int getPreviousView();

  void setPreviousView(int numView);

  // Letzter Status einer Ansicht
  CoreState getCoreState(int numView);

  void setCoreState(int numView, CoreState coreState);

  // laufende Aktion
  enum Action {
    SUBMIT, NAVIGATION, RESTORE, NONE
  }

  Action getAction();

  void setAction(Action action);

  // Zustände aller Ansichten –
  // wird vom Code nicht verwendet, ist aber für die Serialisierung/Deserialisierung erforderlich jSON
  CoreState[] getCoreStates();

  void setCoreStates(CoreState[] coreStates);

  // Nummer der zuletzt ausgewählten Registerkarte
  int getPreviousTab();

  void setPreviousTab(int position);

  // Navigation bei Auswahl einer Registerkarte
  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: den Status einer bestimmten Ansicht;
  • Zeilen 17–24: Wir führen den Begriff der laufenden Aktion ein. Es gibt vier davon (Zeile 17):
    • RESTORE: Ein Speichern/Wiederherstellen ist im Gange. Es findet kein Ansichtswechsel statt;
    • NAVIGATION: Eine Navigation ist im Gange. Als Navigation bezeichnen wir hier einen Wechsel der Ansicht, bei dem die neue Ansicht aus ihrem zuletzt in der Sitzung gespeicherten Zustand wiederhergestellt werden kann;
    • SUBMIT: Der Typ [SUBMIT] wird einer laufenden Aktion zugewiesen, wenn ein Ansichtswechsel stattfindet und die neue Ansicht vom allgemeinen Status der Aktivität abhängt und nicht nur von ihrem eigenen Status. Manchmal ist die Unterscheidung zwischen NAVIGATION und SUBMIT schwierig. In diesem Fall wird der allgemeinere Fall SUBMIT herangezogen;
    • NONE: Wert der Aktion, wenn diese noch keinen ersten Wert erhalten hat;
  • Zeilen 26–30: Die Zustände der Aktivität und der Fragmente werden in einem Array vom Typ CoreState[] gespeichert. Damit dieses bei der Serialisierung und Deserialisierung von jSON korrekt verarbeitet wird, muss es über einen Getter und einen Setter verfügen;
  • Zeilen 32–35: Nummer der zuletzt ausgewählten Registerkarte. Wird beim Speichern und Wiederherstellen verwendet, um die Registerkarte wieder auszuwählen, die vor der Drehung des Geräts ausgewählt war;
  • Zeilen 37–40: Verwaltung eines booleschen Werts, 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 {
  // Nummer der vorherigen Ansicht
  private int preViousView;

  // Status der Ansichten
  private CoreState[] coreStates = new CoreState[0];

  // laufende Aktion
  private Action action = Action.NONE;

  // zuvor ausgewählte Registerkarte
  private int previousTab;

  // Navigation bei Registerkartenauswahl
  @JsonIgnore
  private boolean navigationOnTabSelectionNeeded = true;

  // Konstruktor
  public AbstractSession() {
    // Das Array der Fragmentzustände wird initialisiert
    coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
    for (int i = 0; i < coreStates.length; i++) {
      coreStates[i] = new CoreState();
    }
  }


  // Schnittstelle 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 Nummer 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 Navigation über Registerkarten der Fall. Die angezeigte Ansicht kann dann erkennen, welche Ansicht zuvor angezeigt wurde;
  • Zeile 12: die Statusübersicht aller von der Aktivität angezeigten Fragmente;
  • Zeile 18: Die Nummer der zuvor ausgewählten Registerkarte. Spielt eine ähnliche Rolle wie die Nummer der vorherigen Ansicht in Zeile 9. Diese Information ist nützlich, wenn das Gerät gedreht wird und man wieder zu der Registerkarte zurückkehren muss, die vor der Drehung ausgewählt war;
  • Zeile 22: Ein boolescher Wert, der angibt, ob die Auswahl einer Registerkarte mit einem Wechsel des angezeigten Fragments einhergehen soll. Es ist zu beachten, dass das Projekt [client-android-skel] Registerkarten und Fragmente getrennt verwaltet, um auch in Fällen eingesetzt werden zu können, in denen die Anzahl der Registerkarten geringer ist als die Anzahl der Fragmente. Es gibt zwei Arten der Auswahl:
    • eine Auswahl durch den Benutzer, wenn er auf eine Registerkarte klickt. In diesem Fall muss sich das angezeigte Fragment in der Regel ändern;
    • eine softwaregesteuerte Auswahl über die Methode [Tablayout.Tab.select()]. In diesem Fall ist eine Änderung des angezeigten Fragments nicht immer erwünscht. Hier zwei Beispiele:
      • Bei einer Drehung des Geräts wird die Aktivität neu erstellt, ebenso wie die Registerkarten. Wenn jedoch die erste Registerkarte erstellt wird, wird sie automatisch einer Softwareoperation [select] unterzogen. Es ist daher nicht wünschenswert, das angezeigte Fragment zu ändern, da sich die Aktivität gerade in einer Neuanlegungsphase befindet, in der das letztendlich angezeigte Fragment nicht unbedingt dasjenige ist, das der ersten Registerkarte zugeordnet ist;
      • da die Verwaltung der Registerkarten von der der Fragmente getrennt ist, möchte man möglicherweise die Registerkarten aktualisieren (löschen, hinzufügen), ohne die zugehörigen Fragmente zu beeinträchtigen. Einige dieser Vorgänge können jedoch wiederum einen impliziten Softwarevorgang [select] auf einer der Registerkarten auslösen. Diese Auswahl muss dann nicht zwangsläufig zu einer Navigation zum zugehörigen Fragment führen;
  • Zeile 21: Das Feld [navigationOnTabSelectionNeeded] ist nicht dafür vorgesehen, beim Speichern der Aktivität und ihrer Fragmente mitgespeichert zu werden. Die Anmerkung [@JsonIgnore] bewirkt, dass das Feld bei der Serialisierung/Deserialisierung jSON ignoriert wird;
  • Zeilen 25–31: Der Konstruktor initialisiert das Statusarray der Anwendungsfragmente [FRAGMENTS_COUNT]. Die Elemente dieses Arrays werden mit dem Feld [hasBeeenVisited=false] initialisiert. Diese Information wird verwendet, um festzustellen, ob es sich um den ersten Besuch des Fragments handelt 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 {
  // Daten, die zwischen den Fragmenten untereinander sowie zwischen Fragmenten und Aktivitäten ausgetauscht werden sollen
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen
  // Die für die Serialisierung/Deserialisierung erforderlichen Getter und Setter dürfen nicht vergessen werden: jSON
}
  • Zeile 5: Die Klasse [Session] erweitert die soeben betrachtete Klasse [AbstractSession]. Der Entwickler platziert dort die Elemente, die zwischen den Fragmenten untereinander sowie zwischen Fragmenten und der Aktivität geteilt werden sollen. Es ist zu beachten, dass die Klasse [Session] nicht mehr durch die Annotation AA [@EBean] annotiert ist. Sie ist zu einer normalen Klasse geworden;

2.5.7. Die abstrakte Klasse [AbstractActivity]

  

2.5.7.1. Squelette

Die Klasse [AbstractActivity] umfasst mehr als 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 {
  // Schicht [DAO]
  private IDao dao;
  // die Sitzung
  protected Session session;

  // der Fragment-Container
  protected MyPager mViewPager;
  // die Symbolleiste
  private Toolbar toolbar;
  // das Ladebild
  private ProgressBar loadingPanel;
  // Registerkartenleiste
  protected TabLayout tabLayout;

  // der Fragment- oder Abschnittsmanager
  private FragmentPagerAdapter mSectionsPagerAdapter;
  // Klassenname
  protected String className;
  // Mapper jSON
  private ObjectMapper jsonMapper;

  // Konstruktor
  public AbstractActivity() {
    // Klassenname
    className = getClass().getSimpleName();
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "constructeur");
    }
    // jsonMapper
    jsonMapper = new ObjectMapper();
  }

  // Implementierung IMainActivity --------------------------------------------------------------------
  ...

  // Lebenszyklus – Sichern/Wiederherstellen der Aktivität ------------------------------------
  ...

  // Verwaltung des Standby-Images ---------------------------------
  ...

  // Schnittstelle IDao -----------------------------------------------------
  ...

  // Fragment-Manager --------------------------------
  ...

  // Unterklassen
  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);
  • verwaltet das Speichern und Wiederherstellen der Aktivität und ihrer Fragmente bei einer Drehung des Geräts (Zeile 58);
  • verwaltet das Wartebild bei einem Austausch mit dem Webserver / jSON (Zeile 61);
  • implementiert die Schnittstelle IDao der Schicht [DAO] (Zeile 64);
  • implementiert den Fragment-Manager (Zeile 67);
  • schreibt seinen untergeordneten Klassen das Vorhandensein von sechs Methoden vor (Zeilen 71–81);

2.5.7.2. Implementierung der Schnittstelle [IMainActivity]

Die Implementierung der Schnittstelle [IMainActivity] (siehe Abschnitt 2.5.4) lautet wie folgt:


  // Implementierung 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));
    }
    // Anzeige eines neuen Fragments
    mViewPager.setCurrentItem(position);
    // Die aktuelle Aktion wird bei diesem Ansichtswechsel vermerkt
    session.setAction(action);
}

2.5.7.3. Speichern des Zustands der Aktivität und ihrer Fragmente

Der Status der Aktivität und ihrer Fragmente befindet sich vollständig in der Sitzung. Es geht also darum, diese zu speichern. Wir greifen hier auf das zurück, was im Projekt [Exemple-22] (siehe Abschnitt 1.23) umgesetzt wurde:


  // Verwaltung des Speicherns/Wiederherstellens der Aktivität ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // übergeordnet
    super.onSaveInstanceState(outState);
    // Speicherung der Sitzung in Form einer Zeichenkette jSON
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // Protokoll
    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. Wiederherstellung des Aktivitätsstatus und seiner Fragmente

Es geht darum, die Sitzung wiederherzustellen. Wir verfahren dabei wie in [Exemple-22] gezeigt:


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // übergeordnet
    super.onCreate(savedInstanceState);
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    // Gibt es etwas wiederherzustellen?
    if (savedInstanceState != null) {
      // Sitzungswiederherstellung
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // Protokoll
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    } else {
      // Sitzung
      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: Der Fall, in dem der Parameter [Bundle savedInstanceState] in Zeile 2 den Wert null hat, entspricht dem ersten Start der Aktivität. In diesem Fall wird eine leere Sitzung angelegt;

2.5.7.5. Initialisierung der Schicht [DAO]


@Override
  protected void onCreate(Bundle savedInstanceState) {
    // übergeordnet
    super.onCreate(savedInstanceState);
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
    ...
    // Schicht [DAO]
    dao = getDao();
    if (dao != null) {
      // Schichtkonfiguration [DAO]
      setDebugMode(IS_DEBUG_ENABLED);
      setTimeout(TIMEOUT);
      setDelay(DELAY);
      setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
    }
...
  // Unterklassen
  protected abstract IDao getDao();
....
}
  • Zeile 11: Von der untergeordneten Aktivität (Zeile 21) wird eine Referenz auf die Schicht [DAO] angefordert;
  • Zeilen 14–17: Wenn die Schicht [DAO] existiert, wird sie anhand der in der Schnittstelle [IMainActivity] enthaltenen Informationen konfiguriert;

2.5.7.6. Initialisierung der zur Aktivität gehörenden Ansicht

Die der Aktivität zugeordnete 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>

  <!-- Fragmentcontainer -->
  <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) {
    // übergeordnete Klasse
    super.onCreate(savedInstanceState);
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // zugehörige Ansicht
    setContentView(R.layout.activity_main);
    // Komponenten der Ansicht ---------------------
    // Symbolleiste
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // Ladebild?
    if (IS_WAITING_ICON_NEEDED) {
      // Das Platzhalterbild wird hinzugefügt
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding loadingPanel");
      }
      // Erstellung von ProgressBar
      loadingPanel = new ProgressBar(this);
      loadingPanel.setVisibility(View.INVISIBLE);
      // Hinzufügen von ProgressBar zur Symbolleiste
      toolbar.addView(loadingPanel);
    }
...
  • Zeile 11: Die Ansicht XML [activity_main] ist der Aktivität zugeordnet;
  • Zeilen 14–15: Die Symbolleiste wird integriert und unterstützt;
  • Zeilen 17–27: Eventuelles Hinzufügen eines Ladebildes: wenn der Boolesche Wert [IS_WAITING_ICON_NEEDED] in der Schnittstelle [IMainActivity] auf „wahr“ gesetzt ist;
  • Zeile 23: Erstellung des Wartebildes vom Typ [ProgressBar], auf das im Feld [loadingPanel] verwiesen wird;
  • Zeile 24: Dieses Bild ist zunächst ausgeblendet;
  • Zeile 26: Es wird zur Symbolleiste hinzugefügt;

2.5.7.7. Verwaltung der Registerkarten

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


// Registerkartenleiste
  protected TabLayout tabLayout;
...

    // Registerkartenleiste?
    if (ARE_TABS_NEEDED) {
      // Die Registerkartenleiste wird hinzugefügt
      if (IS_DEBUG_ENABLED) {
        Log.d(className, "adding tablayout");
      }
      // keine Navigation bei Auswahl, bis ein Fragment angezeigt wird
      session.setNavigationOnTabSelectionNeeded(false);
      // Erstellung der Registerkartenleiste
      tabLayout = new CustomTabLayout(this);
      tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
      // Hinzufügen der Registerkartenleiste zur Anwendungsleiste
      AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
      appBarLayout.addView(tabLayout);
      // Ereignismanager der Registerkartenleiste
      tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
          // Eine Registerkarte wurde ausgewählt
          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()) {
            // Position der Registerkarte
            int position = tab.getPosition();
            // Speicher
            session.setPreviousTab(position);
            // Zugehöriges Fragment anzeigen?
            navigateOnTabSelected(position);
          }
        }

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

        }

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

        }
      });
    }

...
  // Unterklassen
  protected abstract void navigateOnTabSelected(int position);
...
  • Zeilen 12–48: Hinzufügen und Verwalten einer Registerkartenleiste;
  • Zeile 6: Die Registerkartenleiste wird hinzugefügt, wenn die Konstante [ARE_TABS_NEEDED] in der Schnittstelle [IMainActivity] auf vrai gesetzt ist;
  • Zeile 12: Beim Erstellen der Registerkartenleiste können implizite Vorgänge vom Typ [Tablayout.Tab.select] auftreten (diese werden nicht vom Benutzer ausgelöst). Der Boolesche Wert [session.navigationOnTabSelectionNeeded] wird auf faux gesetzt, um jegliche Navigation während dieser falschen Auswahlen zu verhindern. Es obliegt dem Entwickler, das anzuzeigende Fragment mit der Methode [navigateToView] auszuwählen. Der Boolesche Wert [session.navigationOnTabSelectionNeeded] wird auf vrai zurückgesetzt, sobald dieses Fragment angezeigt wird (siehe Klasse AbstractFragment);
  • Zeile 14: Erstellung einer Registerkartenleiste, auf die das Feld [tabLayout] verweist. Wir verwenden eine benutzerdefinierte Registerkartenleiste [CustomTabLayout], auf die wir noch zurückkommen werden;
  • Zeile 15: Wir legen die Farben der Registerkartentitel fest. Diese sind in der folgenden Datei „[res / color / tab_txt.xml]“ zu finden:

<?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 der Registerkartenüberschrift, wenn diese ausgewählt ist;
    • Zeile (d): die Farbe der Registerkartenüberschrift, wenn diese nicht ausgewählt ist;

Diese Datei kann natürlich bearbeitet werden. Die Hexadezimalcodes der Farben finden Sie beispielsweise hier.

  • Zeilen 17–18: Hinzufügen dieser Registerkartenleiste zur Anwendungsleiste in der Ansicht XML [activity_main];
  • Zeilen 20–47: Ereignisbehandler für die Registerleiste;
  • Zeilen 22–36: Es wird nur das Ereignis [onTabSelected] verarbeitet. Es entspricht einem Klick auf die Registerkarte [Tab tab], die als Parameter an die Methode übergeben wurde, oder einer Softwareoperation [TabLayout.Tab.select];
  • Zeile 30: Position der ausgewählten Registerkarte;
  • Zeile 32: Diese Position wird in der Sitzung gespeichert;
  • Zeile 34: Nun muss das diesem Reiter zugeordnete Fragment angezeigt werden. Nur die untergeordnete Klasse (Zeile 52) kann diese Zuordnung vornehmen. Es ist zu beachten, dass die Reibeleiste nicht mit dem Fragment-Container [mViewPager] verknüpft wird, wie dies in einigen der untersuchten Beispiele der Fall war. Hier wird die Verwaltung der Registerleiste vollständig von der der Fragmente getrennt. Deshalb muss man beim Anklicken eines Registers angeben, welche Ansicht angezeigt werden soll;
  • Zeile 28: Hier wird zwischen der Auswahl einer Registerkarte mit oder ohne Navigation unterschieden. Im Allgemeinen wird bei einem Klick des Benutzers auf eine Registerkarte eine Navigation gewünscht, bei einer programmgesteuerten Auswahl hingegen nicht. Der Entwickler unterscheidet diese beiden Fälle mithilfe des Elements [session.navigationOnTabSelectionNeeded]. Wenn keine Navigation erfolgt, wird die Nummer der zuletzt ausgewählten Registerkarte nicht in der Sitzung gespeichert. Dies muss der Entwickler selbst übernehmen;

2.5.7.8. Der Registerkarten-Manager [CustomTabLayout]

  

Wir verwenden einen benutzerdefinierten Tab-Manager, um die Titel der Tabs mit verschiedenen Schriftarten anzeigen zu können. 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 Anpassung der Schriftart für die Registerkartentitel erfolgt in den Zeilen 30 und 44;

Die Datei [fonts] sieht wie folgt aus:

  

Quellen:

  • Der Code der Klasse [CustomTabLayout] wurde in URL und [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout] gefunden;
  • die Schriftarten wurden in den Klassen URL und [https://www.fontsquirrel.com/fonts/roboto] gefunden;

2.5.7.9. Letzte Initialisierungen


  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // übergeordnete Klasse
    super.onCreate(savedInstanceState);
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreate");
    }
  ...
    // Instanziierung des Fragment-Managers
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
    // Der Fragmentcontainer ist mit dem Fragmentmanager verknüpft
    // d. h., das Fragment Nr. i des Fragment-Containers ist das vom Fragment-Manager bereitgestellte Fragment Nr. i
    mViewPager = (MyPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // Das Wischen zwischen Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);
    // Aneinanderreihen der Fragmente
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
    // Die erste Ansicht wird angezeigt
    if (session.getAction() == ISession.Action.NONE) {
      navigateToView(getFirstView(), ISession.Action.NONE);
    }
    // die Kontrolle wird an die untergeordnete Aktivität übergeben
    onCreateActivity();
  }
...
  // Tochterklassen
  protected abstract void onCreateActivity();
  protected abstract int getFirstView();
...
  • Zeilen 10–19: Hier findet sich Code, der in den untersuchten Beispielen häufig vorkommt;
  • 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 Annahme bezüglich des ersten anzuzeigenden Fragments. In unseren Beispielen war dies oft das Fragment Nr. 0, aber nicht immer (siehe Beispiel 22). Wir werden daher die untergeordnete Aktivität (Zeile 30) auffordern, uns mitzuteilen, um welche erste Ansicht es sich handelt;
  • Zeile 25: Hier wurde alles mögliches herausgefasst. Nun muss die untergeordnete Klasse ihre eigenen Initialisierungen vornehmen (Zeile 29);

2.5.7.10. Verwaltung des Ladebildes

In der Klasse [AbstractActivity] wird das Wartebild durch die beiden folgenden Methoden verwaltet:


  // Verwaltung des Ladebildes ---------------------------------
  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 {
  // Schicht [DAO]
  private IDao dao;
...
  // Schnittstelle 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: Es sei daran erinnert, 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-Handler wie folgt implementiert:


...
  // der Fragment-Manager --------------------------------
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    private AbstractFragment[] fragments;

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
      // Fragmente der untergeordneten Klasse
      fragments = getFragments();
    }

    // soll das Fragment an Position Nr. ausgeben
    @Override
    public AbstractFragment getItem(int position) {
      // Das Fragment wird gerendert
      return fragments[position];
    }

    // gibt die Anzahl der zu verarbeitenden Fragmente zurück
    @Override
    public int getCount() {
      return fragments.length;
    }

    // gibt den Titel des Fragments an Position Nr. zurück
    @Override
    public CharSequence getPageTitle(int position) {
      return getFragmentTitle(position);
    }
  }

  // Unterklassen
  protected abstract AbstractFragment[] getFragments();

  protected abstract CharSequence getFragmentTitle(int position);
...
}
  • Zeile 5: Das Array der der Aktivität zugeordneten Fragmente. Alle Fragmente werden von der Klasse [AbstractFragment] abgeleitet;
  • Zeilen 8–12: Dies ist der Konstruktor, der das Fragment-Array initialisiert. Er fordert die Fragmente von der untergeordneten Klasse der Aktivität an (Zeile 35);
  • Zeilen 28–31: Die 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 der Aktivität zugeordnete Ansicht sichtbar wird, ausgeführt. Sie wird hier verwendet, um nach einem Speichern/Wiederherstellen eine Registerkarte auszuwählen:


  @Override
  public void onResume() {
    // übergeordnete Klasse
    super.onResume();
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onResume");
    }
    // Bei einer Wiederherstellung muss die zuletzt ausgewählte Registerkarte wiederhergestellt werden
    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. Dabei ist 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 unterbunden wurde. Hier erfolgt also die Auswahl einer Registerkarte, jedoch kein Fragmentwechsel;

2.5.7.14. Résumé

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

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


  // Unterklassen
  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 Elemente ihrer übergeordneten Klasse:


  // die Sitzung
  protected ISession session;
  // der Fragment-Container
  protected MyPager mViewPager;
  // Registerkartenleiste
  protected CustomTabLayout tabLayout;
  // Klassenname
protected String className;

2.5.8. Die Aktivität [MainActivity]

  

Die Klasse [MainActivity] kann einen anderen Namen haben. Ihre einzige Vorgabe ist, die Schnittstelle [IMainActivity] zu implementieren. Die bereitgestellte Basisklasse 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 {

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

  // Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Sitzung
    this.session = (Session) super.session;
    // To-Do: Die von der übergeordneten Klasse begonnenen Initialisierungen fortsetzen
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // To-Do: Fragmente hier definieren
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // To-Do: Hier die Titel der Fragmente definieren
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // To-do: Navigation über Registerkarten – die anzuzeigende Ansicht definieren
  }

  @Override
  protected int getFirstView() {
    // To-Do: Navigation über Registerkarten – die erste anzuzeigende Ansicht festlegen
    return 0;
  }
}
  • Zeile 14: Damit die Notation AA [@Bean] in Zeile 19 verstanden wird, muss die Aktivität die Notation AA [@EActivity] haben;
  • Zeile 15: Die Aktivität ist dem Menü XML [menu_main] zugeordnet. Derzeit ist dieses Menü leer. Der Entwickler muss es bei Bedarf vervollständigen;
  • Zeile 16: Die Klasse erweitert die Klasse [AbstractActivity];
  • Zeilen 19–20: Ein Verweis auf die Schicht [DAO]. Diese wird von der Bibliothek AA instanziiert, bevor dieses Feld initialisiert wird. Dies hat zur Folge, dass die Bean AA [Dao] vorhanden sein muss. Dies ist bei der von uns gelieferten Skelettanwendung stets der Fall. Selbst in einer Anwendung ohne die Schicht [DAO] kann das Paket [dao] bestehen bleiben. Dies führt zu keinen Komplikationen;
  • Zeile 22: Die Session als Instanz des Typs [Session]. Die Session existiert in der übergeordneten Klasse [AbstractActivity], jedoch als Instanz der Schnittstelle [ISession] (Zeile 32);
  • Zeilen 24–63: die sechs von der übergeordneten Klasse [AbstractActivity] vorgeschriebenen Methoden;
  • Zeilen 36–39: Die Methode [getDao] gibt eine Referenz auf die Schicht [DAO] zurück. Hier ist diese Referenz niemals null. In der übergeordneten Klasse [AbstractActivity] wurde jedoch der Fall vorgesehen, dass die untergeordnete Klasse eine Referenz null zurückgibt, um anzuzeigen, dass keine Schicht [DAO] vorhanden ist. Wenn man diese Möglichkeit nutzen möchte (meiner Meinung nach nicht besonders nützlich), muss man hier den Zeiger null zurückgeben;

2.6. Die Ebene [DAO]

Image

  

2.6.1. Die Schnittstelle IDao

Sie wurde in Abschnitt 2.5.5 vorgestellt:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

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

  // Client-Timeout
  void setTimeout(int timeout);

  // Basis-Authentifizierung
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);

  // Wartezeit des Clients in Millisekunden vor der Anfrage
  void setDelay(int delay);

  // To-Do: Deklarieren Sie hier Ihre Schnittstelle
}

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

2.6.2. Die Schnittstelle [WebClient]

  

Die Schnittstelle [WebClient] lautet wie folgt:


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

  // To-Do: Deklarieren Sie hier die zu erreichenden URL
}

Der Entwickler fügt ab Zeile 17 die Methoden hinzu, die mit den vom Server jSON bereitgestellten URL 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 {

  // Benutzer
  private String user;
  // Passwort
  private String mdp;

  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    // Header HTTP der abgefangenen Anfrage HTTP
    HttpHeaders headers = request.getHeaders();
    // der Header „HTTP“ für die Basisauthentifizierung
    HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
    // Ergänzung der Header von HTTP
    headers.setAuthorization(auth);
    // Der Lebenszyklus der Anfrage HTTP wird fortgesetzt
    return execution.execute(request, body);
  }

  // Elemente der Authentifizierung
  public void setUser(String user, String mdp) {
    this.user = user;
    this.mdp = mdp;
  }
}

Diese Klasse generiert den folgenden Authentifizierungsheader HTTP:

Authorization: Basic code

wobei [code] der Base64-Code der Zeichenfolge „user:mp“ ist. Diese Klasse wird nur verwendet, wenn der Server jSON diese Form der Authentifizierung erwartet. Es gibt noch weitere Klassen.

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

2.6.4. Die Klasse [AbstractDao]

  

Die Klasse [AbstractDao] lautet wie folgt:


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-Modus
  protected boolean isDebugEnabled;
  // Name der Klasse
  protected String className;
  // Wartezeit vor der Ausführung der Anfrage
  private int delay;

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

  // geschützte Methoden ----------------------------------------------------------
  // generische Schnittstelle
  protected interface IRequest<T> {
    T getResponse();
  }

  // generische Anfrage an einen Webdienst / jSON
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("delay=%s", delay));
    }
    // Dienstausführung – es wird eine einzige Antwort erwartet
    return Observable.create(new Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // Dienstausführung
        try {
          // Warten?
          if (delay > 0) {
            Thread.sleep(delay);
          }
          // Die synchrone Anfrage wird ausgeführt
          T response = request.getResponse();
          // Protokoll
          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()));
          }
          // Die Antwort wird an den Beobachter gesendet
          subscriber.onNext(response);
          // Das Ende des Beobachtbaren wird gemeldet
          subscriber.onCompleted();
        } catch (InterruptedException | JsonProcessingException | RuntimeException e) {
          // Protokoll
          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"));
            }
          }
          // Es wird eine Ausnahme ausgelöst
          subscriber.onError(new DaoException(e, 100));
        }
      }
    });
  }

  // Debug-Modus
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
  }

  public void setDelay(int delay) {
    this.delay = delay;
  }
}
  • Zeilen 35–81: Die Methode [getResponse] verwendet die Bibliothek RxAndroid, um einen Typ [Observable<T>] zu rendern. Im Gegensatz zu einigen zuvor gesehenen Beispielen wird hier kein Typ [Response<T>] gerendert, bei dem es sich um einen proprietären Typ handelt, sondern ein beliebiger Typ T;
  • Zeile 35: Die Methode [getResponse] erhält als Parameter eine Instanz des Typs [IRequest<T>] aus den Zeilen 30–32, deren Typ T die Methode [IRequest.getReponse()] durch eine synchrone Operation HTTP ermittelt;
  • Zeilen 48–50: Künstlich wird eine Wartezeit von [delay] Millisekunden festgelegt. In der Produktion wird [delay=0] verwendet. In der Debugging-Phase wird [delay=qqs secondes] verwendet, um dem Benutzer die Möglichkeit zu geben, die asynchrone Operation abzubrechen und so zu sehen, wie sich der Code dann 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 übergeben;
  • Zeile 66: Es wird angegeben, dass keine weitere Ausgabe erfolgt. Hier handelt es sich um den 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 Klasse [Dao]

  

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 {

  // Webservice-Client
  @RestService
  protected WebClient webClient;
  // Sicherheit
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // der RestTemplate
  private RestTemplate restTemplate;
  // Factory des RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // Protokoll
    Log.d(className, "afterInject");
    // man baut den restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Der Konverter jSON wird befestigt
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // man legt den restTemplate des Web-Clients fest
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // Der URL des Webdienstes wird festgelegt
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // Der Benutzer wird im Interceptor registriert
    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));
    }
    // Konfigurationsfabrik
    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));
    }
    // Authentifizierungs-Interceptor?
    if (isBasicAuthentificationNeeded) {
      // Der Authentifizierungs-Interceptor wird hinzugefügt
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

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

  // To-Do: Implementierung IDao
}
  • Zeilen 21–22: Einbindung der Beans AA und [WebClient], die den Datenaustausch mit dem Webserver / jSON gewährleisten;
  • Zeilen 24–25: Einbindung des Authentifizierungs-Interceptors;
  • Zeilen 31–42: Methode, die nach dem Einfügen der Felder aus den Zeilen 21–25 ausgeführt wird;
  • Zeile 37: Das Objekt [RestTemplate], das den Austausch zwischen Client und Server gewährleistet, wird aus einem factory erstellt. Dies ist zwar nicht zwingend erforderlich, aber über das Objekt factory lassen sich die Timeouts für den Datenaustausch konfigurieren. Aus diesem Grund verwenden wir nicht den Konstruktor ohne Parameter [RestTemplate()];
  • Zeile 39: Wir fügen einen Konverter jSON zu den Konvertern des [RestTemplate] hinzu. Dies wird der einzige Konverter sein. Wenn also eine Methode des Clients [WebClient] eine Zeichenkette jSON vom Server empfängt, wird diese automatisch in das Objekt deserialisiert, das die Methode zurückgeben soll;
  • Zeile 41: Das so konfigurierte Objekt [RestTemplate] wird an den Web-Client übergeben, der mithilfe dieses Objekts den Austausch zwischen Client und Server gewährleistet;
  • Zeilen 44–48: Das Stammobjekt des Webservers / jSON wird auf URL festgelegt. Alle in der Klasse [WebClient] deklarierten URL sind relative URL zu diesem Stamm-URL;
  • Zeilen 50–54: Mit dieser Methode kann der Eigentümer der Verbindung angegeben werden, wenn diese durch eine Berechtigung vom Typ „Basic“ kontrolliert wird (siehe Abschnitt 2.6.3);
  • Zeilen 56–64: Legen die timeouts für den Client-Server-Austausch fest. Dies erfolgt über die factory des Objekts [RestTemplate], das den Austausch steuert;
  • Zeilen 66–78: Mit dieser Methode wird angegeben, dass es sich bei dem Server um einen Server handelt, der durch eine Basic-Authentifizierung geschützt ist;
  • Zeilen 72–77: Wird eine Basic-Authentifizierung angefordert, wird der in Zeile 25 eingefügte Authentifizierungs-Interceptor zu den Interceptoren des Objekts [RestTemplate] hinzugefügt. Dieser Interceptor fügt automatisch allen Anfragen des Web-Clients die vom Server erwartete Basic-Authentifizierungszeile HTTP hinzu;
  • der Entwickler implementiert die Schnittstelle [IDao] ab Zeile 87;

2.7. Die Fragmente

  

2.7.1. Die Klasse [MenuItemState]

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


package client.android.architecture;

public class MenuItemState {

  // ID der Menüoption
  private int menuItemId;
  // Sichtbarkeit der Option
  private boolean isVisible;

  // Konstruktoren
  public MenuItemState() {

  }

  public MenuItemState(int menuItemId, boolean isVisible) {
    this.menuItemId = menuItemId;
    this.isVisible = isVisible;
  }

  // Getter und Setter
...
}

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 {

  // Liste der Fehlermeldungen einer Ausnahme – Version 1
  static public List<String> getMessagesFromException(Throwable ex) {
    // Es wird eine Liste mit den Fehlermeldungen des Ausnahmestapels erstellt
    List<String> messages = new ArrayList<>();
    Throwable th = ex;
    while (th != null) {
      messages.add(th.getMessage());
      th = th.getCause();
    }
    return messages;
  }

  // Liste der Meldungen einer Ausnahme – Version 2
  static public String getMessageForAlert(Throwable th) {
    // Der anzuzeigende Text wird zusammengestellt
    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--;
    }
    // Ergebnis
    return texte.toString();
  }

  // Liste der Meldungen einer Ausnahme – Version 3
  static public String getMessageForAlert(List<String> messages) {
    // Erstellung des anzuzeigenden Textes
    StringBuilder texte = new StringBuilder();
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // Ergebnis
    return texte.toString();
  }
}

2.7.3. Die übergeordnete Klasse [AbstractFragment]

Die Klasse [AbstractFragment] fasst die gemeinsamen Elemente aller Fragmente der Anwendung zusammen. Wie in der Klasse [AbstractActivity] ist ihr Code komplex. Auch hier werden wir ihn schrittweise 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 {

  // Vertrauliche Daten ------------------------------------------------------------
  // Abonnements für Observables
  private List<Subscription> abonnements = new ArrayList<>();
  // Menü des Fragments
  private Menu menu;
  private MenuItemState[] menuOptionsStates = new MenuItemState[0];
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Status des Fragments
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
  // asynchrone Aufgaben
  private boolean runningTasksHaveBeenCanceled;

  // Daten, auf die untergeordnete Klassen zugreifen können ---------------------------------------
  // Debug-Modus
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // Klassenname
  protected String className;
  // asynchrone Aufgaben
  protected int numberOfRunningTasks;
  // Aktivität
  protected IMainActivity mainActivity;
  protected Activity activity;
  // Sitzung
  protected Session session;


  // Fragment aktualisieren ----------------------------------------------------------------------------------
 ...

  // Menüverwaltung ------------------------------------------
  ...

  // Wartungsverwaltung -------------------------------------------------------------
...

  // Verwaltung asynchroner Vorgänge --------------------------------------------------------------------
...

  // Ausnahmebehandlung -------------------------------------------------------------------
....

  // Verwaltung des Fragment-Lebenszyklus --------------------------------------------------------
...

  // Unterklassen -----------------------------------------------------
  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: die geschützten Daten, auf die die untergeordneten Klassen zugreifen können;
  • Zeilen 61–62: Code, der das anzuzeigende Fragment aktualisiert;
  • Zeilen 64–65: Hilfscode zur Verwaltung des eventuellen Menüs;
  • Zeilen 67–68: Hilfscode zur Verwaltung der Wartezeit bei einer asynchronen Operation;
  • Zeilen 70–71: Code zur Erleichterung der Kommunikation des Fragments mit der Schicht [DAO];
  • 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 Konstruktor der Klasse lautet wie folgt:


  // Klassenname
  protected String className;
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
...
  // Konstruktor ----------------------
  public AbstractFragment() {
    // Init
    className = getClass().getSimpleName();
    fragmentHasToBeInitialized = true;
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "constructeur");
    }
}
  • Zeile 9: Hier wird der Name der Tochterklasse vermerkt, die hier instanziiert wird. Dieser Name wird in allen Protokollen der übergeordneten Klasse verwendet;
  • Zeile 10: Es wird vermerkt, dass das Fragment erstellt wird. Diese Information wird verwendet, wenn das untergeordnete Fragment aufgefordert wird, sich 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 nämlich gezeigt, dass zu dem Zeitpunkt, an dem die Methode [onCreateOptionsMenu] ausgeführt wird – was geschieht, wenn das Fragment über ein Menü verfügt –, das Fragment bereits seiner Aktivität, seiner Ansicht und seinem Menü zugeordnet wurde und sichtbar wird. Dies ist also der Zeitpunkt, zu dem die Aktualisierung der Benutzeroberfläche und des Menüs erfolgen kann. In dieser Methode [onCreateOptionsMenu] fordern wir das untergeordnete Fragment auf, sich zu aktualisieren.

Die Menüverwaltung umfasst Hilfsmethoden, mit denen das untergeordnete Fragment Menüpunkte anzeigen oder ausblenden kann:


  // Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
...
  // Menüverwaltung ------------------------------------------
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
    // Alle Menüpunkte durchlaufen
    for (int i = 0; i < menu.size(); i++) {
      // Menüpunkt Nr. i
      MenuItem menuItem = menu.getItem(i);
      menuOptionsIds.add(menuItem.getItemId());
      // Wenn Menüpunkt Nr. i ein Untermenü ist, dann von vorne beginnen
      if (menuItem.hasSubMenu()) {
        // Rekursion
        getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
      }
    }
  }

  private void getMenuOptionsStates(Menu menu) {
    // Ergebnis
    if (isDebugEnabled) {
      Log.d(className, "getMenuOptionsStates(Menu)");
    }
    // Die Kennungen der Menüoptionen werden abgerufen
    List<Integer> menuOptionsIds = new ArrayList<>();
    getMenuOptions(menu, menuOptionsIds);
    // Die Menüoptionen werden in ein Array übertragen
    menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // Options-ID
      int id = menuOptionsIds.get(i);
      // Status der Option
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // Ergebnis
    if (isDebugEnabled) {
      Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
    }
  }

  // Status der Menüoptionen
  private MenuItemState[] getMenuOptionsStates() {
    MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
    for (int i = 0; i < menuOptionsStates.length; i++) {
      // Status
      MenuItemState state = this.menuOptionsStates[i];
      // Menü-ID
      int id = state.getMenuItemId();
      // Statusinitialisierung
      menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
    }
    // Ergebnis
    return menuOptionsStates;
  }

  // Anzeige der Menüoptionen -----------------------------------
  protected void setAllMenuOptionsStates(boolean isVisible) {
    // Alle Menüoptionen werden aktualisiert
    for (MenuItemState menuItemState : menuOptionsStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
    }
  }

  protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
    // Bestimmte Menüoptionen werden aktualisiert
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}
  • Zeile 6–18: Mit dieser Methode lassen sich die numerischen Kennungen aller Menüoptionen abrufen;
  • Zeile 6: Die Methode [getMenuOptions] erhält zwei Parameter:
    • [Menu menu]: das Menü des Fragments;
    • [List<Integer> menuOptionsIds]: die Liste der Android-Identifikatoren der Menüoptionen. Zu Beginn ist diese Liste leer. Sie wird anschließend durch einen rekursiven Durchlauf (Zeile 15) des Menübaums gefüllt;
  • Zeilen 20–40: Ausgehend vom Menü wird das Array mit den Zuständen (ID, Sichtbarkeit) der Menüoptionen erstellt. 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 denselben Vorgang aus, doch anstatt die Kennungen aller Menüoptionen neu zu berechnen – was bereits geschehen ist –, verwendet sie die Kennungen aus dem Statusarray in Zeile 3;
  • Zeilen 58–63: Die Methode [setAllMenuOptionsStates] ermöglicht es, alle Optionen des Fragmentmenüs ein- oder auszublenden;
  • Zeilen 65–69: Die Methode [setMenuOptionsStates] ermöglicht es, bestimmte Menüoptionen selektiv ein- oder auszublenden;
  • Die Methoden [getMenuOptions, getMenuOptionsStates] sind als privat deklariert, da sie ausschließlich in [AbstractFragment] verwendet werden. Die Methoden [setAllMenuOptionsStates] (Zeile 58) und [setMenuOptionsStates] (Zeile 65) sind als „protected“ deklariert, damit sie für untergeordnete Klassen verfügbar sind;

2.7.3.4. Verwaltung des Wartens auf das Ende einer asynchronen Aufgabe


   // Abonnements für Beobachtungsgrößen
  private List<Subscription> abonnements = new ArrayList<>();
// Asynchrone Aufgaben
  protected int numberOfRunningTasks;
  protected boolean tasksInBackgroundHaveBeenCanceled;
...

  // Verwaltung des Wartens auf das Ende einer asynchronen Operation -------------------------------------
  protected void beginRunningTasks(int numberOfRunningTasks) {
    // Die Anzahl der Aufgaben, die ausgeführt werden, wird notiert
    this.numberOfRunningTasks = numberOfRunningTasks;
    // Das Wartezustandsbild wird gesetzt
    mainActivity.beginWaiting();
    // Die Liste der Abonnements wird geleert
    abonnements.clear();
    // noch keine Stornierung
    runningTasksHaveBeenCanceled = false;
  }

  protected void cancelWaitingTasks() {
    // Das Ladebild wird ausgeblendet
    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: Der Parameter der Methode wird gespeichert;
  • Zeile 13: Das Wartebild wird angezeigt;
  • Zeile 15: Die Liste der Abonnements für asynchrone Vorgänge wird bereinigt. Diese wurden vom Tochterfragment noch nicht angelegt;
  • Zeile 17: Es wird ein boolescher Wert verwaltet, um anzuzeigen, dass die vom Tochterfragment angeforderten asynchronen Aufgaben abgebrochen wurden. Zu Beginn hat dieser Wert den Wert false;
  • Zeilen 20–25: Das Tochterfragment ruft die übergeordnete Methode [cancelWaitingTasks] auf, um anzugeben, dass es die von ihm gestarteten Aufgaben abbrechen möchte;
  • Zeile 22: Das Warteziel wird ausgeblendet;

2.7.3.5. Ausnahmebehandlung


  // Ausnahmebehandlung -------------------------------------------------------------------

  // Warnmeldung bei Ausnahme anzeigen
  protected void showAlert(Throwable th) {
    // Meldungen aus dem Ausnahmestapel des Throwable th anzeigen
    new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
  }

  // Anzeige der Meldungsliste
  protected void showAlert(List<String> messages) {
    // Anzeige der Meldungsliste
    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 des Ausnahmestapels der als Parameter übergebenen Methode 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. Verwaltung asynchroner Vorgänge


...
  // Abonnements für Observables
  private List<Subscription> abonnements = new ArrayList<>();
  // Asynchrone Aufgaben
  private boolean runningTasksHaveBeenCanceled;
  protected int numberOfRunningTasks;
...
  // Ausführung einer asynchronen Aufgabe mit RxAndroid
  protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
    // Prozess: das auszuführende bzw. zu beobachtende Observable
    // consumeResult: Die Methode, die die erhaltene Antwort auswertet
    // 
    // Neue Abonnements werden nur erstellt, wenn keine Kündigung vorliegt
    if (!runningTasksHaveBeenCanceled) {
      // Ausführung auf dem E/A-Thread und Beobachtung auf dem UI-Thread
      process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
      // Das Observable wird ausgeführt
      try {
        abonnements.add(process.subscribe(
          // Verarbeitung des Ergebnisses
          consumeResult,
          // Ausnahme verarbeitet
          new Action1<Throwable>() {
            @Override
            public void call(Throwable th) {
              consumeThrowable(th);
            }
          },
          // Aufgabenende
          new Action0() {

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

  private void endOfTask() {
...
  }

  // Eine asynchrone Operation hat eine Ausnahme ausgelöst
  // oder während der Ausführung einer asynchronen Operation ist eine Ausnahme aufgetreten
  private void consumeThrowable(Throwable th) {
...
  }

  • Zeilen 9–41: führen eine asynchrone Aufgabe aus;
  • Zeile 9: Die Methode [executeInBackground] erwartet zwei Parameter:
    • [Observable<T> process]: den auszuführenden asynchronen Prozess;
    • [Action1<T> consumeResult]: die Methode des Tochterfragments, die aufgerufen werden soll, um ihr die vom Prozess ausgegebenen Elemente zu übermitteln. 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 nicht bereits eine Abbruchaktion durch den Benutzer oder das Programm (aufgrund einer Ausnahme) stattgefunden hat;
  • 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 die Ausführung des Prozesses im E/A-Thread. Innerhalb dieses Threads läuft die Ausführung synchron ab, da wir eine Bibliothek HTTP verwenden, die synchron ist;
  • 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 aufgetreten ist. Die Verarbeitung 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 [abonnements] gespeichert, in dem alle gestarteten asynchronen Aufgaben erfasst werden. Dies ermöglicht es, sie bei Bedarf abzubrechen;
  • Zeilen 37–39: Diese Methode wird ausgeführt, wenn während der Verarbeitung der asynchronen Aufgabe eine Ausnahme aufgetreten ist. Die Verarbeitung wird an die Methode [consumeThrowable] in Zeile 49 delegiert;

Die Methode [endOfTask] lautet wie folgt:


  // asynchrone Aufgaben
  protected int numberOfRunningTasks;
...
  private void endOfTask() {
    // Eine Aufgabe weniger, auf die man warten muss
    numberOfRunningTasks--;
    // Fertig?
    if (numberOfRunningTasks == 0) {
      // Wartezeit beendet
      cancelWaitingTasks();
      // Das Ende der Aufgaben wird an die untergeordnete Klasse gemeldet
      notifyEndOfTasks(false);
    }
  }
...
  // Tochterklassen -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • Zeile 6: Eine asynchrone Aufgabe wurde gerade abgeschlossen. Der Zähler für aktive Aufgaben wird verringert;
  • Zeile 8: Wenn keine aktiven Aufgaben mehr vorhanden sind, hat das Tochterfragment alle seine Antworten erhalten;
  • Zeile 10: Die Wartezeit wird abgebrochen;
  • Zeile 12: Dem Tochterfragment wird durch Aufruf seiner Methode [notifyEndOfTasks] mitgeteilt, dass alle von ihm gestarteten Aufgaben abgeschlossen sind. Der Parameter dieser Methode gibt an, wie die Aufgaben beendet wurden: normal, durch Abbruch seitens des Benutzers oder des Codes oder aufgrund einer aufgetretenen Ausnahme. In Zeile 12 wird ein normales Ende gemeldet. Es ist zu beachten, dass sich das Tochterfragment nicht darum kümmern muss, den Überblick über die noch aktiven Aufgaben zu behalten. Dies übernimmt die übergeordnete Klasse für sie;

Die Methode [consumeThrowable] lautet wie folgt:


  // asynchrone Aufgaben
  protected int numberOfRunningTasks;
  private boolean runningTasksHaveBeenCanceled;
...
    // Bei einer asynchronen Operation ist eine Ausnahme aufgetreten
  // oder während der Ausführung einer asynchronen Operation ist eine Ausnahme aufgetreten
  private void consumeThrowable(Throwable th) {
    // th: Die zu bearbeitende Ausnahme
    // 
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "Exception reçue");
    }
    // bereits gestartete Aufgaben werden abgebrochen
    cancelRunningTasks();
    // Fehlermeldungen werden angezeigt
    showAlert(th);
  }

  // Abbruch der Aufgaben
  protected void cancelRunningTasks() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "Annulation des tâches lancées");
    }
    // Alle gespeicherten asynchronen Aufgaben werden abgebrochen
    for (Subscription abonnement : abonnements) {
      abonnement.unsubscribe();
    }
    // Abbruch wird protokolliert
    runningTasksHaveBeenCanceled = true;
    numberOfRunningTasks = 0;
    // Wartezeit beendet
    cancelWaitingTasks();
    // Die Stornierung der Aufgaben wird an das untergeordnete Fragment gemeldet
    notifyEndOfTasks(true);
}

...
  // Tochterklassen -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
  • Zeile 3: Die Methode [consumeThrowable] empfängt die aufgetretene Ausnahme;
  • Zeile 15: Alle noch aktiven Aufgaben werden abgebrochen;
  • Zeile 17: Der Text der Ausnahme wird angezeigt;
  • Zeilen 21–37: Abbruch aller Aufgaben;
  • Zeilen 27–29: Alle Abonnements werden gekündigt;
  • Zeile 31: Es wird vermerkt, dass eine Stornierung stattgefunden hat;
  • Zeile 32: Der Aufgabenzähler wird auf Null zurückgesetzt;
  • Zeile 34: Die Wartezeit wird abgebrochen;
  • Zeile 36: Dem Tochterfragment wird das Ende der Aufgaben bei Stornierung gemeldet;

2.7.3.7. Verwaltung des Lebenszyklus des Fragments


  // Lebenszyklus --------------------------------------------------------
  @Override
  public void onDestroyView() {
    // übergeordnet
    super.onDestroyView();
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "onDestroyView");
    }
  }

  @Override
  public void onDestroy() {
    // übergeordnet
    super.onDestroy();
    // Protokoll
    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 der Protokollierung. Diese ermöglichen es dem Entwickler, den Lebenszyklus der Fragmente besser nachzuvollziehen;

Das Speichern des Fragments bei einer Drehung des Geräts erfolgt über die folgenden Methoden [setUserVisibleHint, onSaveInstanceState, saveState]:


  // Lebenszyklus des Fragments
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
...

@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // übergeordnet
    super.setUserVisibleHint(isVisibleToUser);
    // Speichern?
    if (this.isVisibleToUser && !isVisibleToUser) {
      // Das Fragment wird ausgeblendet – es wird gesichert
      if (!saveFragmentDone) {
        saveState();
      }
    }
    // Speicher
    this.isVisibleToUser = isVisibleToUser;
  }

  private void saveState() {
...
  }

  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
    }
    // übergeordnet
    super.onSaveInstanceState(outState);
    // Das Fragment wird nur gespeichert, wenn es sichtbar ist
    if (isVisibleToUser) {
      // Möglicherweise wurde die Sicherung bereits durchgeführt
      if (!saveFragmentDone) {
        saveState();
      }
      // Wiederherstellung in jedem Fall erforderlich
      session.setAction(ISession.Action.RESTORE);
    }
}
  • Zeilen 6–19: Das Fragment wird gespeichert, wenn es vom angezeigten in den ausgeblendeten Zustand wechselt (Zeile 11). Diese Information liefert uns die Methode [setUserVisibleHint];
  • Zeile 14: Die Speicherung erfolgt durch die private Methode in den Zeilen 21–23;
  • Zeilen 25–41: Bei einer Drehung des Geräts 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, onSaveInstanceState] nicht beide ausgeführt werden können, wenn das Fragment sichtbar ist, und dass daher die Verwaltung des Booleschen Werts [saveFragmentDone] überflüssig ist. Im Zweifelsfall habe ich mich dafür entschieden, diesen zu verwenden;
  • Zeile 40: Nach dem Speichern folgt die Wiederherstellung. Für den Fall, dass das Fragment das nächste Mal aktualisiert werden muss, ist zu beachten, dass dies über eine Operation [RESTORE] erfolgen muss;

Es sei auf die beiden Zeitpunkte hingewiesen, zu denen eine Sicherung des Fragments angefordert wird:

  1. wenn es vom sichtbaren in den ausgeblendeten Zustand wechselt;
  2. wenn eine Drehung des Geräts stattfindet;

Die private Methode [saveState] lautet wie folgt:


...
  private void saveState() {
    // Aufgaben abbrechen?
    if (numberOfRunningTasks != 0) {
      // Die Aufgaben werden abgebrochen
      cancelRunningTasks();
    }
    // Der Status des Fragments wird gesichert
    CoreState currentState = saveFragment();
    // Das Fragment wurde aufgerufen
    currentState.setHasBeenVisited(true);
    // Menüstatus speichern
    currentState.setMenuOptionsState(getMenuOptionsStates());
    // Anmeldung
    session.setCoreState(getNumView(), currentState);
    // Speicherung abgeschlossen
    saveFragmentDone = true;
    // Protokoll
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }


...
  // Unterklassen -----------------------------------------------------
public abstract CoreState saveFragment();

protected abstract int getNumView();
  • Zeile 4–7: Die Drehung des Geräts kann stattfinden, während asynchrone Vorgänge laufen. Hier wird beschlossen, alle diese Vorgänge abzubrechen. Das ist keine gute Entscheidung für den Nutzer, der nun eine neue, möglicherweise langwierige Anfrage stellen muss, obwohl er lediglich sein Smartphone oder Tablet bewegt oder einen Anruf erhalten hat. Es ist möglich, die Netzwerkverbindungen über einen Sicherungs-/Wiederherstellungszyklus hinweg aufrechtzuerhalten. Allerdings sind die Lösungen nicht offensichtlich, und ich habe beschlossen, sie in diesem Einsteigerkurs nicht zu behandeln. Der richtige Weg besteht darin, diese Netzwerkverbindungen über ein Fragment herzustellen, das keine visuelle Benutzeroberfläche hat und beim Sicherungs-/Wiederherstellungszyklus nicht zerstört wird. Dazu reicht es aus, die Anweisung [Fragment.setRetainInstance(true)] zu verwenden;
  • Zeile 9: Das Tochterfragment wird aufgefordert, seinen Zustand in einem von [CoreState] abgeleiteten Typ zu speichern (Zeile 31);
  • Zeile 11: Es wird vermerkt, dass das Fragment aufgerufen wurde. Diese Information ist nützlich. Wenn ein Fragment zum ersten Mal aufgerufen wird, kann seine Aktualisierung von den nachfolgenden abweichen, da es zu diesem Zeitpunkt noch keinen vorherigen Zustand in der Sitzung hat;
  • Zeile 13: Der Zustand des Menüs wird gespeichert, wodurch wir es automatisch wiederherstellen können;
  • Zeile 15: Dieser aktuelle Zustand wird in der Sitzung gespeichert. In dieser werden die Zustände nach Ansicht/Fragment gruppiert, wobei jede Ansicht einen eigenen Zustand hat. Die Nummer der Ansicht wird vom untergeordneten Fragment bereitgestellt (Zeile 33);
  • Zeile 17: Es wird vermerkt, dass das Fragment gespeichert wurde. Dies geschieht, da zwei Methoden die Methode [saveState] aufrufen können und es unnötig ist, zwei Speichervorgänge durchzuführen;

Die Neugenerierung der mit dem Fragment verbundenen Ansicht erfolgt durch die folgende Methode:


  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // übergeordnete Klasse
    super.onActivityCreated(savedInstanceState);
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // Die Ansicht muss wiederhergestellt werden
    viewHasToBeInitialized = true;
}

Im Lebenszyklus wird die Methode [onActivityCreated] unmittelbar nach der Methode [onCreateView] ausgeführt. Der Aufruf dieser letzteren Methode signalisiert, dass die dem Fragment zugeordnete Ansicht neu aufgebaut werden muss. Dies wird lediglich in Zeile 10 vermerkt.

2.7.3.8. Aktualisierung des Fragments

Die Aktualisierung des Fragments ist der letzte Vorgang, der am Fragment durchgeführt wird, bevor es sichtbar wird und auf Benutzeraktionen wartet. Dies wird durch den folgenden Code gewährleistet:


  // Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Zustände des Fragments
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...

  // Fragment aktualisieren ----------------------------------------------------------------------------------
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "onCreateOptionsMenu");
    }
    // Speicher
    this.menu = menu;
    // Die # Menüoptionen werden abgerufen, falls dies noch nicht geschehen ist
    if (fragmentHasToBeInitialized) {
      // Die # Menüoptionen werden abgerufen
      getMenuOptionsStates(menu);
      // Aktivität
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = (Session) this.mainActivity.getSession();
    }
    // Der vorherige Status des Fragments wird abgerufen (beim allerersten Mal hat nur der Boolesche Wert hasBeenVisited eine Bedeutung)
    previousState = session.getCoreState(getNumView());
    // Aktualisierung des untergeordneten Fragments in mehreren Schritten
    // Schritt 1 – Ist dies der erste Besuch?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
  ...
    } else {
      // Es ist nicht der erste Besuch
      // Schritt 2: Muss das Fragment initialisiert werden?
      ...
      // Schritt 3: Muss die Ansicht initialisiert werden?
      ...
    }
    // Schritt 4: Ein Submit, eine Navigation, ein Restore?
    ...

    // Schritt 5: Aktualisierungen am Endgerät ----------------------
...
  }
...
  // Unterklassen -----------------------------------------------------
  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: Zur Aktualisierung des Fragments wird die Methode [onCreateOptionsMenu] verwendet. Aus diesem Grund muss das Fragment über ein Menü verfügen, das bei Bedarf leer sein kann. Wenn diese Methode ausgeführt wird, wurde das Fragment bereits seiner Ansicht und seiner Aktivität zugeordnet und ist zudem sichtbar;
  • Zeile 25: Das Menü, das als Parameter (Zeile 22) an die Methode übergeben wurde, wird gespeichert;
  • Zeilen 27–34: Falls das Fragment initialisiert werden muss:
    • Zeile 29: Die Zustände der Menüoptionen werden in das Array [menuOptionsStates] aus Zeile 3 geschrieben;
    • 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. Die Typumwandlung ist erforderlich, da die Methode [mainActivity.getSession()] einen Typ [ISession] zurückgibt;
  • Zeile 36: Der vorherige Status des Fragments wird aus der Sitzung abgerufen. Handelt es sich um den ersten Besuch des Fragments, ist nur der boolesche Wert [previousState.hasBeenVisited] von Bedeutung;
  • Zeilen 39–44: Code, der ausgeführt wird, wenn das Fragment zum ersten Mal aufgerufen wird. In diesem Fall ist sein vorheriger Status nicht relevant;
  • Zeilen 44–50: Code, der ausgeführt wird, wenn es sich nicht um den ersten Aufruf des Fragments handelt;
  • Zeilen 46–47: Code, der ausgeführt wird, wenn der Konstruktor des Fragments aufgerufen wurde (fragmentHasToBeInitialized == true);
  • Zeilen 48–49: Der Code wird ausgeführt, wenn die dem Fragment zugeordnete Ansicht neu aufgebaut wurde (viewHasToBeInitialized==true);
  • Zeilen 51–52: Der Code wird je nach der aktuellen Aktion (SUBMIT, NAVIGATION, RESTORE) ausgeführt;
  • Zeilen 54–55: Der Code wird weiterhin ausgeführt;

Die fünf Schritte der Aktualisierung sind wie folgt:

Schritt 1


  // Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Fragmentzustände
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // Der vorherige Zustand des Fragments wird abgerufen (beim allerersten Mal hat nur der Boolesche Wert hasBeenVisited eine Bedeutung)
    previousState = session.getCoreState(getNumView());
    // Aktualisierung des untergeordneten Fragments in mehreren Schritten
    // Schritt 1 – Ist dies der erste Besuch?
    if (!previousState.getHasBeenVisited()) {
      if (isDebugEnabled) {
        Log.d(className, "initFragment initView updateForFirstVisit");
      }
      // Initialisierung von Fragment und Ansicht
      initFragment(null);
      initView(null);
      // previousState für den weiteren Verlauf zurücksetzen
      previousState = null;
    } else {
      // Dies ist nicht der erste Besuch
...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • Zeile 19: Der vorherige Status des Fragments wird aus der Sitzung abgerufen;
  • Zeilen 22–31: Der Code wird ausgeführt, 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 es sich um den ersten Besuch handelt;
  • Zeile 28: Die untergeordnete Klasse wird aufgefordert, die dem Fragment zugeordnete 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 mitzuteilen, dass es sich um den ersten Besuch handelt;
  • Zeile 30: Der vorherige Zustand wird für die folgenden Schritte auf null gesetzt;

Schritte 2 und 3


// Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Zustände des Fragments
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // Der vorherige Status des Fragments wird abgerufen (beim allerersten Mal hat nur der boolesche Wert hasBeenVisited eine Bedeutung)
    previousState = session.getCoreState(getNumView());
    // Aktualisierung des untergeordneten Fragments in mehreren Schritten
    // Schritt 1 – Ist dies der erste Besuch?
    if (!previousState.getHasBeenVisited()) {
...
    } else {
      // Es ist nicht der erste Besuch
      // Schritt 2: Muss das Fragment initialisiert werden?
      if (fragmentHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation fragment");
        }
        // Unterfragment
        initFragment(previousState);
      }
      // Schritt 3: Muss die Ansicht initialisiert werden?
      if (viewHasToBeInitialized) {
        if (isDebugEnabled) {
          Log.d(className, "initialisation vue");
        }
        // Unterfragment
        initView(previousState);
      }
    }

...

  protected abstract void initFragment(CoreState previousState);

protected abstract void initView(CoreState previousState);
  • Zeilen 24–42: werden ausgeführt, wenn es sich nicht um den ersten Besuch des Fragments handelt;
  • Zeilen 27–33: Wenn das Fragment gerade neu aufgebaut wurde, wird es durch Aufruf der Methode [initFragment] der untergeordneten Klasse (Zeilen 32, 46) zurückgesetzt. Dabei wird der vorherige Zustand des Fragments übergeben;
  • Zeilen 35–51: Wenn die dem Fragment zugeordnete Ansicht initialisiert oder zurückgesetzt werden muss, wird das Tochterfragment damit beauftragt (Zeilen 40, 48). Auch hier wird ihm der letzte bekannte Zustand des Fragments übergeben;

Schritt 4


// Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Zustände des Fragments
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // Der vorherige Zustand des Fragments wird abgerufen (beim allerersten Mal hat nur der Boolesche Wert hasBeenVisited eine Bedeutung)
    previousState = session.getCoreState(getNumView());
    // Aktualisierung des untergeordneten Fragments in mehreren Schritten
 ...

    // Schritt 4: Ein Submit, eine Navigation, ein Restore?
    // Protokoll
    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();
      }
    }
    // Aktion läuft
    ISession.Action action = session.getAction();
    switch (action) {
      case SUBMIT:
        if (isDebugEnabled) {
          Log.d(className, "updateOnSubmit");
        }
        // Tochterfragment
        updateOnSubmit(previousState);
        break;
      case NAVIGATION:
        if (isDebugEnabled) {
          Log.d(className, "updateForNavigation");
        }
        if (previousState != null) {
          // Wiederherstellung des Menüs
          setMenuOptionsStates(previousState.getMenuOptionsState());
          // Tochterfragment
          updateOnRestore(previousState);
        } else {
          // Es handelt sich um einen ersten Besuch – nichts zu tun
        }
        break;
      case RESTORE:
        // Wiederherstellung
        if (isDebugEnabled) {
          Log.d(className, "updateOnRestore");
        }
        // Menü-Wiederherstellung (previousState darf nicht null sein)
        setMenuOptionsStates(previousState.getMenuOptionsState());
        // Tochter-Fragment
        updateOnRestore(previousState);
        break;
    }
....
  protected abstract void updateOnSubmit(CoreState previousState);

protected abstract void updateOnRestore(CoreState previousState);
  • Zeilen 34–66: Die aktuelle Aktion wird verarbeitet, wobei es sich um eine der folgenden drei handeln kann:
    • RESTORE: Das Fragment wird nach einer Drehung des Geräts wiederhergestellt;
    • NAVIGATION: Man kehrt zu dem Fragment zurück und möchte es in dem Zustand wiederfinden, in dem man es bei der letzten Verwendung hinterlassen hat;
    • SUBMIT: alle anderen Fälle;
  • Zeile 34: Die aktuelle Aktion wird abgerufen;
  • Zeilen 36–42: Bei einer Aktion vom Typ SUBMIT wird die Methode [updateOnSubmit] des untergeordneten Fragments (Zeilen 41, 68) aufgerufen, wobei der letzte bekannte Zustand des Fragments übergeben wird;
  • 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. Der Vorgang NAVIGATION kann mit einem ersten Besuch einhergehen. Dies wäre beispielsweise in einer Anwendung mit Registerkarten der Fall: Wenn ich von Registerkarte 1 zu Registerkarte 4 wechsle:
    • muss ich das Fragment von Registerkarte 4 initialisieren, wenn es sich um den ersten Besuch handelt;
    • das Fragment von Registerkarte 4 in seinen vorherigen Zustand zurücksetzen, wenn es sich nicht um den ersten Besuch handelt;
  • Zeilen 52–54: Bei einem ersten Besuch wird nichts unternommen. Diese Initialisierung übernimmt die untergeordnete Methode [initView(CoreState previousState)]. Der erste Besuch wird durch die Bedingung [previousState==null] gekennzeichnet;
  • Zeile 49: Wenn es sich nicht um den ersten Besuch des Fragments handelt, wird ihm sein Menü wiederhergestellt;
  • Zeile 51: Die untergeordnete Klasse wird aufgefordert, sich durch Aufruf der Methode in Zeile 70 zu aktualisieren. Der vorherige Zustand des Fragments wird an sie übergeben, damit sie ihre Aufgabe ausführen kann;
  • Zeilen 56–66: Im Falle einer Wiederherstellung des Fragments wird genauso vorgegangen wie bei einer Navigation außerhalb des ersten Besuchs;

Schritt 5


// Fragment-Menü
  private Menu menu;
  private MenuItemState[] menuOptionsStates;
  // Lebenszyklus des Fragments
  private boolean initDone = false;
  private boolean isVisibleToUser = false;
  private boolean saveFragmentDone = false;
  // Zustände des Fragments
  private CoreState previousState;
  // Mapper jSON
  private ObjectMapper jsonMapper = new ObjectMapper();
  // Lebenszyklus des Fragments
  private boolean fragmentHasToBeInitialized = false;
  private boolean viewHasToBeInitialized = false;
...


    // Schritt 5: Endgültige Aktualisierungen ----------------------
    // Die Ansicht wurde gewechselt
    session.setPreviousView(getNumView());
    // Keine Aktion mehr im Gange
    session.setAction(ISession.Action.NONE);
    // Wenn dieses Fragment verlassen wird, muss es gespeichert werden
    saveFragmentDone = false;
    // Solange das Fragment nicht neu aufgebaut ist, muss es nicht initialisiert werden
    fragmentHasToBeInitialized = false;
    // Solange die Ansicht nicht neu aufgebaut ist, muss sie nicht initialisiert werden
    viewHasToBeInitialized = false;
    // Es wird zum normalen Betrieb der Registerkartenauswahl zurückgekehrt
    session.setNavigationOnTabSelectionNeeded(true);

    // dem Fragment wird mitgeteilt, dass die Ansicht bereit ist
    if (isDebugEnabled) {
      Log.d(className, "notifyEndOfUpdates");
    }
    notifyEndOfUpdates();
...
  protected abstract void notifyEndOfUpdates();
  • Zeilen 18–30: Wenn wir hier angelangt sind, wurde das Fragment initialisiert und ist bereit zur Anzeige. Wir setzen nun alle Indikatoren, die bei der Lebenszyklusverwaltung des Fragments verwendet werden, auf ihren Ausgangszustand zurück;
  • Zeile 20: Die Ansicht wurde gewechselt: Dies wird in der Sitzung vermerkt;
  • Zeile 22: Es ist keine Aktion mehr im Gange;
  • Zeile 24: Wenn das derzeit angezeigte Fragment nun verlassen wird, muss es beim Verlassen gespeichert werden;
  • Zeile 26: Das Fragment muss nicht mehr neu erstellt werden. Dieser Indikator wird auf vrai zurückgesetzt, sobald der Konstruktor des Fragments erneut ausgeführt wird;
  • Zeile 28: Die dem Fragment zugeordnete Ansicht muss nicht mehr initialisiert werden. Dieser Indikator wird auf vrai zurückgesetzt, sobald die Methode [onActivityCreated] erneut ausgeführt wird;
  • Zeile 30: Das Fragment wird möglicherweise in einer Anwendung mit Registerkarten angezeigt. In diesem Fall muss ein Fragmentwechsel erfolgen, wenn der Benutzer auf eine der Registerkarten klickt;
  • Zeile 36: Der untergeordneten Klasse wird mitgeteilt, dass das Fragment bereit ist. Diese kann in der Methode [notifyEndOfUpdates] Aktualisierungen vornehmen, die in jedem Fall durchgeführt werden müssen, eine asynchrone Operation starten, um neue Daten abzurufen, …

2.7.4. Ein Beispiel für ein Fragment

  

Im Projekt [client-android-skel] wurde ein Beispielfragment eingefügt, um dem Leser die typische Struktur eines Fragments 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 {

  // Von der übergeordneten Klasse geerbte Felder -------------------------------------------------------

  // Debug-Modus
  //-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  // Klassenname
  //-- geschützter String className;
  // asynchrone Aufgaben
  //-- protected int numberOfRunningTasks;
  // Aktivität
  //-- protected IMainActivity mainActivity;
  //-- geschützte Aktivität;
  // Sitzung
  //-- protected Session session;

  // von der übergeordneten Klasse geerbte Methoden -------------------------------------------------------

  // Anzeige der Menüoptionen
  //-- protected void setAllMenuOptionsStates(boolean isVisible) {
  //-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
  // Verwaltung des Wartens auf das Ende einer Reihe asynchroner Aufgaben
  //-- protected void beginRunningTasks(int numberOfRunningTasks) {
  //-- protected void cancelWaitingTasks() {
  // Ausführung einer asynchronen Aufgabe mit RxAndroid
  //-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
  // Abbruch von Aufgaben
  //-- protected void cancelRunningTasks() {
  // Anzeige einer Warnmeldung bei einer Ausnahme
  //-- protected void showAlert(Throwable th) {
  // Anzeige der Nachrichtenliste
  //-- protected void showAlert(List<String> messages) {

  // Von der übergeordneten Klasse vorgeschriebene Methoden -------------------------------------------------------

  @Override
  public CoreState saveFragment() {
    // Das Fragment muss gespeichert werden
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // Wennes nichts zu speichern gibt, [return new CoreState();] ausführen und die Klasse [DummyFragmentState] löschen
  }

  @Override
  protected int getNumView() {
    // Die Nummer des Fragments muss in die Tabelle der von der Aktivität verwalteten Fragmente zurückgeschrieben werden (siehe MainActivity)
    return 0;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // Das Fragment wird sichtbar und wurde in diesem oder einem vorherigen Schritt erstellt
    // Dies geschieht beim Start der Anwendung und bei jeder Drehung des Android-Geräts
    // darauf folgt zwangsläufig die Ausführung von [initView]
    // Die Felder des neu erstellten Fragments müssen initialisiert werden
    // previousState ist die letzte Speicherung des Fragments – hat den Wert null, wenn es sich um den ersten Besuch des Fragments handelt
  }

  @Override
  protected void initView(CoreState previousState) {
    // Das Fragment wird sichtbar und die zugehörige Ansicht wurde in diesem oder einem vorherigen Schritt neu aufgebaut
    // Dies geschieht jedes Mal, wenn [initFragment] ausgeführt wird, und jedes Mal, wenn das Fragment die Nachbarschaft des angezeigten Fragments verlässt
    // Die Komponenten der neu erstellten Ansicht müssen initialisiert werden
    // previousState ist die letzte Speicherung des Fragments – hat den Wert null, wenn es sich um den ersten Besuch des Fragments handelt

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // wird nach [initFragment, initView] ausgeführt, wenn diese Methoden ausgeführt werden
    // wird die Ansicht nach einer Operation vom Typ SUBMIT angezeigt
    // In der Regel müssen das Fragment und die zugehörige Ansicht aus der Sitzung initialisiert werden
    // previousState ist die letzte Speicherung des Fragments – hat den Wert null, wenn es sich um den ersten Besuch des Fragments handelt
    // Es ist nichts zu tun, wenn das Fragment nicht über eine Operation SUBMIT erreicht werden kann
    // Wenn man über Operationen SUBMIT von verschiedenen Fragmenten aus zu dem Fragment gelangen kann, lässt sich die vorherige Ansicht über [session.getPreviousView] ermitteln
    // Wenn man über mehrere Operationen SUBMIT von demselben Fragment aus zu diesem Fragment gelangen kann, muss ein Indikator gesetzt werden, um die verschiedenen Arten von SUBMIT ausgehend von diesem Fragment zu unterscheiden
  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // wird nach [initFragment, initView] ausgeführt, wenn diese Methoden ausgeführt werden
    // wird die Ansicht nach einer Operation vom Typ RESTORE oder NAVIGATION angezeigt
    // previousState ist die letzte Sicherung des Fragments – ist niemals null
    // Die Ansicht muss in ihren vorherigen Zustand zurückversetzt werden

  }

  @Override
  protected void notifyEndOfUpdates() {
    // wird nach den Methoden [updateOnSubmit, updateOnRestore] aufgerufen
    // An dieser Stelle ist die Ansicht bereits aufgebaut und initialisiert
    // Oft gibt es hier nichts zu tun, aber man kann hier auch Aktionen zusammenfassen, die unabhängig davon ausgeführt werden müssen, wie man zu dieser Ansicht gelangt
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // wird aufgerufen, wenn die vom Fragment gestarteten asynchronen Aufgaben entweder abgeschlossen oder abgebrochen wurden
    // Diese beiden Fälle lassen sich anhand des Parameters runningTasksHaveBeenCanceled unterscheiden
    // In der Regel muss die Ansicht in einen anderen Zustand versetzt werden als den, in dem sie sich befand, während sie auf die Antworten der asynchronen Aufgaben wartete

  }
}

Die Klasse [DummyFragment] muss nicht unbedingt einen Status haben. Hier wurde einer hinzugefügt, um zu verdeutlichen, was darin erwartet wird:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class DummyFragmentState extends CoreState {
  // Zustand des Fragments [DummyFragment]
  // Es dürfen nur serialisierbare Felder in jSON gesetzt werden
  // Die Annotation @JsonIgnore auf die anderen setzen, aber es ist unklar, wozu sie dienen könnten
  // Die Getter/Setter nicht vergessen – sie werden für die Serialisierung/Deserialisierung verwendet
}

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 damit, bereits geschriebene Beispiele umzugestalten.

2.8.1. Beispiel 17B

Wir greifen das in Abschnitt 1.18 behandelte Beispiel 17 wieder auf. Es handelt sich um eine Anwendung mit einem einzigen Fragment ohne asynchrone Aufgaben und ohne Registerkarten. Wir untersuchen sie, um zu sehen, wie sie sich bei einer Drehung des Geräts verhält. Wir geben Folgendes ein:

Image

Anschließend drehen wir bei [1] das Gerät zweimal. Die neue Ansicht sieht dann wie folgt aus:

Image

Wenn wir die Ansichten vergleichen, ist alles erhalten geblieben, außer der Liste [2], die nun leer ist.

Wenn man außerdem auf die Schaltfläche [Valider] klickt, erscheint ein Dialogfeld, das die im Formular vorgenommenen Eingaben anzeigt. Dreht man das Gerät in diesem Moment, verschwindet das Dialogfeld.

Wir müssen daher bei einer Drehung Folgendes neu generieren:

  • die Dropdown-Liste und das darin ausgewählte Element;
  • das Dialogfeld, falls es zum Zeitpunkt der Drehung angezeigt wurde;

2.8.1.1. Das Projekt [Exemple-17B]

Wir duplizieren das Projekt [client-android-skel] in „Beispiele/Beispiel-17B“. Anschließend laden wir das neue Projekt [1]:

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

Der obere Rand der Ansicht [vue1.xml] wird auf 80 dp geändert:


    <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 kann man eine erste Kompilierung versuchen, um die Fehler zu überprüfen. Die ersten gemeldeten Fehler stammen aus imports und betreffen Pakete, deren Position sich geändert hat. Diese werden korrigiert (Strg-Umschalt-O). Weitere Fehler entstehen dadurch, dass die Ansicht [Vue1Fragment] nicht alle Methoden implementiert, die von ihrer übergeordneten Klasse [AbstractParent] vorgeschrieben sind:

Image

Die fehlenden Methoden werden generiert (Alt-Enter).

Ein weiterer gemeldeter Kompilierungsfehler lautet wie folgt:

Image

Dies wird in der Datei [build.gradle] des Moduls korrigiert (Zeile 20 unten):

 

An dieser Stelle kann man erneut kompilieren, um die verbleibenden Fehler zu überprüfen. Der einzige gemeldete Fehler betrifft die Methode [Vue1Fragment.updateFragment]:

 

Die Anmerkung [@Override] in Zeile 135 muss entfernt werden. Nun liegen keine Fehler mehr vor. Auf dieser Grundlage werden wir das Projekt weiter bearbeiten.

2.8.1.2. Der Status des Fragments [Vue1Fragment]

Das Fragment [Vue1Fragment] muss beim Drehen des Geräts Informationen speichern, damit es vollständig wiederhergestellt werden kann. Dazu erstellen wir eine Klasse [Vue1FragmentState]:

  

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. Anpassung des Projekts

  

Im Ordner „[custom]“ befinden sich die vom Entwickler anpassbaren Architekturkomponenten.

Die Konstanten der 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 {

  // Zugriff auf die Sitzung
  ISession getSession();

  // Wechsel der Ansicht
  void navigateToView(int position, ISession.Action action);

  // Wartungsverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Anwendungskonstanten -------------------------------------

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 0;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // Fragment-Adjazenz
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = false;

  // Ladebild
  boolean IS_WAITING_ICON_NEEDED = false;

  // Anzahl der Anwendungsfragmente
  int FRAGMENTS_COUNT = 1;

}
  • Zeilen 24–31: Die Anwendung nutzt hier ihre Schicht [DAO] nicht. Diese Konstanten werden nicht verwendet;
  • Zeile 34: Eine Fragment-Adjazenz von 1, was dem Standardwert entspricht. Da die Anwendung nur ein Fragment hat (Zeile 43), spielt dieser Wert keine Rolle;
  • Zeilen 39–40: Da keine Operationen mit der Ebene „[DAO]“ stattfinden, ist ein Wartebild überflüssig;
  • Zeile 37: Es handelt sich nicht um eine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die Klasse [Session] lautet wie folgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen

}

Sie ist leer. Da es nur ein Fragment gibt, ist keine Kommunikation zwischen den Fragmenten mit einer Sitzung erforderlich.

Schließlich lautet die Klasse [CoreState] wie folgt:


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 besucht oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeilen 11–13: Wir müssen alle von [CoreState] abgeleiteten Klassen einfügen, die den Status der verschiedenen Fragmente speichern. Hier gibt es nur eine (Zeile 12);

2.8.1.4. Die Aktivität [MainActivity]

Die Aktivität [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 {

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

  // Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Sitzung
    this.session = (Session) super.session;
    // To-Do: Die von der übergeordneten Klasse begonnenen Initialisierungen fortsetzen
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // To-Do: Fragmente hier definieren
    return new AbstractFragment[0];
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // To-Do: Hier die Titel der Fragmente definieren
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // To-do: Navigation über Registerkarten – die anzuzeigende Ansicht definieren, wenn die Registerkarte Nr. [position] ausgewählt ist
  }

  @Override
  protected int getFirstView() {
    // To-do: Nummer der ersten anzuzeigenden Ansicht (Fragment) festlegen
    return 0;
  }
}

Die Kommentare zu [//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 {

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

  // Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Sitzung
    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 Anwendung zurückgeben. In Zeile 43 darf nicht vergessen werden, den Unterstrich hinter den Namen des Fragments zu setzen.

2.8.1.5. Der Status des Fragments [FragmentState]

Nach den Rotationstests am Projekt [Exemple-17] wurde beschlossen, die folgenden Elemente des Fragments zu speichern:

  • die Liste der Werte der Dropdown-Liste;
  • die Position des ausgewählten Elements in dieser Liste;
  • die vom Dialogfeld angezeigte Meldung, falls dieses zum Zeitpunkt der Rotation 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 {

  // Werte der Dropdown-Liste
  private List<String> list;
  // das in der Dropdown-Liste ausgewählte Element
  private int listSelectedPosition;
  // die im Dialogfeld angezeigte Meldung
  private String message;

  // Getter und Setter
...
}

2.8.1.6. Das Fragment [AbstractFragment]

Derzeit wird der Lebenszyklus des Fragments durch zwei Methoden verwaltet (Zeilen 6 und 32):


// Dropdown-Liste
  private List<String> list;
  private ArrayAdapter<String> dataAdapter;

  @AfterViews
  void afterViews() {
    // Die erste Schaltfläche wird aktiviert
    radioButton1.setChecked(true);
    // der Kalender
    datePicker1.setCalendarViewShown(false);
    // das 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));
      }
    });
    // die Dropdown-Liste
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }
...
  protected void updateFragment() {
    // Initialisierung des Adapters für die Dropdown-Liste
    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 dieser beiden Methoden wird wie folgt in die von der Klasse [AbstractFragment] vorgegebenen Methoden migriert:


// Verwaltung des Lebenszyklus des Fragments ---------------------------------------------------------------------
  @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) {
    // Erster Besuch?
    if (previousState == null) {
      // Die Werte der Dropdown-Liste werden erstellt
      list = new ArrayList<>();
      list.add("list 1");
      list.add("list 2");
      list.add("list 3");
    } else {
      // Die Werte der Dropdown-Liste werden zurückgegeben
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      list = state.getList();
      // und die Meldung im Dialogfeld
      message = state.getMessage();
    }
    // Initialisierung des Adapters für die Dropdown-Liste
    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) {
    // der Kalender
    datePicker1.setCalendarViewShown(false);
    // der 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));
      }
    });
    // Initialisierung des Adapters für die Dropdown-Liste
    dropDownList.setAdapter(dataAdapter);
    // Erster Besuch?
    if (previousState == null) {
      // Die erste Schaltfläche wird aktiviert
      radioButton1.setChecked(true);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Wert der Schieberegler
    seekBarValue.setText(String.valueOf(seekBar.getProgress()));
    // Ausgewähltes Element in der Dropdown-Liste
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    dropDownList.setSelection(state.getListSelectedPosition());
    // Dialogfeld sichtbar?
    if (message != null) {
      // wird angezeigt
      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 deren Instanz zurückgeben;
  • Zeilen 11–14: Die Methode [getNumView] muss die Nummer des Fragments zurückgeben. Hier gibt es nur ein Fragment mit der Nummer 0;
  • Zeilen 16–34: Die Methode [initFragment] muss die Felder des Fragments initialisieren. Sie erhält den vorherigen Zustand des Fragments. Wenn [previousState] den Wert null hat, handelt es sich um den ersten Besuch;
  • Zeilen 19–25: Beim ersten Besuch werden die Werte der Dropdown-Liste erstellt;
  • Zeilen 26–30: Handelt es sich nicht um den ersten Besuch, werden die Felder [list, message] des Fragments aus dem vorherigen Status wiederhergestellt;
  • Zeilen 33–34: Initialisierung des Feldes [dataAdapter] des Fragments. Dies ist die Datenquelle der Dropdown-Liste;
  • Zeilen 37–62: Die Methode [initView] dient zur Initialisierung der Komponenten der Benutzeroberfläche. Sie erhält als Parameter den vorherigen Zustand [previousState]. Ist [previousState==null], handelt es sich um den ersten Besuch;
  • Hier findet man das wieder, was zuvor in der Methode [@AfterViews] enthalten war;
  • Zeilen 57–61: Beim ersten Besuch wird sichergestellt, dass das erste Optionsfeld markiert 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 Aktion [NAVIGATION] möglich;
  • Zeile 72: Der Wert von TextView seekBarValue wird neu berechnet (nicht wiederhergestellt). Tatsächlich ging dieser Wert bei Drehungen manchmal verloren;
  • Zeilen 74–75: Die Liste wird auf das Element positioniert, das vor der Drehung ausgewählt war. Andernfalls würde die Liste auf ihr erstes Element springen;
  • Zeilen 76–80: Das Dialogfeld wird erneut angezeigt, wenn die Meldung des vorherigen Status nicht null lautet. Wir werden später auf die Methode [showMessage] (Zeile 79) zurückkommen;
  • Zeilen 83–86: Die Methode [notifyEndOfUpdates] ist die letzte Methode, die von der übergeordneten Klasse aufgerufen wird, bevor das untergeordnete Fragment in Ruhe 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;

Die Wiederherstellung des Dialogfelds erfolgt wie folgt:


  // Der Text des Dialogfelds
  private String message;
...
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // Liste der anzuzeigenden Meldungen
    List<String> messages = new ArrayList<>();
    ...
    // Anzeige
    doAfficher(messages);
  }

  private void doAfficher(final List<String> messages) {
    // Der anzuzeigende Text wird erstellt
    StringBuilder texte = new StringBuilder();
    for (String message : messages) {
      texte.append(String.format("%s\n", message));
    }
    // die Meldung wird gespeichert
    message = texte.toString();
    // Anzeige
    showMessage();
  }

  private void showMessage() {
    // Anzeige
    new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
      @Override
      public void onClick(DialogInterface dialog, int which) {
        // Meldung zurücksetzen
        message = null;
      }
    }).show();
}

Wenn der Benutzer das Formular absendet, erstellt die Methode [doValider] (Zeile 5) eine Liste von 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: Diese Meldung wird im Dialogfeld angezeigt, und genau diese Meldung lässt die Methode [updateOnRestore] anzeigen;
  • Zeile 27: Der zweite Parameter der Methode [setNeutralButton] ist die Methode, die ausgeführt wird, wenn der Benutzer im Dialogfeld auf die Schaltfläche [Fermer] klickt;
  • Zeile 31: Beim Schließen des Dialogfelds wird die Meldung an null zurückgegeben, um anzuzeigen, dass das Dialogfeld nicht mehr vorhanden ist;

2.8.1.7. Tests

Der Leser wird gebeten, 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

Auf einigen Websites werden Wetterinformationen in Form von Zeichenfolgen wie jSON bereitgestellt. Hier ein Beispiel:

Image

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

  • city: die Stadt, für die man die Wettervorhersage haben möchte, hier Angers;
  • country: das Land der Stadt, hier Frankreich (fr);
  • APPID: ein Schlüssel, den man bei der Registrierung auf der Website [https://home.openweathermap.org/users/sign_up] erhält;

2.8.2.1. Das Projekt

  

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

  • Es besteht nur aus einem Fragment, dessen Zustand nicht beibehalten werden muss;
  • es führt asynchrone Abfragen durch;

2.8.2.2. Anpassung des Projekts

  

Über die Schnittstelle [IMainActivity] lassen sich bestimmte Merkmale des Projekts festlegen:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // Zugriff auf die Sitzung
  ISession getSession();

  // Ansicht wechseln
  void navigateToView(int position, ISession.Action action);

  // Wartungsverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Anwendungskonstanten -------------------------------------

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 5000;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // Nachbarbeziehung der Fragmente
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = false;

  // Ladebild
  boolean IS_WAITING_ICON_NEEDED = true;

  // Anzahl der Anwendungsfragmente
  int FRAGMENTS_COUNT = 1;

}
  • Zeilen 25, 28, 31, 40: Eigenschaften der Ebene [DAO]. Zeile 31: Es ist keine Basic-Authentifizierung erforderlich;
  • Zeile 34: Adjazenz der Fragmente. Hier spielt diese Konstante keine Rolle, da es nur ein Fragment gibt;
  • Zeile 37: Es handelt sich nicht um eine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die Klasse [CoreState], 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)
// To-Do: Hier die Unterklassen von [CoreState] hinzufügen
/*@JsonSubTypes({
  @JsonSubTypes.Type(value = Class1.class),
  @JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
  // Fragment bereits aufgerufen oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeilen 10–13: Es gibt nichts zu deklarieren, da es in dieser Anwendung nur ein Fragment gibt, dessen Zustand nicht gespeichert wird;

Die Klasse [Session] lautet wie folgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen
}

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

2.8.2.3. Die Schicht [DAO]

  

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

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

Die Schnittstelle [WebClient] 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);

  // Wetterdienst
  @Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
  String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
  • Zeilen 18–19: URL des Wetterdienstes. Zur Erinnerung: Diese bezieht sich auf die Stamm-URL (RestClientRootUrl, Zeile 12) des Kunden. Hier lautet diese Stamm-URL „[http://api.openweathermap.org/]“;

Die Schnittstelle [IDao] sieht wie folgt aus:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

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

  // Client-Timeout
  void setTimeout(int timeout);

  // Basis-Authentifizierung
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);

  // Wartezeit des Clients in Millisekunden vor der Anfrage
  void setDelay(int delay);

  //  Wetterdienst
  Observable<String> getWeatherForecast(String city, String country, String APPID);
}
  • Zur Erinnerung: Die Methoden in den Zeilen 6–22 sind standardmäßig in der Schnittstelle IDao des Projekts [client-android-skel] vorhanden;
  • Zeile 25: Mit der Methode [getWeatherForecast] lässt sich die Zeichenfolge jSON für die Wettervorhersage der Stadt [city] im Land [country] abrufen. Der dritte Parameter ist der auf der Website [https://home.openweathermap.org/users/sign_up] abgerufene Schlüssel;

Die Schnittstelle [IDao] wird durch die folgende Klasse [Dao] 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 {

  // Webdienst-Client
  @RestService
  protected WebClient webClient;
  // Sicherheit
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // der RestTemplate
  private RestTemplate restTemplate;
  // Factory von RestTemplate
  private SimpleClientHttpRequestFactory factory;
  // Zeitüberschreitung
  private int timeout;

  @AfterInject
  public void afterInject() {
    // Protokoll
    Log.d(className, "afterInject");
    // man erstellt den restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Der Konverter jSON wird festgelegt
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // man legt den restTemplate des Web-Clients fest
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // Der URL des Webdienstes wird festgelegt
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // Der Benutzer wird im Interceptor registriert
    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));
    }
    // Speicher
    this.timeout = timeout;
    // Konfigurationsfabrik
    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));
    }
    // Authentifizierungs-Interceptor?
    if (isBasicAuthentificationNeeded) {
      // Der Authentifizierungs-Interceptor wird hinzugefügt
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }


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

  // Wetterdienst ---------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
    // Protokoll
    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));
    }
    // Ergebnis
    return getResponse(new IRequest<String>() {
      @Override
      public String getResponse() {
        return webClient.getWeatherForecast(city, country, APPID);
      }
    });
  }
}
  • Zur Erinnerung: Die Zeilen 17–90 sind standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] vorhanden. Es müssen lediglich die anwendungsspezifischen Implementierungsmethoden der Schnittstelle [IDao] hinzugefügt werden (Zeile 92);
  • Zeilen 93–105: Implementierung der Methode [getWeatherForecast]. Diese ist sehr einfach und umfasst 6 Zeilen (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 vom Server erwarteten Antwort ist, in diesem Fall ein String, da eine Zeichenkette vom Typ jSON erwartet wird. Der Typ T von [IRequest<T>] muss dem Typ T der Methode [Observable<T> getWeatherForecast] entsprechen;
  • die Schnittstelle [IRequest<T>] verfügt nur über eine Methode: getResponse. Diese hat die Aufgabe, die Antwort vom Typ T bereitzustellen, die die Methode [Observable<T> getWeatherForecast] zurückgeben muss;
  • Zeile 103: Die Schnittstelle [WebClient] liefert diese Antwort. Ihr werden die drei in Zeile 94 empfangenen Parameter übergeben. Aus diesem Grund müssen diese das Attribut „final“ haben;

2.8.2.4. Die Aktivität [MainActivity]

  

Die Aktivität [MainActivity] lautet 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.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 {

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

  // Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    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;
  }

  // Schnittstelle IDao ---------------------------------------------------------------------
  @Override
  public Observable<String> getWeatherForecast(String city, String country, String APPID) {
    return dao.getWeatherForecast(city, country, APPID);
  }
}
  • Zur Erinnerung: Die Zeilen 15–55 sind standardmäßig im Projekt [client-android-skel] vorhanden. Sie müssen lediglich angepasst werden;
  • Zeilen 37–40: die Fragmenttabelle. Hier gibt es nur ein Fragment;
  • 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 Schnittstelle [IDao]. Hier gibt es nichts weiter zu tun, als die Arbeit an die Ebene [DAO] aus Zeile 21 zu delegieren;

2.8.2.5. Das Fragment [MeteoFragment]

  

Das Fragment [MeteoFragment] fragt den Webdienst / jSON für Wetterdaten 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 aus 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, diese Anfrage abzubrechen, falls sie gerade läuft;
  • Zeilen 16–18: Diese Menüoption beendet die Anwendung;

Der vollständige Code des Fragments 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 {

  // lokale Daten
  private int nbReponsesRecues;

  // Ereignisverwaltung ---------------------------------------------------------------------------------------
  // Städte, für die Wetterdaten gewünscht werden
  final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

  @OptionsItem(R.id.actionMeteo)
  protected void doMeteo() {
    // sein Land
    String country = "fr";
    // Erhalten Sie eine API-ID, indem Sie ein Konto erstellen: [https://home.openweathermap.org/users/sign_up]
    String APPID = "xyz";
    // URL des Webdienstes / jSON
    mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
    // Beginn der Wartezeit für asynchrone Aufgaben [paysDeLoire.length]
    beginWaiting(paysDeLoire.length);
    // Anzahl der empfangenen Antworten
    nbReponsesRecues = 0;
    // Die asynchronen Aufrufe werden parallel ausgeführt
    for (String city : paysDeLoire) {
      // Wetter
      executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
        @Override
        public void call(String response) {
          // Auswertung der Antwort
          consumeResponse(response);
          // Eine positive Antwort
          nbReponsesRecues++;
        }
      });
    }
  }

  // Auswertung der Serverantwort
  private void consumeResponse(String response) {
    // Protokoll
    Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
  }

  // Wartezeit beginnt
  protected void beginWaiting(int numberOfRunningTasks) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "beginWaiting");
    }
    // übergeordnet
    beginRunningTasks(numberOfRunningTasks);
    // die Option wird angezeigt [Annuler]
    setAllMenuOptionsStates(false);
    setMenuOptionsStates(new MenuItemState[]{
      new MenuItemState(R.id.menuActions, true),
      new MenuItemState(R.id.actionAnnuler, true)});

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // Menü
    initMenu();
    // Ergebnisanzeige
    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 Methoden -----------------------------------
  private void initMenu() {
    if (isDebugEnabled) {
      Log.d(className, "initMenu");
    }
    // Menü
    setAllMenuOptionsStates(true);
    setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
  }

  // Lebenszyklusmanagement ---------------------------------------------------------------------------------------
...
}
  • Zeilen 25–50: Verarbeitung des Klicks auf die Menüoption [Météo];
  • Zeile 32: Aufbau des Webdienstes URL / des Wetterdienstes jSON. Dieser wird anschließend über die Aktivität an die Schicht [DAO] übergeben;
  • Zeile 34: Die Wartezeit beginnt. Wir übergeben die Anzahl der Aufgaben, die gestartet werden sollen, damit die übergeordnete Klasse uns das Ende dieser Aufgaben melden kann. Hier sind es fünf Aufgaben, da wir die Wetterdaten der fünf Städte aus Zeile 23 abfragen werden;
  • Zeile 16: Wir zählen die Anzahl der empfangenen Antworten, um sie anzeigen zu können;
  • Zeilen 38–50: Wir durchlaufen die Städte, für die wir die Wetterdaten abfragen möchten;
  • Zeile 40: Wir führen 5 HTTP-Abfragen parallel durch;
  • Zeile 40: Wir weisen die übergeordnete Klasse [AbstractParent] an, den Webdienst /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, sobald die Antwort des asynchronen Dienstes empfangen wird. Der Typ T von [Action1<T>] muss dem Typ T des Ergebnisses der Methode [getWeatherForecast] entsprechen;
  • Zeile 44: Eine Antwort wurde empfangen. Diese wird an die Methode [consumeResponse] in Zeile 53 übergeben;
  • Zeile 46: Der Zähler für empfangene Antworten wird erhöht;
  • Zeilen 53–56: Verarbeitung einer Antwort jSON vom Wetterdienst;
  • Zeile 55: Die Zeichenkette jSON wird lediglich protokolliert;
  • Zeilen 59–72: Code, der vor dem Start der asynchronen Aufgaben ausgeführt wird;
  • Zeile 65: Die Anzahl der auszuführenden Aufgaben wird an die übergeordnete Klasse [AbstractParent] übergeben. Dadurch kann diese uns benachrichtigen, sobald alle Aufgaben abgeschlossen sind;
  • Zeilen 67–70: Vorbereitung des Menüs für eine Wartephase. Es wird nur die Option [Actions/Annuler] beibehalten, die es dem Benutzer ermöglicht, die gestarteten Aufgaben abzubrechen;
  • Zeilen 74–92: Code, der ausgeführt wird, wenn die übergeordnete Klasse uns mitteilt, dass alle gestarteten Aufgaben abgeschlossen sind;
  • Zeile 77: Das Menü wird in seinen Ausgangszustand zurückgesetzt. Die Methode [initMenu] (Zeilen 95–102) zeigt das Menü mit allen Optionen an, mit Ausnahme der Option [Actions/Annuler], die ausgeblendet ist;
  • Zeilen 80–91: Die Anzahl der erhaltenen Antworten wird angezeigt;

Der Klick auf die Menüoption [Annuler] wird durch den folgenden Code verarbeitet:


  @OptionsItem(R.id.actionAnnuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // Asynchrone Aufgaben abbrechen
    cancelRunningTasks();
}
  • Zeile 7: Die übergeordnete Klasse wird aufgefordert, die noch aktiven Aufgaben zu stornieren;

Der Klick auf die Menüoption [Terminer] wird durch den folgenden Code verarbeitet:


  @OptionsItem(R.id.actionTerminer)
  protected void doTerminer() {
    // alles wird angehalten
    System.exit(0);
}

Die Verwaltung des Lebenszyklus des Fragments erfolgt über die folgenden Methoden:


  // Lebenszyklusmanagement ---------------------------------------------------------------------------------------

  @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) {
    // Erster Besuch?
    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 wie hier keinen zu speichernden Status hat, wird lediglich eine Instanz von [CoreState] zurückgegeben. null darf nicht zurückgegeben werden, da dies später zu einem Absturz führen würde;
  • Zeilen 8–11: müssen die Nummer der Ansicht zurückgeben. Hier hat das Fragment [MeteoFragment] die Nummer 0;
  • Zeilen 13–16: dienen dazu, das Fragment zu initialisieren, sobald es erstellt (previousState == null) oder neu erstellt wurde (previousState != null). Hier ist nichts zu tun. Das einzige Feld, das initialisiert werden könnte, ist das folgende:

  // Städte, für die Wetterdaten gewünscht werden
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};

, aber es initialisiert sich von selbst;

  • Zeilen 18–24: dienen dazu, die dem Fragment zugeordnete Ansicht zu initialisieren, sobald sie erstellt (previousState==null) oder neu erstellt (previousState!=null) wurde;
  • Zeilen 21–23: Wenn es sich um den ersten Besuch des Fragments handelt, wird dessen Menü initialisiert, um die Option [Annuler] auszublenden;
  • Zeilen 27–30: werden aufgerufen, wenn zum Fragment über eine Aktion vom Typ [SUBMIT] navigiert wurde. Hier findet keine Navigation zwischen Fragmenten statt, da es nur ein Fragment gibt;
  • Zeilen 32–35: werden bei einem Speicher-/Wiederherstellungszyklus aufgrund einer Drehung des Geräts oder aus einem anderen Grund aufgerufen. Da hier kein Status gespeichert wurde, gibt es nichts zu tun;
  • Zeilen 37–40: werden aufgerufen, wenn alle vorherigen Aktualisierungen durchgeführt wurden. Hier gibt es nichts zu tun;

2.8.2.6. Tests

Wir führen nun das Beispiel aus:

Image

Image

Die Protokolle lauten dann 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: Die Antworten jSON werden über E/A-Threads abgerufen
  • Zeilen 37–41: Das Fragment ruft die 5 Antworten aus dem UI-Thread ab;

Nun führen wir die Anfrage mit einer falschen Kennung API durch:


    String APIID = "";

Image

Die Logs 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 Aufrufe von HTTP haben 5 Ausnahmen ausgelöst;
  • Zeile 7: Das Fragment [MeteoFragment] empfängt die erste Ausnahme. Es bricht daraufhin alle Aufgaben ab;

Legen wir nun eine Wartezeit von 5 Sekunden für [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: Die Wartezeit wird abgebrochen, da ein Abbruch stattgefunden hat;
  • Zeilen 6–10: Das Abbrechen der Aufgaben löst in jedem der fünf Threads der Aufgaben eine Ausnahme aus. Der Ausnahmetyp hängt von den jeweiligen Anwendungen ab. Die Ausnahme lautet hier [java.lang.InterruptedException], da die Aufgaben unterbrochen wurden, während sie die Anweisung [Thread.sleep(delay)] ausführten, die sie künstlich [delay] Millisekunden lang warten lässt;

2.8.3. Beispiel 16B

Wir überarbeiten hier Beispiel 16 aus Abschnitt 1.17. Es zeigt einen Codeausschnitt, der asynchrone Aufrufe an einen Zufallszahlengenerator durchführt. Schauen wir uns an, wie er sich bei einer Drehung des Geräts verhält:

Image

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

Image

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

2.8.3.1. Das Projekt „Beispiel-16B“

Wir kopieren das Projekt [client-android-skel] in das Projekt [exemples/Exemple-16B] und laden anschließend das neue Projekt:

  

Aus dem ursprünglichen Projekt [Exemple-16] kopieren wir folgende Elemente in das Projekt [Exemple-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 [Vue1Fragment]:
  
  • die Klasse [dao / service / Response]:
  

An dieser Stelle kann man einen ersten Kompilierungsversuch unternehmen:

  • Eine erste Fehlerart betrifft die Klasse imports. Bei der Migration zu [Exemple-16B] wurden einige Klassen in ein anderes Paket verschoben. Zunächst werden diese Fehler behoben;
  • eine zweite Fehlerart wird bei der Klasse [Vue1Fragment] gemeldet, da sie die von der übergeordneten Klasse [AbstractParent] vorgeschriebenen Methoden nicht implementiert. Diese werden automatisch generiert;

Wir versuchen eine zweite Kompilierung:

  • Alle verbleibenden Fehler konzentrieren sich nun auf die Klasse [Vue1Fragment], die Klasse, an der die meisten Änderungen vorgenommen werden müssen;

2.8.3.2. Erstellung eines Berichts für das Fragment [Vue1Fragment]

Wir haben gesehen, dass bestimmte Informationen des Fragments bei einer Rotation gespeichert werden müssen, um das Fragment in den Zustand vor der Rotation zurückzusetzen. Daher erstellen wir vorerst einen leeren Status [Vue1FragmentState]:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class Vue1FragmentState extends CoreState {

}

2.8.3.3. Anpassung des Projekts

  

Über die Benutzeroberfläche [IMainActivity] lassen sich bestimmte Eigenschaften des Projekts festlegen:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // Anmeldung
  ISession getSession();

  // Ansichtswechsel
  void navigateToView(int position, ISession.Action action);

  // Warteschlangenverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Anwendungskonstanten -------------------------------------

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 5000;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // Aneinandergrenzung der Fragmente
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = false;

  // Wartebild
  boolean IS_WAITING_ICON_NEEDED = true;

  // Anzahl der Anwendungsfragmente
  int FRAGMENTS_COUNT = 1;

}
  • Zeilen 25, 28, 31, 40: Eigenschaften der Ebene [DAO]. Eine Basic-Authentifizierung ist nicht erforderlich;
  • Zeile 34: Adjazenz der Fragmente. Hier spielt diese Konstante keine Rolle, da es nur ein Fragment gibt;
  • Zeile 37: Es handelt sich nicht um eine Anwendung mit Registerkarten;
  • Zeile 43: Es gibt nur ein Fragment;

Die Klasse [CoreState], 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 bereits besucht oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeile 12: Wir deklarieren die Klasse für den Fragmentstatus [Vue1Fragment];

Die Klasse [Session] lautet wie folgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen
}

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

2.8.3.4. Die Ebene [DAO]

  

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

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

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


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status der Operation
    private int status;
    // Eventuelle Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

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

    // Getter und Setter
...
}

Die Schnittstelle [WebClient] 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 Zufallszahl im Intervall [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);

}
  • Zeilen 18–19: die Schnittstelle URL des Zufallszahlengenerators. Zur Erinnerung: Diese bezieht sich auf die Stamm-URL (RestClientRootUrl, Zeile 12) des Kunden. Hier lautet diese Stamm-URL „[http://localhost:8080]“;

Die Schnittstelle [IDao] sieht wie folgt aus:


package client.android.dao.service;

import rx.Observable;

public interface IDao {
  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

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

  // Client-Timeout
  void setTimeout(int timeout);

  // Basis-Authentifizierung
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);

  // Wartezeit des Clients in Millisekunden vor der Anfrage
  void setDelay(int delay);

  // Zufallszahlengenerator
  Observable<Response<Integer>> getAlea(int a, int b);

}
  • Zur Erinnerung: Die Methoden in den Zeilen 6–22 sind standardmäßig in der Schnittstelle IDao des Projekts [client-android-skel] vorhanden;
  • Zeile 25: Mit der Methode [getAlea] lässt sich eine Zufallszahl im Intervall [a,b] ermitteln. Diese Zahl wird in einer Antwort vom Typ [Response<Integer>] zurückgegeben, wobei sich die Zufallszahl im Feld [body] dieses Typs befindet;

Die Schnittstelle [IDao] wird durch die folgende Klasse [Dao] 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 {

  // Webdienst-Client
  @RestService
  protected WebClient webClient;
  // Sicherheit
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // der RestTemplate
  private RestTemplate restTemplate;
  // Factory des RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // Protokoll
    Log.d(className, "afterInject");
    // man baut den restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // Der Konverter jSON wird befestigt
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Man legt den restTemplate des Web-Clients fest
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // Der URL des Webdienstes wird festgelegt
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // Der Benutzer wird im Interceptor registriert
    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));
    }
    // Konfigurationsfabrik
    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));
    }
    // Authentifizierungs-Interceptor?
    if (isBasicAuthentificationNeeded) {
      // Der Authentifizierungs-Interceptor wird hinzugefügt
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

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

  // Zufallszahlengenerator
  @Override
  public Observable<Response<Integer>> getAlea(final int a, final int b) {
    // Web-Client-Ausführung
    return getResponse(new IRequest<Response<Integer>>() {
      @Override
      public Response<Integer> getResponse() {
        return webClient.getAlea(a, b);
      }
    });
  }

}
  • Zur Erinnerung: Die Zeilen 17–85 sind standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] vorhanden. Es müssen lediglich die Methoden zur Implementierung der Schnittstelle [IDao] hinzugefügt werden;
  • Zeilen 88–97: Implementierung der Methode [getAlea]. Diese ist sehr einfach und umfasst 6 Zeilen (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 Typ Response<Integer>. Der Typ T von [IRequest<T>] (Zeile 91) muss dem Typ T der Methode [Observable<T> getAlea] (Zeile 89) entsprechen;
  • Die Schnittstelle [IRequest<T>] verfügt nur über eine Methode: getResponse. Diese hat die Aufgabe, 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 liefert. Ihr werden die beiden in Zeile 89 empfangenen Parameter übergeben. Aus diesem Grund müssen diese das Attribut „final“ haben;

2.8.3.5. Die Aktivität [MainActivity]

  

Die Aktivität [MainActivity] lautet 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.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 {

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

  // Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Fortsetzung der von der übergeordneten Klasse begonnenen Initialisierungen
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // Fragmente hier definieren
    return new AbstractFragment[]{new Vue1Fragment_()};
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // Hier die Titel der Fragmente definieren
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // Navigation über Registerkarten – die anzuzeigende Ansicht festlegen
  }

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

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

}
  • Zur Erinnerung: Die Zeilen 15–61 sind standardmäßig im Projekt [client-android-skel] vorhanden. Sie müssen lediglich angepasst werden;
  • Zeilen 40–44: die Fragmenttabelle. Hier gibt es nur ein Fragment;
  • 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 Schnittstelle [IDao]. Hier gibt es nichts weiter zu tun, als die Arbeit an die Schicht [DAO] aus Zeile 23 zu delegieren;

2.8.3.6. Der Status 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 {

  // Fragmentstatus ------------------------
  // Liste der Antworten
  private List<String> reponses = new ArrayList<>();
  // Ansichtsstatus ------------------------
  // Fehlermeldung zur Anzahl der angeforderten Zufallszahlen
  private boolean txtErrorAleasVisible = false;
  // Fehlermeldung zum Generierungsintervall [a,b]
  private boolean txtErrorIntervalleVisible = false;
  // Fehlermeldung bezüglich des URL des Webdienstes
  private boolean txtMsgErreurUrlServiceWebVisible = false;
  // Fehlermeldung bezüglich der Wartezeit
  private boolean textViewErreurDelayVisible = false;
  // Sichtbarkeit der Schaltfläche „Ausführen“
  private boolean btnExecuterVisible = true;

  // Getter und Setter
...
}

Um herauszufinden, was im Fragment gespeichert werden musste, wurden verschiedene Drehungen des Geräts in unterschiedlichen Situationen durchgeführt und beobachtet, was bei der Wiederherstellung verloren gegangen war. Man kam zu dem Schluss, dass die Informationen der Zeilen 10–23 gespeichert werden mussten.

2.8.3.7. Das Fragment [Vue1Fragment]

  

Derzeit weist die Ansicht [Vue1Fragment] verschiedene Fehler auf, die darauf zurückzuführen sind, dass sich die übergeordnete Klasse [AbstractFragment], von der sie abgeleitet ist, geändert hat. Anstatt die vorzunehmenden Änderungen einzeln zu beschreiben, werden wir direkt die endgültige Version erläutern.

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: Es sei daran erinnert, dass jedes Fragment ein Menü haben muss, auch wenn es leer ist. Dies ist hier der Fall.

2.8.3.7.1. Verarbeitung des Klicks auf die Schaltfläche [Exécuter]

@Click(R.id.btn_Executer)
  protected void doExecuter() {
    // Die eingegebenen Daten werden überprüft
    if (!isPageValid()) {
      return;
    }
    // Vorherige Antworten werden gelöscht
    reponses.clear();
    dataAdapterReponses.notifyDataSetChanged();
    // Der Antwortzähler wird auf 0 zurückgesetzt
    nbReponses = 0;
    infoReponses.setText("Liste des réponses (0)");
    // Aktivität wird initialisiert
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // Die Zufallsaufgabe wird vorbereitet
    beginWaiting(1);
    // Zufallszahlen werden angefordert
    getAleasInBackground(nbAleas, a, b);
  }

  void getAleasInBackground(int nbAleas, int a, int b) {
    // Der zu beobachtende Prozess wird erstellt
    Observable<Response<Integer>> process = Observable.empty();
    for (int i = 0; i < nbAleas; i++) {
      process = process.mergeWith(mainActivity.getAlea(a, b));
    }
    // Zufallszahlen werden angefordert
    executeInBackground(process, new Action1<Response<Integer>>() {

      @Override
      public void call(Response<Integer> response) {
        // die Antwort wird verarbeitet
        consumeAleaResponse(response);
      }
    });
  }

  protected void consumeAleaResponse(Response<Integer> response) {
    // Protokoll
    if (isDebugEnabled) {
      try {
        Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
    // eine Antwort von +
    nbReponses++;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
    // Die Antwort wird analysiert
    // Fehler?
    if (response.getStatus() != 0) {
      // Anzeige
      showAlert(response.getMessages());
      // Abbruch
      doAnnuler();
      // Zurück zur Benutzeroberfläche
      return;
    }
    // Die Information wird zur Liste der Antworten hinzugefügt
    reponses.add(0, String.valueOf(response.getBody()));
    // Antworten werden aktualisiert
    dataAdapterReponses.notifyDataSetChanged();
  }

  // Abbruch ----------
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // asynchrone Aufgaben werden abgebrochen
    cancelRunningTasks();
}

  private void beginWaiting(int nbRunningTasks) {
    // Die Sanduhr wird angezeigt
    beginRunningTasks(nbRunningTasks);
    // Die Schaltfläche [Annuler] ersetzt die Schaltfläche [Exécuter]
    btnExecuter.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
  }
  • Zeilen 4–6: Zunächst wird überprüft, ob die Eingaben gültig sind. Dabei können Fehlermeldungen erscheinen;
  • Zeilen 8–9: Die Liste der Antworten wird geleert. Diese Änderung wird an das ListView weitergeleitet, das sie anzeigt;
  • Zeilen 11–12: Die Anzahl der empfangenen Antworten wird auf Null gesetzt;
  • Zeile 14: Der URL des Zufallszahlengenerators wird festgelegt. Diese Information wird an die Schicht [DAO] weitergeleitet;
  • Zeile 15: Die Wartezeit vor der Abfrage des Zufallszahlengenerators wird festgelegt. Diese Information wird an die Schicht [DAO] weitergeleitet;
  • Zeile 17: Es werden Vorbereitungen getroffen, um eine asynchrone Aufgabe zu starten (und nicht N, wir werden sehen, warum);
  • Zeilen 24–27: Aus N asynchronen Aufgaben wird jeweils eine Abfolge von Operationen gebildet [merge];
  • Zeilen 29–36: Die übergeordnete Klasse [AbstractParent] wird aufgefordert, den Webdienst /jSON nach Zufallszahlen abzufragen;
  • Zeilen 29–36: Die Methode [executeInBackground] erwartet zwei Parameter:
    • Zeile 29: Der zu beobachtende und auszuführende Prozess ist derjenige, der in den vorangegangenen Zeilen berechnet wurde;
    • Zeilen 29–36: Die Instanz [Action1], die ausgeführt werden soll, sobald die Antwort des asynchronen Dienstes eingeht. Der Typ T von [Action1<T>] muss dem Typ T des Ergebnisses der Methode [getAlea] entsprechen, d. h. einem Typ [Response<Integer>];
  • Zeile 34: Wenn eine Antwort eintrifft (eine Zufallszahl), wird sie in der Methode in Zeile 39 verarbeitet;
  • Zeilen 49–50: Es wird vermerkt und gemeldet, dass eine neue Antwort eingegangen ist;
  • 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 die noch aktiven Aufgaben ab (Zeile 74);
  • Zeile 62: Die Antwort wird der Antwortliste hinzugefügt, die die Datenquelle für ListView darstellt;
  • Zeile 64: ListView wird aktualisiert;
  • Zeilen 77–83: Die Methode [beginWaiting(int nbRunningTasks)] bereitet die Ansicht auf das Warten vor (Zeilen 81–82) und teilt der übergeordneten Klasse mit, dass Aufgaben von [nbRunningTasks] bald ausgeführt werden (Zeile 79);

2.8.3.7.2. Der Lebenszyklus des Fragments

Der Lebenszyklus des Fragments wird durch die folgenden Methoden gewährleistet:


  // lokale Daten
  private List<String> reponses;
  private ArrayAdapter<String> dataAdapterReponses;
  private int nbReponses = 0;
...
  // Lebenszyklusmanagement ---------------------------------------------------------
  @Override
  public CoreState saveFragment() {
    // aktueller Status der Ansicht
    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) {
    // Erster Besuch?
    if (previousState != null) {
      Vue1FragmentState state = (Vue1FragmentState) previousState;
      reponses = state.getReponses();
    } else {
      reponses = new ArrayList<>();
    }
    // Datenquelle von listView
    dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    // Anzahl der Antworten
    nbReponses = reponses.size();
  }

  @Override
  protected void initView(CoreState previousState) {
    // Verknüpfung zwischen Listview und Adapter
    listReponses.setAdapter(dataAdapterReponses);
    // Erster Besuch?
    if (previousState == null) {
      // Fehlermeldungen werden ausgeblendet
      txtErrorAleas.setVisibility(View.INVISIBLE);
      txtErrorIntervalle.setVisibility(View.INVISIBLE);
      txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
      textViewErreurDelay.setVisibility(View.INVISIBLE);
      // Die Schaltflächen
      btnAnnuler.setVisibility(View.INVISIBLE);
      btnExecuter.setVisibility(View.VISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
    // Vorheriger Zustand der Ansicht
    Vue1FragmentState state = (Vue1FragmentState) previousState;
    // Fehlermeldungen anzeigen / ausblenden
    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);
    // Schaltflächen
    btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
    btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
    // Anzahl der Antworten
    infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // Die Schaltfläche [Exécuter] ersetzt die Schaltfläche [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);

}
  • Zeilen 7–18: sorgen für die Speicherung des Fragments, wenn die übergeordnete Klasse dies anfordert;
  • Zeile 11: Anzeige der Fehlermeldung bezüglich der Wartezeit;
  • Zeile 12: Anzeige der Fehlermeldung zur Anzahl der angeforderten Zufallszahlen;
  • Zeile 13: Anzeige der Fehlermeldung bezüglich des URL des Webdienstes / jSON;
  • Zeile 14: Anzeige der Fehlermeldung zum Intervall [a,b] für die Zufallszahlengenerierung;
  • Zeile 15: Sichtbarkeit der Schaltfläche [Exécuter];
  • Zeile 16: Liste der empfangenen Antworten;
  • Zeilen 20–23: müssen die Nummer der Ansicht zurückgeben. Die Nummer des Fragments ist hier 0, da es nur eines gibt;
  • Zeilen 25–38: Initialisierung der Felder des Fragments, entweder bei einem ersten Besuch (previousState == null) oder bei einem späteren Besuch;
    • Zeilen 29–30: Wenn es sich nicht um den ersten Besuch handelt, wird das Feld [reponses] aus dem vorherigen Zustand des Fragments wiederhergestellt;
    • Zeilen 31–33: Handelt es sich um den ersten Besuch, wird das Feld [reponses] mit einer leeren Liste initialisiert;
    • Zeilen 34–37: Ausgehend vom Feld [reponses] lassen sich die Datenquelle für ListView des Fragments (Zeile 35) sowie die Anzahl der Antworten (Zeile 37) ermitteln;
  • Zeilen 40–55: Diese werden ausgeführt, um die dem Fragment zugeordnete Ansicht zu initialisieren, entweder beim ersten Besuch (previousState == null) oder bei einem späteren Besuch;
    • Zeile 43: Das ListView des Fragments wird der Datenquelle zugeordnet, die gerade in der Methode [initFragment] erstellt wurde;
    • Zeilen 45–54: Handelt es sich um den ersten Besuch, wird die Ansicht für ihre erste Anzeige vorbereitet;
  • Zeilen 57–60: werden bei einer fragmentübergreifenden Navigation ausgeführt, die mit einer Aktion vom Typ [SUBMIT] verbunden ist. Hier gibt es nur ein Fragment und somit keine fragmentübergreifende Navigation;
  • Zeilen 63–76: werden bei einer fragmentübergreifenden Navigation ausgeführt, die mit einer Aktion vom Typ [NAVIGATION] verbunden ist, oder bei einem Speicher-/Wiederherstellungszyklus aufgrund einer Drehung des Geräts oder aus einem anderen Grund. Hier kann nur der letztgenannte Fall eintreten. Es ist zu beachten, dass hier in jedem Fall [previousState] immer ungleich null ist;
  • Zeile 65: Der vorherige Status wird in den Typ des Fragmentstatus umgewandelt;
  • Zeilen 66–75: Der Inhalt des vorherigen Zustands wird verwendet, um die Ansicht wiederherzustellen;
  • Zeilen 78–81: werden aufgerufen, wenn alle vorherigen Aktualisierungen durchgeführt wurden. Hier gibt es nichts zu tun;
  • Zeilen 83–89: werden ausgeführt, wenn alle asynchronen Aufgaben abgeschlossen sind. Hier wird die Schaltfläche [Annuler] ausgeblendet und durch die Schaltfläche [Exécuter] ersetzt;

2.8.3.8. Die Tests

Der Leser wird gebeten, die folgenden Tests durchzuführen:

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

2.8.4. Beispiel-22B

Wir greifen hier auf Beispiel 22 zurück, um es gemäß dem Modell des Projekts [client-android-skel] umzugestalten. Zur Erinnerung: Das Projekt [Exemple-22] verwaltet den Zyklus aus Speichern und Wiederherstellen der Fragmente während einer Rotation korrekt und diente als Grundlage für das Projekt [client-android-skel].

Wir duplizieren das Projekt [client-android-skel] in [exemples/Exemple-22B] und laden dieses letzte Projekt:

  

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

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

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

Der obere Rand der beiden Ansichten wird auf 120 dp geändert:

[vue1.xml]:


  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="120dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>

[fragment_main]:


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

Anschließend kopieren wir die Elemente [Vue1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:

 

An dieser Stelle können wir einen ersten Kompilierungsversuch unternehmen. Dabei tritt eine erste Art von Fehlern auf: die fehlerhaften imports, da sich die Pakete der Klassen geändert haben. Diese imports werden korrigiert. Eine zweite Art von Fehlern ist darauf zurückzuführen, dass die Fragmente nicht alle Methoden ihrer übergeordneten Klasse [AbstractFragment] implementieren. Diese werden mit (Alt+Eingabetaste) korrigiert.

Die verbleibenden Fehler sind auf Unterschiede zwischen der alten und der neuen Klasse [AbstractFragment] zurückzuführen. Diese werden vorerst ignoriert.

2.8.4.1. Anpassung des Projekts

  

Im Ordner [custom] befinden sich die vom Entwickler anpassbaren Architekturkomponenten.

Über die Schnittstelle [IMainActivity] lassen sich bestimmte Merkmale des Projekts festlegen:


package client.android.architecture.custom;

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

public interface IMainActivity extends IDao {

  // Zugriff auf die Sitzung
  ISession getSession();

  // Ansichtswechsel
  void navigateToView(int position, ISession.Action action);

  // Wartungsverwaltung
  void beginWaiting();

  void cancelWaiting();

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;

  // maximale Wartezeit auf die Antwort des Servers
  int TIMEOUT = 1000;

  // Wartezeit vor der Ausführung der Client-Anfrage
  int DELAY = 0;

  // Basis-Authentifizierung
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // Aneinandergrenzung der Fragmente
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // Registerkartenleiste
  boolean ARE_TABS_NEEDED = true;

  // Wartebild
  boolean IS_WAITING_ICON_NEEDED = false;

  // Anzahl der Fragmente
  int FRAGMENTS_COUNT = 5;

}
  • Zeilen 23, 26, 29, 38: Merkmale der Ebene [DAO]. Hier gibt es keine;
  • Zeile 41: Hier gibt es fünf Fragmente;
  • Zeile 32: Nachbarschaft der Fragmente. Diese Konstante kann hier einen Wert aus [1,4] annehmen. Der Leser wird dazu angeregt, diesen Wert zu variieren, um zu sehen, ob die Anwendung weiterhin funktioniert;
  • Zeile 35: Es handelt sich um eine Anwendung mit Registerkarten;

Die Klasse [CoreState], 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.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 bereits besucht oder nicht
  protected boolean hasBeenVisited = false;
  // Status des eventuellen Menüs des Fragments
  protected MenuItemState[] menuOptionsState;

  // Getter und Setter
...
}
  • Zeile 12: Wir deklarieren die Klasse für den Fragmentstatus [PlaceHolderFragment]. Das Fragment [Vue1Fragment] hat hingegen keinen Status;

Die Klasse [Session] lautet wie folgt:


package client.android.architecture.custom;

import client.android.architecture.core.AbstractSession;

public class Session extends AbstractSession {
  // Daten, die zwischen den Fragmenten untereinander sowie zwischen Fragmenten und der Aktivität ausgetauscht werden sollen
  // Elemente, die nicht in jSON serialisiert werden können, müssen die Annotation @JsonIgnore tragen
  // Die für die Serialisierung/Deserialisierung erforderlichen Getter und Setter dürfen nicht vergessen werden: jSON

  // Anzahl der besuchten Fragmente
  private int numVisit;
  // Nummer des Fragments vom Typ [PlaceholderFragment], das im zweiten Reiter angezeigt wird
  private int numFragment = -1;

  // Getter und Setter
...
}

Dies ist die Sitzung des Projekts [Exemple-22].

2.8.4.2. Die Aktivität [MainActivity]

  

Die Aktivität [MainActivity] lautet wie folgt:


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 {

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

  // Menüverwaltung-----------------------
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
...
  }

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

  // Implementierung der Methoden der übergeordneten Klasse ---------------------------------------------------
  ...
}

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

  • Es müssen Registerkarten verwaltet werden;
  • es muss ein Menü verwaltet werden;

2.8.4.2.1. Implementierung der Methoden der übergeordneten Klasse

// Methoden der übergeordneten Klasse -----------------------
  @Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Fortsetzung der von der übergeordneten Klasse begonnenen Initialisierungen
    // Sitzung
    this.session = (Session) super.session;
    ...
  }

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

  @Override
  protected AbstractFragment[] getFragments() {
    // Fragmentnummer
    final String ARG_SECTION_NUMBER = "section_number";
    // Initialisierung des Fragment-Arrays
    AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
    int i;
    for (i = 0; i < fragments.length - 1; i++) {
      // Ein Fragment wird erstellt
      fragments[i] = new PlaceholderFragment_();
      // Dem Fragment können Argumente übergeben werden
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, i + 1);
      fragments[i].setArguments(args);
    }
    // ein Fragment von +
    fragments[i] = new Vue1Fragment_();
    // Ergebnis
    return fragments;
  }


  @Override
  protected CharSequence getFragmentTitle(int position) {
    // Hier keine Überschriften
    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 im Rahmen 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. Die Typumwandlung ist darauf zurückzuführen, dass 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] davon, eine Anzahl, die in [IMainActivity] definiert ist. Die ersten [FRAGMENTS_COUNT-1] Fragmente sind vom Typ [PlaceHolderFragment] und das letzte vom Typ [Vue1Fragment];
  • Zeilen 41–45: Die Methode [getFragmentTitle] soll die Titel der Fragmente ausgeben, wenn diese Informationen nützlich sein können. Das 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 ist es das Fragment [Vue1Fragment], das als Erstes angezeigt werden soll. Die Methode [getFirstView] könnte vorteilhafterweise durch eine Konstante in [IMainActivity] ersetzt werden;

2.8.4.2.2. Verwaltung der Registerkarten

Die Registerkarten werden durch die folgenden Methoden verwaltet:


@Override
  protected void onCreateActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // Die von der übergeordneten Klasse begonnenen Initialisierungen werden fortgesetzt
    // Sitzung
    this.session = (Session) super.session;
    // 1. Registerkarte
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2. Registerkarte?
    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) {
    // Nummer des anzuzeigenden Fragments
    int numFragment;
    switch (position) {
      case 0:
        // Fragmentnummer [Vue1Fragment]
        numFragment = getFirstView();
        break;
      default:
        // Fragmentnummer [PlaceholderFragment]
        numFragment = session.getNumFragment();
    }
    // Fragment anzeigen
    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 bei einem 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. Die Typumwandlung ist darauf zurückzuführen, dass 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 in der Sitzung eine Fragmentnummer gespeichert ist (Zeile 15). Diese Nummer hat beim ersten Aufbau der Aktivität zunächst den Wert -1;
  • Zeilen 23–39: Diese Methode wird von der übergeordneten Klasse aufgerufen, wenn der Benutzer auf eine Registerkarte klickt;
  • Zeilen 28–31: Wenn auf die 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 die Registerkarte 1 geklickt wird, muss das Fragment angezeigt werden, dessen Nummer in der Sitzung gespeichert ist;
  • Zeilen 37–39: Es wird zum ausgewählten Fragment navigiert. Die zugehörige Aktion ist [SUBMIT]. Hätte es auch [NAVIGATION] sein können? In diesem Dokument wird [NAVIGATION] nur verwendet, wenn für die Anzeige des neuen Fragments lediglich dessen vorheriger Zustand bekannt sein muss. Hier ist dies nicht der Fall, da sich die Darstellung des angezeigten Fragments im Vergleich zu seinem 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>

das Folgendes anzeigt:

  

Die Menüverwaltung erfolgt über die folgenden Methoden:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onOptionsItemSelected");
    }
    // Verarbeitung der Menüoptionen
    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;
      }
    }
    // behandeltes Element
    return true;
  }

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // Keine Navigation bei der softwaregesteuerten Auswahl einer Registerkarte
      session.setNavigationOnTabSelectionNeeded(false);
      // Die beiden Registerkarten werden aufgrund eines Problems mit der Schriftart der Überschriften neu erstellt
      tabLayout.removeAllTabs();
      tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
      tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
      // Die Nummer des anzuzeigenden Fragments wird in die Sitzung übernommen
      session.setNumFragment(i);
      // Die Registerkarte Nr. 2 wird über die Navigation ausgewählt
      session.setNavigationOnTabSelectionNeeded(true);
      tabLayout.getTabAt(1).select();
    }
  }
  • Zeilen 16–31: Verarbeitung des Klicks auf eine Menüoption vom Typ [Fragmenti];
  • Zeilen 37–50: Anzeige des Fragments Nr. i (es handelt sich um Fragmente vom Typ PlaceHolderFragment) auf Registerkarte Nr. 1 (2. Registerkarte);
  • Zeilen 42–44: Es wird beschlossen, die vorhandenen Registerkarten zu löschen, um zwei neue zu erstellen. Diese Entscheidung wurde getroffen, um das folgende Problem zu umgehen: Wenn man das Fragment lediglich in der bestehenden Registerkarte 1 anzeigt (ohne sie also zu löschen), sieht sein Titel seltsamerweise anders aus (Schriftart, Schriftgröße) als der Titel der Registerkarte 0;
  • Zeilen 43–44: Die beiden Registerkarten werden erstellt, aber nicht ausgewählt (letzter Parameter bei false);
  • Zeile 40: Die Vorgänge in den Zeilen 42–44 können möglicherweise Operationen [select] auf den Registerkarten auslösen, wodurch der Handler [onTabSelected] aufgerufen wird. Wenn nichts unternommen wird, erfolgt eine Navigation zu einem Fragment. Dies wird vermieden, indem der Boolesche Wert [navigationOnTabSelectionNeeded] in der Sitzung auf faux gesetzt wird. Dieser Boolesche Wert wird von der Klasse [AbstractFragment] automatisch auf vrai zurückgesetzt, sobald ein Fragment sichtbar wird;
  • Zeile 46: Die Nummer des anzuzeigenden Fragments wird in der Sitzung notiert;
  • Zeilen 48–50: Die Registerkarte Nr. 2 mit Navigation wird ausgewählt (Zeile 48). Dadurch wird die Prozedur [onTabSelected] ausgelöst, die Folgendes bewirkt:
    • das Fragment anzeigt, dessen Nummer in die Sitzung übernommen wurde;
    • die Nummer der ausgewählten Registerkarte in der Sitzung speichert;

2.8.4.3. Das Fragment [Vue1Fragment]

Hier geben wir die endgültige Version des Fragments an:


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 {

  // Die Elemente der Benutzeroberfläche
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;

  // Ereignismanager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // Der eingegebene Name wird angezeigt
    Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }

  // Lebenszyklus des Fragments -----------------------------------------------
  private void initFragment() {
    // keine Aktion erforderlich
  }

  // Fragmentstatus speichern
  @Override
  public CoreState saveFragment() {
    // Zustand der Ansicht – nichts zu speichern
    return new CoreState();
  }

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

  @Override
  protected void initFragment(CoreState previousState) {
    // nichts zu tun
  }

  @Override
  protected void initView(CoreState previousState) {
    // Erster Besuch?
    if (previousState == null) {
      // Besuchsnummer wird angezeigt
      showNumVisit();
    }

  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // Besuchsnummer wird angezeigt
    showNumVisit();

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {

  }

  @Override
  protected void notifyEndOfUpdates() {

  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {

  }

  // private Methoden -------------------------------------
  // Besuchsnummer anzeigen
  private void showNumVisit() {
    // Besuchsnummer erhöhen
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // Die Besuchsnummer wird angezeigt
    Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}

Die Klasse ist fast leer.

  • Zeilen 35–39: werden von der übergeordneten Klasse aufgerufen, wenn das Fragment seinen Zustand speichern muss. Das Fragment [Vue1Fragment] hat keinen Zustand, der gespeichert werden muss. Es wird einfach eine Instanz der Basisklasse [CoreState] zurückgegeben (zur Erinnerung: Es darf nicht null zurückgegeben werden);
  • Zeilen 41–44: Müssen die Nummer des Fragments zurückgeben. Das Fragment [Vue1Fragment] hat konstruktionsbedingt die Nummer [FRAGMENTS_COUNT-1];
  • Zeilen 51–59: werden von der übergeordneten Klasse aufgerufen, wenn das Fragment zum ersten Mal erstellt wird (previousState == null) oder bei den folgenden Malen (previousState != null);
    • Zeilen 54–57: Handelt es sich um den ersten Besuch, wird die Besuchsnummer erhöht und angezeigt (Zeilen 85–92);
  • Zeilen 61–65: werden aufgerufen, wenn das Fragment in Verbindung mit einer Aktion [SUBMIT] angezeigt werden soll. Die Besuchsnummer wird erhöht und angezeigt. Hier ist es nicht möglich, dass die Besuchsnummer im Lebenszyklus zweimal erhöht wird. Tatsächlich erfolgt der erste Besuch des Fragments [Vue1Fragment] beim Start der Anwendung, wenn die Aktion in der Sitzung konstruktionsbedingt den Wert [NONE] hat. Dies stellt sicher, dass die Methode [updateOnSubmit] nicht aufgerufen wird. Danach handelt es sich nie wieder um den ersten Aufruf, und die Methode [initView] führt keine Aktion aus;
  • Zeilen 68–71: werden in einem Speicher-/Wiederherstellungszyklus aufgerufen. Da das Fragment keinen Status hat, gibt es hier nichts wiederherzustellen;
  • Zeilen 73–76: werden aufgerufen, wenn alle vorherigen Aktualisierungen durchgeführt wurden. Hier gibt es nichts mehr zu tun;
  • Zeilen 78–81: werden aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;

2.8.4.4. Der Status [PlaceHolderFragmentState]

Der Status des Fragments [PlaceHolderFragment] lautet wie folgt:


package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

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

  // Konstruktoren
  public PlaceHolderFragmentState() {

  }

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

  // Getter und Setter
 ...
}
  • Wenn der Status des Fragments gespeichert werden muss, wird der angezeigte Text (Zeile 7) gespeichert;

2.8.4.5. Das Fragment [PlaceHolderFragment]

Das Fragment [PlaceHolderFragment] lautet wie folgt:


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 {

  // Komponenten der Benutzeroberfläche
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  @ViewById(R.id.textView1)
  protected TextView textView1;

  // Daten
  private String text;

  // Fragmentnummer
  private static final String ARG_SECTION_NUMBER = "section_number";

  // Implementierung der Methoden der übergeordneten Klasse ----------------------------
  @Override
  public CoreState saveFragment() {
    // Der Zustand des Fragments wird gespeichert
    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) {
    // Originaltext
    text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
  }

  @Override
  protected void initView(CoreState previousState) {
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {
    // Der angezeigte Text wird aktualisiert
    // Besuchsnummer erhöhen
    int numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // geänderter Text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
    // Protokoll
    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) {
    // Der angezeigte Text wird wiederhergestellt
    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: Geben die Nummer des Fragments zurück. Diese hängt von der Abschnittsnummer ab, die bei seiner Erstellung als Argument übergeben wurde;
  • Zeilen 43–47: werden beim ersten Aufbau des Fragments (previousState == null) oder bei den folgenden Aufrufen (previousState != null) aufgerufen;
    • Zeile 46: Hier wird der vorherige Bericht nicht verwendet. Der ursprüngliche Text [text] (Zeile 24), der beim ersten Aufruf angezeigt wird, wird jedes Mal neu berechnet. Das ist fragwürdig. Man hätte diese Information auch in den Bericht des Fragments aufnehmen können;
  • Zeilen 49–51: werden beim ersten Aufbau der dem Fragment zugeordneten Ansicht aufgerufen (previousState == null) oder bei den folgenden Aufrufen (previousState != null). Hier gibt es nichts zu tun;
  • Zeilen 53–56: werden aufgerufen, wenn das Fragment in Verbindung mit einer Aktion [SUBMIT] angezeigt wird. Dies ist immer der Fall, außer im Speichern-/Wiederherstellungszyklus, bei dem die Aktion [RESTORE] lautet. Daher wird die Besuchsnummer erhöht und angezeigt;
  • Zeilen 68–74: werden in einem Speicher-/Wiederherstellungszyklus aufgerufen. Der Text, der im Status des Fragments gespeichert wurde, wird wiederhergestellt;
  • Zeilen 76–79: werden aufgerufen, wenn alle vorherigen Aktualisierungen durchgeführt wurden. Hier gibt es nichts mehr zu tun;
  • Zeilen 82–83: werden aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;

2.8.4.6. Tests

Der Leser wird gebeten, die Anwendung zu testen, indem er das Gerät dreht, um zu überprüfen, ob das angezeigte Fragment seinen Zustand beibehält. Außerdem werden die Protokolle überprüft.

2.9. Conclusion

Am Ende dieses Kapitels verfügen wir über ein Musterprojekt [client-android-skel] für einen Android-Client, der mit einem Webdienst / jSON kommuniziert und folgende Eigenschaften aufweist:

  • Die asynchrone Kommunikation mit dem Webserver / jSON erfolgt über die Bibliothek RxJava;
  • Der Lebenszyklus eines Fragments (Update, Save, Restore) wird von seiner übergeordneten Klasse [AbstractFragment] verwaltet, die zu bestimmten Zeitpunkten bestimmte Methoden ihrer untergeordneten Klassen aufruft. Das untergeordnete Fragment muss sich somit nicht um die Schritte des Lebenszyklus kümmern, sondern lediglich bestimmte Methoden implementieren, die von seiner übergeordneten Klasse vorgegeben werden;
  • Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ebenfalls von der untergeordneten Aktivität die Implementierung bestimmter Methoden verlangt;
  • Die Klasse [AbstractActivity] ist in der Lage, eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild sowie mit oder ohne einfache Authentifizierung beim Webserver / jSON zu verwalten. Das Vorhandensein oder Fehlen dieser Elemente wird über die Konfiguration festgelegt;

Wir werden nun eine Fallstudie vorstellen, die komplexer ist als die vorangegangenen Beispiele. Die neue Anwendung basiert auf dem Modellprojekt [client-android-skel].