2. Grundgerüst eines Android-Clients, der mit einem Webdienst / JSON kommuniziert
Wir stellen nun ein Gerüst für eine Android-Anwendung bereit, die mit einem oder mehreren Webdiensten / JSON kommuniziert. Dies ist das Projekt [client-android-skel], das sich im Ordner [architecture] der Beispiele befindet:
![]() |
Die Betrachtung dieser Skelettanwendung bietet die Gelegenheit, bestimmte Punkte zu wiederholen, auf die wir in den vorherigen Beispielen gestoßen sind. Diese Anwendung dient als Grundlage für alle zukünftigen Anwendungen. Sie ist das Ergebnis zahlreicher Überarbeitungen. Ihr Ziel ist es, so viele Elemente wie möglich aus den Anwendungen, die wir in Kürze erstellen werden, in abstrakte Klassen auszulagern, um zu vermeiden, dass wir immer wieder denselben Code schreiben müssen, der sich nur in Details unterscheidet. Sie weist folgende Merkmale auf:
- Die asynchrone Kommunikation mit dem Webserver/JSON wird mithilfe der RxJava-Bibliothek abgewickelt;
- Der Lebenszyklus eines Fragments (Aktualisieren, Speichern, Wiederherstellen) wird von seiner übergeordneten Klasse [AbstractFragment] verwaltet, die zu bestimmten Zeitpunkten bestimmte Methoden ihrer untergeordneten Klassen aufruft. Die untergeordnete Klasse muss sich somit nicht um die Lebenszyklusphasen kümmern, sondern lediglich bestimmte Methoden implementieren, die von ihrer übergeordneten Klasse gefordert werden;
- Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ebenfalls verlangt, dass die untergeordnete Aktivität bestimmte Methoden implementiert;
- Die Klasse [AbstractActivity] ist in der Lage, eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild und mit oder ohne Basisauthentifizierung gegenüber dem Webserver/JSON zu verwalten. Das Vorhandensein oder Fehlen dieser Elemente wird durch die Konfiguration bestimmt;
Dieses Skelett wurde für alle nachfolgenden Beispiele verwendet. Aufgrund ihrer Vielfalt funktionierte das, was für ein Beispiel galt, möglicherweise nicht für das nächste. Da das Skelett für insgesamt sieben Beispiele verwendet wurde, fanden zahlreiche Iterationen statt. Würden wir es für ein achtes Beispiel verwenden, ist es möglich, dass wir erneut feststellen würden, dass die Besonderheiten dieses neuen Beispiels neue Fehler verursachen. Dennoch wird die Verwendung dieses Skeletts das Schreiben zukünftiger Beispiele erheblich vereinfachen. Tatsächlich ist die Verwaltung des Lebenszyklus eines Fragments (Aktualisieren, Speichern, Wiederherstellen) in Verbindung mit dem Konzept der Fragment-Nachbarschaft besonders komplex. Hier ist dies vollständig in der Klasse [AbstractFragment] verborgen.
2.1. Android-Client-Architektur
Der vorgeschlagene Android-Client basiert auf der folgenden Architektur:
![]() |
- Die [DAO]-Schicht implementiert eine [IDao]-Schnittstelle. Sie ist für die Kommunikation mit dem Web-/JSON-Server zuständig;
- es gibt nur eine Aktivität, die ebenfalls die [IDao]-Schnittstelle implementiert. Die Ansichten greifen darauf zu, um auf den Server zuzugreifen;
- Die Ansichten werden durch Fragmente implementiert;
Das Android-Projekt spiegelt diese Architektur wider:
![]() |
Wir werden die verschiedenen Elemente dieses Projekts nacheinander vorstellen.
2.2. Die Gradle-Konfiguration
![]() |
buildscript {
repositories {
mavenCentral()
}
dependencies {
// Since Android's Gradle plugin 0.11, you have to use android-apt >= 1.3
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
apply plugin: 'com.android.application'
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// options de packaging nécessaires pour être capable de produire l'APK
packagingOptions {
exclude 'META-INF/ASL2.0'
exclude 'META-INF/NOTICE'
exclude 'META-INF/LICENSE'
exclude 'META-INF/notice.txt'
exclude 'META-INF/license.txt'
}
}
def AAVersion = '4.0.0'
dependencies {
apt "org.androidannotations:androidannotations:$AAVersion"
compile "org.androidannotations:androidannotations-api:$AAVersion"
apt "org.androidannotations:rest-spring:$AAVersion"
compile "org.androidannotations:rest-spring-api:$AAVersion"
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
compile 'io.reactivex:rxandroid:1.2.0'
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
}
repositories {
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
- Alle Versionsnummern können sich ändern. Sie können jedoch mit den aktuellen Nummern beginnen, wenn Sie Android Studio so konfigurieren, dass diese Versionen der Android-Tools (Zeilen 15–16, 47–48) vorhanden sind (siehe Abschnitt 6.11);
2.3. Das Anwendungsmanifest
![]() |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="client.android">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.MainActivity_"
android:label="@string/app_name"
android:windowSoftInputMode="stateHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
- Zeile 3: Ändern Sie das Anwendungspaket;
- Zeilen 10, 15: Wir werden den Wert des Elements [app_name] in der Datei [res/values/strings.xml] festlegen. Derzeit lautet er wie folgt:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- application name -->
<string name="app_name">[Donnez un nom à votre application]</string>
</resources>
2.4. Die Organisation von Java-Code
![]() |
- [Architektur] fasst die Hauptelemente der Code-Organisation zusammen;
- [activity] enthält die einzelne Aktivität der Anwendung;
- [fragments] fasst die Fragmente oder Ansichten der Anwendung zusammen;
- [dao] fasst die Elemente für die Kommunikation mit dem Webserver / JSON zusammen;
2.5. Aktivitätselemente
![]() | ![]() |

2.5.1. Die mit der Aktivität verknüpfte Ansicht
Die mit der Aktivität verknüpfte Ansicht [activity_main.xml] sieht wie folgt aus:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- fragment container -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
- Zeile 29: Es wird ein bestimmter Fragment-Container verwendet;
Die Aktivität verfügt außerdem über ein Menü [res/menu/menu_main.xml] für ihre Ansicht:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
</menu>
Derzeit ist er leer. Der Entwickler wird ihn nach Bedarf ausfüllen.
2.5.2. Der Fragment-Container [MyPager]
![]() |
package client.android.architecture;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class MyPager extends ViewPager {
// swipe control
private boolean isSwipeEnabled;
// controls scrolling
private boolean isScrollingEnabled;
// manufacturers
public MyPager(Context context) {
super(context);
}
public MyPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
// methods to be redefined to manage swiping
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onInterceptTouchEvent(event);
} else {
return false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// swipe allowed?
if (isSwipeEnabled) {
return super.onTouchEvent(event);
} else {
return false;
}
}
// scrolling control
@Override
public void setCurrentItem(int position){
super.setCurrentItem(position,isScrollingEnabled);
}
// setters
public void setSwipeEnabled(boolean isSwipeEnabled) {
this.isSwipeEnabled = isSwipeEnabled;
}
public void setScrollingEnabled(boolean scrollingEnabled) {
isScrollingEnabled = scrollingEnabled;
}
}
Diese Klasse erweitert die Standard-Android-Klasse [ViewPager] ausschließlich, um das Wischen (Zeile 11) und Scrollen (Zeile 13) zwischen Ansichten zu ermöglichen.
- Zeilen 26–43: Methoden, die das Wischen deaktivieren, falls es ausgeschaltet wurde;
- Zeilen 46–49: Neudefinition der Methode [setCurrentItem], die zum Ändern der angezeigten Ansicht verwendet wird. Wenn das Scrollen deaktiviert wurde, wechselt die Ansicht ohne Scrollen. Beachten Sie, dass der Entwickler dieses Verhalten durch die Verwendung der Methode [setCurrentItem(int position, boolean smoothScrolling)] überschreiben kann, wodurch er das gewünschte Scrollverhalten festlegen kann;
2.5.3. Die Klasse [CoreState]
![]() |
Die Klasse [CoreState] ist die Oberklasse für die Zustände der verschiedenen Fragmente:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeile 16: Jedes Fragment verfügt in seinem Status über einen booleschen Wert [hasBeenVisited], der angibt, ob es bereits aufgerufen wurde oder nicht. Dies ist notwendig, da manchmal, wenn ein Fragment zum ersten Mal angezeigt wird, bestimmte Aktionen durchgeführt werden müssen;
- Zeile 18: Das Projekt [client-android-skel] speichert und stellt Fragmentmenüs automatisch wieder her, sofern vorhanden. Im Array MenuItemState[] menuOptionsState speichern wir den sichtbaren oder ausgeblendeten Status aller Menüoptionen;
- Zeilen 10–13: Wie in [Beispiel-22] wird der Zustand der Aktivität und ihrer Fragmente in der Sitzung gespeichert, die wiederum als JSON-Zeichenkette gespeichert wird. Wir werden sehen, dass die Sitzung ein Array von Elementen des Typs [CoreState] speichert. Wenn wir nichts unternehmen, wird die JSON-Zeichenkette vom Typ [CoreState] gespeichert. Wir möchten jedoch die Zustände der Fragmente speichern, die von [CoreState] abgeleitet sind. Um sicherzustellen, dass die JSON-Zeichenkette des abgeleiteten Typs und nicht die des übergeordneten Typs generiert wird, müssen die abgeleiteten Typen wie in den Zeilen 10–13 gezeigt deklariert werden. Die Klasse [CoreState] ist eine der Architekturklassen, die der Entwickler für jede neue Anwendung anpassen muss (Zeilen 10–13);
2.5.4. Die Schnittstelle [IMainActivity]
![]() |
Die Schnittstelle [IMainActivity] definiert, welche Anforderungen Fragmente in der folgenden Architektur an die Aktivität stellen können:

package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// application constants (to be modified) -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 0;
// todo add your constants and other methods here
}
- Zeile 6: Die Schnittstelle [IMainActivity] erweitert die Schnittstelle [IDao] der [DAO]-Schicht;
- Zeile 9: Dies ist die Aktivität, die Zugriff auf die Sitzung in Form einer Instanz der Schnittstelle [ISession] bietet;
- Zeile 12: Dies ist die Aktivität, die zum Wechseln zwischen Ansichten verwendet wird. Der zweite Parameter ist die Aktion, die diesen Ansichtswechsel auslöst, einer der Werte SUBMIT, NAVIGATION oder RESTORE;
- Zeilen 15–17: Dies ist die Aktivität, die den Ladebildschirm verwaltet;
- Zeile 22: zum Debuggen der Anwendung;
- Zeile 25: um zu vermeiden, dass zu lange gewartet wird, wenn der Server nicht mehr reagiert;
- Zeile 28: Setzen Sie diesen Wert während des Debuggens auf einige Sekunden, um Zeit zu haben, den Vorgang mit dem Server abzubrechen und zu sehen, was passiert;
- Zeile 31: Setzen Sie diesen Wert auf „true“, wenn der JSON-Dienst eine Basisauthentifizierung erfordert;
- Zeile 34: Fragment-Adjazenz;
- Zeile 37: Setze auf „true“, wenn die Anwendung Registerkarten enthält;
- Zeile 39: Setze auf „true“, wenn die Anwendung mit einem Web-/JSON-Server kommuniziert und du während des Datenaustauschs ein Lade-Symbol anzeigen möchtest;
- Zeile 43: Die Anzahl der von der Anwendung verwalteten Fragmente;
Die Schnittstelle [IMainActivity] ist das zweite Element der Architektur, das der Entwickler implementieren muss (Zeile 45).
2.5.5. Die [IDao]-Schnittstelle
Die Schnittstelle [IMainActivity] erweitert die folgende Schnittstelle [IDao]:
![]() |
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
- Zeile 24: Der Entwickler wird die Schnittstelle hier vervollständigen;
2.5.6. Die Sitzung
![]() |
Die Klasse [Session] kapselt Elemente, die von der Aktivität und den Fragmenten gemeinsam genutzt werden. Sie implementiert die folgende Schnittstelle [ISession]:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
public interface ISession {
// number of last view displayed
int getPreviousView();
void setPreviousView(int numView);
// last state of a view
CoreState getCoreState(int numView);
void setCoreState(int numView, CoreState coreState);
// action in progress
enum Action {
SUBMIT, NAVIGATION, RESTORE, NONE
}
Action getAction();
void setAction(Action action);
// status of all views -
// not used by code but required for serialization / deserialization jSON
CoreState[] getCoreStates();
void setCoreStates(CoreState[] coreStates);
// last selected tab number
int getPreviousTab();
void setPreviousTab(int position);
// tab selection navigation
boolean isNavigationOnTabSelectionNeeded();
void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelection);
}
Wir führen die Schnittstelle [ISession] ein, um das Vorhandensein bestimmter Methoden in der Sitzung vorzuschreiben:
- Zeilen 7–10: die Nummer der zuletzt angezeigten Ansicht (Fragment);
- Zeilen 12–15: der Status einer bestimmten Ansicht;
- Zeilen 17–24: Wir führen das Konzept einer laufenden Aktion ein. Es gibt vier (Zeile 17):
- RESTORE: Ein Speichervorgang ist im Gange. Es findet kein Ansichtswechsel statt;
- NAVIGATION: Die Navigation ist im Gange. Hier definieren wir Navigation als einen Ansichtswechsel, bei dem die neue Ansicht aus ihrem letzten, während der Sitzung gespeicherten Zustand wiederhergestellt werden kann;
- SUBMIT: Wir weisen einer ausstehenden Aktion den Typ [SUBMIT] zu, wenn eine Ansichtsänderung vorliegt und die neue Ansicht vom Gesamtzustand der Aktivität abhängt, nicht nur von ihrem eigenen Zustand. Manchmal ist es schwierig, zwischen NAVIGATION und SUBMIT zu unterscheiden. In solchen Fällen verwenden wir den allgemeineren Fall von SUBMIT;
- NONE: Der Wert der Aktion, wenn sie noch keinen ersten Wert erhalten hat;
- Zeilen 26–30: Die Zustände der Aktivität und der Fragmente werden in einem CoreState[]-Array gespeichert. Um sicherzustellen, dass dies bei der JSON-Serialisierung/Deserialisierung korrekt gehandhabt wird, muss es über einen Getter und einen Setter verfügen;
- Zeilen 32–35: Nummer der zuletzt ausgewählten Registerkarte. Wird während des Speicher-/Wiederherstellungszyklus verwendet, um die Registerkarte wieder auszuwählen, die vor dem Drehen des Geräts ausgewählt war;
- Zeilen 37–40: Verwaltet einen booleschen Wert, der angibt, ob die Auswahl einer Registerkarte mit einem Fragmentwechsel einhergehen soll;
Die Schnittstelle [ISession] wird durch die folgende abstrakte Klasse [AbstractSession] implementiert:
package client.android.architecture.core;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class AbstractSession implements ISession {
// previous view no
private int preViousView;
// view status
private CoreState[] coreStates = new CoreState[0];
// action in progress
private Action action = Action.NONE;
// previously selected tab
private int previousTab;
// tab selection navigation
@JsonIgnore
private boolean navigationOnTabSelectionNeeded = true;
// manufacturer
public AbstractSession() {
// initialize the fragment status table
coreStates = new CoreState[IMainActivity.FRAGMENTS_COUNT];
for (int i = 0; i < coreStates.length; i++) {
coreStates[i] = new CoreState();
}
}
// interface ISession ---------------------------------------------------------
@Override
public int getPreviousView() {
return preViousView;
}
@Override
public void setPreviousView(int numView) {
this.preViousView = numView;
}
@Override
public CoreState getCoreState(int numView) {
return coreStates[numView];
}
@Override
public void setCoreState(int numView, CoreState coreState) {
coreStates[numView] = coreState;
}
@Override
public Action getAction() {
return action;
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public CoreState[] getCoreStates() {
return coreStates;
}
@Override
public void setCoreStates(CoreState[] coreStates) {
this.coreStates = coreStates;
}
@Override
public int getPreviousTab() {
return previousTab;
}
@Override
public void setPreviousTab(int position) {
this.previousTab = position;
}
@Override
public boolean isNavigationOnTabSelectionNeeded() {
return navigationOnTabSelectionNeeded;
}
@Override
public void setNavigationOnTabSelectionNeeded(boolean navigationOnTabSelectionNeeded) {
this.navigationOnTabSelectionNeeded = navigationOnTabSelectionNeeded;
}
}
- Zeile 9: Die ID der Ansicht, die vor der aktuell angezeigten Ansicht angezeigt wurde. Diese Information ist nützlich, wenn eine Ansicht von mehreren Stellen aus aufgerufen werden kann. Dies ist typischerweise bei der tabbasierten Navigation der Fall. Die angezeigte Ansicht kann dann feststellen, welche Ansicht zuvor angezeigt wurde;
- Zeile 12: Das Array der Zustände für alle von der Aktivität angezeigten Fragmente;
- Zeile 18: Die ID der zuvor ausgewählten Registerkarte. Spielt eine ähnliche Rolle wie die ID der vorherigen Ansicht in Zeile 9. Diese Information ist nützlich, wenn das Gerät gedreht wird und man zu der Registerkarte zurückkehren muss, die vor der Drehung ausgewählt war;
- Zeile 22: Ein boolescher Wert, der angibt, ob die Auswahl eines Tabs zu einer Änderung des angezeigten Fragments führen soll. Beachten Sie, dass das Projekt [client-android-skel] Tabs und Fragmente separat verwaltet, sodass es in Fällen verwendet werden kann, in denen die Anzahl der Tabs geringer ist als die Anzahl der Fragmente. Es gibt zwei Arten der Auswahl:
- eine Auswahl, die der Benutzer durch Klicken auf eine Registerkarte vornimmt. In diesem Fall muss das angezeigte Fragment in der Regel geändert werden;
- eine softwaregesteuerte Auswahl über die Methode [Tablayout.Tab.select()]. In diesem Fall ist eine Änderung des angezeigten Fragments nicht immer wünschenswert. Hier sind zwei Beispiele:
- Wenn das Gerät gedreht wird, wird die Aktivität neu erstellt, ebenso wie die Registerkarten. Beim Erstellen der ersten Registerkarte wird jedoch automatisch ein softwaregesteuerter [select]-Vorgang ausgeführt. Es ist daher nicht wünschenswert, das angezeigte Fragment zu ändern, da wir uns in einer Phase der Neuerstellung der Aktivität befinden, in der das letztendlich angezeigte Fragment nicht unbedingt dasjenige ist, das der ersten Registerkarte zugeordnet ist;
- Da die Tab-Verwaltung von der Fragmentverwaltung getrennt ist, möchten Sie möglicherweise die Tabs aktualisieren (löschen, hinzufügen), ohne die zugehörigen Fragmente zu beeinträchtigen. Einige dieser Vorgänge können jedoch wiederum eine implizite [select]-Softwareoperation auf einem der Tabs auslösen. Diese Auswahl führt nicht zwangsläufig zur Navigation zum zugehörigen Fragment;
- Zeile 21: Das Feld [navigationOnTabSelectionNeeded] soll bei Speicheroperationen für die Aktivität und ihre Fragmente nicht gespeichert werden. Die Annotation [@JsonIgnore] bewirkt, dass das Feld bei der JSON-Serialisierung/Deserialisierung ignoriert wird;
- Zeilen 25–31: Der Konstruktor initialisiert das Array der Zustände für die [FRAGMENTS_COUNT] Fragmente der Anwendung. Die Elemente dieses Arrays werden mit dem Feld [hasBeenVisited=false] initialisiert. Diese Information wird verwendet, um festzustellen, ob dies der erste Besuch des Fragments ist oder nicht;
Die Klasse [Session] sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
}
- Zeile 5: Die Klasse [Session] erweitert die soeben vorgestellte Klasse [AbstractSession]. Der Entwickler platziert hier die Elemente, die zwischen den Fragmenten untereinander sowie zwischen den Fragmenten und der Aktivität gemeinsam genutzt werden sollen. Beachten Sie, dass die Klasse [Session] nicht mehr mit der Annotation [@EBean] versehen ist. Sie ist zu einer normalen Klasse geworden;
2.5.7. Die abstrakte Klasse [AbstractActivity]
![]() |
2.5.7.1. Grundgerüst
Die Klasse [AbstractActivity] ist eine Klasse mit über 300 Zeilen. Wir werden sie Schritt für Schritt untersuchen. Ihr Grundgerüst sieht wie folgt aus:
package client.android.architecture;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import client.android.R;
import client.android.dao.service.IDao;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// layer [DAO]
private IDao dao;
// the session
protected Session session;
// the fragment container
protected MyPager mViewPager;
// the toolbar
private Toolbar toolbar;
// the waiting image
private ProgressBar loadingPanel;
// tab bar
protected TabLayout tabLayout;
// the fragment or section manager
private FragmentPagerAdapter mSectionsPagerAdapter;
// class name
protected String className;
// mapper jSON
private ObjectMapper jsonMapper;
// manufacturer
public AbstractActivity() {
// class name
className = getClass().getSimpleName();
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "constructeur");
}
// jsonMapper
jsonMapper = new ObjectMapper();
}
// implémentation IMainActivity --------------------------------------------------------------------
...
// life cycle - backup / restore ------------------------------------
...
// hold image management ---------------------------------
...
// interface IDao -----------------------------------------------------
...
// the fragment manager --------------------------------
...
// girls' classes
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
}
Die Klasse [AbstractActivity]:
- implementiert die Schnittstelle [IMainActivity] (Zeilen 21, 55);
- übernimmt das Speichern und Wiederherstellen der Aktivität und ihrer Fragmente, wenn das Gerät gedreht wird (Zeile 58);
- übernimmt die Verwaltung des Ladebildschirms während der Kommunikation mit dem Webserver / JSON (Zeile 61);
- implementiert die IDao-Schnittstelle der [DAO]-Schicht (Zeile 64);
- implementiert den Fragment-Manager (Zeile 67);
- verlangt von seinen untergeordneten Klassen, dass sie sechs Methoden enthalten (Zeilen 71–81);
2.5.7.2. Implementierung der [IMainActivity]-Schnittstelle
Die Implementierung der [IMainActivity]-Schnittstelle (siehe Abschnitt 2.5.4) sieht wie folgt aus:
// implémentation IMainActivity --------------------------------------------------------------------
@Override
public Session getSession() {
return session;
}
@Override
public void navigateToView(int position, ISession.Action action) {
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("navigation vers vue %s sur action %s", position, action));
}
// display new fragment
mViewPager.setCurrentItem(position);
// we note the action in progress when the view changes
session.setAction(action);
}
2.5.7.3. Speichern des Zustands der Aktivität und ihrer Fragmente
Der Zustand der Aktivität und ihrer Fragmente ist vollständig in der Sitzung enthalten. Daher müssen wir die Sitzung speichern. Hier verwenden wir das wieder, was im Projekt [Beispiel-22] gemacht wurde (siehe Abschnitt 1.23):
// backup / restore management ------------------------------------
@Override
protected void onSaveInstanceState(Bundle outState) {
// parent
super.onSaveInstanceState(outState);
// save session as jSON string
try {
outState.putString("session", jsonMapper.writeValueAsString(session));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
2.5.7.4. Wiederherstellen des Zustands der Aktivität und ihrer Fragmente
Dies beinhaltet das Wiederherstellen der Sitzung. Wir gehen dabei wie in [Beispiel-22] gezeigt vor:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
// something to restore?
if (savedInstanceState != null) {
// session recovery
try {
session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
});
} catch (IOException e) {
e.printStackTrace();
}
// log
if (IS_DEBUG_ENABLED) {
try {
Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
} else {
// session
session = new Session();
}
...
- Zeilen 10–26: Wenn der Parameter [Bundle savedInstanceState] in Zeile 2 nicht null ist, wird die Sitzung wiederhergestellt (Zeilen 12–17);
- Zeilen 26–29: Wenn der Parameter [Bundle savedInstanceState] in Zeile 2 null ist, entspricht dies dem ersten Start der Aktivität. Es wird dann eine leere Sitzung erstellt;
2.5.7.5. Initialisierung der [DAO]-Schicht
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// layer [DAO]
dao = getDao();
if (dao != null) {
// layer configuration [DAO]
setDebugMode(IS_DEBUG_ENABLED);
setTimeout(TIMEOUT);
setDelay(DELAY);
setBasicAuthentification(IS_BASIC_AUTHENTIFICATION_NEEDED);
}
...
// girls' classes
protected abstract IDao getDao();
....
}
- Zeile 11: Von der untergeordneten Aktivität wird eine Referenz auf die [DAO]-Schicht angefordert (Zeile 21);
- Zeilen 14–17: Wenn die [DAO]-Schicht vorhanden ist, wird sie anhand der in der [IMainActivity]-Schnittstelle enthaltenen Informationen konfiguriert;
2.5.7.6. Initialisierung der mit der Aktivität verbundenen Ansicht
Die mit der Aktivität verbundene Ansicht wurde in Abschnitt 2.5.1 vorgestellt:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".activity.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"
app:layout_scrollFlags="scroll|enterAlways">
</android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<!-- fragment container -->
<client.android.architecture.core.MyPager
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="20dp"
android:background="@color/floral_white"/>
</android.support.design.widget.CoordinatorLayout>
Diese Ansicht wird mit dem folgenden Code initialisiert:
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// associated view
setContentView(R.layout.activity_main);
// view components ---------------------
// toolbar
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// waiting image?
if (IS_WAITING_ICON_NEEDED) {
// we add the waiting image
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding loadingPanel");
}
// creation ProgressBar
loadingPanel = new ProgressBar(this);
loadingPanel.setVisibility(View.INVISIBLE);
// added ProgressBar to toolbar
toolbar.addView(loadingPanel);
}
...
- Zeile 11: Die XML-Ansicht [activity_main] wird der Aktivität zugeordnet;
- Zeilen 14–15: Die Symbolleiste ist integriert und wird unterstützt;
- Zeilen 17–27: optionales Hinzufügen eines Ladesymbols: wenn der boolesche Wert [IS_WAITING_ICON_NEEDED] in der Schnittstelle [IMainActivity] wahr ist;
- Zeile 23: Erstellung des Ladesymbols vom Typ [ProgressBar], auf das das Feld [loadingPanel] verweist;
- Zeile 24: Dieses Bild ist zunächst ausgeblendet;
- Zeile 26: Es wird zur Symbolleiste hinzugefügt;
2.5.7.7. Registerkartenverwaltung
Die Schnittstelle [IMainActivity] kann eine Registerkartenleiste anfordern. Diese wird wie folgt hinzugefügt und verwaltet:
// tab bar
protected TabLayout tabLayout;
...
// tab bar?
if (ARE_TABS_NEEDED) {
// add the tab bar
if (IS_DEBUG_ENABLED) {
Log.d(className, "adding tablayout");
}
// no selection navigation until a fragment is displayed
session.setNavigationOnTabSelectionNeeded(false);
// tab bar creation
tabLayout = new CustomTabLayout(this);
tabLayout.setTabTextColors(ContextCompat.getColorStateList(this, R.color.tab_text));
// tab bar added to application bar
AppBarLayout appBarLayout = (AppBarLayout) findViewById(R.id.appbar);
appBarLayout.addView(tabLayout);
// tab bar event manager
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// a tab has been selected
if (IS_DEBUG_ENABLED) {
Log.d(className, String.format("onTabSelected n° %s, action=%s, tabCount=%s isNavigationOnTabSelectionNeeded=%s",
tab.getPosition(), session.getAction(), tabLayout.getTabCount(), session.isNavigationOnTabSelectionNeeded()));
}
if (session.isNavigationOnTabSelectionNeeded()) {
// miter position
int position = tab.getPosition();
// memory
session.setPreviousTab(position);
// associated fragment display?
navigateOnTabSelected(position);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
}
...
// girls' classes
protected abstract void navigateOnTabSelected(int position);
...
- Zeilen 12–48: Hinzufügen und Verwalten einer Tab-Leiste;
- Zeile 6: Die Registerkartenleiste wird hinzugefügt, wenn die Konstante [ARE_TABS_NEEDED] in der Schnittstelle [IMainActivity] auf „true“ gesetzt ist;
- Zeile 12: Beim Erstellen der Tab-Leiste können implizite [Tablayout.Tab.select]-Operationen auftreten (diese werden nicht vom Benutzer ausgelöst). Wir setzen den booleschen Wert [session.navigationOnTabSelectionNeeded] auf „false“, um jegliche Navigation während dieser falschen Auswahlen zu verhindern. Es obliegt dem Entwickler, das anzuzeigende Fragment mithilfe der Methode [navigateToView] auszuwählen. Der boolesche Wert [session.navigationOnTabSelectionNeeded] wird wieder auf „true“ gesetzt, wenn dieses Fragment angezeigt wird (siehe Klasse AbstractFragment);
- Zeile 14: Erstellung einer Tab-Leiste, auf die das Feld [tabLayout] verweist. Wir verwenden eine benutzerdefinierte Tab-Leiste [CustomTabLayout], auf die wir später noch eingehen werden;
- Zeile 15: Wir legen die Farben der Tab-Titel fest. Diese finden sich in der folgenden Datei [res/color/tab_txt.xml]:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:color="#FFFF00" />
<item android:state_selected="false" android:color="#FFFFFF" />
</selector>
- Zeile (c): die Farbe des Registerkartentitels, wenn die Registerkarte ausgewählt ist;
- Zeile (d): die Farbe des Registerkartentitels, wenn die Registerkarte nicht ausgewählt ist;
Diese Datei ist natürlich editierbar. Die hexadezimalen Farbcodes finden Sie beispielsweise hier.
- Zeilen 17–18: Hinzufügen dieser Registerkartenleiste zur Anwendungsleiste in der XML-Ansicht [activity_main];
- Zeilen 20–47: Ereignisbehandler für die Registerleiste;
- Zeilen 22–36: Es wird nur das [onTabSelected]-Ereignis behandelt. Es entspricht einem Klick auf die Registerkarte [Tab], die als Parameter an die Methode übergeben wird, oder einem Softwarebefehl [TabLayout.Tab.select];
- Zeile 30: Position der ausgewählten Registerkarte;
- Zeile 32: Diese Position wird in der Sitzung gespeichert;
- Zeile 34: Das mit dieser Registerkarte verknüpfte Fragment muss nun angezeigt werden. Nur die Unterklasse (Zeile 52) kann diese Verknüpfung herstellen. Beachten Sie, dass wir die Registerkartenleiste nicht mit dem Fragment-Container [mViewPager] verknüpfen, wie es in einigen der untersuchten Beispiele der Fall war. Hier trennen wir die Verwaltung der Registerkartenleiste vollständig von der der Fragmente. Deshalb müssen wir beim Klicken auf eine Registerkarte angeben, welche Ansicht angezeigt werden soll;
- Zeile 28: Wir unterscheiden zwischen der Auswahl einer Registerkarte mit oder ohne Navigation. Im Allgemeinen wird erwartet, dass eine Navigation erfolgt, wenn der Benutzer auf eine Registerkarte klickt, während dies bei einer programmgesteuerten Auswahl nicht der Fall ist. Der Entwickler unterscheidet diese beiden Fälle mithilfe des Elements [session.navigationOnTabSelectionNeeded]. Wenn keine Navigation durchgeführt wird, wird die Nummer der zuletzt ausgewählten Registerkarte nicht in der Sitzung gespeichert. Es obliegt dem Entwickler, dies zu tun;
2.5.7.8. Der Tab-Manager [CustomTabLayout]
![]() |
Wir verwenden einen benutzerdefinierten Tab-Manager, um Tab-Titel in verschiedenen Schriftarten anzuzeigen. Die Klasse [CustomTabLayout] sieht wie folgt aus:
package client.android.architecture.custom;
import android.content.Context;
import android.graphics.Typeface;
import android.support.design.widget.TabLayout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class CustomTabLayout extends TabLayout {
private Typeface mTypeface;
public CustomTabLayout(Context context) {
super(context);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mTypeface = Typeface.createFromAsset(getContext().getAssets(), "fonts/Roboto-Bold.ttf");
}
@Override
public void addTab(Tab tab) {
super.addTab(tab);
ViewGroup mainView = (ViewGroup) getChildAt(0);
ViewGroup tabView = (ViewGroup) mainView.getChildAt(tab.getPosition());
int tabChildCount = tabView.getChildCount();
for (int i = 0; i < tabChildCount; i++) {
View tabViewChild = tabView.getChildAt(i);
if (tabViewChild instanceof TextView) {
((TextView) tabViewChild).setTypeface(mTypeface, Typeface.NORMAL);
}
}
}
}
- Die Schriftart für die Registerkartentitel wird in den Zeilen 30 und 44 angepasst;
Der Ordner [fonts] sieht wie folgt aus:
![]() |
Quellen:
- Der Code für die Klasse [CustomTabLayout] wurde unter der URL [http://stackoverflow.com/questions/31067265/change-the-font-of-tab-text-in-android-design-support-tablayout] gefunden;
- Die Schriftarten wurden unter der URL [https://www.fontsquirrel.com/fonts/roboto] gefunden;
2.5.7.9. Neueste Initialisierungen
@Override
protected void onCreate(Bundle savedInstanceState) {
// parent
super.onCreate(savedInstanceState);
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreate");
}
...
// fragment manager instantiation
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// the fragment container is associated with the fragment manager
// i.e. fragment no. i in the fragment container is fragment no. i delivered by the fragment manager
mViewPager = (MyPager) findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
// inhibit swiping between fragments
mViewPager.setSwipeEnabled(false);
// fragment adjacency
mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
// display 1st view
if (session.getAction() == ISession.Action.NONE) {
navigateToView(getFirstView(), ISession.Action.NONE);
}
// we hand over to the daughter activity
onCreateActivity();
}
...
// girls' classes
protected abstract void onCreateActivity();
protected abstract int getFirstView();
...
- Zeilen 10–19: Dieser Code kommt häufig in den Beispielen vor, die wir behandelt haben;
- Zeilen 21–23: Anzeige der allerersten Ansicht. Es gibt zweifellos mehrere Möglichkeiten, diesen Fall zu unterscheiden. Hier haben wir die Tatsache genutzt, dass für die allererste Ansicht der Wert der Aktion, die den Ansichtswechsel auslöst, NONE ist;
- Zeile 22: Wir treffen keine Annahmen darüber, welches Fragment als erstes angezeigt werden soll. In unseren Beispielen war dies oft Fragment Nr. 0, aber nicht immer (siehe Beispiel 22). Wir werden daher die untergeordnete Aktivität (Zeile 30) bitten, uns mitzuteilen, um welche Ansicht es sich handelt;
- Zeile 25: Wir haben hier alles herausgearbeitet, was wir konnten. Nun muss die untergeordnete Klasse ihre eigenen Initialisierungen durchführen (Zeile 29);
2.5.7.10. Behandlung des Ladebildes
In der Klasse [AbstractActivity] wird das Platzhalterbild durch die folgenden zwei Methoden verwaltet:
// hold image management ---------------------------------
public void cancelWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.INVISIBLE);
}
}
public void beginWaiting() {
if (loadingPanel != null) {
loadingPanel.setVisibility(View.VISIBLE);
}
}
2.5.7.11. Implementierung der Schnittstelle [IDao]
In der Klasse [AbstractActivity] wird die Schnittstelle [IDao] (siehe Abschnitt 2.5.5) wie folgt implementiert:
public abstract class AbstractActivity extends AppCompatActivity implements IMainActivity {
// layer [DAO]
private IDao dao;
...
// interface IDao -----------------------------------------------------
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setUser(String user, String mdp) {
dao.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
dao.setBasicAuthentification(isBasicAuthentificationNeeded);
}
@Override
public void setDebugMode(boolean isDebugEnabled) {
dao.setDebugMode(isDebugEnabled);
}
@Override
public void setDelay(int delay) {
dao.setDelay(delay);
}
- Zeile 3: Erinnern Sie sich daran, dass der Wert dieses Feldes von der untergeordneten Aktivität in der Methode [onCreate] bereitgestellt wurde;
2.5.7.12. Implementierung des Fragment-Managers
In der Klasse [AbstractActivity] wird der Fragment-Manager wie folgt implementiert:
...
// the fragment manager --------------------------------
public class SectionsPagerAdapter extends FragmentPagerAdapter {
private AbstractFragment[] fragments;
// manufacturer
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
// daughter class fragments
fragments = getFragments();
}
// must render fragment no. position
@Override
public AbstractFragment getItem(int position) {
// we return the fragment
return fragments[position];
}
// makes the number of fragments to manage
@Override
public int getCount() {
return fragments.length;
}
// makes the title of fragment no. position
@Override
public CharSequence getPageTitle(int position) {
return getFragmentTitle(position);
}
}
// girls' classes
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
...
}
- Zeile 5: das Array der mit der Aktivität verbundenen Fragmente. Alle Fragmente werden von der Klasse [AbstractFragment] abgeleitet;
- Zeilen 8–12: Dies ist der Konstruktor, der das Array der Fragmente initialisiert. Er fordert diese von der Unterklasse der Aktivität an (Zeile 35);
- Zeilen 28–31: Fragmenttitel können in einer Anwendung verwendet werden, in der es genauso viele Registerkarten wie Fragmente gibt. In diesem Fall kann der Registerkarte der Titel des Fragments zugewiesen werden. Hier werden diese Titel von der Unterklasse angefordert (Zeile 37);
2.5.7.13. Die Methode [onResume]
Die Methode [onResume] wird kurz bevor die mit der Aktivität verbundene Ansicht sichtbar wird, ausgeführt. Sie wird hier verwendet, um nach einem Speichern/Wiederherstellen-Vorgang eine Registerkarte auszuwählen:
@Override
public void onResume() {
// parent
super.onResume();
if (IS_DEBUG_ENABLED) {
Log.d(className, "onResume");
}
// if restore, then restore last selected tab
if (ARE_TABS_NEEDED && session.getAction() == ISession.Action.RESTORE) {
tabLayout.getTabAt(session.getPreviousTab()).select();
}
}
- Zeile 10: Auswahl der Registerkarte, die vor dem Speichern/Wiederherstellen ausgewählt war. Es ist wichtig zu beachten, dass in der Methode [onCreate] – die im Lebenszyklus der Aktivität vor der Methode [onResume] ausgeführt wird – die Navigation bei der Auswahl einer Registerkarte deaktiviert wurde. Daher wird hier zwar eine Registerkarte ausgewählt, es findet jedoch kein Fragmentwechsel statt;
2.5.7.14. Zusammenfassung
Die abstrakte Klasse [AbstractActivity] ist die übergeordnete Klasse der einzigen Aktivität der Anwendung.
Die untergeordnete Aktivität muss die folgenden sechs Methoden implementieren:
// girls' classes
protected abstract void onCreateActivity();
protected abstract IDao getDao();
protected abstract AbstractFragment[] getFragments();
protected abstract CharSequence getFragmentTitle(int position);
protected abstract void navigateOnTabSelected(int position);
protected abstract int getFirstView();
Die untergeordnete Aktivität hat außerdem Zugriff auf die folgenden geschützten Mitglieder ihrer übergeordneten Klasse:
// the session
protected ISession session;
// the fragment container
protected MyPager mViewPager;
// tab bar
protected CustomTabLayout tabLayout;
// class name
protected String className;
2.5.8. Die [MainActivity]-Aktivität
![]() |
Die Klasse [MainActivity] kann einen anderen Namen haben. Die einzige Voraussetzung ist, dass sie die Schnittstelle [IMainActivity] implementiert. Die bereitgestellte Standardklasse lautet wie folgt:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.AbstractActivity;
import client.android.architecture.AbstractFragment;
import client.android.architecture.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
// todo: tabbed browsing - define the first view to be displayed
return 0;
}
}
- Zeile 14: Damit die AA-Annotation [@Bean] in Zeile 19 gültig ist, muss die Aktivität die AA-Annotation [@EActivity] aufweisen;
- Zeile 15: Die Aktivität ist mit dem XML-Menü [menu_main] verknüpft. Derzeit ist dieses Menü leer. Der Entwickler muss es bei Bedarf ausfüllen;
- Zeile 16: Die Klasse erweitert die Klasse [AbstractActivity];
- Zeilen 19–20: Ein Verweis auf die [DAO]-Schicht. Diese wird von der AA-Bibliothek instanziiert, bevor dieses Feld initialisiert wird. Das bedeutet, dass die AA-Bean [Dao] vorhanden sein muss. Dies ist bei der von uns bereitgestellten Skeleton-Anwendung immer der Fall. Selbst in einer Anwendung ohne [DAO]-Schicht können Sie das [dao]-Paket an Ort und Stelle belassen. Dies verursacht keine Komplikationen;
- Zeile 22: Die Session als Instanz vom Typ [Session]. Die Session existiert in der übergeordneten Klasse [AbstractActivity], jedoch als Instanz der Schnittstelle [ISession] (Zeile 32);
- Zeilen 24–63: die sechs Methoden, die von der übergeordneten Klasse [AbstractActivity] benötigt werden;
- Zeilen 36–39: Die Methode [getDao] gibt eine Referenz auf die [DAO]-Schicht zurück. Hier ist diese Referenz niemals null. In der übergeordneten Klasse [AbstractActivity] haben wir jedoch den Fall vorgesehen, dass die untergeordnete Klasse eine null-Referenz zurückgibt, um anzuzeigen, dass keine [DAO]-Schicht vorhanden ist. Wenn Sie diese Option nutzen möchten (meiner Meinung nach nicht sehr nützlich), müssen Sie den Zeiger an dieser Stelle auf null setzen;
2.6. Die [DAO]-Schicht

![]() |
2.6.1. Die IDao-Schnittstelle
Es wurde in Abschnitt 2.5.5 vorgestellt:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// todo: declare your interface here
}
Der Entwickler fügt die Methoden für seine [DAO]-Schicht ab Zeile 24 hinzu.
2.6.2. Die [WebClient]-Schnittstelle
![]() |
Die [WebClient]-Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// todo : declare here the URL to be reached
}
Der Entwickler fügt ab Zeile 17 die Methoden hinzu, die mit den vom JSON-Server bereitgestellten URLs kommunizieren.
2.6.3. Der Authentifizierungs-Interceptor [MyAuthInterceptor]
![]() |
Die Klasse [MyAuthInterceptor] sieht wie folgt aus:
package client.android.dao.service;
import org.androidannotations.annotations.EBean;
import org.springframework.http.HttpAuthentication;
import org.springframework.http.HttpBasicAuthentication;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import java.io.IOException;
@EBean(scope = EBean.Scope.Singleton)
public class MyAuthInterceptor implements ClientHttpRequestInterceptor {
// user
private String user;
// password
private String mdp;
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// headers HTTP of the HTTP request intercepted
HttpHeaders headers = request.getHeaders();
// the HTTP basic authentication header
HttpAuthentication auth = new HttpBasicAuthentication(user, mdp);
// added to HTTP headers
headers.setAuthorization(auth);
// continue the query life cycle HTTP
return execution.execute(request, body);
}
// authentication elements
public void setUser(String user, String mdp) {
this.user = user;
this.mdp = mdp;
}
}
Diese Klasse generiert den folgenden HTTP-Authentifizierungsheader:
wobei [code] die Base64-kodierte Zeichenfolge 'user:mp' ist. Diese Klasse wird nur verwendet, wenn der JSON-Server diese Form der Authentifizierung erwartet. Es gibt auch andere Formen.
Hinweis: Die Verwendung dieser Klasse wird in Abschnitt 3.6.3.1 veranschaulicht.
2.6.4. Die Klasse [AbstractDao]
![]() |
Die Klasse [AbstractDao] sieht wie folgt aus:
package client.android.dao.service;
import android.util.Log;
import client.android.architecture.core.Utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscriber;
public abstract class AbstractDao {
// mapper jSON
private ObjectMapper mapper = new ObjectMapper();
// debug mode
protected boolean isDebugEnabled;
// class name
protected String className;
// timeout before request execution
private int delay;
// manufacturer
public AbstractDao() {
// class name
className = getClass().getName();
Log.d("AbstractDao", String.format("constructeur, thread=%s", Thread.currentThread().getName()));
}
// méthodes protégées ----------------------------------------------------------
// generic interface
protected interface IRequest<T> {
T getResponse();
}
// generic request to a web service / jSON
protected <T> Observable<T> getResponse(final IRequest<T> request) {
// log
if (isDebugEnabled) {
Log.d(String.format("%s", className), String.format("delay=%s", delay));
}
// service execution - a single response is expected
return Observable.create(new Observable.OnSubscribe<T>() {
@Override
public void call(Subscriber<? super T> subscriber) {
DaoException ex = null;
// service execution
try {
// waiting?
if (delay > 0) {
Thread.sleep(delay);
}
// execute the synchronous request
T response = request.getResponse();
// log
if (isDebugEnabled) {
String log;
if (response instanceof String) {
log = (String) response;
} else {
log = mapper.writeValueAsString(response);
}
Log.d(className, String.format("response=%s sur thread [%s]", log, Thread.currentThread().getName()));
}
// the response is sent to the observer
subscriber.onNext(response);
// we signal the end of the observable
subscriber.onCompleted();
} catch (InterruptedException | JsonProcessingException | RuntimeException e) {
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("Thread [%s], Exception communication avec serveur : %s", Thread.currentThread().getName(), mapper.writeValueAsString(Utils.getMessagesFromException(e))));
} catch (JsonProcessingException e1) {
Log.d(className, String.format("Erreur jSON imprévue"));
}
}
// an exception is issued
subscriber.onError(new DaoException(e, 100));
}
}
});
}
// debug mode
public void setDebugMode(boolean isDebugEnabled) {
this.isDebugEnabled = isDebugEnabled;
}
public void setDelay(int delay) {
this.delay = delay;
}
}
- Zeilen 35–81: Die Methode [getResponse] verwendet die RxAndroid-Bibliothek, um einen Typ [Observable<T>] zurückzugeben. Im Gegensatz zu einigen zuvor gesehenen Beispielen gibt sie keinen Typ [Response<T>] zurück – bei dem es sich um einen proprietären Typ handelt –, sondern einen beliebigen Typ T;
- Zeile 35: Die Methode [getResponse] nimmt als Parameter eine Instanz vom Typ [IRequest<T>] aus den Zeilen 30–32 entgegen, deren Methode [IRequest.getResponse()] den Typ T über eine synchrone HTTP-Operation abruft;
- Zeilen 48–50: Künstlich warten wir [delay] Millisekunden. In der Produktion setzen wir [delay=0]. Während der Fehlersuche setzen wir [delay=einige Sekunden], um dem Benutzer die Möglichkeit zu geben, den asynchronen Vorgang abzubrechen und so zu sehen, wie sich der Code in diesem Fall verhält;
- Zeile 52: Die erwartete Antwort wird mit einer synchronen Anfrage angefordert;
- Zeile 64: Sobald die Antwort empfangen wurde, wird sie an den Beobachter weitergeleitet;
- Zeile 66: Wir geben an, dass keine weiteren Emissionen erfolgen werden. Dies ist der Sonderfall einer asynchronen Aktion, die nur ein Element zurückgibt;
- Zeilen 67–78: Im Falle einer Ausnahme wird die Ausnahme an den Beobachter weitergeleitet (Zeile 77);
2.6.5. Die [Dao]-Klasse
![]() |
Die Klasse [Dao] sieht wie folgt aus:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// factory configuration
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// todo: implementation IDao
}
- Zeilen 21–22: Einbindung der AA-Bean [WebClient], die die Kommunikation mit dem Webserver / JSON übernimmt;
- Zeilen 24–25: Einbindung des Authentifizierungs-Interceptors;
- Zeilen 31–42: Methode, die nach der Injektion der Felder aus den Zeilen 21–25 ausgeführt wird;
- Zeile 37: Das [RestTemplate]-Objekt, das die Client-Server-Kommunikation übernimmt, wird über eine Factory erstellt. Dies ist nicht zwingend erforderlich, aber die Factory ermöglicht es uns, Zeitüberschreitungen bei der Kommunikation zu konfigurieren. Deshalb verwenden wir nicht den parameterlosen Konstruktor [RestTemplate()];
- Zeile 39: Wir fügen den Konvertern des [RestTemplate] einen JSON-Konverter hinzu. Dies wird der einzige Konverter sein. Wenn also eine Methode des [WebClient] eine JSON-Zeichenkette vom Server erhält, wird diese automatisch in das Objekt deserialisiert, das die Methode zurückgeben soll;
- Zeile 41: Das auf diese Weise konfigurierte [RestTemplate]-Objekt wird an den Web-Client übergeben, der damit die Client-Server-Kommunikation abwickelt;
- Zeilen 44–48: Wir legen die Stamm-URL des Web-/JSON-Servers fest. Alle in der [WebClient]-Klasse deklarierten URLs beziehen sich relativ auf diese Stamm-URL;
- Zeilen 50–54: Mit dieser Methode können Sie den Verbindungsinhaber angeben, wenn die Verbindung durch eine Basisauthentifizierung gesteuert wird (siehe Abschnitt 2.6.3);
- Zeilen 56–64: Legen Sie die Zeitüberschreitungen für den Austausch zwischen Client und Server fest. Dies geschieht über die Factory des [RestTemplate]-Objekts, die den Austausch steuert;
- Zeilen 66–78: Diese Methode legt fest, dass der Server durch die Basisauthentifizierung geschützt ist;
- Zeilen 72–77: Wenn eine Basisauthentifizierung erforderlich ist, wird der in Zeile 25 eingefügte Authentifizierungs-Interceptor zu den Interceptoren des [RestTemplate]-Objekts hinzugefügt. Dieser Interceptor fügt automatisch den vom Server erwarteten HTTP-Header für die Basisauthentifizierung zu allen Web-Client-Anfragen hinzu;
- Der Entwickler implementiert die [IDao]-Schnittstelle ab Zeile 87;
2.7. Fragmente
![]() |
2.7.1. Die Klasse [MenuItemState]
Die Klasse [MenuItemState] kapselt den Status einer Menüoption:
package client.android.architecture;
public class MenuItemState {
// menu option identify
private int menuItemId;
// option visibility
private boolean isVisible;
// manufacturers
public MenuItemState() {
}
public MenuItemState(int menuItemId, boolean isVisible) {
this.menuItemId = menuItemId;
this.isVisible = isVisible;
}
// getters and setters
...
}
2.7.2. Die Klasse [Utils]
Die Klasse [Utils] enthält statische Hilfsmethoden:
package client.android.architecture;
import java.util.ArrayList;
import java.util.List;
public class Utils {
// list of exception messages - version 1
static public List<String> getMessagesFromException(Throwable ex) {
// create a list of error msgs from the exception stack
List<String> messages = new ArrayList<>();
Throwable th = ex;
while (th != null) {
messages.add(th.getMessage());
th = th.getCause();
}
return messages;
}
// exception message list - version 2
static public String getMessageForAlert(Throwable th) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
List<String> messages = getMessagesFromException(th);
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
// list of exception messages - version 3
static public String getMessageForAlert(List<String> messages) {
// build the text to be displayed
StringBuilder texte = new StringBuilder();
int n = messages.size();
for (String message : messages) {
texte.append(String.format("%s : %s\n", n, message));
n--;
}
// result
return texte.toString();
}
}
2.7.3. Die übergeordnete Klasse [AbstractFragment]
Die Klasse [AbstractFragment] enthält die Elemente, die allen Fragmenten in der Anwendung gemeinsam sind. Wie bei der Klasse [AbstractActivity] ist ihr Code komplex. Wir werden ihn ebenfalls Schritt für Schritt analysieren.
2.7.3.1. Das Grundgerüst
package client.android.architecture.core;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action0;
import rx.functions.Action1;
import rx.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.List;
public abstract class AbstractFragment extends Fragment {
// données privées ------------------------------------------------------------
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates = new MenuItemState[0];
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment status
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
// data accessible to daughter classes ---------------------------------------
// debug mode
final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
protected String className;
// asynchronous tasks
protected int numberOfRunningTasks;
// activity
protected IMainActivity mainActivity;
protected Activity activity;
// session
protected Session session;
// update Fragment ----------------------------------------------------------------------------------
...
// menu management ------------------------------------------
...
// wait management -------------------------------------------------------------
...
// asynchronous operation management --------------------------------------------------------------------
...
// gestion exception -------------------------------------------------------------------
....
// fragment lifecycle management --------------------------------------------------------
...
// classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
}
- Zeilen 28–45: die privaten Daten der Klasse;
- Zeilen 47–58: geschützte Daten, auf die Unterklassen zugreifen können;
- Zeilen 61–62: Code, der das anzuzeigende Fragment aktualisiert;
- Zeilen 64–65: Hilfscode zur Verwaltung des Menüs, falls vorhanden;
- Zeilen 67–68: Hilfscode zur Abwicklung der Wartezeit während einer asynchronen Operation;
- Zeilen 70–71: Code zur Erleichterung der Kommunikation zwischen dem Fragment und der [DAO]-Schicht;
- Zeilen 73–74: Hilfscode zur standardmäßigen Behandlung von Ausnahmen;
- Zeilen 76–77: Code zur Verwaltung des Lebenszyklus des Fragments;
- Zeilen 80–94: Die übergeordnete Klasse schreibt ihren untergeordneten Klassen 8 Methoden vor;
2.7.3.2. Der Konstruktor
Der Klassenkonstruktor sieht wie folgt aus:
// class name
protected String className;
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
...
// constructeur ----------------------
public AbstractFragment() {
// init
className = getClass().getSimpleName();
fragmentHasToBeInitialized = true;
// log
if (isDebugEnabled) {
Log.d(className, "constructeur");
}
}
- Zeile 9: Der Name der hier instanziierten untergeordneten Klasse wird vermerkt. Dieser Name wird in allen Protokollen der übergeordneten Klasse verwendet;
- Zeile 10: Wir vermerken, dass das Fragment erstellt wird. Diese Information wird verwendet, wenn das untergeordnete Fragment aufgefordert wird, sich selbst zu aktualisieren;
2.7.3.3. Menüverwaltung
In unserer Architektur muss jedes Fragment über ein Menü verfügen, auch wenn dieses leer ist. Die Protokolle haben tatsächlich gezeigt, dass bei Ausführung der Methode [onCreateOptionsMenu] – die ausgeführt wird, wenn das Fragment über ein Menü verfügt – das Fragment bereits mit seiner Aktivität, Ansicht und seinem Menü verknüpft ist und kurz davor steht, sichtbar zu werden. Dies ist daher der Moment, in dem die visuelle Oberfläche und das Menü aktualisiert werden können. Innerhalb dieser [onCreateOptionsMenu]-Methode weisen wir das untergeordnete Fragment an, sich selbst zu aktualisieren.
Die Menüverwaltung umfasst Hilfsmethoden, mit denen das untergeordnete Fragment Menüelemente ein- oder ausblenden kann:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
...
// menu management ------------------------------------------
private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
// scroll through all menu items
for (int i = 0; i < menu.size(); i++) {
// item n° i
MenuItem menuItem = menu.getItem(i);
menuOptionsIds.add(menuItem.getItemId());
// if item n° i is a sub-menu, then start again
if (menuItem.hasSubMenu()) {
// recursivity
getMenuOptions(menuItem.getSubMenu(), menuOptionsIds);
}
}
}
private void getMenuOptionsStates(Menu menu) {
// result
if (isDebugEnabled) {
Log.d(className, "getMenuOptionsStates(Menu)");
}
// we retrieve the identifiers of the menu options
List<Integer> menuOptionsIds = new ArrayList<>();
getMenuOptions(menu, menuOptionsIds);
// transfer menu options to a table
menuOptionsStates = new MenuItemState[menuOptionsIds.size()];
for (int i = 0; i < menuOptionsStates.length; i++) {
// identify option
int id = menuOptionsIds.get(i);
// status option
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
if (isDebugEnabled) {
Log.d(className, String.format("Nombre d'options de menu=%s", menuOptionsStates.length));
}
}
// menu option status
private MenuItemState[] getMenuOptionsStates() {
MenuItemState[] menuOptionsStates = new MenuItemState[this.menuOptionsStates.length];
for (int i = 0; i < menuOptionsStates.length; i++) {
// status
MenuItemState state = this.menuOptionsStates[i];
// menu id
int id = state.getMenuItemId();
// initialization status
menuOptionsStates[i] = new MenuItemState(id, menu.findItem(id).isVisible());
}
// result
return menuOptionsStates;
}
// display menu options -----------------------------------
protected void setAllMenuOptionsStates(boolean isVisible) {
// update all menu options
for (MenuItemState menuItemState : menuOptionsStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(isVisible);
}
}
protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// update certain menu options
for (MenuItemState menuItemState : menuItemStates) {
menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
}
}
- Zeilen 6–18: Diese Methode ruft die numerischen Kennungen aller Menüoptionen ab;
- Zeile 6: Die Methode [getMenuOptions] benötigt zwei Parameter:
- [Menu menu]: das Menü des Fragments;
- [List<Integer> menuOptionsIds]: die Liste der Android-IDs für die Menüoptionen. Zu Beginn ist diese Liste leer. Sie wird dann durch eine rekursive Durchquerung (Zeile 15) des Menübaums gefüllt;
- Zeilen 20–40: Erstellt auf Basis des Menüs das Array mit den Zuständen (ID, Sichtbarkeit) für die Menüoptionen. Dieses Array wird in Zeile 3 gespeichert. Die Klasse [MenuItemState] wurde in Abschnitt 2.7.1 beschrieben;
- Zeilen 43–55: eine Variante der vorherigen Methode. Sie führt dasselbe aus, verwendet jedoch anstelle einer Neuberechnung der Identifikatoren für alle Menüoptionen – was bereits geschehen ist – die Identifikatoren aus dem Zustandsarray in Zeile 3;
- Zeilen 58–63: Mit der Methode [setAllMenuOptionsStates] können Sie alle Menüoptionen des Fragments ausblenden oder einblenden;
- Zeilen 65–69: Mit der Methode [setMenuOptionsStates] können Sie bestimmte Menüoptionen selektiv ein- oder ausblenden;
- Die Methoden [getMenuOptions, getMenuOptionsStates] sind als privat deklariert, da sie nur innerhalb von [AbstractFragment] verwendet werden. Die Methoden [setAllMenuOptionsStates] (Zeile 58) und [setMenuOptionsStates] (Zeile 65) sind als geschützt deklariert, damit sie für untergeordnete Klassen verfügbar sind;
2.7.3.4. Behandlung des Wartens auf den Abschluss einer asynchronen Aufgabe
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
protected int numberOfRunningTasks;
protected boolean tasksInBackgroundHaveBeenCanceled;
...
// management of waiting for the end of an asynchronous operation -------------------------------------
protected void beginRunningTasks(int numberOfRunningTasks) {
// the number of tasks to be executed is noted
this.numberOfRunningTasks = numberOfRunningTasks;
// we put the image on hold
mainActivity.beginWaiting();
// empty the subscription list
abonnements.clear();
// no cancellations yet
runningTasksHaveBeenCanceled = false;
}
protected void cancelWaitingTasks() {
// we hide the waiting image
mainActivity.cancelWaiting();
}
- Zeilen 9–18: Um eine oder mehrere asynchrone Operationen zu starten, ruft das untergeordnete Fragment die übergeordnete Methode [beginRunningTasks] auf. Der Parameter dieser Methode ist die Anzahl der asynchronen Aufgaben, die das untergeordnete Fragment starten wird;
- Zeile 11: Wir speichern den Parameter der Methode;
- Zeile 13: Der Ladebildschirm wird eingeblendet;
- Zeile 15: Die Liste der Abonnements für asynchrone Operationen wird gelöscht. Diese wurden vom untergeordneten Fragment noch nicht erstellt;
- Zeile 17: Es wird ein boolescher Wert verwaltet, um anzuzeigen, dass die vom untergeordneten Fragment angeforderten asynchronen Aufgaben abgebrochen wurden. Zu Beginn hat der boolesche Wert den Wert „false“;
- Zeilen 20–25: Das untergeordnete Fragment ruft die übergeordnete Methode [cancelWaitingTasks] auf, um anzuzeigen, dass es die von ihm gestarteten Aufgaben abbrechen möchte;
- Zeile 22: Das wartende Bild wird ausgeblendet;
2.7.3.5. Ausnahmebehandlung
// gestion exception -------------------------------------------------------------------
// exception alert display
protected void showAlert(Throwable th) {
// display messages from the Throwable th exception stack
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(th)).setNeutralButton("Fermer", null).show();
}
// message list display
protected void showAlert(List<String> messages) {
// the message list is displayed
new android.app.AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessageForAlert(messages)).setNeutralButton("Fermer", null).show();
}
- Zeilen 4–7: Die Methode [showAlert(Throwable)] ermöglicht es einem untergeordneten Fragment, die Meldungen aus dem Ausnahmestapel des als Parameter übergebenen Throwable in einem Fenster anzuzeigen;
- Zeilen 10–13: Die Methode [showAlert(List<String>)] ermöglicht es einem untergeordneten Fragment, die als Parameter übergebene Liste von Meldungen in einem Fenster anzuzeigen;
- Die in den Zeilen 6 und 12 verwendete Klasse [Utils] wurde in Abschnitt 2.7.2 beschrieben;
2.7.3.6. Behandlung asynchroner Operationen
...
// subscriptions to observables
private List<Subscription> abonnements = new ArrayList<>();
// asynchronous tasks
private boolean runningTasksHaveBeenCanceled;
protected int numberOfRunningTasks;
...
// asynchronous task execution with RxAndroid
protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// process: the observable to be executed/observed
// consumeResult: the method that uses the response obtained
//
// new subscriptions are only created if they have not been cancelled
if (!runningTasksHaveBeenCanceled) {
// execution on I/O thread and observation on Ui thread
process = process.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
// the observable is executed
try {
abonnements.add(process.subscribe(
// consumption result
consumeResult,
// consumption exception
new Action1<Throwable>() {
@Override
public void call(Throwable th) {
consumeThrowable(th);
}
},
// end of task
new Action0() {
@Override
public void call() {
endOfTask();
}
}));
} catch (Throwable th) {
consumeThrowable(th);
}
}
}
private void endOfTask() {
...
}
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
...
}
- Zeilen 9–41: Führen Sie eine asynchrone Aufgabe aus;
- Zeile 9: Die Methode [executeInBackground] erwartet zwei Parameter:
- [Observable<T> process]: der auszuführende asynchrone Prozess;
- [Action1<T> consumeResult]: die Methode des untergeordneten Fragments, die aufgerufen werden soll, um ihr die vom Prozess ausgegebenen Elemente zu übergeben. In unseren vorherigen Beispielen haben die Prozesse immer nur ein Element ausgegeben. Der Typ T von [Action1<T>] ist der Typ T des vom beobachteten Prozess zurückgegebenen Ergebnisses;
- Zeile 14: Die asynchrone Aufgabe wird nur gestartet, wenn sie nicht bereits vom Benutzer oder vom Programm (aufgrund einer Ausnahme) abgebrochen wurde;
- Zeile 16: Der Prozess ist so konfiguriert, dass er in einem E/A-Thread ausgeführt und im UI-Thread beobachtet wird;
- Zeile 16: Die Anweisung [process.subscribe] startet den Prozess im I/O-Thread. Innerhalb dieses Threads werden Operationen synchron ausgeführt, da wir eine synchrone HTTP-Bibliothek verwenden;
- Zeile 19: Die Methode [process.subscribe] hat drei Parameter:
- Zeile 21: [consumeResult]: die Methode des untergeordneten Fragments, die die vom Prozess ausgegebenen Elemente verarbeitet;
- Zeilen 22–28: Die Methode, die ausgeführt wird, wenn während der Verarbeitung der asynchronen Aufgabe eine Ausnahme auftritt. Die Behandlung wird an die Methode [consumeThrowable] in Zeile 49 delegiert;
- Zeilen 29–36: Die Methode, die ausgeführt wird, wenn die Aufgabe die Benachrichtigung über das Ende der Ausgabe sendet. Die Verarbeitung wird an die Methode [endOfTask] in Zeile 43 delegiert;
- Zeile 19: Die soeben gestartete asynchrone Aufgabe wird im Feld [subscriptions] erfasst, das alle gestarteten asynchronen Aufgaben nachverfolgt. Dadurch können diese bei Bedarf abgebrochen werden;
- Zeilen 37–39: Methode, die ausgeführt wird, wenn während der Verarbeitung der asynchronen Aufgabe eine Ausnahme auftritt. Die Behandlung wird an die Methode [consumeThrowable] in Zeile 49 delegiert;
Die Methode [endOfTask] lautet wie folgt:
// asynchronous tasks
protected int numberOfRunningTasks;
...
private void endOfTask() {
// one less job to wait for
numberOfRunningTasks--;
// finished?
if (numberOfRunningTasks == 0) {
// end waiting
cancelWaitingTasks();
// the end of tasks is signalled to the daughter class
notifyEndOfTasks(false);
}
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- Zeile 6: Eine asynchrone Aufgabe wurde gerade abgeschlossen. Der Zähler für aktive Aufgaben wird dekrementiert;
- Zeile 8: Wenn keine aktiven Aufgaben mehr vorhanden sind, hat der untergeordnete Thread alle seine Antworten erhalten;
- Zeile 10: Das Warten wird abgebrochen;
- Zeile 12: Wir benachrichtigen das untergeordnete Fragment darüber, dass alle von ihm gestarteten Aufgaben beendet sind, indem wir seine Methode [notifyEndOfTasks] aufrufen. Der Parameter dieser Methode gibt an, wie die Aufgaben beendet wurden – normal oder aufgrund einer Abbruch durch den Benutzer oder den Code, weil eine Ausnahme aufgetreten ist. In Zeile 12 signalisieren wir ein normales Ende. Beachten Sie, dass das untergeordnete Fragment nicht nachverfolgen muss, welche Aufgaben noch aktiv sind. Das übernimmt seine übergeordnete Klasse für es;
Die Methode [consumeThrowable] sieht wie folgt aus:
// asynchronous tasks
protected int numberOfRunningTasks;
private boolean runningTasksHaveBeenCanceled;
...
// an asynchronous operation has thrown an exception
// or an exception has occurred during the execution of an asynchronous operation
private void consumeThrowable(Throwable th) {
// th: the exception to be dealt with
//
// log
if (isDebugEnabled) {
Log.d(className, "Exception reçue");
}
// cancel tasks already started
cancelRunningTasks();
// error messages are displayed
showAlert(th);
}
// cancel tasks
protected void cancelRunningTasks() {
// log
if (isDebugEnabled) {
Log.d(className, "Annulation des tâches lancées");
}
// cancel all registered asynchronous tasks
for (Subscription abonnement : abonnements) {
abonnement.unsubscribe();
}
// we note the cancellation
runningTasksHaveBeenCanceled = true;
numberOfRunningTasks = 0;
// end of wait
cancelWaitingTasks();
// the cancellation of tasks is reported to the daughter fragment
notifyEndOfTasks(true);
}
...
// classes filles -----------------------------------------------------
...
protected abstract void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled);
- Zeile 3: Die Methode [consumeThrowable] fängt die aufgetretene Ausnahme ab;
- Zeile 15: Alle noch aktiven Aufgaben werden abgebrochen;
- Zeile 17: Der Ausnahmetext wird angezeigt;
- Zeilen 21–37: Alle Aufgaben werden abgebrochen;
- Zeilen 27–29: Alle Abonnements werden abgebrochen;
- Zeile 31: Es wird vermerkt, dass eine Abbruchaktion stattgefunden hat;
- Zeile 32: Der Aufgabenzähler wird auf Null zurückgesetzt;
- Zeile 34: Die Wartezeit wird abgebrochen;
- Zeile 36: Das untergeordnete Fragment wird darüber informiert, dass die Aufgaben nach der Stornierung beendet wurden;
2.7.3.7. Verwaltung des Fragment-Lebenszyklus
// life cycle --------------------------------------------------------
@Override
public void onDestroyView() {
// parent
super.onDestroyView();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroyView");
}
}
@Override
public void onDestroy() {
// parent
super.onDestroy();
// log
if (isDebugEnabled) {
Log.d(className, "onDestroy");
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
...
}
private void saveState() {
...
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
...
}
- Zeilen 2–20: Die Methoden [onDestroyView, onDestroy] dienen ausschließlich zu Protokollierungszwecken. Sie ermöglichen es dem Entwickler, den Lebenszyklus des Fragments besser zu verstehen;
Das Speichern des Fragments bei einer Drehung des Geräts wird durch die folgenden Methoden abgewickelt: [setUserVisibleHint, onSaveInstanceState, saveState]:
// fragment life cycle
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
...
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
// parent
super.setUserVisibleHint(isVisibleToUser);
// backup?
if (this.isVisibleToUser && !isVisibleToUser) {
// the fragment will be hidden - save it
if (!saveFragmentDone) {
saveState();
}
}
// memory
this.isVisibleToUser = isVisibleToUser;
}
private void saveState() {
...
}
@Override
public void onSaveInstanceState(final Bundle outState) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("onSaveInstanceState isVisibleToUser=%s, saveFragmentDone=%s", isVisibleToUser, saveFragmentDone));
}
// parent
super.onSaveInstanceState(outState);
// save fragment only if visible
if (isVisibleToUser) {
// perhaps the backup has already been made
if (!saveFragmentDone) {
saveState();
}
// restoration to be carried out in all cases
session.setAction(ISession.Action.RESTORE);
}
}
- Zeilen 6–19: Das Fragment wird gespeichert, wenn es vom sichtbaren in den ausgeblendeten Zustand wechselt (Zeile 11). Die Methode [setUserVisibleHint] liefert diese Information;
- Zeile 14: Das Speichern erfolgt durch die private Methode in den Zeilen 21–23;
- Zeilen 25–41: Wenn das Gerät gedreht wird, wird die Methode [onSaveInstanceState] aufgerufen. Das Fragment wird unter zwei Bedingungen gespeichert:
- es ist sichtbar (Zeile 34);
- es wurde noch nicht gespeichert (Zeile 36). Es ist möglich, dass die Methoden [setUserVisibleHint] und [onSaveInstanceState] nicht beide ausgeführt werden, wenn das Fragment sichtbar ist, und dass daher die Verwaltung des booleschen Werts [saveFragmentDone] unnötig ist. Im Zweifelsfall habe ich mich dafür entschieden, ihn zu verwenden;
- Zeile 40: Nach dem Speichern folgt das Wiederherstellen. Beachten Sie, dass das Fragment sich beim nächsten Mal, wenn es aktualisiert werden muss, über einen [RESTORE]-Vorgang aktualisiert;
Beachten Sie die beiden Momente, in denen das Speichern eines Fragments angefordert wird:
- wenn es vom sichtbaren in den ausgeblendeten Zustand wechselt;
- wenn das Gerät gedreht wird;
Die private Methode [saveState] lautet wie folgt:
...
private void saveState() {
// tasks to cancel?
if (numberOfRunningTasks != 0) {
// cancel tasks
cancelRunningTasks();
}
// save fragment state
CoreState currentState = saveFragment();
// the fragment has been visited
currentState.setHasBeenVisited(true);
// save menu status
currentState.setMenuOptionsState(getMenuOptionsStates());
// session setting
session.setCoreState(getNumView(), currentState);
// backup done
saveFragmentDone = true;
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(currentState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
...
// classes filles -----------------------------------------------------
public abstract CoreState saveFragment();
protected abstract int getNumView();
- Zeilen 4–7: Während asynchrone Vorgänge laufen, kann es zu einer Drehung des Geräts kommen. Hier wird entschieden, alle Vorgänge abzubrechen. Dies ist keine gute Entscheidung für den Nutzer, der eine neue, möglicherweise zeitaufwändige Anfrage stellen muss, nur weil er sein Smartphone oder Tablet bewegt hat oder einen Anruf erhalten hat. Es ist möglich, Netzwerkverbindungen durch einen Speicher-/Wiederherstellungszyklus aufrechtzuerhalten. Die Lösungen sind jedoch nicht ganz einfach, und ich habe beschlossen, sie in diesem Anfängerkurs nicht zu behandeln. Der Weg nach vorne besteht darin, diese Netzwerkverbindungen über ein Fragment herzustellen, das keine UI hat und während des Speichern/Wiederherstellen-Zyklus nicht zerstört wird. Verwenden Sie dazu einfach die Anweisung [Fragment.setRetainInstance(true)];
- Zeile 9: Wir weisen das untergeordnete Fragment an, seinen Zustand in einem von [CoreState] abgeleiteten Typ zu speichern (Zeile 31);
- Zeile 11: Wir vermerken, dass das Fragment aufgerufen wurde. Diese Information ist nützlich. Wenn ein Fragment zum ersten Mal aufgerufen wird, kann sich seine Aktualisierung von den nachfolgenden unterscheiden, da es keinen vorherigen Zustand in der Sitzung hat;
- Zeile 13: Wir speichern den Zustand des Menüs, wodurch wir es automatisch wiederherstellen können;
- Zeile 15: Dieser aktuelle Zustand wird in der Sitzung gespeichert. In der Sitzung werden Zustände nach Ansicht/Fragment gruppiert, wobei jedes einen Zustand hat. Die Ansichtsnummer wird vom untergeordneten Fragment bereitgestellt (Zeile 33);
- Zeile 17: Wir vermerken, dass das Fragment gespeichert wurde. Dies geschieht, da zwei Methoden die Methode [saveState] aufrufen könnten und es unnötig ist, zwei Speichervorgänge durchzuführen;
Die mit dem Fragment verbundene Ansicht wird durch die folgende Methode neu generiert:
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// parent
super.onActivityCreated(savedInstanceState);
// log
if (isDebugEnabled) {
Log.d(className, "onActivityCreated");
}
// the view must be restored
viewHasToBeInitialized = true;
}
Im Lebenszyklus wird die Methode [onActivityCreated] unmittelbar nach der Methode [onCreateView] ausgeführt. Der Aufruf der letzteren Methode bedeutet, dass die mit dem Fragment verbundene Ansicht neu erstellt werden muss. Wir vermerken dies einfach in Zeile 10.
2.7.3.8. Aktualisieren des Fragments
Das Aktualisieren des Fragments ist der letzte Vorgang, der am Fragment durchgeführt wird, bevor es sichtbar wird und auf Benutzereingaben wartet. Dies wird durch den folgenden Code abgewickelt:
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// update Fragment ----------------------------------------------------------------------------------
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// log
if (isDebugEnabled) {
Log.d(className, "onCreateOptionsMenu");
}
// memory
this.menu = menu;
// retrieve # menu options if not already done
if (fragmentHasToBeInitialized) {
// retrieve the # menu options
getMenuOptionsStates(menu);
// activity
this.activity = getActivity();
this.mainActivity = (IMainActivity) activity;
this.session = (Session) this.mainActivity.getSession();
}
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
...
} else {
// this is not the 1st visit
// step 2: does the fragment need to be initialized?
...
// step 3: should the view be initialized?
...
}
// step 4: a submit, a browse, a restore?
...
// step 5: terminal updates ----------------------
...
}
...
// classes filles -----------------------------------------------------
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
protected abstract void notifyEndOfUpdates();
- Zeile 19: Die Methode [onCreateOptionsMenu] wird verwendet, um das Fragment zu aktualisieren. Aus diesem Grund muss das Fragment über ein Menü verfügen, auch wenn dieses leer ist. Wenn diese Methode ausgeführt wird, ist das Fragment bereits mit seiner Ansicht und seiner Aktivität verknüpft und zudem sichtbar;
- Zeile 25: Das als Parameter (Zeile 22) an die Methode übergebene Menü wird gespeichert;
- Zeilen 27–34: Falls das Fragment initialisiert werden muss:
- Zeile 29: Die Zustände der Menüoptionen werden im Array [menuOptionsStates] aus Zeile 3 gespeichert;
- Zeile 31: Die Aktivität wird als Instanz des Android-Typs [Activity] gespeichert;
- Zeile 32: Die Aktivität wird als Instanz der Schnittstelle [IMainActivity] gespeichert;
- Zeile 33: Die Sitzung wird gespeichert. Der Typumwandlung ist notwendig, da die Methode [mainActivity.getSession()] einen Typ [ISession] zurückgibt;
- Zeile 36: Der vorherige Zustand des Fragments wird aus der Sitzung abgerufen. Wenn dies der erste Besuch des Fragments ist, ist nur der boolesche Wert [previousState.hasBeenVisited] relevant;
- Zeilen 39–44: Code, der ausgeführt wird, wenn dies der erste Besuch des Fragments ist. In diesem Fall ist sein vorheriger Zustand nicht relevant;
- Zeilen 44–50: Code, der ausgeführt wird, wenn dies nicht der erste Besuch des Fragments ist;
- Zeilen 46–47: Code, der ausgeführt wird, wenn der Konstruktor des Fragments aufgerufen wurde (fragmentHasToBeInitialized == true);
- Zeilen 48–49: Code, der ausgeführt wird, wenn die mit dem Fragment verknüpfte Ansicht neu aufgebaut wurde (viewHasToBeInitialized == true);
- Zeilen 51–52: Code, der je nach aktueller Aktion (SUBMIT, NAVIGATION, RESTORE) ausgeführt wird;
- Zeilen 54–55: Code, der immer ausgeführt wird;
Die fünf Schritte der Aktualisierung sind wie folgt:
Schritt 1
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
if (isDebugEnabled) {
Log.d(className, "initFragment initView updateForFirstVisit");
}
// fragment and view initialization
initFragment(null);
initView(null);
// raz previousState for the suite
previousState = null;
} else {
// this is not the 1st visit
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- Zeile 19: Der vorherige Zustand des Fragments wird aus der Sitzung abgerufen;
- Zeilen 22–31: Code, der ausgeführt wird, wenn das Fragment noch nie aufgerufen wurde;
- Zeile 27: Die untergeordnete Klasse wird aufgefordert, das Fragment zu initialisieren. Der Parameter der Methode [initFragment] in Zeile 35 ist der vorherige Zustand des Fragments. Hier wird null übergeben, um dem untergeordneten Fragment mitzuteilen, dass dies der erste Aufruf ist;
- Zeile 28: Die untergeordnete Klasse wird aufgefordert, die mit dem Fragment verbundene Ansicht zu initialisieren. Der Parameter der Methode [initView] in Zeile 37 ist der vorherige Zustand des Fragments. Hier wird null übergeben, um dem untergeordneten Fragment anzuzeigen, dass dies der erste Besuch ist;
- Zeile 30: Wir setzen den vorherigen Zustand für die folgenden Schritte auf null;
Schritte 2 und 3
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// we retrieve the previous fragment state (the very 1st time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
// step 1 - is this your 1st visit?
if (!previousState.getHasBeenVisited()) {
...
} else {
// ce n'is not the 1st visit
// step 2: does the fragment need to be initialized?
if (fragmentHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation fragment");
}
// girl fragment
initFragment(previousState);
}
// step 3: should the view be initialized?
if (viewHasToBeInitialized) {
if (isDebugEnabled) {
Log.d(className, "initialisation vue");
}
// girl fragment
initView(previousState);
}
}
...
protected abstract void initFragment(CoreState previousState);
protected abstract void initView(CoreState previousState);
- Zeilen 24–42: wird ausgeführt, wenn dies nicht der erste Besuch des Fragments ist;
- Zeilen 27–33: Wenn das Fragment gerade neu aufgebaut wurde, wird es durch Aufruf der Methode [initFragment] der untergeordneten Klasse neu initialisiert (Zeilen 32, 46). Der vorherige Zustand des Fragments wird an diese Methode übergeben;
- Zeilen 35–51: Wenn die mit dem Fragment verbundene Ansicht initialisiert oder zurückgesetzt werden muss, wird das untergeordnete Fragment dazu aufgefordert (Zeilen 40, 48). Auch hier wird der letzte bekannte Zustand des Fragments an das Fragment übergeben;
Schritt 4
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// retrieve the fragment's previous state (the very first time, only the boolean hasBeenVisited represents anything)
previousState = session.getCoreState(getNumView());
// multi-stage update of the daughter fragment
...
// step 4: a submit, a browse, a restore?
// log
if (isDebugEnabled) {
try {
Log.d(className, String.format("session=%s", jsonMapper.writeValueAsString(session)));
Log.d(className, String.format("état précédent=%s", jsonMapper.writeValueAsString(previousState)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// action in progress
ISession.Action action = session.getAction();
switch (action) {
case SUBMIT:
if (isDebugEnabled) {
Log.d(className, "updateOnSubmit");
}
// girl fragment
updateOnSubmit(previousState);
break;
case NAVIGATION:
if (isDebugEnabled) {
Log.d(className, "updateForNavigation");
}
if (previousState != null) {
// catering menu
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
} else {
// 1st visit - nothing to do
}
break;
case RESTORE:
// restoration
if (isDebugEnabled) {
Log.d(className, "updateOnRestore");
}
// menu restoration (previousState cannot be null)
setMenuOptionsStates(previousState.getMenuOptionsState());
// girl fragment
updateOnRestore(previousState);
break;
}
....
protected abstract void updateOnSubmit(CoreState previousState);
protected abstract void updateOnRestore(CoreState previousState);
- Zeilen 34–66: Wir verarbeiten die aktuelle Aktion, die eine der folgenden drei sein kann:
- RESTORE: Wir stellen das Fragment nach einer Geräte-Drehung wieder her;
- NAVIGATION: Wir kehren zu dem Fragment zurück, um es in dem Zustand vorzufinden, in dem wir es bei der letzten Verwendung verlassen haben;
- SUBMIT: alle anderen Fälle;
- Zeile 34: Abrufen der aktuellen Aktion;
- Zeilen 36–42: Bei einer Aktion vom Typ SUBMIT rufen wir die Methode [updateOnSubmit] des untergeordneten Fragments auf (Zeilen 41, 68) und übergeben ihr den letzten bekannten Zustand des Fragments;
- Zeilen 43–55: für eine Aktion vom Typ NAVIGATION;
- Zeilen 47–54: Wir möchten das Fragment in seinen letzten bekannten Zustand zurücksetzen. Die NAVIGATION-Operation kann mit einem ersten Besuch zusammenfallen. Dies wäre beispielsweise in einer Anwendung mit Registerkarten der Fall: Wenn ich von Registerkarte 1 zu Registerkarte 4 wechsle:
- muss ich das Fragment für Registerkarte 4 initialisieren, wenn dies der erste Besuch ist;
- das Fragment von Registerkarte 4 in seinen vorherigen Zustand zurücksetzen, wenn es sich nicht um den ersten Besuch handelt;
- Zeilen 52–54: nichts tun, wenn es sich um den ersten Besuch handelt. Die untergeordnete Methode [initView(CoreState previousState)] übernimmt diese Initialisierung. Der erste Besuch wird durch die Bedingung [previousState == null] identifiziert;
- Zeile 49: Wenn dies nicht der erste Besuch des Fragments ist, stelle sein Menü wieder her;
- Zeile 51: Wir fordern die untergeordnete Klasse auf, sich selbst zu aktualisieren, indem wir die Methode in Zeile 70 aufrufen. Wir übergeben ihr den vorherigen Zustand des Fragments, damit sie ihre Aufgabe erfüllen kann;
- Zeilen 56–66: Im Falle einer Fragment-Wiederherstellung gehen wir genauso vor wie bei der Navigation außerhalb des ersten Besuchs;
Schritt 5
// fragment menu
private Menu menu;
private MenuItemState[] menuOptionsStates;
// fragment life cycle
private boolean initDone = false;
private boolean isVisibleToUser = false;
private boolean saveFragmentDone = false;
// fragment states
private CoreState previousState;
// mapper jSON
private ObjectMapper jsonMapper = new ObjectMapper();
// fragment life cycle
private boolean fragmentHasToBeInitialized = false;
private boolean viewHasToBeInitialized = false;
...
// step 5: terminal updates ----------------------
// we've changed our view
session.setPreviousView(getNumView());
// more action in progress
session.setAction(ISession.Action.NONE);
// when leaving this fragment, it must be saved
saveFragmentDone = false;
// as long as the fragment has not been rebuilt, it does not need to be initialized
fragmentHasToBeInitialized = false;
// as long as the view has not been rebuilt, it does not need to be initialized
viewHasToBeInitialized = false;
// returns to normal tab selection operation
session.setNavigationOnTabSelectionNeeded(true);
// the fragment is notified that the view is ready
if (isDebugEnabled) {
Log.d(className, "notifyEndOfUpdates");
}
notifyEndOfUpdates();
...
protected abstract void notifyEndOfUpdates();
- Zeilen 18–30: Wenn wir diesen Punkt erreichen, ist das Fragment initialisiert und bereit zur Anzeige. Anschließend setzen wir alle Indikatoren, die im Lebenszyklusmanagement des Fragments verwendet werden, auf ihren Ausgangszustand zurück;
- Zeile 20: Die Ansicht hat sich geändert; dies wird in der Sitzung vermerkt;
- Zeile 22: Es sind keine Aktionen mehr im Gange;
- Zeile 24: Wenn wir das aktuell angezeigte Fragment verlassen, müssen wir es beim Beenden speichern;
- Zeile 26: Das Fragment muss nicht mehr neu aufgebaut werden. Dieses Flag wird auf „true“ gesetzt, wenn der Konstruktor des Fragments erneut ausgeführt wird;
- Zeile 28: Die mit dem Fragment verbundene Ansicht muss nicht mehr initialisiert werden. Dieses Flag wird wieder auf „true“ gesetzt, wenn die Methode [onActivityCreated] erneut ausgeführt wird;
- Zeile 30: Das Fragment kann in einer Anwendung mit Registerkarten angezeigt werden. In diesem Fall muss ein Fragmentwechsel erfolgen, wenn der Benutzer auf eine der Registerkarten klickt;
- Zeile 36: Die untergeordnete Klasse wird darüber informiert, dass das Fragment bereit ist. Sie kann die Methode [notifyEndOfUpdates] verwenden, um Aktualisierungen durchzuführen, die ohnehin erforderlich wären, eine asynchrone Operation zum Abrufen neuer Daten zu starten usw.
2.7.4. Ein Beispiel für ein Fragment
![]() |
Wir haben im Projekt [client-android-skel] ein Beispiel für ein Fragment eingefügt, um dem Leser die typische Struktur eines Fragments in einer auf diesem Projekt basierenden Anwendung zu veranschaulichen.
Die Klasse [DummyFragment] sieht wie folgt aus:
package client.android.fragments.behavior;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
public class DummyFragment extends AbstractFragment {
// fields inherited from parent class -------------------------------------------------------
// debug mode
//-- final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
// class name
//-- protected String className;
// asynchronous tasks
//-- protected int numberOfRunningTasks;
// activity
//-- protected IMainActivity mainActivity;
//-- protected Activity activity;
// session
//-- protected Session session;
// methods inherited from the parent class -------------------------------------------------------
// display menu options
//-- protected void setAllMenuOptionsStates(boolean isVisible) {
//-- protected void setMenuOptionsStates(MenuItemState[] menuItemStates) {
// management of waiting for the end of a series of asynchronous tasks
//-- protected void beginRunningTasks(int numberOfRunningTasks) {
//-- protected void cancelWaitingTasks() {
// asynchronous task execution with RxAndroid
//-- protected <T> void executeInBackground(Observable<T> process, Action1<T> consumeResult) {
// cancel tasks
//-- protected void cancelRunningTasks() {
// exception alert display
//-- protected void showAlert(Throwable th) {
// message list display
//-- protected void showAlert(List<String> messages) {
// methods imposed by the parent class -------------------------------------------------------
@Override
public CoreState saveFragment() {
// save the fragment
DummyFragmentState state=new DummyFragmentState();
// ...
return state;
// if there's nothing to save, do [return new CoreState();] and delete class [DummyFragmentState]
}
@Override
protected int getNumView() {
// return the fragment number in the table of fragments managed by the activity (cf MainActivity)
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// the fragment becomes visible and has undergone construction in this or a previous stage
// this happens on application startup and every time the Android device is rotated
// is necessarily followed by the execution of [initView]
// the fields of the fragment that has been rebuilt must be initialized
// previousState is the fragment's last save - is null if this is the fragment's 1st visit
}
@Override
protected void initView(CoreState previousState) {
// the fragment becomes visible and the associated view has been reconstructed in this or a previous step
// this happens every time [initFragment] is executed and every time the fragment leaves the adjacency of the displayed fragment
// initialize the components of the view that has been rebuilt
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type SUBMIT
// the fragment and the associated view usually have to be initialized from the session
// previousState is the fragment's last save - is null if it's the fragment's 1st visit
// there's nothing to be done if the fragment can't be reached by a SUBMIT operation
// if the fragment can be reached by SUBMIT operations from different fragments, the previous view can be known by [session.getPreviousView]
// if the fragment can be reached by several SUBMIT operations from the same fragment, then a flag must be set to differentiate between the different types of SUBMIT from this fragment
}
@Override
protected void updateOnRestore(CoreState previousState) {
// is executed after [initFragment, initView] if these methods are executed
// the view will be displayed after an operation of type RESTORE or NAVIGATION
// previousState is the fragment's last backup - never null
// restore the view to its previous state
}
@Override
protected void notifyEndOfUpdates() {
// comes after methods [updateOnSubmit, updateOnRestore]
// when we're there, the view has been built and initialized
// there's often nothing to do here, but you can also factor in actions that need to be done no matter how you arrive at this view
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// called when asynchronous tasks launched by the fragment are either completed or cancelled
// these two cases can be differentiated using parameter runningTasksHaveBeenCanceled
// the view generally needs to be reset to a different state from the one it had while waiting for responses from asynchronous tasks
}
}
Die Klasse [DummyFragment] darf keinen Status haben. Hier haben wir einen hinzugefügt, um uns daran zu erinnern, was darin erwartet wird:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class DummyFragmentState extends CoreState {
// fragment status [DummyFragment]
// set only serializable fields to jSON
// put the annotation @JsonIgnore on the others, but it's hard to see what use they could be
// don't forget the getters/setters - they are used for serialization/deserialization
}
Um die Verwendung des Projekts [client-android-skel] zu veranschaulichen, werden wir zunächst einfache Beispiele verwenden, bevor wir zu einer umfassenderen Fallstudie übergehen.
2.8. Illustrative Übungen
Wir beginnen mit der Umgestaltung bereits geschriebener Beispiele.
2.8.1. Beispiel-17B
Wir greifen Beispiel 17 aus Abschnitt 1.18 wieder auf. Dies ist eine App mit einem einzigen Fragment, ohne asynchrone Aufgaben und ohne Registerkarten. Wir werden untersuchen, wie sie sich verhält, wenn das Gerät gedreht wird. Wir geben Folgendes ein:

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

Wenn wir die Ansichten vergleichen, ist alles erhalten geblieben, mit Ausnahme der Liste [2], die nun leer ist.
Wenn Sie außerdem auf die Schaltfläche [Absenden] klicken, erscheint ein Dialogfeld, das die im Formular vorgenommenen Eingaben anzeigt. Wenn Sie das Gerät in diesem Moment drehen, verschwindet das Dialogfeld.
Daher müssen wir während einer Drehung Folgendes neu generieren:
- die Dropdown-Liste und den darin ausgewählten Eintrag;
- das Dialogfeld, falls es während der Drehung angezeigt wurde;
2.8.1.1. Das Projekt [Beispiel-17B]
Wir duplizieren das Projekt [client-android-skel] in examples/Example-17B. Anschließend laden wir das neue Projekt [1]:
![]() | ![]() | ![]() |
- In [2-3], im Ordner [behavior], fügen wir das Fragment [Vue1Fragment] aus dem Projekt [Example-17] ein;
![]() | ![]() | ![]() |
- in [4-5] fügen wir im Ordner [layout] von [Example-17B] die Ansicht [vue1.xml] aus [Example-17] ein. Dies ist die Ansicht, die mit dem Fragment verknüpft ist;
- in [6] wird der Ordner [values] aus [Beispiel-17B] durch den Ordner [values] aus [Beispiel-17] ersetzt;
Wir ändern den oberen Rand der Ansicht [vue1.xml] auf 80 dp:
<TextView
android:id="@+id/textViewFormulaireTitre"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginLeft="10dp"
android:layout_marginTop="80dp"
android:text="@string/titre_vue1"
android:textSize="30sp"/>
An dieser Stelle können wir eine erste Kompilierung durchführen, um nach Fehlern zu suchen. Die ersten gemeldeten Fehler stammen von Paketimporten, die verschoben wurden. Wir beheben sie (Strg-Umschalt-O). Andere Fehler, wie z. B. , treten auf, weil die Ansicht [Vue1Fragment] nicht alle von ihrer übergeordneten Klasse [AbstractParent] geforderten Methoden implementiert:

Generieren Sie die fehlenden Methoden (Alt-Enter).
Ein weiterer gemeldeter Kompilierungsfehler lautet wie folgt:

Wir beheben dies in der Datei [build.gradle] des Moduls (Zeile 20 unten):
![]() |
An dieser Stelle können wir neu kompilieren, um die verbleibenden Fehler anzuzeigen. Der einzige gemeldete Fehler betrifft die Methode [Vue1Fragment.updateFragment]:
![]() |
Sie müssen die Annotation [@Override] aus Zeile 135 entfernen. Es gibt nun keine Fehler mehr. Wir werden dies als Ausgangspunkt für die Änderung des Projekts verwenden.
2.8.1.2. Der Zustand des [Vue1Fragment]-Fragments
Das [Vue1Fragment]-Fragment muss Informationen speichern, wenn das Gerät gedreht wird, damit es vollständig wiederhergestellt werden kann. Dazu erstellen wir eine [Vue1FragmentState]-Klasse:
![]() |
Derzeit ist diese Klasse leer:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.1.3. Projektanpassung
![]() |
Der Ordner [custom] enthält Architekturelemente, die vom Entwickler angepasst werden können.
Die Konstanten für die Schnittstelle [IMainActivity] lauten wie folgt:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Zeilen 24–31: Die Anwendung nutzt hier ihre [DAO]-Schicht nicht. Diese Konstanten werden nicht verwendet;
- Zeile 34: eine Fragment-Nachbarschaft von 1, was dem Standardwert entspricht. Da die Anwendung nur ein Fragment hat (Zeile 43), ist dieser Wert irrelevant;
- Zeilen 39–40: Da keine Operationen mit der [DAO]-Ebene stattfinden, ist kein Platzhalterbild erforderlich;
- Zeile 37: Dies ist keine Anwendung mit Registerkarten;
- Zeile 43: Es gibt nur ein Fragment;
Die Klasse [Session] sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Es ist leer. Da es nur ein Fragment gibt, ist es tatsächlich nicht erforderlich, die Kommunikation zwischen Fragmenten über eine Sitzung zu gewährleisten.
Schließlich sieht die Klasse [CoreState] wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeilen 11–13: Wir müssen alle von [CoreState] abgeleiteten Klassen auflisten, die den Status der verschiedenen Fragmente speichern. Hier gibt es nur eine (Zeile 12);
2.8.1.4. Die [MainActivity]
Die [MainActivity] sieht derzeit wie folgt aus:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
// todo: we continue the initializations started by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// todo: define fragments here
return new AbstractFragment[0];
}
@Override
protected CharSequence getFragmentTitle(int position) {
// todo: define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// todo: tabbed navigation - define the view to be displayed when tab no. [position] is selected
}
@Override
protected int getFirstView() {
// todo: define the number of the first view (fragment) to be displayed
return 0;
}
}
Die Kommentare [//todo] geben an, was der Entwickler tun muss. Die Klasse [MainActivity] entwickelt sich wie folgt:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// session
this.session = (Session) super.session;
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
}
Nur die Methode in den Zeilen 41–44 muss geändert werden. Sie muss das Array der Fragmente der App zurückgeben. Vergessen Sie in Zeile 43 nicht, den Unterstrich nach dem Fragmentnamen hinzuzufügen.
2.8.1.5. Der Fragmentstatus [FragmentState]
Nach den im Projekt [Beispiel-17] durchgeführten Rotationstests haben wir uns entschieden, die folgenden Elemente des Fragments zu speichern:
- die Liste der Werte in der Dropdown-Liste;
- die Position des ausgewählten Elements in dieser Liste;
- die vom Dialogfeld angezeigte Meldung, falls diese zum Zeitpunkt der Drehung vorhanden ist;
Die Klasse [Vue1FragmentState] sieht wie folgt aus:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// drop-down list values
private List<String> list;
// the item selected from the drop-down list
private int listSelectedPosition;
// the message displayed in the
private String message;
// getters and setters
...
}
2.8.1.6. Das [AbstractFragment]-Fragment
Derzeit wird der Lebenszyklus des Fragments durch zwei Methoden verwaltet (Zeilen 6 und 32):
// drop-down list
private List<String> list;
private ArrayAdapter<String> dataAdapter;
@AfterViews
void afterViews() {
// check the first button
radioButton1.setChecked(true);
// the calendar
datePicker1.setCalendarViewShown(false);
// on seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// the drop-down list
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
}
...
protected void updateFragment() {
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
dropDownList.setAdapter(dataAdapter);
}
Der Code für diese beiden Methoden wird wie folgt in die von der Klasse [AbstractFragment] definierten Methoden verschoben:
// fragment lifecycle management ---------------------------------------------------------------------
@Override
public CoreState saveFragment() {
Vue1FragmentState state = new Vue1FragmentState();
state.setList(list);
state.setListSelectedPosition(dropDownList.getSelectedItemPosition());
state.setMessage(message);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// 1st visit?
if (previousState == null) {
// create drop-down list values
list = new ArrayList<>();
list.add("list 1");
list.add("list 2");
list.add("list 3");
} else {
// returns values from the drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
list = state.getList();
// and the
message = state.getMessage();
}
// initialize drop-down list adapter
dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
protected void initView(CoreState previousState) {
// the calendar
datePicker1.setCalendarViewShown(false);
// on seekBar
seekBar.setMax(100);
seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
public void onStopTrackingTouch(SeekBar seekBar) {
}
public void onStartTrackingTouch(SeekBar seekBar) {
}
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
seekBarValue.setText(String.valueOf(progress));
}
});
// initialize drop-down list adapter
dropDownList.setAdapter(dataAdapter);
// 1st visit?
if (previousState == null) {
// check the first button
radioButton1.setChecked(true);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// seekbar value
seekBarValue.setText(String.valueOf(seekBar.getProgress()));
// item selected from drop-down list
Vue1FragmentState state = (Vue1FragmentState) previousState;
dropDownList.setSelection(state.getListSelectedPosition());
// visible dialogue?
if (message != null) {
// we display it
showMessage();
}
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
- Zeilen 2–9: Die Methode [saveFragment] muss die zu speichernden Elemente des Fragments in eine von [CoreState] abgeleitete Klasse einfügen und eine Instanz dieser Klasse zurückgeben;
- Zeilen 11–14: Die Methode [getNumView] muss die Fragmentnummer zurückgeben. Hier gibt es nur ein Fragment, dessen Nummer 0 ist;
- Zeilen 16–34: Die Methode [initFragment] muss die Felder des Fragments initialisieren. Sie erhält den vorherigen Zustand des Fragments. Ist [previousState] null, handelt es sich um den ersten Aufruf;
- Zeilen 19–25: Beim ersten Besuch werden die Werte für die Dropdown-Liste erstellt;
- Zeilen 26–30: Wenn dies nicht der erste Besuch ist, werden die Felder [list, message] des Fragments aus dem vorherigen Zustand wiederhergestellt;
- Zeilen 33–34: Initialisierung des Feldes [dataAdapter] des Fragments. Dies ist die Datenquelle für die Dropdown-Liste;
- Zeilen 37–62: Die Methode [initView] dient zur Initialisierung der Komponenten der Benutzeroberfläche. Sie erhält den vorherigen Zustand [previousState] als Parameter. Ist [previousState == null], handelt es sich um den ersten Aufruf;
- Hier sehen wir, was zuvor in der Methode [@AfterViews] stand;
- Zeilen 57–61: Beim ersten Besuch stellen wir sicher, dass das erste Optionsfeld ausgewählt ist;
- Zeilen 64–67: Die Methode [updateOnSubmit] wird ausgeführt, wenn die aktuelle Aktion [SUBMIT] ist. Hier findet keine fragmentübergreifende Navigation statt und es gibt daher keine aktuelle Aktion;
- Zeilen 69–81: Die Methode [updateOnRestore] wird ausgeführt, wenn die aktuelle Aktion [NAVIGATION] oder [RESTORE] ist. Hier findet keine Navigation zwischen Fragmenten statt und daher ist keine [NAVIGATION]-Aktion möglich;
- Zeile 72: Wir berechnen den Wert von TextView seekBarValue neu (und stellen ihn nicht wieder her). Der Grund dafür ist, dass sein Wert während der Drehungen manchmal verloren ging;
- Zeilen 74–75: Die Liste wird auf das Element positioniert, das vor der Drehung ausgewählt war. Ohne diese Maßnahme würde die Liste standardmäßig zum ersten Element springen;
- Zeilen 76–80: Das Dialogfeld wird erneut angezeigt, wenn die Meldung aus dem vorherigen Zustand nicht null ist. Wir kehren zur Methode [showMessage] (Zeile 79) zurück;
- Zeilen 83–86: Die Methode [notifyEndOfUpdates] ist die letzte Methode, die von der übergeordneten Klasse aufgerufen wird, bevor das untergeordnete Fragment allein gelassen wird. Hier gibt es nichts zu tun;
- Zeilen 88–91: Die Methode [notifyEndOfTasks] signalisiert das Ende der vom Fragment gestarteten asynchronen Aufgaben. Hier gibt es keine;
Das Dialogfeld wird wie folgt wiederhergestellt:
// dialog box message
private String message;
...
@Click(R.id.formulaireButtonValider)
protected void doValider() {
// list of messages to display
List<String> messages = new ArrayList<>();
...
// display
doAfficher(messages);
}
private void doAfficher(final List<String> messages) {
// poster text is created
StringBuilder texte = new StringBuilder();
for (String message : messages) {
texte.append(String.format("%s\n", message));
}
// the message is memorized
message = texte.toString();
// we display it
showMessage();
}
private void showMessage() {
// we display it
new AlertDialog.Builder(activity).setTitle("Valeurs saisies").setMessage(message).setNeutralButton("Fermer", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// message reset
message = null;
}
}).show();
}
Wenn der Benutzer das Formular absendet, erstellt die Methode [doValider] (Zeile 5) eine Liste mit Meldungen, die sie anschließend (Zeile 10) im Dialogfeld anzeigt.
- Zeilen 14–20: Die Liste der Meldungen wird zu einer einzigen Meldung verkettet, die in Zeile 2 gespeichert wird;
- Zeilen 25–33: Dies ist die vom Dialogfeld angezeigte Meldung, und es handelt sich um dieselbe Meldung, die die Methode [updateOnRestore] anzeigt;
- Zeile 27: Der zweite Parameter der Methode [setNeutralButton] ist die Methode, die ausgeführt wird, wenn der Benutzer im Dialogfeld auf die Schaltfläche [Schließen] klickt;
- Zeile 31: Wenn das Dialogfeld geschlossen wird, wird die Meldung auf null gesetzt, um anzuzeigen, dass das Dialogfeld nicht mehr vorhanden ist;
2.8.1.7. Tests
Leser sind eingeladen, dieses Projekt zu testen und zu überprüfen, ob das Fragment nach einer oder mehreren aufeinanderfolgenden Drehungen erhalten bleibt.
2.8.2. Beispiel 23: Wetter-Client
Einige Websites stellen Wetterinformationen in Form von JSON-Strings bereit. Hier ist ein Beispiel:

Die URL hat folgende Form: http://api.openweathermap.org/data/2.5/weather?q={city},{country}&APPID={APPID}, wobei:
- city: die Stadt, für die Sie das Wetter wünschen, hier Angers;
- country: das Land der Stadt, in diesem Fall Frankreich (fr);
- APPID: ein Schlüssel, den Sie durch Registrierung auf der Website [https://home.openweathermap.org/users/sign_up] erhalten;
2.8.2.1. Das Projekt
![]() |
Das Projekt wurde auf der Grundlage des [client-android-skel]-Projekts erstellt. Es weist folgende Merkmale auf:
- Es verfügt über nur ein Fragment, dessen Zustand nicht verwaltet werden muss;
- es führt asynchrone Anfragen durch;
2.8.2.2. Projektanpassung
![]() |
Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Zeilen 25, 28, 31, 40: Eigenschaften der [DAO]-Schicht. Zeile 31: Eine Basisauthentifizierung ist nicht erforderlich;
- Zeile 34: Fragment-Adjazenz. Hier ist diese Konstante irrelevant, da es nur ein Fragment gibt;
- Zeile 37: Dies ist keine Anwendung mit Registerkarten;
- Zeile 43: Es gibt nur ein Fragment;
Die [CoreState]-Klasse, die den Status der Fragmente speichert, sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
// todo: add subclasses of [CoreState] here
/*@JsonSubTypes({
@JsonSubTypes.Type(value = Class1.class),
@JsonSubTypes.Type(value = Class2.class)}
)*/
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeilen 10–13: Es gibt nichts zu deklarieren, da diese Anwendung nur ein Fragment hat, dessen Zustand nicht gespeichert wird;
Die Klasse [Session] sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Es ist leer, da in dieser Anwendung keine Kommunikation zwischen Fragmenten stattfindet.
2.8.2.3. Die [DAO]-Schicht
![]() |
In der [DAO]-Schicht müssen drei Klassen angepasst werden:
- die IDao-Schnittstelle;
- die Dao-Implementierung;
- die WebClient-Schnittstelle für die Kommunikation mit dem Webserver / JSON;
Die [WebClient]-Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// weather service
@Get("/data/2.5/weather?q={city},{country}&APPID={APPID}")
String getWeatherForecast(@Path String city, @Path String country, @Path String APPID);
}
- Zeilen 18–19: Die URL des Wetterdienstes. Beachten Sie, dass diese relativ zur Stamm-URL des Clients (RestClientRootUrl, Zeile 12) ist. Hier lautet diese Stamm-URL [http://api.openweathermap.org/];
Die [IDao]-Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// weather service
Observable<String> getWeatherForecast(String city, String country, String APPID);
}
- Beachten Sie, dass die Methoden in den Zeilen 6–22 standardmäßig in der IDao-Schnittstelle des Projekts [client-android-skel] enthalten sind;
- Zeile 25: Die Methode [getWeatherForecast] ruft die JSON-Zeichenkette für das Wetter in der Stadt [city] des Landes [country] ab. Der dritte Parameter ist der Schlüssel, der von der Website [https://home.openweathermap.org/users/sign_up] bezogen wird;
Die [IDao]-Schnittstelle wird durch die folgende [Dao]-Klasse implementiert:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
// timeout
private int timeout;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// memory
this.timeout = timeout;
// factory configuration
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// service météo ---------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(final String city, final String country, final String APPID) {
// log
if (isDebugEnabled) {
Log.d(className, String.format("getWeatherForecast city=%s, country=%s, APIID=%s, thread=%s, timeout=%s", city, country, APPID, Thread.currentThread().getName(), timeout));
}
// result
return getResponse(new IRequest<String>() {
@Override
public String getResponse() {
return webClient.getWeatherForecast(city, country, APPID);
}
});
}
}
- Beachten Sie, dass die Zeilen 17–90 standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] enthalten sind. Sie müssen lediglich die für die Anwendung spezifischen Implementierungsmethoden für die Schnittstelle [IDao] hinzufügen (Zeile 92);
- Zeilen 93–105: Implementierung der Methode [getWeatherForecast]. Diese ist sehr einfach und nimmt 6 Zeilen ein, Zeilen 100–105;
- Zeile 100: Die Methode [getResponse] ist eine Methode der übergeordneten Klasse [AbstractDao]. Sie erwartet einen Parameter vom Typ [IRequest<T>], wobei T der Typ der erwarteten Antwort vom Server ist; hier ist es ein String, da wir einen JSON-String erwarten. Der Typ T von [IRequest<T>] muss der Typ T der Methode [Observable<T> getWeatherForecast] sein;
- die Schnittstelle [IRequest<T>] hat nur eine Methode: getResponse. Ihre Aufgabe ist es, die Antwort vom Typ T bereitzustellen, die die Methode [Observable<T> getWeatherForecast] zurückgeben muss;
- Zeile 103: Es ist die Schnittstelle [WebClient], die diese Antwort bereitstellt. Wir übergeben ihr die drei in Zeile 94 empfangenen Parameter. Aus diesem Grund müssen diese das Attribut final aufweisen;
2.8.2.4. Die [MainActivity]
![]() |
Die [MainActivity]-Aktivität sieht wie folgt aus:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.MeteoFragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
return new AbstractFragment[]{new MeteoFragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ---------------------------------------------------------------------
@Override
public Observable<String> getWeatherForecast(String city, String country, String APPID) {
return dao.getWeatherForecast(city, country, APPID);
}
}
- Beachte, dass die Zeilen 15–55 standardmäßig im Projekt [client-android-skel] enthalten sind. Du musst sie lediglich anpassen;
- Zeilen 37–40: das Fragment-Array. Hier gibt es nur eines;
- Zeilen 43–46: Es sind keine Fragmenttitel erforderlich;
- Zeilen 48–50: Hier gibt es keine Registerkarten;
- Zeilen 52–55: Die erste anzuzeigende Ansicht ist Ansicht Nr. 0, die von [MeteoFragment];
- Zeilen 58–61: Implementierung der [IDao]-Schnittstelle. Hier gibt es nichts weiter zu tun, als die Arbeit in Zeile 21 an die [DAO]-Schicht zu delegieren;
2.8.2.5. Das [MeteoFragment]-Fragment
![]() |
Das [MeteoFragment] fragt den Wetter-Webdienst / JSON ab. Sein Grundgerüst sieht wie folgt aus:
package client.android.fragments;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.AbstractFragment;
import client.android.architecture.MenuItemState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action0;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class FirstFragment extends AbstractFragment {
...
}
- Zeile 14: Die Ansicht [res/layout/meteo_fragment.xml] sieht wie folgt aus:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Construisez votre interface visuelle"
android:id="@+id/textView" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_marginLeft="64dp" android:layout_marginStart="64dp"
android:layout_marginTop="120dp"/>
</RelativeLayout>
Die Ansicht zeigt nur den Text ab Zeile 10 an;
- Zeile 15: Das Menü [res / menu / menu_meteo.xml] sieht wie folgt aus:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".activity.MainActivity">
<item
android:id="@+id/menuActions"
app:showAsAction="ifRoom"
android:title="@string/menuActions">
<menu>
<item
android:id="@+id/actionMeteo"
android:title="@string/actionMeteo"/>
<item
android:id="@+id/actionAnnuler"
android:title="@string/actionAnnuler"/>
<item
android:id="@+id/actionTerminer"
android:title="@string/actionTerminer"/>
</menu>
</item>
</menu>
- Zeilen 10–12: Diese Menüoption dient dazu, die Wettervorhersage für eine Stadt abzufragen;
- Zeilen 14–15: Diese Menüoption dient dazu, die Anfrage abzubrechen, falls sie gerade läuft;
- Zeilen 16–18: Diese Menüoption schließt die Anwendung;
Der vollständige Code für das Fragment lautet wie folgt:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.MenuItemState;
import client.android.architecture.custom.CoreState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import rx.functions.Action1;
@EFragment(R.layout.meteo_fragment)
@OptionsMenu(R.menu.menu_meteo)
public class MeteoFragment extends AbstractFragment {
// local data
private int nbReponsesRecues;
// gestion des événements ---------------------------------------------------------------------------------------
// cities whose weather we want
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
@OptionsItem(R.id.actionMeteo)
protected void doMeteo() {
// his country
String country = "fr";
// get a API login by creating an account [https://home.openweathermap.org/users/sign_up]
String APPID = "xyz";
// URL web service / jSON
mainActivity.setUrlServiceWebJson("http://api.openweathermap.org");
// start waiting for [paysDeLoire.length] asynchronous tasks
beginWaiting(paysDeLoire.length);
// number of responses received
nbReponsesRecues = 0;
// asynchronous calls are made in parallel
for (String city : paysDeLoire) {
// weather
executeInBackground(mainActivity.getWeatherForecast(city, country, APPID), new Action1<String>() {
@Override
public void call(String response) {
// exploiting the answer
consumeResponse(response);
// a + response
nbReponsesRecues++;
}
});
}
}
// exploitation server response
private void consumeResponse(String response) {
// log
Log.d(className, String.format("thread=%s, response=%s", Thread.currentThread().getName(), response));
}
// start of waiting
protected void beginWaiting(int numberOfRunningTasks) {
// log
if (isDebugEnabled) {
Log.d(className, "beginWaiting");
}
// parent
beginRunningTasks(numberOfRunningTasks);
// the [Cancel] option is displayed
setAllMenuOptionsStates(false);
setMenuOptionsStates(new MenuItemState[]{
new MenuItemState(R.id.menuActions, true),
new MenuItemState(R.id.actionAnnuler, true)});
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// menu
initMenu();
// displaying results
String message;
switch (nbReponsesRecues) {
case 0:
message = "Aucune réponse n'a été reçue";
break;
case 1:
message = "Une réponse a été reçue. Consultez vos logs...";
break;
default:
message = String.format("%s réponses ont été reçues. Consultez vos logs...", nbReponsesRecues);
break;
}
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
}
// private methods -----------------------------------
private void initMenu() {
if (isDebugEnabled) {
Log.d(className, "initMenu");
}
// menu
setAllMenuOptionsStates(true);
setMenuOptionsStates(new MenuItemState[]{new MenuItemState(R.id.actionAnnuler, false)});
}
// life cycle management ---------------------------------------------------------------------------------------
...
}
- Zeilen 25–50: Verarbeitung des Klicks auf die Menüoption [Wetter];
- Zeile 32: Erstellung der Webservice-URL / JSON für den Wetterdienst. Diese wird dann über die Aktivität an die [DAO]-Schicht übergeben;
- Zeile 34: Wir starten die Wartezeit. Wir übergeben die Anzahl der zu startenden Aufgaben, damit die übergeordnete Klasse uns benachrichtigen kann, wenn diese abgeschlossen sind. Hier gibt es fünf Aufgaben, da wir das Wetter für die fünf in Zeile 23 aufgeführten Städte abfragen;
- Zeile 16: Wir zählen die Anzahl der empfangenen Antworten, damit wir sie anzeigen können;
- Zeilen 38–50: Wir durchlaufen die Städte, für die wir das Wetter abfragen möchten;
- Zeile 40: Wir stellen 5 HTTP-Anfragen parallel;
- Zeile 40: Wir bitten die übergeordnete Klasse [AbstractParent], den Webservice / JSON abzufragen;
- Zeilen 40–48: Die Methode [executeInBackground] erwartet zwei Parameter:
- Zeile 40: Der zu beobachtende und auszuführende Prozess wird von der Methode [mainActivity.getWeatherForecast] bereitgestellt;
- Zeilen 40–48: die Instanz [Action1], die ausgeführt werden soll, wenn die Antwort vom asynchronen Dienst empfangen wird. Der Typ T von [Action1<T>] muss der Typ T des Ergebnisses der Methode [getWeatherForecast] sein;
- Zeile 44: Eine Antwort wurde empfangen. Sie wird an die Methode [consumeResponse] in Zeile 53 übergeben;
- Zeile 46: Der Zähler für empfangene Antworten wird erhöht;
- Zeilen 53–56: Verarbeiten einer JSON-Antwort vom Wetterdienst;
- Zeile 55: Wir protokollieren einfach die JSON-Zeichenkette;
- Zeilen 59–72: Code, der vor dem Start der asynchronen Aufgaben ausgeführt wird;
- Zeile 65: Wir übergeben die Anzahl der auszuführenden Aufgaben an die übergeordnete Klasse [AbstractParent]. Dadurch kann diese uns benachrichtigen, wenn alle Aufgaben abgeschlossen sind;
- Zeilen 67–70: Vorbereitung des Menüs für eine Wartephase. Wir behalten nur die Option [Actions/Cancel] bei, mit der der Benutzer die gestarteten Aufgaben abbrechen kann;
- Zeilen 74–92: Code, der ausgeführt wird, wenn die übergeordnete Klasse uns mitteilt, dass alle gestarteten Aufgaben abgeschlossen sind;
- Zeile 77: Wir setzen das Menü auf seinen Ausgangszustand zurück. Die Methode [initMenu] (Zeilen 95–102) zeigt das Menü mit allen Optionen an, mit Ausnahme der Option [Actions/Cancel], die ausgeblendet ist;
- Zeilen 80–91: Die Anzahl der empfangenen Antworten wird angezeigt;
Das Klicken auf die Menüoption [Abbrechen] wird durch den folgenden Code verarbeitet:
@OptionsItem(R.id.actionAnnuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
- Zeile 7: Wir bitten die übergeordnete Klasse, die noch aktiven Aufgaben abzubrechen;
Das Klicken auf die Menüoption [Fertigstellen] wird durch den folgenden Code verarbeitet:
@OptionsItem(R.id.actionTerminer)
protected void doTerminer() {
// we stop everything
System.exit(0);
}
Der Lebenszyklus des Fragments wird durch die folgenden Methoden verwaltet:
// life cycle management ---------------------------------------------------------------------------------------
@Override
public CoreState saveFragment() {
return new CoreState();
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
}
@Override
protected void initView(CoreState previousState) {
// 1st visit?
if (previousState == null) {
initMenu();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
- Zeilen 3–6: Dienen dazu, den Status des Fragments in einer von [CoreState] abgeleiteten Klasse zu speichern. Wenn das Fragment keinen Status zu speichern hat, wie in diesem Fall, geben wir einfach eine Instanz von [CoreState] zurück. Geben Sie nicht null zurück, da dies letztendlich zu einem Absturz führen würde;
- Zeilen 8–11: Hier muss die View-ID zurückgegeben werden. In diesem Fall hat das [MeteoFragment] die ID 0;
- Zeilen 13–16: dienen dazu, das Fragment zu initialisieren, sobald es erstellt (previousState == null) oder neu erstellt (previousState != null) wurde. Hier gibt es nichts zu tun. Das einzige Feld, das initialisiert werden kann, ist das folgende:
// villes dont on veut la météo
final String[] paysDeLoire = new String[]{"angers", "le mans", "nantes", "laval", "la roche sur yon"};
aber es initialisiert sich selbst;
- Zeilen 18–24: dienen dazu, die mit dem Fragment verbundene Ansicht zu initialisieren, sobald es erstellt (previousState == null) oder neu erstellt (previousState != null) wurde;
- Zeilen 21–23: Wenn dies der erste Besuch des Fragments ist, wird dessen Menü so initialisiert, dass die Option [Abbrechen] ausgeblendet wird;
- Zeilen 27–30: werden aufgerufen, wenn die Navigation zum Fragment eine [SUBMIT]-Aktion beinhaltete. Hier findet keine Navigation zwischen Fragmenten statt, da es nur ein Fragment gibt;
- Zeilen 32–35: Wird während eines Speicher-/Wiederherstellungszyklus aufgrund einer Geräte-Drehung oder aus einem anderen Grund aufgerufen. Da hier kein Status gespeichert wurde, gibt es nichts zu tun;
- Zeilen 37–40: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts zu tun;
2.8.2.6. Tests
Wir führen nun das Beispiel aus:


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

Die Protokolle sehen dann wie folgt aus:
07-23 13:34:43.853 11240-11240/client.android D/MeteoFragment_: beginWaiting
...
07-23 13:34:49.121 11240-11464/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.121 11240-11466/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11468/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.162 11240-11467/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Exception reçue
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: Annulation des tâches lancées
07-23 13:34:49.163 11240-11240/client.android D/MeteoFragment_: initMenu
07-23 13:34:49.167 11240-11465/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [org.springframework.web.client.HttpClientErrorException,["401 Unauthorized"]]
- Zeilen 3–6, 10: Die 5 HTTP-Aufrufe haben 5 Ausnahmen generiert;
- Zeile 7: Das Fragment [MeteoFragment] empfängt die erste Ausnahme. Es bricht daraufhin alle Aufgaben ab;
Legen wir nun ein 5-Sekunden-Timeout [IMainActivity.DELAY] fest und brechen den Vorgang ab. Die Protokolle sehen dann wie folgt aus:
07-21 13:16:20.329 20390-20390/client.android D/MeteoFragment_: beginWaiting
...
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation demandée
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: Annulation des tâches lancées
07-21 13:16:23.635 20390-20390/client.android D/MeteoFragment_: initMenu
07-21 13:25:02.948 29965-30197/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-6], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30195/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-4], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.948 29965-30194/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-3], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30193/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-2], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
07-21 13:25:02.951 29965-30196/client.android D/client.android.dao.service.Dao_: Thread [RxIoScheduler-5], Exception communication avec serveur : [java.lang.InterruptedException,[null]]
- Zeile 3: Abbruchanforderung;
- Zeile 4: Das Warten wird abgebrochen, da eine Stornierung stattgefunden hat;
- Zeilen 6–10: Das Abbrechen der Aufgaben löst in jedem der fünf Aufgabenthreads eine Ausnahme aus. Der Ausnahmetyp hängt von den Anwendungen ab. Die Ausnahme hier ist [java.lang.InterruptedException], da die Aufgaben während der Ausführung der Anweisung [Thread.sleep(delay)] unterbrochen wurden, wodurch sie künstlich für [delay] Millisekunden warten;
2.8.3. Beispiel 16B
Hier refaktorisieren wir Beispiel 16 aus Abschnitt 1.17. Es zeigt einen Ausschnitt, der asynchrone Aufrufe an einen Zufallszahlenserver durchführt. Schauen wir uns an, wie er sich bei einer Geräte-Drehung verhält:

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

Wir sehen, dass alle Fehlermeldungen verloren gegangen sind. Wir werden versuchen, dies zu verbessern.
2.8.3.1. Das Projekt „Example-16B“
Wir kopieren das Projekt [client-android-skel] in das Projekt [examples/Example-16B] und laden dann das neue Projekt:
![]() |
Aus dem ursprünglichen Projekt [Beispiel-16] kopieren wir die folgenden Elemente in [Beispiel-16B]:
- die Datei [res/layout/vue1.xml], den Ordner [res/values]:
![]() |
Wir ändern den oberen Rand der Ansicht [vue1.xml] auf 80 dp:
<TextView
android:id="@+id/txt_Titre2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="@string/aleas"
android:textAppearance="?android:attr/textAppearanceLarge" />
- das Fragment [View1Fragment]:
![]() |
- die Klasse [DAO / Service / Response]:
![]() |
An dieser Stelle können wir einen ersten Kompilierungsversuch unternehmen:
- Die erste Art von Fehlern betrifft Importe. Einige Klassen wurden während der Migration zu [Beispiel-16B] in andere Pakete verschoben. Wir beginnen damit, diese Fehler zu beheben;
- Eine zweite Art von Fehler wird für die Klasse [Vue1Fragment] gemeldet, da sie die von der übergeordneten Klasse [AbstractParent] geforderten Methoden nicht implementiert. Wir generieren diese Methoden automatisch;
Wir versuchen eine zweite Kompilierung:
- Alle verbleibenden Fehler konzentrieren sich nun auf die Klasse [Vue1Fragment], die die meisten Änderungen erfahren wird;
2.8.3.2. Erstellen eines Zustands für das Fragment [Vue1Fragment]
Wir haben gesehen, dass bestimmte Informationen aus dem Fragment während einer Drehung gespeichert werden müssen, um das Fragment in seinen Zustand vor der Drehung zurückzusetzen. Wir erstellen daher einen [Vue1FragmentState]-Zustand, der vorerst leer ist:
![]() |
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class Vue1FragmentState extends CoreState {
}
2.8.3.3. Projektanpassung
![]() |
Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// constant application -------------------------------------
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 5000;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = false;
// waiting image
boolean IS_WAITING_ICON_NEEDED = true;
// number of application fragments
int FRAGMENTS_COUNT = 1;
}
- Zeilen 25, 28, 31, 40: Eigenschaften der [DAO]-Schicht. Eine Basisauthentifizierung ist nicht erforderlich;
- Zeile 34: Fragment-Nachbarschaft. Hier ist diese Konstante irrelevant, da es nur ein Fragment gibt;
- Zeile 37: Dies ist keine Anwendung mit Registerkarten;
- Zeile 43: Es gibt nur ein Fragment;
Die [CoreState]-Klasse, die den Status der Fragmente speichert, sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = Vue1FragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeile 12: Wir deklarieren die Fragment-State-Klasse [Vue1Fragment];
Die Klasse [Session] sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
}
Sie ist leer, da es in dieser Anwendung keine Kommunikation zwischen Fragmenten gibt.
2.8.3.4. Die [DAO]-Schicht
![]() |
In der [DAO]-Schicht müssen drei Klassen angepasst werden:
- die IDao-Schnittstelle;
- die Dao-Implementierung;
- die WebClient-Schnittstelle für die Kommunikation mit dem Webserver / JSON;
Die Klasse [Response] stammt aus dem Projekt [Example-16], das sie verwendet:
package client.android.dao.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
Die [WebClient]-Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
// RestTemplate
void setRestTemplate(RestTemplate restTemplate);
// 1 random number in the range [a,b]
@Get("/{a}/{b}")
Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
- Zeilen 18–19: Die URL für den Zufallszahlendienst. Beachten Sie, dass diese URL relativ zur Stamm-URL des Clients (RestClientRootUrl, Zeile 12) ist. Hier lautet die Stamm-URL [http://localhost:8080];
Die [IDao]-Schnittstelle sieht wie folgt aus:
package client.android.dao.service;
import rx.Observable;
public interface IDao {
// Web service url
void setUrlServiceWebJson(String url);
// user
void setUser(String user, String mdp);
// customer timeout
void setTimeout(int timeout);
// basic authentication
void setBasicAuthentification(boolean isBasicAuthentificationNeeded);
// debug mode
void setDebugMode(boolean isDebugEnabled);
// client wait time in milliseconds before request
void setDelay(int delay);
// random number service
Observable<Response<Integer>> getAlea(int a, int b);
}
- Beachten Sie, dass die Methoden in den Zeilen 6–22 standardmäßig in der IDao-Schnittstelle des Projekts [client-android-skel] vorhanden sind;
- Zeile 25: Die Methode [getAlea] gibt eine Zufallszahl im Bereich [a,b] zurück. Diese Zahl wird in einer [Response<Integer>]-Antwort zurückgegeben, wobei die Zufallszahl im Feld [body] dieses Typs enthalten ist;
Die [IDao]-Schnittstelle wird durch die folgende [Dao]-Klasse implementiert:
package client.android.dao.service;
import android.util.Log;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {
// web service customer
@RestService
protected WebClient webClient;
// safety
@Bean
protected MyAuthInterceptor authInterceptor;
// on RestTemplate
private RestTemplate restTemplate;
// factory du RestTemplate
private SimpleClientHttpRequestFactory factory;
@AfterInject
public void afterInject() {
// log
Log.d(className, "afterInject");
// we build the restTemplate
factory = new SimpleClientHttpRequestFactory();
restTemplate = new RestTemplate(factory);
// set the jSON converter
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
// set the restTemplate of the web client
webClient.setRestTemplate(restTemplate);
}
@Override
public void setUrlServiceWebJson(String url) {
// set the URL of the web service
webClient.setRootUrl(url);
}
@Override
public void setUser(String user, String mdp) {
// the user is registered in the interceptor
authInterceptor.setUser(user, mdp);
}
@Override
public void setTimeout(int timeout) {
if (isDebugEnabled) {
Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
}
// factory configuration
factory.setReadTimeout(timeout);
factory.setConnectTimeout(timeout);
}
@Override
public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
if (isDebugEnabled) {
Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
}
// authentication interceptor?
if (isBasicAuthentificationNeeded) {
// add the authentication interceptor
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(authInterceptor);
restTemplate.setInterceptors(interceptors);
}
}
// méthodes privées -------------------------------------------------
private void log(String message) {
if (isDebugEnabled) {
Log.d(className, message);
}
}
// random number service
@Override
public Observable<Response<Integer>> getAlea(final int a, final int b) {
// web client execution
return getResponse(new IRequest<Response<Integer>>() {
@Override
public Response<Integer> getResponse() {
return webClient.getAlea(a, b);
}
});
}
}
- Beachten Sie, dass die Zeilen 17–85 standardmäßig in der Klasse [Dao] des Projekts [client-android-skel] enthalten sind. Sie müssen lediglich die Methoden hinzufügen, um die Schnittstelle [IDao] zu implementieren;
- Zeilen 88–97: Implementierung der Methode [getAlea]. Dies ist sehr einfach und nimmt 6 Zeilen ein, Zeilen 91–96;
- Zeile 91: Die Methode [getResponse] ist eine Methode der übergeordneten Klasse [AbstractDao]. Sie erwartet einen Parameter vom Typ [IRequest<T>], wobei T der Typ der erwarteten Antwort ist, in diesem Fall ein Response<Integer>-Typ. Der Typ T von [IRequest<T>] (Zeile 91) muss der Typ T der Methode [Observable<T> getAlea] (Zeile 89) sein;
- Die Schnittstelle [IRequest<T>] hat nur eine Methode: getResponse. Ihre Aufgabe ist es, die Antwort vom Typ T bereitzustellen, die die Methode [Observable<T> getAlea] zurückgeben muss;
- Zeile 94: Es ist die Schnittstelle [WebClient], die diese Antwort bereitstellt. Ihr werden die beiden in Zeile 89 empfangenen Parameter übergeben. Aus diesem Grund müssen diese das Attribut final aufweisen;
2.8.3.5. Die [MainActivity]
![]() |
Die [MainActivity]-Aktivität sieht wie folgt aus:
package client.android.activity;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// define fragments here
return new AbstractFragment[]{new Vue1Fragment_()};
}
@Override
protected CharSequence getFragmentTitle(int position) {
// define fragment titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
// tabbed browsing - define the view to be displayed
}
@Override
protected int getFirstView() {
return 0;
}
// interface IDao ------------------------------------------
@Override
public Observable<Response<Integer>> getAlea(int a, int b) {
return dao.getAlea(a, b);
}
}
- Beachte, dass die Zeilen 15–61 standardmäßig im Projekt [client-android-skel] enthalten sind. Du musst sie lediglich anpassen;
- Zeilen 40–44: das Fragment-Array. Hier gibt es nur eines;
- Zeilen 47–51: Es sind keine Fragmenttitel erforderlich;
- Zeilen 53–56: Hier gibt es keine Registerkarten;
- Zeilen 58–61: Die erste anzuzeigende Ansicht ist Ansicht Nr. 0, die von [Vue1Fragment];
- Zeilen 64–67: Implementierung der [IDao]-Schnittstelle. Hier gibt es nichts weiter zu tun, als die Arbeit in Zeile 23 an die [DAO]-Schicht zu delegieren;
2.8.3.6. Der Zustand des Fragments [Vue1Fragment]
![]() |
Die Klasse [Vue1FragmentState] sieht wie folgt aus:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
import java.util.ArrayList;
import java.util.List;
public class Vue1FragmentState extends CoreState {
// fragment status ------------------------
// list of answers
private List<String> reponses = new ArrayList<>();
// condition view ------------------------
// error msg on the number of random numbers requested
private boolean txtErrorAleasVisible = false;
// error msg on generation interval [a,b]
private boolean txtErrorIntervalleVisible = false;
// error msg on the URL of the web service
private boolean txtMsgErreurUrlServiceWebVisible = false;
// waiting time error msg
private boolean textViewErreurDelayVisible = false;
// whether or not the Execute button is visible
private boolean btnExecuterVisible = true;
// getters and setters
...
}
Um festzustellen, was im Fragment gespeichert werden musste, drehten wir das Gerät in verschiedenen Situationen und beobachteten, was bei der Wiederherstellung verloren ging. Wir kamen zu dem Schluss, dass die Informationen in den Zeilen 10–23 gespeichert werden mussten.
2.8.3.7. Das Fragment [View1Fragment]
![]() |
Derzeit enthält die Ansicht [Vue1Fragment] verschiedene Fehler, da sich die übergeordnete Klasse [AbstractFragment], von der sie abgeleitet ist, geändert hat. Anstatt die vorzunehmenden Änderungen einzeln zu beschreiben, werden wir direkt auf die endgültige Version eingehen.
Das Grundgerüst des Fragments sieht wie folgt aus:
package client.android.fragments.behavior;
import android.util.Log;
import android.view.View;
import android.widget.*;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.dao.service.Response;
import client.android.fragments.state.Vue1FragmentState;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
import rx.Observable;
import rx.functions.Action1;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vide)
public class Vue1Fragment extends AbstractFragment {
...
}
- Zeile 26: Beachten Sie, dass jedes Fragment ein Menü haben muss, auch wenn es leer ist. Dies ist hier der Fall.
2.8.3.7.1. Behandlung des Klicks auf die Schaltfläche [Execute]
@Click(R.id.btn_Executer)
protected void doExecuter() {
// check the data entered
if (!isPageValid()) {
return;
}
// delete previous answers
reponses.clear();
dataAdapterReponses.notifyDataSetChanged();
// reset the response counter to 0
nbReponses = 0;
infoReponses.setText("Liste des réponses (0)");
// activity initialization
mainActivity.setUrlServiceWebJson(urlServiceWebJson);
mainActivity.setDelay(delay);
// prepare the random task
beginWaiting(1);
// we ask for the random numbers
getAleasInBackground(nbAleas, a, b);
}
void getAleasInBackground(int nbAleas, int a, int b) {
// create the process to be observed
Observable<Response<Integer>> process = Observable.empty();
for (int i = 0; i < nbAleas; i++) {
process = process.mergeWith(mainActivity.getAlea(a, b));
}
// we ask for the random numbers
executeInBackground(process, new Action1<Response<Integer>>() {
@Override
public void call(Response<Integer> response) {
// we consume the answer
consumeAleaResponse(response);
}
});
}
protected void consumeAleaResponse(Response<Integer> response) {
// log
if (isDebugEnabled) {
try {
Log.d(String.format("%s", className), String.format("consumeAleaResponse(%s)", jsonMapper.writeValueAsString(response)));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
// a + response
nbReponses++;
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
// we analyze the response
// mistake?
if (response.getStatus() != 0) {
// display
showAlert(response.getMessages());
// cancellation
doAnnuler();
// back to Ui
return;
}
// we add the information to the list of answers
reponses.add(0, String.valueOf(response.getBody()));
// refreshing the answers
dataAdapterReponses.notifyDataSetChanged();
}
// cancellation ----------
@Click(R.id.btn_Annuler)
protected void doAnnuler() {
if (isDebugEnabled) {
Log.d(className, "Annulation demandée");
}
// asynchronous tasks are cancelled
cancelRunningTasks();
}
private void beginWaiting(int nbRunningTasks) {
// we set the hourglass
beginRunningTasks(nbRunningTasks);
// the [Cancel] button replaces the [Execute] button
btnExecuter.setVisibility(View.INVISIBLE);
btnAnnuler.setVisibility(View.VISIBLE);
}
- Zeilen 4–6: Zunächst prüfen wir, ob die Einträge gültig sind. Es können dann Fehlermeldungen erscheinen;
- Zeilen 8–9: Die Liste der Antworten wird gelöscht. Diese Änderung wird in der ListView übernommen, die sie anzeigt;
- Zeilen 11–12: Die Anzahl der empfangenen Antworten wird auf Null zurückgesetzt;
- Zeile 14: Wir legen die URL für den Zufallszahlendienst fest. Diese Information wird an die [DAO]-Schicht weitergeleitet;
- Zeile 15: Die Zeitüberschreitung vor dem Senden der Anfrage an den Zufallszahlendienst wird festgelegt. Diese Information wird an die [DAO]-Schicht weitergeleitet;
- Zeile 17: Wir bereiten den Start einer asynchronen Aufgabe vor (nicht N; wir werden sehen, warum);
- Zeilen 24–27: Wir fassen die N asynchronen Aufgaben zu einer einzigen Abfolge von Operationen zusammen [merge];
- Zeilen 29–36: Wir weisen die übergeordnete Klasse [AbstractParent] an, den Zufallszahlen-Webdienst / JSON abzufragen;
- Zeilen 29–36: Die Methode [executeInBackground] erwartet zwei Parameter:
- Zeile 29: Der zu beobachtende und auszuführende Prozess ist der in den vorangegangenen Zeilen berechnete;
- Zeilen 29–36: die Instanz [Action1], die ausgeführt werden soll, wenn die Antwort vom asynchronen Dienst empfangen wird. Der Typ T von [Action1<T>] muss der Typ T des Ergebnisses der Methode [getAlea] sein, d. h. ein Typ [Response<Integer>];
- Zeile 34: Wenn eine Antwort eintrifft (eine Zufallszahl), wird sie in der Methode in Zeile 39 verarbeitet;
- Zeilen 49–50: Wir protokollieren und melden, dass eine neue Antwort empfangen wurde;
- Zeilen 53–60: Der Typ [Response<T>] verfügt über ein Feld [status], das einen Fehlercode enthält. Ist dieser Code ungleich Null, ist beim Server ein Problem aufgetreten;
- Zeile 55: Es wird eine Fehlermeldung angezeigt. Die Methode [showAlert] gehört zur übergeordneten Klasse;
- Zeile 57: Die Methode in den Zeilen 68–75 wird aufgerufen. Sie bricht alle noch aktiven Aufgaben ab (Zeile 74);
- Zeile 62: Die Antwort wird der Liste der Antworten hinzugefügt, die die Datenquelle für die ListView darstellt;
- Zeile 64: Die ListView wird aktualisiert;
- Zeilen 77–83: Die Methode [beginWaiting(int nbRunningTasks)] bereitet die Ansicht auf das Warten vor (Zeilen 81–82) und benachrichtigt die übergeordnete Klasse, dass [nbRunningTasks] Aufgaben bald ausgeführt werden (Zeile 79);
2.8.3.7.2. Der Lebenszyklus des Fragments
Der Lebenszyklus des Fragments wird durch die folgenden Methoden verwaltet:
// local data
private List<String> reponses;
private ArrayAdapter<String> dataAdapterReponses;
private int nbReponses = 0;
...
// life cycle management ---------------------------------------------------------
@Override
public CoreState saveFragment() {
// current view status
Vue1FragmentState state = new Vue1FragmentState();
state.setTextViewErreurDelayVisible(textViewErreurDelay.getVisibility() == View.VISIBLE);
state.setTxtErrorAleasVisible(txtErrorAleas.getVisibility() == View.VISIBLE);
state.setTxtMsgErreurUrlServiceWebVisible(txtMsgErreurUrlServiceWeb.getVisibility() == View.VISIBLE);
state.setTxtErrorIntervalleVisible(txtErrorIntervalle.getVisibility() == View.VISIBLE);
state.setBtnExecuterVisible(btnExecuter.getVisibility() == View.VISIBLE);
state.setReponses(reponses);
return state;
}
@Override
protected int getNumView() {
return 0;
}
@Override
protected void initFragment(CoreState previousState) {
// 1st visit?
if (previousState != null) {
Vue1FragmentState state = (Vue1FragmentState) previousState;
reponses = state.getReponses();
} else {
reponses = new ArrayList<>();
}
// listView data source
dataAdapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
// nre of responses
nbReponses = reponses.size();
}
@Override
protected void initView(CoreState previousState) {
// listview / adapter link
listReponses.setAdapter(dataAdapterReponses);
// 1st visit?
if (previousState == null) {
// hide error messages
txtErrorAleas.setVisibility(View.INVISIBLE);
txtErrorIntervalle.setVisibility(View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
textViewErreurDelay.setVisibility(View.INVISIBLE);
// buttons
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
}
@Override
protected void updateOnRestore(CoreState previousState) {
// previous view status
Vue1FragmentState state = (Vue1FragmentState) previousState;
// show / hide error msg
txtErrorAleas.setVisibility(state.isTxtErrorAleasVisible() ? View.VISIBLE : View.INVISIBLE);
txtErrorIntervalle.setVisibility(state.isTxtErrorIntervalleVisible() ? View.VISIBLE : View.INVISIBLE);
txtMsgErreurUrlServiceWeb.setVisibility(state.isTxtMsgErreurUrlServiceWebVisible() ? View.VISIBLE : View.INVISIBLE);
textViewErreurDelay.setVisibility(state.isTextViewErreurDelayVisible() ? View.VISIBLE : View.INVISIBLE);
// buttons
btnAnnuler.setVisibility(state.isBtnExecuterVisible() ? View.INVISIBLE : View.VISIBLE);
btnExecuter.setVisibility(state.isBtnExecuterVisible() ? View.VISIBLE : View.INVISIBLE);
// no. of responses
infoReponses.setText(String.format("Liste des réponses (%s)", nbReponses));
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
// the [Execute] button replaces the [Cancel] button
btnAnnuler.setVisibility(View.INVISIBLE);
btnExecuter.setVisibility(View.VISIBLE);
}
- Zeilen 7–18: Stellen sicher, dass das Fragment gespeichert wird, wenn die übergeordnete Klasse dies anfordert;
- Zeile 11: Zeigt die Fehlermeldung bezüglich des Timeouts an;
- Zeile 12: Anzeige der Fehlermeldung bezüglich der Anzahl der angeforderten Zufallszahlen;
- Zeile 13: Anzeige der Fehlermeldung bezüglich der Webservice-URL / JSON;
- Zeile 14: Zeigt die Fehlermeldung bezüglich des Bereichs [a,b] für die Zufallszahlengenerierung an;
- Zeile 15: Sichtbarkeit der Schaltfläche [Ausführen];
- Zeile 16: die Liste der empfangenen Antworten;
- Zeilen 20–23: muss die View-ID zurückgeben. Die Fragment-ID ist hier 0, da es nur eine gibt;
- Zeilen 25–38: Initialisierung der Felder des Fragments, entweder beim ersten Besuch (previousState == null) oder bei einem späteren Besuch;
- Zeilen 29–30: Wenn dies nicht der erste Besuch ist, wird das Feld [reponses] aus dem vorherigen Zustand des Fragments wiederhergestellt;
- Zeilen 31–33: Wenn dies der erste Besuch ist, wird das Feld [reponses] mit einer leeren Liste initialisiert;
- Zeilen 34–37: Mithilfe des Felds [reponses] können wir die Datenquelle für die ListView des Fragments (Zeile 35) sowie die Anzahl der Antworten (Zeile 37) erstellen;
- Zeilen 40–55: werden ausgeführt, um die mit dem Fragment verbundene Ansicht zu initialisieren, entweder beim ersten Besuch (previousState == null) oder bei einem späteren Besuch;
- Zeile 43: Die ListView des Fragments wird an die Datenquelle gebunden, die gerade in der Methode [initFragment] erstellt wurde;
- Zeilen 45–54: Wenn dies der erste Besuch ist, wird die Ansicht für ihre erste Anzeige vorbereitet;
- Zeilen 57–60: werden während der fragmentübergreifenden Navigation im Zusammenhang mit einer [SUBMIT]-Aktion ausgeführt. Hier gibt es nur ein Fragment und daher keine fragmentübergreifende Navigation;
- Zeilen 63–76: Wird während der Navigation zwischen Fragmenten im Zusammenhang mit einer [NAVIGATION]-Aktion oder während eines Speicher-/Wiederherstellungszyklus aufgrund einer Geräte-Drehung oder aus einem anderen Grund ausgeführt. Hier kann nur der letztere Fall eintreten. Beachten Sie, dass hier in allen Fällen [previousState] immer nicht null ist;
- Zeile 65: Der vorherige Zustand wird in den Fragment-Zustandstyp umgewandelt;
- Zeilen 66–75: Der Inhalt des vorherigen Zustands wird zum Wiederherstellen der Ansicht verwendet;
- Zeilen 78–81: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts zu tun;
- Zeilen 83–89: Wird ausgeführt, wenn alle asynchronen Aufgaben abgeschlossen sind. Hier wird die Schaltfläche [Abbrechen] ausgeblendet und durch die Schaltfläche [Ausführen] ersetzt;
2.8.3.8. Tests
Der Leser ist eingeladen, die folgenden Tests durchzuführen:
- Erzeugen Sie Fehler und führen Sie das Gerät aus: Die Fehlermeldungen müssen weiterhin angezeigt werden;
- Zufallszahlen generieren und das Gerät ausführen: Die generierten Zufallszahlen müssen weiterhin angezeigt werden;
- Stellen Sie eine Wartezeit von mehreren Sekunden ein und lassen Sie das Gerät während der Wartezeit laufen: Die Aufgaben müssen abgebrochen worden sein (dies ist in den Protokollen ersichtlich);
2.8.4. Beispiel-22B
Hier greifen wir Beispiel 22 wieder auf, um es gemäß dem [client-android-skel]-Projektmodell umzugestalten. Erinnern Sie sich daran, dass das [Beispiel-22]-Projekt den Fragment-Speicher-/Wiederherstellungszyklus während der Drehung korrekt handhabt und dass es als Grundlage für das [client-android-skel]-Projekt diente.
Wir duplizieren das Projekt [client-android-skel] in [examples/Example-22B] und laden das letztere Projekt:
![]() |
Anschließend kopieren wir verschiedene Elemente aus dem Projekt [Beispiel-22] in das Projekt [Beispiel-22B].
Zunächst kopieren wir Elemente aus dem Ordner [res]:
- [layout/fragment_main.xml, layout/view1.xml, menu/menu_fragment.xml, menu/menu_main.xml, den Ordner [values];
![]() |
Wir ändern den oberen Rand beider Ansichten auf 120 dp:
[view1.xml]:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="@string/titre_vue1"
android:id="@+id/textViewTitreVue1"
android:layout_marginTop="120dp"
android:textSize="50sp"
android:layout_gravity="center|left"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"/>
[fragment_main]:
<TextView
android:id="@+id/section_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="120dp"/>
Als Nächstes kopieren wir die Elemente [View1Fragment, PlaceHolderFragment, PlaceHolderFragmentState]:
![]() |
An dieser Stelle können wir einen ersten Kompilierungsversuch unternehmen. Es tritt eine erste Fehlerart auf: falsche Importe, da sich die Pakete der Klassen geändert haben. Wir korrigieren diese Importe. Eine zweite Fehlerart ist darauf zurückzuführen, dass die Fragmente nicht alle Methoden ihrer übergeordneten Klasse [AbstractFragment] implementieren. Wir beheben dies durch Drücken von (Alt+Enter).
Die verbleibenden Fehler resultieren aus Unterschieden zwischen der alten und der neuen Klasse [AbstractFragment]. Diese ignorieren wir vorerst.
2.8.4.1. Projektanpassung
![]() |
Der Ordner [custom] enthält Architekturelemente, die vom Entwickler angepasst werden können.
Über die Schnittstelle [IMainActivity] können Sie bestimmte Projekteigenschaften festlegen:
package client.android.architecture.custom;
import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;
public interface IMainActivity extends IDao {
// session access
ISession getSession();
// change of view
void navigateToView(int position, ISession.Action action);
// wait management
void beginWaiting();
void cancelWaiting();
// debug mode
boolean IS_DEBUG_ENABLED = true;
// maximum time to wait for server response
int TIMEOUT = 1000;
// waiting time before executing customer request
int DELAY = 0;
// basic authentication
boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;
// fragment adjacency
int OFF_SCREEN_PAGE_LIMIT = 1;
// tab bar
boolean ARE_TABS_NEEDED = true;
// waiting image
boolean IS_WAITING_ICON_NEEDED = false;
// number of fragments
int FRAGMENTS_COUNT = 5;
}
- Zeilen 23, 26, 29, 38: Merkmale der [DAO]-Schicht. Hier gibt es keine;
- Zeile 41: Hier gibt es fünf Fragmente;
- Zeile 32: Fragment-Nachbarschaft. Diese Konstante kann hier einen Wert zwischen [1,4] annehmen. Der Leser wird dazu ermutigt, diesen Wert zu variieren, um zu sehen, ob die Anwendung weiterhin funktioniert;
- Zeile 35: Dies ist eine Anwendung mit Registerkarten;
Die Klasse [CoreState], die den Zustand der Fragmente speichert, sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.PlaceHolderFragmentState;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
@JsonSubTypes.Type(value = PlaceHolderFragmentState.class)}
)
public class CoreState {
// fragment visited or not
protected boolean hasBeenVisited = false;
// status of any fragment menu
protected MenuItemState[] menuOptionsState;
// getters and setters
...
}
- Zeile 12: Wir deklarieren die Fragment-State-Klasse [PlaceHolderFragment]. Das Fragment [Vue1Fragment] selbst hat keinen State;
Die Klasse [Session] sieht wie folgt aus:
package client.android.architecture.custom;
import client.android.architecture.core.AbstractSession;
public class Session extends AbstractSession {
// data to be shared between fragments themselves and between fragments and activities
// elements that cannot be serialized as jSON must be annotated with @JsonIgnore
// don't forget the getters and setters required for serialization / deserialization jSON
// number of fragments visited
private int numVisit;
// n° fragment type [PlaceholderFragment] displayed in second tab
private int numFragment = -1;
// getters and setters
...
}
Dies ist die Sitzung für das Projekt [Beispiel-22].
2.8.4.2. Die [MainActivity]
![]() |
Die [MainActivity]-Aktivität sieht wie folgt aus:
package client.android.activity;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.util.Log;
import android.view.MenuItem;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.fragments.behavior.PlaceholderFragment_;
import client.android.fragments.behavior.Vue1Fragment_;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {
// layer [DAO]
@Bean(Dao.class)
protected IDao dao;
// session
private Session session;
// menu management-----------------------
@Override
public boolean onOptionsItemSelected(MenuItem item) {
...
}
private void showFragment(int i) {
...
}
// implementation of parent class methods ---------------------------------------------------
...
}
Hier ist die Klasse [MainActivity] aus zwei Gründen umfangreicher als in den vorherigen Beispielen:
- Es müssen Registerkarten verwaltet werden;
- es gibt ein Menü zu verwalten;
2.8.4.2.1. Implementierung der Methoden der übergeordneten Klasse
// methods parent class -----------------------
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
...
}
@Override
protected IDao getDao() {
return dao;
}
@Override
protected AbstractFragment[] getFragments() {
// fragment no
final String ARG_SECTION_NUMBER = "section_number";
// initialization of fragment table
AbstractFragment[] fragments = new AbstractFragment[FRAGMENTS_COUNT];
int i;
for (i = 0; i < fragments.length - 1; i++) {
// create a fragment
fragments[i] = new PlaceholderFragment_();
// you can pass arguments to the
Bundle args = new Bundle();
args.putInt(ARG_SECTION_NUMBER, i + 1);
fragments[i].setArguments(args);
}
// a fragment of +
fragments[i] = new Vue1Fragment_();
// result
return fragments;
}
@Override
protected CharSequence getFragmentTitle(int position) {
// no titles here
return null;
}
@Override
protected void navigateOnTabSelected(int position) {
...
}
@Override
protected int getFirstView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
- Zeilen 2–12: Die Methode [onCreateActivity] wird von der übergeordneten Klasse [AbstractActivity] aufgerufen, wenn die Aktivität zum ersten Mal erstellt oder während eines Speicher-/Wiederherstellungszyklus neu erstellt wird. Wenn diese Methode aufgerufen wird, hat die übergeordnete Klasse die Sitzung bereits wiederhergestellt;
- Zeile 10: Es wird eine lokale Referenz auf die Sitzung abgerufen. Der Typumwandlung ist notwendig, da die Sitzung der übergeordneten Klasse vom Typ [AbstractSession] ist;
- Zeilen 19–38: Die Methode [getFragments] muss der übergeordneten Klasse das Array der von der Anwendung verwalteten Fragmente zurückgeben. Hier gibt es [FRAGMENTS_COUNT] Fragmente, eine in [IMainActivity] definierte Zahl. Die ersten [FRAGMENTS_COUNT-1] Fragmente sind vom Typ [PlaceHolderFragment] und das letzte ist vom Typ [Vue1Fragment];
- Zeilen 41–45: Die Methode [getFragmentTitle] muss die Fragmenttitel zurückgeben, wenn diese Informationen nützlich sind. Dies ist hier nicht der Fall;
- Zeilen 47–50: Diese Methode wird von der übergeordneten Klasse aufgerufen, wenn der Benutzer auf eine Registerkarte klickt. Wir werden im nächsten Abschnitt darauf zurückkommen;
- Zeilen 52–55: Gibt die Nummer der ersten Ansicht zurück, die beim Start der Anwendung angezeigt werden soll. Hier muss das Fragment [Vue1Fragment] zuerst angezeigt werden. Die Methode [getFirstView] könnte vorteilhaft durch eine Konstante in [IMainActivity] ersetzt werden;
2.8.4.2.2. Tab-Verwaltung
Registerkarten werden mit den folgenden Methoden verwaltet:
@Override
protected void onCreateActivity() {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onCreateActivity");
}
// continue the initializations begun by the parent class
// session
this.session = (Session) super.session;
// 1st tab
TabLayout.Tab tab = tabLayout.newTab();
tab.setText("Vue 1");
tabLayout.addTab(tab);
// 2nd tab ?
int numFragment = session.getNumFragment();
if (numFragment != -1) {
TabLayout.Tab tab2 = tabLayout.newTab();
tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
tabLayout.addTab(tab2);
}
}
@Override
protected void navigateOnTabSelected(int position) {
// fragment number to display
int numFragment;
switch (position) {
case 0:
// fragment no. [Vue1Fragment]
numFragment = getFirstView();
break;
default:
// fragment no. [PlaceholderFragment]
numFragment = session.getNumFragment();
}
// fragment display
if (numFragment != mViewPager.getCurrentItem()) {
navigateToView(numFragment, ISession.Action.SUBMIT);
}
}
}
- Zeilen 1–20: Die Methode [onCreateActivity] wird von der übergeordneten Klasse [AbstractActivity] aufgerufen, wenn die Aktivität zum ersten Mal erstellt oder während eines Speicher-/Wiederherstellungszyklus neu erstellt wird. Wenn diese Methode aufgerufen wird, hat die übergeordnete Klasse die Sitzung bereits wiederhergestellt;
- Zeile 9: Es wird eine lokale Referenz auf die Sitzung abgerufen. Der Typumwandlung ist notwendig, da die Sitzung der übergeordneten Klasse vom Typ [AbstractSession] ist;
- Zeilen 11–13: Die erste Registerkarte wird erstellt;
- Zeilen 15–20: Die zweite Registerkarte wird erstellt, wenn eine Fragment-ID in der Sitzung gespeichert ist (Zeile 15). Diese ID wird beim ersten Aufbau der Aktivität zunächst auf -1 gesetzt;
- Zeilen 23–39: Diese Methode wird von der übergeordneten Klasse aufgerufen, wenn der Benutzer auf eine Registerkarte klickt;
- Zeilen 28–31: Wenn auf Registerkarte 0 geklickt wird, muss [Vue1Fragment] angezeigt werden. Wir wissen, dass dies die erste Ansicht ist, die beim Start der Anwendung angezeigt wurde;
- Zeilen 32–35: Wenn auf Registerkarte 1 geklickt wird, muss das Fragment angezeigt werden, dessen Nummer in der Sitzung gespeichert ist;
- Zeilen 37–39: Wir navigieren zum ausgewählten Fragment. Die zugehörige Aktion ist [SUBMIT]. Hätte es auch [NAVIGATION] sein können? In diesem Dokument verwenden wir [NAVIGATION] nur dann, wenn die Anzeige des neuen Fragments lediglich die Kenntnis seines vorherigen Zustands erfordert. Hier ist das nicht der Fall, da sich die Anzeige des Fragments gegenüber dem vorherigen Zustand ändern muss, um einen weiteren Besuch anzuzeigen;
2.8.4.2.3. Menüverwaltung
Die Aktivität ist mit dem folgenden Menü [menu_main.xml] verknüpft:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="exemples.android.MainActivity">
<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment1"
android:title="@string/fragment1"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment2"
android:title="@string/fragment2"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment3"
android:title="@string/fragment3"
android:orderInCategory="100"
app:showAsAction="never"/>
<item android:id="@+id/fragment4"
android:title="@string/fragment4"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>
was Folgendes anzeigt:
![]() |
Das Menü wird über die folgenden Methoden verwaltet:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// log
if (IS_DEBUG_ENABLED) {
Log.d(className, "onOptionsItemSelected");
}
// processing menu options
int id = item.getItemId();
switch (id) {
case R.id.action_settings: {
if (IS_DEBUG_ENABLED) {
Log.d(className, "action_settings selected");
}
break;
}
case R.id.fragment1: {
showFragment(0);
break;
}
case R.id.fragment2: {
showFragment(1);
break;
}
case R.id.fragment3: {
showFragment(2);
break;
}
case R.id.fragment4: {
showFragment(3);
break;
}
}
// item processed
return true;
}
private void showFragment(int i) {
if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
// no navigation on software tab selection
session.setNavigationOnTabSelectionNeeded(false);
// we recreate the two tabs for a title font issue
tabLayout.removeAllTabs();
tabLayout.addTab(tabLayout.newTab().setText("Vue1"), false);
tabLayout.addTab(tabLayout.newTab().setText(String.format("Fragment n° %s", (i + 1))), false);
// the fragment number to be displayed is set in session
session.setNumFragment(i);
// select tab 2 with navigation
session.setNavigationOnTabSelectionNeeded(true);
tabLayout.getTabAt(1).select();
}
}
- Zeilen 16–31: Verarbeiten eines Klicks auf eine Menüoption [Fragmenti];
- Zeilen 37–50: Anzeigen von Fragment #i (es handelt sich um Fragmente vom Typ „PlaceHolderFragment“) in Registerkarte #1 (der zweiten Registerkarte);
- Zeilen 42–44: Wir entscheiden uns, die vorhandenen Registerkarten zu entfernen, um zwei neue zu erstellen. Diese Entscheidung wurde getroffen, um das folgende Problem zu umgehen: Wenn wir das Fragment einfach in der vorhandenen Registerkarte 1 anzeigen (ohne sie zu löschen), erscheint dessen Titel seltsamerweise (Schriftart, Größe) anders als der Titel von Registerkarte 0;
- Zeilen 43–44: Die beiden Registerkarten werden erstellt, aber nicht ausgewählt (letzter Parameter auf „false“ gesetzt);
- Zeile 40: Die Vorgänge in den Zeilen 42–44 können [select]-Operationen auf den Registerkarten auslösen, wodurch der [onTabSelected]-Handler aufgerufen wird. Wenn keine Aktion durchgeführt wird, führt dies zur Navigation zu einem Fragment. Wir verhindern dies, indem wir den booleschen Wert [navigationOnTabSelectionNeeded] in der Sitzung auf „false“ setzen. Dieser boolesche Wert wird von der Klasse [AbstractFragment] automatisch auf „true“ zurückgesetzt, sobald ein Fragment sichtbar wird;
- Zeile 46: Wir speichern die Nummer des anzuzeigenden Fragments in der Sitzung;
- Zeilen 48–50: Wählen Sie Registerkarte Nr. 2 mit Navigation aus (Zeile 48). Dies löst die Prozedur [onTabSelected] aus, die Folgendes bewirkt:
- das Fragment anzeigt, dessen Nummer in der Sitzung gespeichert wurde;
- die Nummer der ausgewählten Registerkarte in der Sitzung speichert;
2.8.4.3. Das Fragment [Vue1Fragment]
Hier ist die endgültige Version des Fragments:
package client.android.fragments.behavior;
import android.widget.EditText;
import android.widget.Toast;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_fragment)
public class Vue1Fragment extends AbstractFragment {
// visual interface elements
@ViewById(R.id.editTextNom)
protected EditText editTextNom;
// event manager
@Click(R.id.buttonValider)
protected void doValider() {
// the name entered is displayed
Toast.makeText(activity, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
}
// fragment life cycle -----------------------------------------------
private void initFragment() {
// nothing to do
}
// save fragment status
@Override
public CoreState saveFragment() {
// view status - nothing to save
return new CoreState();
}
@Override
protected int getNumView() {
return IMainActivity.FRAGMENTS_COUNT - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// nothing to do
}
@Override
protected void initView(CoreState previousState) {
// 1st visit?
if (previousState == null) {
// the visit number is displayed
showNumVisit();
}
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// the visit number is displayed
showNumVisit();
}
@Override
protected void updateOnRestore(CoreState previousState) {
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
// private methods -------------------------------------
// display visit no
private void showNumVisit() {
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// the visit number is displayed
Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
}
}
Die Klasse ist fast leer.
- Zeilen 35–39: Wird von der übergeordneten Klasse aufgerufen, wenn das Fragment seinen Zustand speichern muss. Das Fragment [Vue1Fragment] hat keinen Zustand, der gespeichert werden muss. Wir geben einfach eine Instanz der Basisklasse [CoreState] zurück (zur Erinnerung: Wir dürfen nicht null zurückgeben);
- Zeilen 41–44: Muss die Fragment-ID zurückgeben. Das Fragment [Vue1Fragment] hat standardmäßig die ID [FRAGMENTS_COUNT-1];
- Zeilen 51–59: Wird von der übergeordneten Klasse aufgerufen, wenn das Fragment zum ersten Mal erstellt wird (previousState == null) oder bei nachfolgenden Aufrufen (previousState != null);
- Zeilen 54–57: Wenn dies der erste Besuch ist, wird die Besuchsanzahl erhöht und angezeigt (Zeilen 85–92);
- Zeilen 61–65: Wird aufgerufen, wenn das Fragment im Zusammenhang mit einer [SUBMIT]-Aktion angezeigt werden soll. Die Besuchsanzahl wird erhöht und angezeigt. Hier ist es nicht möglich, dass die Besuchsanzahl während des Lebenszyklus zweimal erhöht wird. Tatsächlich erfolgt der erste Besuch des Fragments [Vue1Fragment] beim Start der Anwendung, wenn die Aktion in der Sitzung designmäßig auf [NONE] gesetzt ist. Dies stellt sicher, dass die Methode [updateOnSubmit] nicht aufgerufen wird. Danach wird es nie wieder der erste Besuch sein, und die Methode [initView] führt keine Aktion aus;
- Zeilen 68–71: Wird während eines Speicher-/Wiederherstellungszyklus aufgerufen. Da das Fragment keinen Status hat, gibt es hier nichts wiederherzustellen;
- Zeilen 73–76: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts mehr zu tun;
- Zeilen 78–81: Wird aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;
2.8.4.4. Der Zustand [PlaceHolderFragmentState]
Der Zustand des [PlaceHolderFragment]-Fragments ist wie folgt:
package client.android.fragments.state;
import client.android.architecture.custom.CoreState;
public class PlaceHolderFragmentState extends CoreState {
// text
private String text;
// manufacturers
public PlaceHolderFragmentState() {
}
public PlaceHolderFragmentState(String text) {
super();
this.text = text;
}
// getters and setters
...
}
- Wenn wir den Status des Fragments speichern müssen, speichern wir den angezeigten Text (Zeile 7);
2.8.4.5. Das [PlaceHolderFragment]-Fragment
Das [PlaceHolderFragment]-Fragment sieht wie folgt aus:
package client.android.fragments.behavior;
import android.util.Log;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.PlaceHolderFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;
@EFragment(R.layout.fragment_main)
@OptionsMenu(R.menu.menu_fragment)
public class PlaceholderFragment extends AbstractFragment {
// visual interface components
@ViewById(R.id.section_label)
protected TextView textViewInfo;
@ViewById(R.id.textView1)
protected TextView textView1;
// data
private String text;
// fragment no
private static final String ARG_SECTION_NUMBER = "section_number";
// implementation of parent class methods ----------------------------
@Override
public CoreState saveFragment() {
// save fragment state
PlaceHolderFragmentState placeHolderFragmentState = new PlaceHolderFragmentState();
placeHolderFragmentState.setText(textViewInfo.getText().toString());
return placeHolderFragmentState;
}
@Override
protected int getNumView() {
return getArguments().getInt(ARG_SECTION_NUMBER) - 1;
}
@Override
protected void initFragment(CoreState previousState) {
// original text
text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
}
@Override
protected void initView(CoreState previousState) {
}
@Override
protected void updateOnSubmit(CoreState previousState) {
// update the text displayed
// increment visit no
int numVisit = session.getNumVisit();
numVisit++;
session.setNumVisit(numVisit);
// modified text
textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
// log
if (isDebugEnabled) {
Log.d(className, String.format("updateForSubmit, numvisit=%s, texte affiché=%s, visibility=%s", numVisit, textViewInfo.getText().toString(), textViewInfo.getVisibility()));
}
}
@Override
protected void updateOnRestore(CoreState previousState) {
// restore displayed text
PlaceHolderFragmentState state = (PlaceHolderFragmentState) previousState;
textViewInfo.setText(state.getText());
}
@Override
protected void notifyEndOfUpdates() {
}
@Override
protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
}
}
- Zeilen 30–36: Wenn die übergeordnete Klasse das Fragment auffordert, seinen Zustand zu speichern, wird der vom Fragment angezeigte Text gespeichert (Zeile 34);
- Zeilen 38–41: Gibt die ID des Fragments zurück. Diese hängt von der Abschnitts-ID ab, die bei der Erstellung des Fragments als Argument übergeben wurde;
- Zeilen 43–47: Wird beim ersten Aufbau des Fragments (previousState == null) oder bei nachfolgenden Aufbauten (previousState != null) aufgerufen;
- Zeile 46: Hier wird der vorherige Zustand nicht verwendet. Der beim ersten Aufruf angezeigte Ausgangstext [text] (Zeile 24) wird jedes Mal neu berechnet. Dies ist umstritten. Wir hätten uns dafür entscheiden können, diese Information ebenfalls in den Zustand des Fragments aufzunehmen;
- Zeilen 49–51: Wird beim ersten Rendern der mit dem Fragment verbundenen Ansicht (previousState == null) oder bei nachfolgenden Renderings (previousState != null) aufgerufen. Es gibt nichts zu tun;
- Zeilen 53–56: Wird aufgerufen, wenn das Fragment im Zusammenhang mit einer [SUBMIT]-Aktion angezeigt werden soll. Dies ist immer der Fall, außer während des Speicher-/Wiederherstellungszyklus, bei dem die Aktion [RESTORE] lautet. Wir erhöhen daher die Besuchsnummer und zeigen sie an;
- Zeilen 68–74: Wird während eines Speicher-/Wiederherstellungszyklus aufgerufen. Wir stellen den Text wieder her, der im Status des Fragments gespeichert wurde;
- Zeilen 76–79: Wird aufgerufen, wenn alle vorherigen Aktualisierungen abgeschlossen sind. Hier gibt es nichts weiter zu tun;
- Zeilen 82–83: Wird aufgerufen, wenn alle gestarteten asynchronen Aufgaben abgeschlossen sind. Hier gibt es keine asynchronen Aufgaben;
2.8.4.6. Tests
Der Leser ist eingeladen, die Anwendung zu testen, indem er das Gerät dreht, um zu überprüfen, ob das angezeigte Fragment seinen Zustand beibehält. Wir werden auch die Protokolle untersuchen.
2.9. Fazit
Am Ende dieses Kapitels verfügen wir über ein Beispielprojekt [client-android-skel] für einen Android-Client, der mit einem Webservice / JSON kommuniziert und folgende Funktionen aufweist:
- Die asynchrone Kommunikation mit dem Web-/JSON-Server wird mithilfe der RxJava-Bibliothek abgewickelt;
- Der Lebenszyklus des Fragments (Aktualisieren, Speichern, Wiederherstellen) wird von seiner übergeordneten Klasse [AbstractFragment] verwaltet, die zu genau festgelegten Zeitpunkten bestimmte Methoden ihrer untergeordneten Klassen aufruft. Das untergeordnete Fragment muss sich somit nicht um die Lebenszyklusphasen kümmern, sondern lediglich bestimmte Methoden implementieren, die von seiner übergeordneten Klasse gefordert werden;
- Der Lebenszyklus der Aktivität (Speichern/Wiederherstellen) wird von einer abstrakten Klasse [AbstractActivity] verwaltet, die ebenfalls verlangt, dass die untergeordnete Aktivität bestimmte Methoden implementiert;
- Die Klasse [AbstractActivity] kann eine Anwendung mit oder ohne Registerkarten, mit oder ohne Ladebild und mit oder ohne Basisauthentifizierung gegenüber dem Webserver / JSON verarbeiten. Das Vorhandensein oder Fehlen dieser Elemente wird durch die Konfiguration bestimmt;
Wir stellen nun eine Fallstudie vor, die komplexer ist als die vorherigen Beispiele. Die neue Anwendung basiert auf dem Vorlagenprojekt [client-android-skel].



















































