Skip to content

1. Android-Programmierung lernen

Das PDF des Dokuments ist |HIER| verfügbar.

Beispiele aus dem Dokument sind |HIER| verfügbar.

1.1. Einleitung

1.1.1. Inhalt

Dieses Dokument ist eine Überarbeitung mehrerer bestehender Dokumente:

  1. Android für J2EE-Entwickler;
  1. Einführung in die Android-Tablet-Programmierung anhand von Beispielen;
  2. Steuerung eines Arduino mit einem Android-Tablet;
  3. Einführung in die Android-Tablet-Programmierung anhand von Beispielen – Version 2

und führt die folgenden neuen Funktionen ein:

  • Dokument 1 stellte eine Architektur namens AVAT (Activity-Views-Actions-Tasks) vor, um die asynchrone Programmierung in einer Android-Anwendung zu erleichtern. In diesem Dokument wird die Standardbibliothek RxJava zur Verwaltung asynchroner Aktionen verwendet;
  • Dokument 2 verwendete die Eclipse-IDE mit einem Android-Plugin. Dieses Dokument verwendet Android Studio;
  • Dokument 3 wird unverändert übernommen;
  • Dokument 4 verwendete die [Android Annotations] (AA)-Bibliothek mit der IntelliJ IDEA Community Edition IDE. Dieses Dokument gibt Dokument 4 vollständig wieder, mit folgenden Unterschieden:
    • Die IDE ist nun Android Studio;
    • Das Build-System ist Gradle für alle Client- oder Server-Projekte (in Dokument 4 wurde teilweise Maven verwendet).
    • Die asynchrone Programmierung wird mithilfe der RxJava-Bibliothek implementiert (in Dokument 4 wurde die AA-Bibliothek verwendet);
  • Dieses Dokument behandelt Bereiche, die in den vorherigen Dokumenten nicht oder nur kurz behandelt wurden:
    • das Konzept der Fragment-Nachbarschaft;
    • das Speichern/Wiederherstellen der Aktivität und ihrer Fragmente;
    • der Lebenszyklus von Fragmenten;

Schließlich wird das Grundgerüst eines Android-Clients vorgestellt, der mit einem Webservice/JSON kommuniziert, wobei wir eine Vielzahl von Elementen herausarbeiten, die in dieser Art von Client häufig vorkommen. Dieses Grundgerüst wird in allen Beispielen ab Kapitel 2 verwendet. Dies ist der wirklich innovative Teil des Dokuments.

Es werden folgende Beispiele vorgestellt:

Beispiel
Natur
1
Importieren eines bestehenden Android-Projekts
2
Ein einfaches Android-Projekt
3
Ein einfaches [Android Annotations]-Projekt
4
Ansichten und Ereignisse
5
Navigation zwischen Ansichten
6
Registerkarten-Navigation
7
Verwendung der Bibliothek [Android Annotations] mit Gradle
8–12
Verwalten von Fragmenten in einer Android-App
13
View-Navigation erneut betrachtet
14
Zweischichtige Architektur
15
Client-Server-Architektur
16
Umgang mit Asynchronität mit RxJava
17, 17B
Komponenten zur Dateneingabe
18
Verwendung eines View-Musters
19
Die ListView-Komponente
20
Verwendung eines Menüs
21
Verwendung einer übergeordneten Klasse für Fragmente
22, 22B
Speichern und Wiederherstellen des Zustands der Aktivität und der Fragmente
23
Wetter-Client
Kapitel 2
Grundgerüst eines Android-Clients, der mit einem Webservice / JSON kommuniziert. Es berücksichtigt eine Vielzahl von Elementen, die bei dieser Art von Android-Client üblicherweise vorkommen.
Kapitel 3
Terminverwaltung für eine Arztpraxis
Kapitel 4
Praxisübung – Grundlegende Lohn- und Gehaltsabrechnung
Kapitel 5
Praktische Übung – Bestellung von Arduino-Boards

Dieses Dokument wurde im Abschlussjahr der Ingenieursschule IstiA an der Universität Angers [istia.univ-angers.fr] verwendet. Dies erklärt den teilweise etwas ungewöhnlichen Ton des Textes. Bei den beiden praktischen Übungen handelt es sich um Laboraufgaben, für die nur die groben Umrisse der Lösung angegeben werden. Die Lösung muss vom Leser selbst erarbeitet werden.

Der Quellcode für die Beispiele ist |HIER| verfügbar. Um diese Beispiele auszuführen, müssen Sie die Vorgehensweise in Abschnitt 6.12 befolgen.

Dieses Dokument ist eine Einführungsanleitung in die Android-Programmierung. Es erhebt keinen Anspruch auf Vollständigkeit. Es richtet sich in erster Linie an Anfänger.

Die Referenzseite für die Android-Programmierung finden Sie unter der URL [http://developer.android.com/guide/components/index.html]. Dort sollten Sie sich einen Überblick über die Android-Programmierung verschaffen.

1.1.2. Voraussetzungen

Um dieses Dokument optimal nutzen zu können, sollten Sie über fundierte Kenntnisse der Programmiersprache Java verfügen.

1.1.3. Verwendete Tools

Die folgenden Beispiele wurden in der folgenden Umgebung getestet:

  • Windows 10 Pro 64-Bit-Rechner;
  • JDK 1.8;
  • Android SDK API 23;
  • Android Studio, Version 2.1;
  • Genymotion-Emulator, Version 2.6.0;

Um diesem Dokument folgen zu können, müssen Sie Folgendes installieren:

  • ein JDK (siehe Abschnitt 6.8);
  • den Genymotion Android-Emulator-Manager (siehe Abschnitt 6.9);
  • den Maven-Abhängigkeitsmanager (siehe Abschnitt 6.10);
  • die [Android Studio]-IDE (siehe Abschnitt 6.11);

1.2. Beispiel 01: Importieren eines Android-Beispiels

1.2.1. Erstellen des Projekts

Erstellen wir unser erstes Android-Projekt mit Android Studio. Zunächst erstellen wir einen leeren Ordner [examples], in dem alle unsere Projekte gespeichert werden:

  

Erstellen Sie dann ein Projekt mit Android Studio. Wir importieren zunächst eines der in der IDE enthaltenen Beispiele [1-5]:

 

Image

Beim Importieren des Projekts können Fehler auftreten, da die Umgebung, in der das Projekt erstellt wurde, nicht mit der Umgebung übereinstimmt, in der es hier ausgeführt wird. Dies ist eine Gelegenheit, um zu sehen, wie man diese Art von Fehler beheben kann. Hier haben wir folgenden Fehler:

Das importierte Projekt wird durch die folgende [build.gradle]-Datei konfiguriert [2]:


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}
 
apply plugin: 'com.android.application'
 
repositories {
    jcenter()
}
 
dependencies {
    compile "com.android.support:support-v4:23.3.0"
    compile "com.android.support:support-v13:23.3.0"
    compile "com.android.support:cardview-v7:23.3.0"
}
 
// The sample build uses multiple directories to
// keep boilerplate and common code separate from
// the main sample code.
List<String> dirs = [
    'main',     // main sample code; look here for the interesting stuff.
    'common',   // components that are reused by multiple samples
    'template'] // boilerplate code that is generated by the sample template process
 
android {
    compileSdkVersion 21
    buildToolsVersion "23.0.3"
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    sourceSets {
        main {
            dirs.each { dir ->
                java.srcDirs "src/${dir}/java"
                res.srcDirs "src/${dir}/res"
            }
        }
        androidTest.setRoot('tests')
        androidTest.java.srcDirs = ['tests/src']
    }
 
    aaptOptions {
        noCompress "pdf"
    }
}
  • Der gemeldete Fehler ist auf die Zeilen 31, 34–35 zurückzuführen: Wir verfügen nicht über SDK 21. Wir ersetzen diese Version durch Version 23, die wir haben.

In der Datei [build.gradle] macht Android Studio Vorschläge wie unten gezeigt:

 

Um die Vorschläge anzunehmen, drücken Sie [Alt-Enter] auf dem Vorschlag:

 

Möglicherweise tritt auch ein Fehler bezüglich der Gradle-Version auf:

 

Dieser Fehler rührt von einer Diskrepanz zwischen der von der [build.gradle]-Datei des Projekts geforderten Gradle-Version (Version 2.10 in Zeile 6 unten) her:


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

und die in der Datei [<project>/gradle/wrapper/gradle-wrapper.properties] aufgeführte:


#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip

Ersetzen Sie in Zeile 6 oben 2.8 durch 2.10.

Um auf die Datei [<project>/gradle/wrapper/gradle-wrapper.properties] zuzugreifen, verwenden Sie die Projektansicht:

Sobald dies korrigiert ist, können Sie die Anwendung kompilieren [1], den Genymotion-Emulator starten [2] und das Projekt ausführen [3]:

 

Image

Beenden wir die Anwendung:

  

Sie können das Projekt nun schließen. Wir werden ein neues erstellen.

  

1.2.2. Ein paar Anmerkungen zur IDE

1.2.2.1. Ansichten

Die Android Studio (AS)-IDE bietet verschiedene Ansichten für die Arbeit an einem Projekt. Wir werden hauptsächlich zwei davon verwenden:

  • die Ansicht [Android] [1]:
  • die Ansicht [Projekt] [4];
 
  

Meistens arbeiten wir mit der [Android]-Ansicht. Wenn wir ein Projekt in ein anderes klonen, benötigen wir die [Projekt]-Ansicht.

1.2.2.2. Ausführungsmanagement

Es gibt mehrere Möglichkeiten, ein AS-Projekt auszuführen, anzuhalten oder erneut auszuführen. Zunächst gibt es die Schaltflächen in der Symbolleiste:

Die Schaltfläche [Rerun] [3] stoppt das Projekt [2] und startet es anschließend neu [1].

1.2.2.3. Cache-Verwaltung

Android Studio verwaltet einen Cache der von ihm verwalteten Projekte, um die Reaktionsgeschwindigkeit der IDE so hoch wie möglich zu halten. Bei Android Studio Version 2.1 (Mai 2016) spiegelte dieser Cache häufig nicht die gerade vorgenommenen Codeänderungen wider. In diesem Fall müssen Sie den Cache ungültig machen:

Bei Android Studio 2.1 (Mai 2016) musste der vorherige Schritt mehrmals durchgeführt werden, und manchmal reichte das nicht aus, um das festgestellte Problem zu beheben. Die Lösung bestand darin, [Instant Run] zu deaktivieren:

  • in [3-4] wurde alles deaktiviert;

In allen folgenden Fällen haben wir mit dieser Cache-Konfiguration gearbeitet und keine Probleme festgestellt.

1.2.2.4. Protokollverwaltung

Bei der Ausführung eines Projekts werden die Protokolle im Android Monitor angezeigt:

Auf der Registerkarte [Android Monitor] [1] werden die Protokolle auf der Registerkarte [logcat] [2] angezeigt. Mit der Schaltfläche [3] können Sie die Protokolle löschen. Diese Schaltfläche ist nützlich, wenn Sie die Protokolle für eine bestimmte Aktion anzeigen möchten:

  • Löschen Sie die Protokolle;
  • führen Sie auf dem Android-Gerät die Aktion aus, für die Sie die Protokolle benötigen;
  • die angezeigten Protokolle beziehen sich auf die ausgeführte Aktion;

Es gibt mehrere Protokollstufen [4]. Standardmäßig ist der Modus [Verbose] ausgewählt. Das bedeutet, dass Protokolle aller Stufen angezeigt werden. Mit [4] können Sie eine bestimmte Stufe auswählen.

Protokolle sind sehr nützlich, um festzustellen, an welchen Stellen während der Ausführung eines Projekts bestimmte Methoden ausgeführt werden. Wir werden sie häufig verwenden. Sehen wir uns den Code für die Klasse [MainActivity] im Projekt [Example-01] an:

 

package com.example.android.pdfrendererbasic;
 
import android.app.Activity;
import android.app.AlertDialog;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
 
public class MainActivity extends Activity {
 
    public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_real);
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction()
                    .add(R.id.container, new PdfRendererBasicFragment(),
                            FRAGMENT_PDF_RENDERER_BASIC)
                    .commit();
        }
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_info:
                new AlertDialog.Builder(this)
                        .setMessage(R.string.intro_message)
                        .setPositiveButton(android.R.string.ok, null)
                        .show();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

Oben sind die Methoden [onCreate, Zeile 14] und [onCreateOptionsMenu, Zeile 26] Methoden der übergeordneten Klasse [Activity] (Zeile 9). Sie werden an verschiedenen Punkten im Lebenszyklus der Anwendung aufgerufen. Manchmal werden sie mehrfach ausgeführt. Selbst beim Lesen der Dokumentation kann es schwierig sein, festzustellen, ob eine bestimmte Lebenszyklusmethode vor oder nach einer von uns selbst geschriebenen Methode ausgeführt wird. Diese Information ist jedoch oft wichtig zu wissen. Wir können daher Logs wie unten gezeigt hinzufügen:


public class MainActivity extends Activity {
 
  public static final String FRAGMENT_PDF_RENDERER_BASIC = "pdf_renderer_basic";
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
    super.onCreate(savedInstanceState);
    ...
  }
 
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("MainActivity","onCreateOptionsMenu");
    getMenuInflater().inflate(R.menu.main, menu);
   ...
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("MainActivity","onOptionsItemSelected");
    switch (item.getItemId()) {
      ...
  }
}
  • In den Zeilen 7, 14 und 21 wird die Klasse [Log] verwendet. Mit dieser Klasse können Sie Protokolle an die Android-Konsole [logcat] schreiben. Protokolle werden in verschiedene Stufen unterteilt (Info, Warnung, Debug, Ausführlich, Fehler). [Log.d] zeigt Protokolle der Stufe [debug] an. Das erste Argument ist die Quelle der Protokollmeldung. Tatsächlich können verschiedene Quellen Meldungen an die Protokollkonsole senden. Um zwischen ihnen zu unterscheiden, verwenden wir dieses erste Argument. Das zweite Argument ist die Meldung, die in die Protokollkonsole geschrieben werden soll;

Wenn wir das Projekt [Example-01] erneut ausführen, erhalten wir die folgenden Protokolle:


05-28 08:37:12.709 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreate
05-28 08:37:12.778 23881-23923/com.example.android.pdfrendererbasic D/OpenGLRenderer: Use EGL_SWAP_BEHAVIOR_PRESERVED: true
 
            [ 05-28 08:37:12.781 23881:23881 D/]
              HostConnection::get() New Host ...
05-28 08:37:12.967 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onCreateOptionsMenu

Wir sehen, dass die Methode [onCreate], die die Android-Aktivität erstellt, vor der Methode [onCreateOptionsMenu] ausgeführt wird, die das App-Menü erstellt.

Wenn wir nun im Android-Emulator auf die Menüoption [1] klicken:

  

Der folgende Eintrag wird der Protokollkonsole hinzugefügt:


05-28 08:41:22.881 23881-23881/com.example.android.pdfrendererbasic D/MainActivity: onOptionsItemSelected

Im weiteren Verlauf werden wir häufig Protokollanweisungen zum Android-Code hinzufügen. Meistens werden wir diese nicht kommentieren. Sie dienen lediglich dazu, den Leser dazu anzuregen, einen Blick auf die Protokollkonsole zu werfen, um nach und nach den Lebenszyklus einer Android-Anwendung zu verstehen.

1.2.2.5. Verwalten des Emulators [Genymotion]

Manchmal stürzt der Genymotion-Emulator ab und lässt sich nicht neu starten. Das liegt daran, dass im Task-Manager noch VirtualBox-Prozesse laufen. Öffnen Sie den Task-Manager [Strg-Alt-Entf] und beenden Sie alle VirtualBox-Prozesse:

Starten Sie anschließend den Genymotion-Emulator über Android Studio neu.

1.2.2.6. Verwalten der erstellten APK-Binärdatei

Durch das Kompilieren des Projekts wird eine Binärdatei mit der Erweiterung .apk erstellt:

Es gibt zwei Versionen: eine namens [debug] und die andere namens [debug-unaligned]. Sie sollten die erste verwenden; die andere ist eine Zwischenversion. Die in [4] erstellte .apk-Datei kann direkt auf einen Emulator oder ein Android-Gerät übertragen werden. Um sie auf einen Emulator zu übertragen, ziehen Sie sie einfach mit der Maus per Drag & Drop auf den Emulator.

1.3. Beispiel-02: Ein einfaches Android-Projekt

Erstellen wir ein neues Android-Projekt mit Android Studio [1-12]:

 

In [13] führen wir die App aus. Anschließend sehen wir die in [14] gezeigte Anzeige auf dem Genymotion-Emulator.

1.3.1. Gradle-Konfiguration

Das erstellte Projekt wird durch die folgende [build.gradle]-Datei konfiguriert:

 

apply plugin: 'com.android.application'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
 
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
  compile 'com.android.support:appcompat-v7:23.4.0'
}

Diese Datei wurde von der IDE anhand ihrer Konfigurationseinstellungen generiert. Es handelt sich um eine minimale Datei, die wir nach und nach erweitern werden.

  • Zeilen 3–12: die Eigenschaften der Android-Anwendung;
  • Zeilen 22–25: ihre Abhängigkeiten. Hier werden wir in erster Linie Änderungen vornehmen, basierend auf den untersuchten Beispielen;

1.3.2. Das Anwendungsmanifest

  

Die Datei [AndroidManifest.xml] [1] definiert die Eigenschaften der Android-Anwendungsdatei. Ihr Inhalt lautet wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • Zeile 3: das Android-Projektpaket;
  • Zeile 10: der Name der Aktivität;

Diese beiden Informationen stammen aus den Angaben, die bei der Erstellung des Projekts gemacht wurden:

  • Zeile 3 des Manifests (Paket) stammt aus Eintrag [4] oben. In diesem Paket werden automatisch eine Reihe von Klassen generiert;
  • Zeile 10 des Manifests (Aktivitätsname) stammt aus Eintrag [1] oben;

Kehren wir zum Manifest zurück:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • Zeile 10: Die Hauptaktivität der Anwendung. Sie verweist auf die oben genannte Klasse [1];
  • Zeile 6: Das Anwendungssymbol [2]. Es kann geändert werden;
  • Zeile 7: Die Bezeichnung der Anwendung. Sie befindet sich in der Datei [strings.xml] [3]:

<resources>
  <string name="app_name">Exemple-02</string>
</resources>

Die Datei [strings.xml] enthält die von der Anwendung verwendeten Zeichenfolgen. Zeile 2: Der Anwendungsname stammt aus dem Eintrag, der beim Erstellen des Projekts vorgenommen wurde [4]:

 
  • Zeile 10: ein Activity-Tag. Eine Android-Anwendung kann mehrere Activities haben;
  • Zeile 12: Die Aktivität wird als Hauptaktivität festgelegt;
  • Zeile 13: und sie muss in der Liste der Apps erscheinen, die auf dem Android-Gerät gestartet werden können.

1.3.3. Die Hauptaktivität

 

Eine Android-App basiert auf einer oder mehreren Aktivitäten. Hier wurde eine Aktivität [1] generiert: [MainActivity]. Eine Aktivität kann je nach Typ eine oder mehrere Ansichten anzeigen. Die generierte Klasse [MainActivity] sieht wie folgt aus:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}
  • Zeile 6: Die Klasse [MyActivity] erweitert die Android-Klasse [AppCompatActivity]. Dies gilt für alle zukünftigen Aktivitäten;
  • Zeile 9: Die Methode [onCreate] wird ausgeführt, wenn die Aktivität erstellt wird. Dies geschieht, bevor die mit der Aktivität verbundene Ansicht angezeigt wird;
  • Zeile 10: Die Methode [onCreate] der übergeordneten Klasse wird aufgerufen. Dies muss immer erfolgen;
  • Zeile 11: Die Datei [activity_main.xml] [2] ist die mit der Aktivität verbundene Ansicht. Die XML-Definition dieser Ansicht lautet wie folgt:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="exemples.android.MainActivity">
 
  <TextView
    android:text="Hello World!"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
</RelativeLayout>
  • Zeilen b–k: der Layout-Manager. Die Standardauswahl ist der Typ [RelativeLayout]. In diesem Containertyp werden Komponenten relativ zueinander positioniert (rechts von, links von, unter, über);
  • Zeilen m–p: eine [TextView]-Komponente zur Anzeige von Text;
  • Zeile n: der angezeigte Text. Es wird nicht empfohlen, Text direkt in Ansichten fest zu codieren. Es ist vorzuziehen, diesen Text in die Datei [res/values/strings.xml] zu verschieben [3]:

Der angezeigte Text lautet somit [Hello World!]. Wo wird er angezeigt? Der [RelativeLayout]-Container füllt den Bildschirm aus. Die [TextView], die sein einziges Element ist, wird oben links in diesem Container und somit oben links auf dem Bildschirm angezeigt;

Was bedeutet [R.layout.activity_main] in Zeile 11? Jeder Android-Ressource (Ansichten, Fragmente, Komponenten usw.) wird eine Kennung zugewiesen. So wird eine [V.xml]-Ansicht im Ordner [res/layout] als [R.layout.V] identifiziert. R ist eine Klasse, die im Ordner [app/build/generated] generiert wird [1-3]:

 

Die Klasse [R] sieht wie folgt aus:


...............
    public static final class string {
        public static final int abc_action_bar_home_description=0x7f060000;
        public static final int abc_action_bar_home_description_format=0x7f060001;
        public static final int abc_action_bar_home_subtitle_description_format=0x7f060002;
        ...
        public static final int app_name=0x7f060014;
    }
 
    public static final class layout {
        public static final int abc_action_bar_title_item=0x7f040000;
        public static final int abc_action_bar_up_container=0x7f040001;
...
        public static final int activity_main=0x7f040019;
...
    }
 
    public static final class mipmap {
        public static final int ic_launcher=0x7f030000;
}
  • Zeile 14: Das Attribut [R.layout.activity_main] ist die Kennung für die Ansicht [res/layout/activity_main.xml];
  • Zeile 7: Das Attribut [R.string.app_name] entspricht der String-ID [app_name] in der Datei [res/values/string.xml]:
  • Zeile 19: Das Attribut [R.mipmap.ic_launcher] ist die Kennung für das Bild [res/mipmap/ic_launcher];

Beachten Sie also, dass Sie, wenn Sie im Code auf [R.layout.activity_main] verweisen, auf ein Attribut der Klasse [R] verweisen. Die IDE hilft Ihnen dabei, die verschiedenen Elemente dieser Klasse zu identifizieren:

1.3.4. Ausführen der Anwendung

Um eine Android-Anwendung auszuführen, müssen wir eine Ausführungskonfiguration erstellen:

  • Wählen Sie in [1] die Option [Konfigurationen bearbeiten];
  • Das Projekt wurde mit einer [app]-Konfiguration erstellt, die wir löschen werden [2], um es neu zu erstellen;
  • Erstellen Sie unter [3] eine neue Ausführungskonfiguration;
  
  • Wählen Sie in [4] die Option [Android-Anwendung] aus;

Image

  • Wählen Sie unter [5] das Modul [app] aus der Dropdown-Liste aus;
  • Behalten Sie in [6–8] die Standardwerte bei;
  • In [7] ist die Standardaktivität diejenige, die in der Datei [AndroidManifest.xml] definiert ist (Zeile 1 unten):

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
</activity>
  • Wählen Sie in [8] die Option [Show Chooser Dialog] aus, um das Gerät auszuwählen, auf dem die App ausgeführt werden soll (Emulator, Tablet);
  • Geben Sie in [9] an, dass diese Auswahl gespeichert werden soll;
  • Bestätigen Sie die Konfiguration;
  
  • Starten Sie in [11] den Emulator-Manager [Genymotion] (siehe Abschnitt 6.9);
  • Wählen Sie in [12] einen Tablet-Emulator aus und starten Sie ihn [13];
  • Führen Sie in [14] die Ausführungskonfiguration [app] aus;
  • in [15] wird das Formular zur Auswahl des Laufzeitegeräts angezeigt. Hier steht nur eine Option zur Verfügung: der zuvor gestartete [Genymotion]-Emulator;

Nach einem Moment zeigt der Software-Emulator die folgende Ansicht an:

Image

1.3.5. Der Lebenszyklus einer Aktivität

Kehren wir zum Code für die [MainActivity]-Aktivität zurück:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

Die Methode [onCreate] in den Zeilen 8–12 ist eine der Methoden, die während des Lebenszyklus einer Aktivität aufgerufen werden können. In der Android-Dokumentation sind diese Methoden aufgeführt:

 
  • [1]: Die Methode [onCreate] wird beim Start der Aktivität aufgerufen. In dieser Methode wird die Aktivität mit einer Ansicht verknüpft und die Verweise auf ihre Komponenten abgerufen;
  • [2-3]: Anschließend werden die Methoden [onStart] und [onResume] aufgerufen. Beachten Sie, dass die Methode [onResume] die letzte Methode ist, die ausgeführt wird, bevor die aktuell ausgeführte Aktivität den Zustand [4] erreicht;

1.4. Beispiel-03: Umschreiben des Projekts [Beispiel-02] unter Verwendung der Bibliothek [Android Annotations]

Wir stellen nun die Bibliothek [Android Annotations] vor, die das Schreiben von Android-Anwendungen vereinfacht. Dupliziere dazu das Beispiel [Beispiel-02] in [Beispiel-03], indem du die Schritte [1-16] befolgst.

  • Wählen Sie in [1] die Ansicht [Projekt] aus, um das gesamte Android-Projekt anzuzeigen;

Hinweis: Zwischen [14] und [15] haben wir von der Ansicht [Android] zur Ansicht [Projekt] gewechselt (siehe Abschnitt 1.2.2.1).

Anschließend ändern wir die Datei [res/values/strings.xml] [17]:

 

Die Datei [strings.xml] wird wie folgt geändert:


<resources>
  <string name="app_name">Exemple-03</string>
</resources>

Nun führen wir die neue Anwendung aus, die alle Konfigurationen aus [Beispiel-02] beibehalten hat:

 

In [19] erhalten wir das gleiche Ergebnis wie in [Beispiel-02], jedoch mit einem neuen Namen.

Wir stellen nun die Bibliothek [Android Annotations] vor, die wir der Kürze halber AA nennen werden. Diese Bibliothek führt neue Klassen zum Annotieren von Android-Quellcode ein. Diese Annotationen werden von einem Prozessor verwendet, der neue Java-Klassen im Modul erstellt; diese Klassen werden genau wie die vom Entwickler geschriebenen Klassen in die Kompilierung des Moduls einbezogen. Wir haben somit die folgende Build-Kette:

Zunächst fügen wir die Abhängigkeiten für den AA-Annotationscompiler (den oben erwähnten Prozessor) zur Datei [build.gradle] hinzu:


def AAVersion = '4.0.0'
 
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
}
  • In den Zeilen 4–5 werden die beiden Abhängigkeiten hinzugefügt, aus denen die AA-Bibliothek besteht;

Die Datei [build.gradle] wird erneut geändert, um ein Plugin namens [android-apt] zu verwenden, das den Kompilierungsprozess in zwei Schritte aufteilt:

  • Verarbeitung von Android-Annotationen, wodurch neue Klassen generiert werden;
  • Kompilierung aller Klassen des Projekts;

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'
  • Zeile 8: Version des [android-apt]-Plugins, nach der im zentralen Maven-Repository gesucht wird (Zeile 3);
  • Zeile 13: Aktivierung dieses Plugins;

Überprüfen Sie an dieser Stelle, ob die Laufkonfiguration [app] noch funktioniert.

Wir werden nun die erste Annotation aus der AA-Bibliothek in die Klasse [MainActivity] einfügen:

  

Die Klasse [MainActivity] sieht derzeit wie folgt aus:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
}

Wir haben diesen Code bereits in Abschnitt 1.3.3 erläutert. Wir ändern ihn wie folgt:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}
  • Zeile 7: Die Annotation [@EActivity] ist eine AA-Annotation (Zeile 3). Ihr Parameter ist die mit der Aktivität verknüpfte Ansicht;

Diese Annotation generiert eine von der Klasse [MainActivity] abgeleitete Klasse [MainActivity_], und diese Klasse ist die eigentliche Aktivität. Wir müssen daher das Projektmanifest [AndroidManifest.xml] wie folgt ändern:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity_">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
 
</manifest>
  • Zeile 11: die neue Aktivität;

Sobald dies erledigt ist, können wir das Projekt kompilieren [1]:

 
  • In [2] sehen wir die Klasse [MainActivity_], die im Ordner [app/build/generated/source/apt/debug] generiert wurde;

Die generierte Klasse [MainActivity_] sieht wie folgt aus:


//
// DO NOT EDIT THIS FILE.
// Generated using AndroidAnnotations 4.0.0.
// 
// You can create a larger work that contains this file and distribute that work under terms of your choice.
//
 
 
package exemples.android;
 
import android.app.Activity;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import org.androidannotations.api.builder.ActivityIntentBuilder;
import org.androidannotations.api.builder.PostActivityStarter;
import org.androidannotations.api.view.HasViews;
import org.androidannotations.api.view.OnViewChangedNotifier;
 
public final class MainActivity_
    extends MainActivity
    implements HasViews
{
    private final OnViewChangedNotifier onViewChangedNotifier_ = new OnViewChangedNotifier();
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        OnViewChangedNotifier previousNotifier = OnViewChangedNotifier.replaceNotifier(onViewChangedNotifier_);
        init_(savedInstanceState);
        super.onCreate(savedInstanceState);
        OnViewChangedNotifier.replaceNotifier(previousNotifier);
        setContentView(R.layout.activity_main);
    }
...
  • Zeilen 24–25: Die Klasse [MainActivity_] erweitert die Klasse [MainActivity];

Wir werden nicht versuchen, den Code der von AA generierten Klassen zu erklären. Sie bewältigen die Komplexität, die die Annotationen verbergen sollen. Es kann jedoch manchmal hilfreich sein, ihn zu untersuchen, wenn Sie verstehen möchten, wie die von Ihnen verwendeten Annotationen „übersetzt“ werden.

Wir können nun die Konfiguration [app] erneut ausführen. Wir erhalten das gleiche Ergebnis wie zuvor. Wir werden dieses Projekt nun als Ausgangspunkt verwenden und es duplizieren, um die Schlüsselkonzepte der Android-Programmierung vorzustellen.

1.5. Beispiel-04: Ansichten und Ereignisse

1.5.1. Erstellen des Projekts

Wir folgen der in Abschnitt 1.4 beschriebenen Vorgehensweise zum Duplizieren von [Beispiel-02] in [Beispiel-03]:

Wir:

  • duplizieren das Projekt [Beispiel-03] in [Beispiel-04] (nachdem wir den Ordner [app/build] aus [Beispiel-03] gelöscht haben);
  • laden das Projekt [Beispiel-04];
  • ändern den Projektnamen in der Datei [app / res / values / strings.xml] (Android-Perspektive);
  • löschen die Datei [Beispiel-04/Beispiel-04.iml] (Projektansicht);
  • kompilieren und führen das Projekt aus;
 

1.5.2. Erstellen einer Ansicht

Wir werden nun den grafischen Editor verwenden, um die vom Projekt [Example-04] angezeigte Ansicht zu ändern:

  • Erstellen Sie in [1-4] eine neue XML-Ansicht;
  • Geben Sie in [5] einen Namen für die Ansicht ein;
  • Geben Sie in [6] das Stamm-Tag der Ansicht an. Hier wählen wir einen [RelativeLayout]-Container. Innerhalb dieses Komponenten-Containers werden Komponenten relativ zueinander positioniert: „rechts von“, „links von“, „unter“, „über“;
  

Die generierte Datei [vue1.xml] [7] 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">
 
</RelativeLayout>
  • Zeile 2: ein leerer [RelativeLayout]-Container, der die gesamte Breite des Tablets (Zeile 3) und dessen gesamte Höhe (Zeile 4) einnimmt;
  • Wählen Sie in [1] in der angezeigten Ansicht [vue1.xml] die Registerkarte [Design] aus;
  • Wechseln Sie in [2-4] in den Tablet-Modus;
  • in [5] stellen Sie den Maßstab für das Tablet auf 1 ein;
  • Wählen Sie in [6] den Querformatmodus für das Tablet aus;
  • Der Screenshot [7] fasst die getroffenen Einstellungen zusammen.
  • Wählen Sie in [1] einen [Großen Text] aus und ziehen Sie ihn auf die Ansicht [2];
  • Doppelklicken Sie in [3] auf die Komponente;
  • Bearbeiten Sie in [4] den angezeigten Text. Anstatt ihn fest in der XML-Ansicht zu programmieren, werden wir ihn in die Datei [res/values/string.xml] auslagern.
  • Fügen Sie in [5] einen neuen Wert zur Datei [strings.xml] hinzu;
  • Weisen Sie in [8] der Zeichenfolge eine Kennung zu;
  • in [9] weisen Sie den String-Wert zu;
  • in [10] die neue Ansicht nach der Validierung des vorherigen Schritts;
  • Nach einem Doppelklick auf die Komponente ändern wir deren ID [11];
  • in [12], in den Komponenteneigenschaften, ändern Sie die Schriftgröße [50pt];
  • in [13] die neue Ansicht;

Die Datei [vue1.xml] hat sich wie folgt geändert:


<?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="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginLeft="213dp" android:layout_marginStart="213dp"
    android:layout_marginTop="50dp" android:layout_alignParentTop="true" android:layout_alignParentLeft="true"
    android:layout_alignParentStart="true" android:textSize="50sp"/>
</RelativeLayout>
  • Die Änderungen in der GUI befinden sich in den Zeilen 10, 11 und 14. Die anderen Attribute des [TextView] sind entweder Standardwerte oder ergeben sich aus der Positionierung der Komponente innerhalb der Ansicht;
  • Zeilen 7–8: Die Größe der Komponente entspricht sowohl in der Höhe als auch in der Breite der des darin enthaltenen Textes (wrap_content);
  • Zeile 13: Der obere Rand der Komponente ist am oberen Rand der Ansicht ausgerichtet (Zeile 13), 50 Pixel darunter (Zeile 13);
  • Zeile 12: Die linke Seite der Komponente ist an der linken Seite der Ansicht ausgerichtet (Zeile 13), 213 Pixel nach rechts (Zeile 12);

Im Allgemeinen werden die genauen Größen der linken, rechten, oberen und unteren Ränder direkt im XML festgelegt.

Erstellen Sie nach dem gleichen Verfahren die folgende Ansicht [1]:

 

Die Komponenten sind wie folgt:

Nr.
ID
Typ
Rolle
1
textViewTitleView1
TextView
Ansichtstitel
2
textView1
TextView
eine Frage
3
editTextName
EditText
Geben Sie einen Namen ein
4
Schaltfläche „Validieren“
Schaltfläche
zum Bestätigen der Eingabe
5
buttonView2
Schaltfläche
zum Wechseln zu Ansicht Nr. 2

Das Positionieren von Komponenten relativ zueinander kann frustrierend sein, da das Verhalten des grafischen Editors manchmal unvorhersehbar ist. Es ist möglicherweise besser, die Eigenschaften der Komponenten zu verwenden:

Die Komponente [textView1] muss 50 Pixel unterhalb des Titels und 50 Pixel vom linken Rand des Containers entfernt platziert werden:

  • in [1] ist die Oberkante der Komponente in einem Abstand von 50 Pixeln an der Unterkante der Komponente [textViewTitreVue1] ausgerichtet [3] (oben);
  • in [2] ist der linke Rand (links) der Komponente in einem Abstand von 50 Pixeln [3] (links) am linken Rand des Containers ausgerichtet;

Die Komponente [editTextNom] muss 60 Pixel rechts von der Komponente [textView1] platziert und unten an dieser Komponente ausgerichtet werden;

 
  • In [1] ist die linke Kante der Komponente in einem Abstand von 60 Pixeln [2] (links) an der rechten Kante der [textView1]-Komponente ausgerichtet. Sie ist an der unteren Kante (bottom:bottom) der [textView1]-Komponente [1] ausgerichtet;

Die [buttonValider]-Komponente muss 60 Pixel rechts von der [editTextNom]-Komponente platziert und unten an dieser Komponente ausgerichtet werden;

 
  • In [1] ist die linke Kante der Komponente in einem Abstand von 60 Pixeln [2] (links) an der rechten Kante der [editTextNom]-Komponente ausgerichtet. Sie ist an der unteren Kante der [editTextNom]-Komponente ausgerichtet (bottom:bottom) [1];

Die [buttonVue2]-Komponente muss 50 Pixel unterhalb der [textView1]-Komponente positioniert und linksbündig zu dieser Komponente ausgerichtet werden;

 
  • in [1] ist die linke Kante der Komponente an der linken Kante der [textView1]-Komponente ausgerichtet und befindet sich in einem Abstand von 50 Pixeln [2] (oben) unterhalb dieser Komponente (top:bottom);

Die generierte XML-Datei 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="@string/titre_vue1"
    android:id="@+id/textViewTitreVue1"
    android:layout_marginTop="49dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"/>
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/txt_nom"
    android:id="@+id/textView1"
    android:layout_below="@+id/textViewTitreVue1"
    android:layout_alignParentLeft="true"
    android:layout_marginLeft="50dp"
    android:layout_marginTop="50dp"
    android:textSize="30sp"/>
 
  <EditText
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/editTextNom"
    android:minWidth="200dp"
    android:layout_toRightOf="@+id/textView1"
    android:layout_marginLeft="60dp"
    android:layout_alignBottom="@+id/textView1"
    android:inputType="textCapCharacters"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_valider"
    android:id="@+id/buttonValider"
    android:layout_alignBottom="@+id/editTextNom"
    android:layout_toRightOf="@+id/editTextNom"
    android:textSize="30sp"
    android:layout_marginLeft="60dp"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_vue2"
    android:id="@+id/buttonVue2"
    android:layout_below="@+id/textView1"
    android:layout_alignLeft="@+id/textView1"
    android:layout_marginTop="50dp"
    android:textSize="30sp"/>
 
</RelativeLayout>

Dies enthält alle grafischen Elemente. Eine weitere Möglichkeit, eine Ansicht zu erstellen, besteht darin, diese Datei direkt zu bearbeiten. Sobald man sich daran gewöhnt hat, kann dies schneller sein als die Verwendung des grafischen Editors.

  • In Zeile 38 befinden sich Informationen, die wir nicht angezeigt haben. Sie werden über die Eigenschaften der Komponente [editTextNom] bereitgestellt [1]:
 

Der gesamte Text stammt aus der folgenden Datei [strings.xml] [2]:


<resources>
  <string name="app_name">Exemple-04</string>
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
</resources>

Nun ändern wir die [MainActivity] so, dass diese Ansicht beim Start der App angezeigt wird:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import org.androidannotations.annotations.EActivity;
 
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
}
  • Zeile 7: Die Ansicht [vue1.xml] wird nun von der Aktivität angezeigt;

Ändern Sie die Datei [AndroidManifest.xml] wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>
</manifest>
  • Zeile 12: Diese Konfigurationszeile verhindert, dass die Tastatur erscheint, sobald die Ansicht [vue1] angezeigt wird. Der Grund dafür ist, dass die Ansicht ein Eingabefeld enthält, das den Fokus hat, wenn die Ansicht angezeigt wird. Standardmäßig führt dieser Fokus dazu, dass die virtuelle Tastatur erscheint;

Führen Sie die Anwendung aus und überprüfen Sie, ob die Ansicht [view1.xml] tatsächlich angezeigt wird:

Image

1.5.3. Ereignisbehandlung

Behandeln wir nun den Klick auf die Schaltfläche [Validate] in der Ansicht [View1]:

Image

Der Code für [MainActivity] ändert sich wie folgt:


package exemples.android;
 
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
 
@EActivity(R.layout.vue1)
public class MainActivity extends AppCompatActivity {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews(){
    Log.d("MainActivity","afterViews");
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(this, String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
}
  • Zeilen 17–18: Wir verknüpfen das Feld [protected EditText editTextNom] mit der Komponente, die in der visuellen Oberfläche durch [R.id.editTextNom] identifiziert wird. Das mit der Komponente verknüpfte Feld muss in der abgeleiteten Klasse [MainActivity_] zugänglich sein und darf aus diesem Grund keinen [private]-Gültigkeitsbereich haben. Das durch [R.id.editTextNom] identifizierte Feld stammt aus der Ansicht [vue1.xml]:

  <EditText
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/editTextNom"
    android:minWidth="200dp"
    android:layout_toRightOf="@+id/textView1"
    android:layout_marginLeft="60dp"
    android:layout_alignBottom="@+id/textView1"
    android:inputType="textCapCharacters"/>

Hinweis: Verwenden Sie keine Zeichen mit Akzenten in [id]-Bezeichnern. AA verarbeitet diese nicht korrekt.

  • Zeile 32: Die Annotation [@Click(R.id.buttonValider)] gibt die Methode an, die das „Click“-Ereignis auf der Schaltfläche mit der ID [R.id.buttonValider] verarbeitet. Diese ID stammt ebenfalls aus der Ansicht [vue1.xml]:

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_valider"
    android:id="@+id/buttonValider"
    android:layout_alignBottom="@+id/editTextNom"
    android:layout_toRightOf="@+id/editTextNom"
    android:textSize="30sp"
    android:layout_marginLeft="60dp"/>
  • Zeile 35: Zeigt den eingegebenen Namen an:
    • Toast.makeText(...).show(): zeigt Text auf dem Bildschirm an,
    • der erste Parameter von makeText ist die Aktivität,
    • der zweite Parameter ist der Text, der in dem von makeText angezeigten Dialogfeld erscheinen soll,
    • der dritte Parameter ist die Dauer der Anzeige: Toast.LENGTH_LONG oder Toast.LENGTH_SHORT;
  • Zeile 26: Die Annotation [@AfterViews] kennzeichnet die Methode, die ausgeführt werden soll, sobald alle mit [@ViewById] annotierten Felder initialisiert wurden. Es ist wichtig zu wissen, wann diese Felder initialisiert werden. Können wir beispielsweise die Referenz aus Zeile 18 in der [onCreate]-Methode verwenden? Um diese Frage zu beantworten, haben wir Logs hinzugefügt;

Führen Sie das Projekt [Example-04] aus und überprüfen Sie, ob etwas passiert, wenn Sie auf die Schaltfläche [Validate] klicken. Wir erhalten die folgenden Log-Einträge:

05-28 09:06:23.751 571-571/exemples.android D/MainActivity: onCreate
05-28 09:06:23.841 571-571/exemples.android D/MainActivity: afterViews

Wir kommen zu dem Schluss, dass die mit [@ViewById] annotierten Felder zum Zeitpunkt der Ausführung der [onCreate]-Methode noch nicht initialisiert sind. Auch hier wird Anfängern empfohlen, diese Art von Log-Ausgaben in die Methoden einzubauen, die den Lebenszyklus der Anwendung verwalten.

1.6. Beispiel-05: Navigation zwischen Ansichten

Im vorherigen Projekt wurde die Schaltfläche [View 2] nicht verwendet. Wir schlagen vor, sie zu nutzen, indem wir eine zweite Ansicht erstellen und zeigen, wie man zwischen Ansichten navigiert. Es gibt mehrere Möglichkeiten, dieses Problem zu lösen. Der hier vorgeschlagene Ansatz besteht darin, jede Ansicht mit einer Aktivität zu verknüpfen. Eine andere Methode ist die Verwendung einer einzigen [AppCompatActivity], die [Fragment]-Ansichten anzeigt. Dies wird die Methode sein, die in zukünftigen Anwendungen verwendet wird.

1.6.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-04] in [Beispiel-05]. Dazu folgen wir der Vorgehensweise, die in Abschnitt 1.4 für das Duplizieren von [Beispiel-02] in [Beispiel-03] beschrieben und in Abschnitt 1.5 wiedergegeben wurde.

1.6.2. Hinzufügen einer zweiten Aktivität

Um eine zweite Ansicht zu verwalten, erstellen wir eine zweite Aktivität. Diese Aktivität verwaltet Ansicht Nr. 2. Wir folgen hier einem Modell mit einer Ansicht pro Aktivität. Andere Modelle sind möglich.

Image

  • In [1-4] erstellen wir eine neue Aktivität;

Image

  • in [5] den Namen der Klasse, die generiert wird;
  • in [6] den Namen der Ansicht (view2.xml), die mit der neuen Aktivität verknüpft ist;
  
  • in [7-8] die von der vorherigen Konfiguration betroffenen Dateien;

Die Aktivität [SecondActivity] sieht wie folgt aus:


package exemples.android;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
 
public class SecondActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.vue2);
  }
}
  • Zeile 11: Die Aktivität ist mit der Ansicht [vue2.xml] verknüpft;

Die Ansicht [vue2.xml] sieht wie folgt aus:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="exemples.android.SecondActivity">
 
</RelativeLayout>

Dies ist derzeit eine leere Ansicht mit einem [RelativeLayout]-Layout-Manager (Zeile 2). In Zeile 11 sehen wir, dass sie der neuen Aktivität zugeordnet wurde.

Das Manifest des Android-Moduls [AndroidManifest.xml] hat sich wie folgt geändert:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity android:name=".SecondActivity">
    </activity>
  </application>
 
</manifest>

Zeile 20: Eine zweite Aktivität wurde registriert.

1.6.3. Navigieren von Ansicht 1 zu Ansicht 2

Kehren wir zum Code der Klasse [MainActivity] zurück, die Ansicht 1 anzeigt. Der Übergang zu Ansicht 2 ist derzeit nicht implementiert:

  

Wir gehen dabei wie folgt vor:


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // navigate to view no. 2 by passing it the name entered in view no. 1
    // create an Intent
    Intent intent = new Intent();
    // we associate this Intent with an activity
    intent.setClass(this, SecondActivity.class);
    // we associate information with this Intent
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // launch the [SecondActivity] activity by passing it the Intent
    startActivity(intent);
}
  • Zeilen 2–3: Die Methode [navigateToView2] verarbeitet den Klick auf die Schaltfläche, die durch [R.id.buttonVue2] identifiziert wird und in der Ansicht [vue1.xml] definiert ist:

  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_vue2"
    android:id="@+id/buttonVue2"
    android:layout_below="@+id/textView1"
    android:layout_alignLeft="@+id/textView1"
    android:layout_marginTop="50dp"
android:textSize="30sp"/>

Die Kommentare beschreiben die Schritte, die für die Änderung der Ansicht zu befolgen sind:

  1. Zeile 6: Erstellen Sie ein Objekt vom Typ [Intent]. Dieses Objekt gibt sowohl die zu startende Aktivität als auch die an sie zu übergebenden Informationen an;
  2. Zeile 8: Verknüpfen Sie den Intent mit einer Aktivität, in diesem Fall einer Aktivität vom Typ [SecondActivity], die für die Anzeige von Ansicht Nr. 2 zuständig ist. Denken Sie daran, dass die [MainActivity] Ansicht Nr. 1 anzeigt. Wir haben also eine Ansicht = eine Aktivität. Wir müssen den Typ [SecondActivity] definieren;
  3. Zeile 10: Fügen Sie optional Informationen zum [Intent]-Objekt hinzu. Diese Informationen sind für die [SecondActivity] bestimmt, die gestartet wird. Die Parameter für [Intent.putExtra] lauten (Object key, Object value). Beachten Sie, dass die Methode [EditText.getText()], die den in das Textfeld eingegebenen Text zurückgibt, nicht den Typ [String], sondern den Typ [Editable] zurückgibt. Sie müssen die Methode [toString] verwenden, um den eingegebenen Text zu erhalten;
  4. Zeile 12: Starten Sie die durch das [Intent]-Objekt definierte Aktivität.

Führen Sie das Projekt [Example-05] aus und überprüfen Sie, ob Ansicht Nr. 2 angezeigt wird (derzeit leer):

1.6.4. Ansicht Nr. 2 erstellen

 
  • In [1-2] entfernen wir die Ansicht [main.xml], die wir nicht mehr benötigen, und ändern dann die Ansicht [vue2.xml] wie folgt:
 

Die Komponenten lauten wie folgt:

Nr.
ID
Typ
Rolle
1
textViewTitleView2
TextView
Ansichtstitel
2
textViewHello
TextView
ein Text
5
btn_view1
Schaltfläche
um zu Ansicht Nr. 1 zu gelangen

Die XML-Datei [vue2.xml] lautet wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="@dimen/activity_horizontal_margin"
  android:paddingRight="@dimen/activity_horizontal_margin"
  android:paddingTop="@dimen/activity_vertical_margin"
  android:paddingBottom="@dimen/activity_vertical_margin"
  tools:context="exemples.android.SecondActivity">
 
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:text="@string/titre_vue2"
    android:id="@+id/textViewTitreVue2"
    android:layout_marginTop="50dp"
    android:textSize="50sp"
    android:layout_gravity="center|left"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"/>
 
  <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/textViewBonjour"
    android:layout_centerVertical="true"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/textViewTitreVue2"
    android:layout_marginTop="50dp"
    android:layout_marginLeft="50dp"
    android:textSize="30sp"
    android:text="Bonjour !"
    android:textColor="#ffffb91b"/>
 
  <Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/btn_vue1"
    android:id="@+id/buttonVue1"
    android:layout_marginTop="50dp"
    android:textSize="30sp"
    android:layout_alignLeft="@+id/textViewBonjour"
    android:layout_below="@+id/textViewBonjour"/>
 
</RelativeLayout>

Führen Sie das Projekt [Example-05] aus und überprüfen Sie, ob die neue Ansicht angezeigt wird, wenn Sie auf die Schaltfläche [View #2] klicken.

1.6.5. Die Aktivität [SecondActivity]

In [MainActivity] haben wir den folgenden Code geschrieben:


    // navigate to view no. 2
    protected void navigateToView2() {
        // navigate to view no. 2 by passing it the name entered in view no. 1
        // create an Intent
        Intent intent = new Intent();
        // we associate this Intent with an activity
        intent.setClass(this, SecondActivity.class);
        // we associate information with this Intent
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // launch the [SecondActivity] activity by passing it the Intent
        startActivity(intent);
}

In Zeile 9 haben wir Informationen an [SecondActivity] übergeben, die nicht verwendet wurden. Wir nutzen sie nun, und dies geschieht im Code für [SecondActivity]:

  

Der Code für [SecondActivity] ändert sich wie folgt:


package exemples.android;
 
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.ViewById;
 
@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {
 
  // visual interface components
  @ViewById
  protected TextView textViewBonjour;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }
 
  @AfterViews
  protected void afterViews() {
    // recover intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // we retrieve the name
        String nom = extras.getString("NOM");
        if (nom != null) {
          // we display it
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }
 
}
  • Zeile 11: Wir verwenden die Annotation [@EActivity], um anzugeben, dass die Klasse [SecondActivity] eine Aktivität ist, die mit der Ansicht [vue2.xml] verknüpft ist;
  • Zeilen 15–16: Wir rufen eine Referenz auf die [TextView]-Komponente ab, die durch [R.id.textViewBonjour] identifiziert wird. Hier haben wir [@ViewById(R.id.textViewBonjour)] nicht geschrieben. In diesem Fall geht AA davon aus, dass die Kennung der Komponente mit dem annotierten Feld identisch ist, hier dem Feld [textViewBonjour];
  • Zeile 23: Die Annotation [@AfterViews] kennzeichnet eine Methode, die ausgeführt werden muss, nachdem die mit [@ViewById] annotierten Felder initialisiert wurden. In der Methode [OnCreate] (Zeile 19) können diese Felder nicht verwendet werden, da sie noch nicht initialisiert wurden. Im Projekt [Example-05] wechseln wir von einer Aktivität zu einer anderen, und es war zunächst unklar, ob die mit [@AfterViews] annotierte Methode einmalig bei der ersten Instanziierung der Aktivität oder bei jedem Start der Aktivität ausgeführt wird. Tests haben gezeigt, dass die zweite Hypothese zutrifft;
  • Zeile 26: Die Klasse [AppCompatActivity] verfügt über eine [getIntent]-Methode, die das mit der Aktivität verknüpfte [Intent]-Objekt zurückgibt;
  • Zeile 28: Die Methode [Intent.getExtras] gibt ein [Bundle]-Objekt zurück, bei dem es sich um eine Art Wörterbuch handelt, das Informationen enthält, die mit dem [Intent]-Objekt der Aktivität verknüpft sind;
  • Zeile 31: Wir rufen den im [Intent]-Objekt der Aktivität gespeicherten Namen ab;
  • Zeile 34: Wir zeigen ihn an.

Zur Erinnerung: Felder, die mit der Annotation [@ViewById] versehen sind, dürfen keine Zeichen mit Akzenten enthalten.

Kehren wir zur Klasse [SecondActivity] zurück. Da wir geschrieben haben:


@EActivity(R.layout.vue2)
public class SecondActivity extends AppCompatActivity {

AA generiert eine von [SecondActivity] abgeleitete Klasse [SecondActivity_], und diese Klasse ist die eigentliche Aktivität. Dies führt dazu, dass wir Änderungen vornehmen müssen in:

[MainActivity]


  // navigate to view no. 2
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // we associate this Intent with an activity
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • In Zeile 6 müssen wir [SecondActivity] durch [SecondActivity_] ersetzen;

[AndroidManifest.xml]


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:windowSoftInputMode="stateHidden">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
 
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
    <activity android:name=".SecondActivity_">
    </activity>
  </application>
 
</manifest>
  • Ersetzen Sie in Zeile 20 [SecondActivity] durch [SecondActivity_];

Testen Sie diese neue Version. Geben Sie einen Namen in Ansicht Nr. 1 ein und überprüfen Sie, ob Ansicht Nr. 2 ihn korrekt anzeigt.

1.6.6. Navigieren von Ansicht Nr. 2 zu Ansicht Nr. 1

Um von Ansicht #2 zu Ansicht #1 zu navigieren, gehen wir wie zuvor beschrieben vor:

  • Fügen Sie den Navigationscode in die Aktivität [SecondActivity] ein, die Ansicht 2 anzeigt;
  • Schreiben Sie die Methode [@AfterViews] in die [MainActivity], die Ansicht 1 anzeigt;

Der Code für [SecondActivity] ändert sich wie folgt:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // we create an Intent for activity [MainActivity]
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // retrieve the Intent of the current activity [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // we put the name in the Intent of [MainActivity]
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // launch [MainActivity]
      startActivity(intent1);
    }
}
  • Zeilen 1–2: Verknüpfen Sie die Methode [navigateToView1] mit einem Klick auf die Schaltfläche [btn_vue1];
  • Zeile 4: Wir erstellen ein neues [Intent];
  • Zeile 5: Verknüpfung mit der Aktivität [MainActivity_];
  • Zeile 7: Rufen Sie den mit [SecondActivity] verknüpften Intent ab;
  • Zeile 9: Rufen Sie die Informationen aus diesem Intent ab;
  • Zeile 12: Der Schlüssel [NAME] wird aus [intent2] abgerufen und mit demselben zugehörigen Wert in [intent1] abgelegt;
  • Zeile 15: Die Aktivität [MainActivity_] wird gestartet.

Im Code für [MainActivity] fügen wir die folgende [@AfterViews]-Methode hinzu:


  @AfterViews
  protected void afterViews() {
    // recover intent if it exists
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // we retrieve the name
        String nom = extras.getString("NOM");
        if (nom != null) {
          // we display it
          editTextNom.setText(nom);
        }
      }
    }
}

Nehmen Sie diese Änderungen vor und testen Sie Ihre App. Wenn Sie nun von Ansicht 2 zu Ansicht 1 zurückkehren, sollte der ursprünglich eingegebene Name angezeigt werden, was bisher nicht der Fall war.

1.6.7. Lebenszyklus einer Aktivität

In Abschnitt 1.3.5 haben wir den Lebenszyklus einer Aktivität vorgestellt. Hier haben wir zwei Aktivitäten, zwischen denen wir während der Ausführung wechseln. Diese Aktivitäten enthalten zwei Methoden – [onCreate] und [afterViews] – und es ist nicht sofort klar, wann die eine im Verhältnis zur anderen aufgerufen wird. Es ist wichtig, dies zu wissen. Um das herauszufinden, fügen wir beiden Aktivitäten Logs hinzu:

In der Klasse [MainActivity] schreiben wir also:


  // manufacturer
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity", "onCreate");
    ...
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
    ...
    }
}
  • Zeilen 2–4: Wir möchten wissen, ob die Klasse [MainActivity] einmal oder mehrmals instanziiert wird;
  • Zeile 8: Wir möchten wissen, ob die Methode [onCreate] einmal oder mehrmals aufgerufen wird;
  • Zeile 14: Wir möchten wissen, ob die Methode [afterViews] einmal oder mehrmals aufgerufen wird;

Genau dasselbe machen wir in der Klasse [SecondActivity].

Beim Start der App sehen wir die folgenden Protokolleinträge:

1
2
3
05-28 09:38:09.429 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:38:09.449 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:38:09.600 26711-26711/exemples.android D/MainActivity: afterViews

Die Methoden [onCreate, afterViews] der ersten Aktivität wurden in dieser Reihenfolge ausgeführt. Wenn Sie auf die Schaltfläche [View #2] klicken, lauten die neuen Protokolleinträge wie folgt:

1
2
3
05-28 09:39:26.607 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:39:26.608 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:39:26.617 26711-26711/exemples.android D/SecondActivity: afterViews

Die Methoden [onCreate, afterViews] der zweiten Aktivität wurden in dieser Reihenfolge ausgeführt. Wenn Sie auf die Schaltfläche [View #1] klicken, lauten die neuen Protokolleinträge wie folgt:

1
2
3
05-28 09:39:56.393 26711-26711/exemples.android D/MainActivity: constructor
05-28 09:39:56.394 26711-26711/exemples.android D/MainActivity: onCreate
05-28 09:39:56.400 26711-26711/exemples.android D/MainActivity: afterViews

Die Klasse [MainActivity] wird daher erneut instanziiert. Wenn Sie auf die Schaltfläche [View #2] klicken, lauten die neuen Protokolleinträge wie folgt:

1
2
3
05-28 09:40:59.099 26711-26711/exemples.android D/SecondActivity: constructor
05-28 09:40:59.102 26711-26711/exemples.android D/SecondActivity: onCreate
05-28 09:40:59.113 26711-26711/exemples.android D/SecondActivity: afterViews

Die Klasse [SecondActivity] wird daher erneut instanziiert.

Beide Aktivitäten werden daher bei jedem Wechsel der Aktivität systematisch neu erstellt.

Wir werden nun eine Architektur mit einer einzigen Aktivität untersuchen, die mehrere Ansichten, sogenannte Fragmente, verwalten kann. Die Aktivität und die Ansichten werden nur einmal instanziiert, im Gegensatz zur vorherigen Methode, bei der eine Aktivität mehrfach instanziiert werden konnte.

1.7. Beispiel-06: Tab-Navigation

Hier werden wir Schnittstellen mit Registerkarten untersuchen. Das Beispiel ist komplex, führt jedoch alle Elemente ein, die wir später verwenden werden: einzelne Aktivität, Fragment-Manager (Ansichten), Fragment-Container, Navigation zwischen Fragmenten. Das Konzept der Registerkarten unterscheidet sich von dem der Fragmente und ist für das, was wir in diesem Beispiel demonstrieren wollen, zweitrangig.

1.7.1. Erstellen des Projekts

Wir erstellen ein neues Projekt:

 
  • Wählen Sie in [7] eine Aktivität mit Registerkarten (Tabbed Activity) aus;
  • Behalten Sie in [10–14] die Standardwerte bei;
  • Wählen Sie in [15] Registerkarten mit einer Titelleiste aus;

Das resultierende Projekt sieht wie folgt aus:

 
  • in [1], die Aktivität;
  • in [2], die Ansichten;

Eine nach dem Modul benannte Laufzeitkonfiguration [app] wurde automatisch erstellt [2b]:

 

Sie können sie ausführen. Daraufhin erscheint ein Fenster mit drei Registerkarten [3-6]:

Image

1.7.2. Gradle-Konfiguration

Das Projekt [Beispiel-06] wurde mit der folgenden [build.gradle]-Datei generiert:

 

apply plugin: 'com.android.application'
 
android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
dependencies {
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
}

Im Vergleich zu dem, was wir bisher gesehen haben, gibt es ein neues Element: Zeile 25. Diese Bibliothek wird für die neuen Komponenten benötigt, die von der generierten Anwendung verwendet werden.

1.7.3. Die Ansicht [activity_main]

  

Die Ansicht [activity_main] ist die Ansicht, die mit der [MainActivity] des Projekts verknüpft ist. Im [Design]-Modus sieht die Ansicht wie folgt aus:

Image

Sie enthält die folgenden Komponenten:

  
  • [main_content] ist die gesamte Ansicht;
  • [appbar] (rotes Feld, 1) ist die Anwendungsleiste. Sie enthält zwei Komponenten:
    • [toolbar] (gelber Kasten 4) ist die Symbolleiste;
    • [tabs] (orangefarbener Kasten 5) ist die Registerkarten-Titelleiste;
  • [container] (grünes Feld, 2) kann verschiedene Fragmente enthalten. Ein Fragment ist eine Ansicht. Somit kann dieselbe Aktivität mehrere Ansichten (Fragmente) in diesem Container anzeigen;
  • [fab] (Komponente 3) wird als schwebende Komponente bezeichnet;

Im [text]-Modus lautet der Code wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context="exemples.android.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.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
 
  </android.support.design.widget.AppBarLayout>
 
  <android.support.v4.view.ViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
 
  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email"/>
 
</android.support.design.widget.CoordinatorLayout>

Wir sehen die zuvor beschriebenen Elemente:

  • Zeilen 2–49: die Definition der Komponente [main_content] (Zeile 5), die die gesamte Ansicht bildet. Wir sehen, dass es sich um ein [CoordinatorLayout]-Layout handelt (Zeile 2);
  • Zeilen 11–33: der [appbar]-Container (Zeile 12). Dies ist ein [AppBarLayout] (Zeile 11);
  • Zeilen 18–24: die [toolbar]-Komponente (Zeile 19) vom Typ [Toolbar] (Zeile 18);
  • Zeilen 28–31: der [tabs]-Container (Zeile 29). Dies ist ein Layout vom Typ [TabLayout] (Zeile 28). Er zeigt die Registerkartentitel an;
  • Zeilen 35–39: die [container]-Komponente (Zeile 36). Dieser Container zeigt die verschiedenen Ansichten der Aktivität an;
  • Zeilen 41–47: die [fab]-Komponente (Zeile 42) vom Typ [FloatingActionButton] (Zeile 41). Dies ist eine Schaltfläche, die angeklickt werden kann. Standardmäßig befindet sie sich unten rechts in der gesamten Ansicht;

Wir werden nicht versuchen, die Bedeutung aller Attribute dieser Komponenten zu verstehen. Wir werden sie so verwenden, wie sie sind. Durch Erfahrung – und oft im [Design]-Modus – entdecken wir ihre Rollen. In diesem Modus stellen wir fest, dass Komponenten Dutzende von Attributen haben. Im Allgemeinen werden nur einige initialisiert, während die anderen ihre Standardwerte beibehalten.

Lassen Sie uns jedoch einige Punkte klären. Die meisten Werte, die die verschiedenen Ansichten konfigurieren, sind im Ordner [res/values] zusammengefasst:

  

Auf diese Werte wird in den Zeilen 15–16, 23, 39 und 46 der Datei [activity_main.xml] verwiesen. Nehmen wir ein Beispiel:

  • Zeile 15:

    android:paddingTop="@dimen/appbar_padding_top"

Die Annotation [@dimen] verweist auf die Datei [res/values/dimens.xml]:


<resources>
  <!-- Default screen margins, per the Android Design guidelines. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
</resources>

Zeile 15 der Datei [activity_main.xml] bezieht sich auf Zeile (f) oben;

Ebenso bezieht sich die Anmerkung:

  • [@string] bezieht sich auf die Ressourcendatei [res/values/strings.xml];
  • [@color] bezieht sich auf die Ressourcendatei [res/values/colors.xml];
  • [@style] bezieht sich auf die Ressourcendatei [res/values/styles.xml];

1.7.4. Die Aktivität

  

Der für die Aktivität generierte Code entspricht der Komplexität der oben beschriebenen Ansicht: Er ist komplex. Wir werden ihn in mehreren Schritten analysieren.

1.7.4.1. Verwaltung von Fragmenten und Registerkarten

Der Code in [MainActivity] in Bezug auf Fragmente und Registerkarten lautet wie folgt:


package exemples.android;
 
import android.support.design.widget.TabLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
 
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
 
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container 
  private ViewPager mViewPager;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // parent
    super.onCreate(savedInstanceState);
    // view
    setContentView(R.layout.activity_main);
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // the fragment manager
    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 = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }
 
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • Zeile 28: Android stellt einen View-Container vom Typ [android.support.v4.view.ViewPager] bereit (Zeile 12). Dieser Container muss mit einem View- oder Fragment-Manager ausgestattet werden. Der Entwickler ist dafür verantwortlich, diesen bereitzustellen;
  • Zeile 25: der in diesem Beispiel verwendete Fragment-Manager. Seine Implementierung befindet sich in den Zeilen 61–63;
  • Zeile 31: Die Methode, die beim Erstellen der Aktivität ausgeführt wird;
  • Zeile 35: Die Ansicht [activity_main.xml] wird der Aktivität zugeordnet;
  • Zeile 37: Wir rufen die Referenz auf die [toolbar]-Komponente der Ansicht über deren Kennung ab;
  • Zeile 38: Diese Symbolleiste wird zur Aktionsleiste der Aktivität (ein Android-Konzept);
  • Zeile 40: Der Fragment-Manager wird instanziiert. Der Konstruktorparameter ist die Android-Klasse [android.support.v4.app.FragmentManager] (Zeile 10);
  • Zeile 44: Wir rufen die Referenz auf den Fragment-Container aus der Ansicht [activity_main.xml] über dessen ID ab;
  • Zeile 45: Der Fragment-Manager wird mit dem Fragment-Container verknüpft. Das bedeutet, dass der Fragment-Container, wenn er aufgefordert wird, Fragment #i anzuzeigen, dieses vom Fragment-Manager anfordert;
  • Zeile 48: Wir rufen eine Referenz auf die Tab-Leiste über deren Kennung ab;
  • Zeile 49: Der Tab-Manager wird mit dem Fragment-Container verknüpft. Das bedeutet, dass der Container das Fragment #i anzeigt, wenn auf den Tab #i geklickt wird. Durch die Verknüpfung zwischen dem Tab-Manager und dem Fragment-Container entfällt die Notwendigkeit einer Tab-Verwaltung. Daher müssen wir keinen Ereignis-Handler für das Klicken auf einen Tab definieren. Die Verknüpfung mit dem Fragment-Container stellt dies standardmäßig bereit. Wir werden ein Beispiel sehen, bei dem es mehr Fragmente als Registerkarten gibt. In diesem Fall nehmen wir diese Verknüpfung nicht vor.

Der Fragment-Handler [SectionsPagerAdapter] sieht wie folgt aus:


// the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return 3;
    }
 
    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      switch (position) {
        case 0:
          return "SECTION 1";
        case 1:
          return "SECTION 2";
        case 2:
          return "SECTION 3";
      }
      return null;
    }
  }
}
  • Die von einer App angezeigten Fragmente hängen von der App selbst ab. Der Fragment-Manager wird vom Entwickler definiert;
  • Zeile 5: Der Fragment-Manager erweitert die Android-Klasse [android.support.v4.app.FragmentPagerAdapter]. Der Konstruktor wird uns bereitgestellt. Wir müssen mindestens die folgenden zwei Methoden definieren:
    • int getCount(): gibt die Anzahl der zu verwaltenden Fragmente zurück;
    • Fragment getItem(i): gibt das Fragment Nr. i zurück;

Die Methode CharSequence getPageTitle(i), die den Titel von Fragment Nr. i zurückgibt, ist optional. Da der Tab-Manager mit dem Fragment-Manager verknüpft wurde, ist der Titel von Tab Nr. i der Titel von Fragment Nr. i. Somit sind die Titel in den Zeilen 27–33 die Tab-Titel;

  • Zeilen 18–21: getCount gibt die Anzahl der verwalteten Fragmente zurück, in diesem Fall drei;
  • Zeilen 11–15: getItem(i) gibt Fragment Nr. i zurück. Hier sind alle Fragmente vom gleichen Typ, [PlaceholderFragment];
  • Zeilen 24–35: getPageTitle(int i) gibt den Titel von Fragment #i zurück;

1.7.4.2. Die angezeigten Fragmente

  

Die Fragmente der Aktivität sind hier alle vom gleichen Typ und sind alle mit der folgenden XML-Ansicht [fragment_main] verknüpft:


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context="exemples.android.MainActivity$PlaceholderFragment">
 
  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
 
</RelativeLayout>
  • Zeilen 1–16: ein [RelativeLayout]-Layout;
  • Zeilen 11–14: die einzige Komponente der Ansicht (Fragment): ein [TextView], gekennzeichnet durch [section_label];

In [MainActivity] sind die verwalteten Fragmente vom folgenden Typ [PlaceholderFragment]:


// a fragment
  public static class PlaceholderFragment extends Fragment {
      // a text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
    }
 
    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // view [fragment_main] is instantiated
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // the [TextView] is found
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // its content is modified
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // we return the view
      return rootView;
    }
  }
  • Zeile 2: Die Klasse [PlaceholderFragment] erweitert die Android-Klasse [Fragment]. Dies ist in der Regel immer der Fall;
  • Zeile 2: Die Klasse [PlaceholderFragment] ist statisch. Mit ihrer Methode [newInstance] (Zeile 10) können Sie Instanzen vom Typ [PlaceholderFragment] abrufen;
  • Zeilen 10–19: Die Methode [newInstance] erstellt ein Objekt vom Typ [PlaceholderFragment] und gibt es zurück;
  • Zeilen 14–16: Das Fragment wird mit einem Argument erstellt;

Ein Fragment muss in Zeile 22 die Methode [onCreateView] definieren. Diese Methode muss die mit dem Fragment verknüpfte Ansicht zurückgeben.

  • Zeile 25: Die Ansicht [fragment_main.xml] ist dem Fragment zugeordnet;
  • Zeile 27: Diese Ansicht enthält eine [TextView]-Komponente, deren Referenz über ihre ID abgerufen wird;
  • Zeile 29: In der [TextView] wird Text angezeigt;
    • [getString] ist eine Methode der übergeordneten Klasse [AppCompatActivity];
    • Das erste Argument ist eine Komponenten-ID. [R.string.section_format] bezieht sich auf die ID der Komponente, die durch [section_format] in der Datei [res/values/strings.xml] (Zeile 4 unten) identifiziert wird:

<resources>
  <string name="app_name">Exemple-06</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>
  • (Fortsetzung)
    • Zeile (d) oben %1$d gibt an, dass Argument #1 (%1) als Ganzzahl ($d) formatiert werden muss;
    • das zweite Argument von [getString] ist der Wert, der dem Argument $1 in Zeile (d) oben zugewiesen werden soll;
    • [getArguments] gibt die Referenz auf das Argument-Bundle des Fragments zurück. Es ist wichtig zu beachten, dass jedes Argument mit dem folgenden Bundle erstellt wurde (Zeilen f–h):

    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // on-board info
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // result
      return fragment;
}
  • (Fortsetzung)
    • getArguments().getInt(ARG_SECTION_NUMBER) gibt daher den Wert [sectionNumber] aus den Zeilen (g) und (b) oben zurück;
  • Zeile 31: Wir geben die so erstellte Ansicht zurück;

1.7.4.3. Menüverwaltung

In der generierten Anwendung gibt es ein Menü:

  

Der Inhalt der Datei [menu_main.xml] lautet wie folgt:


<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"/>
</menu>
  • Zeilen 1–9: das Menü;
  • Zeilen 5–8: ein Menüpunkt, der durch [action_settings] (Zeile 5) identifiziert wird;
  • Zeile 6: die Bezeichnung für die Menüoption. Sie befindet sich in der Datei [res/values/strings.xml] (Zeile (c) unten:

<resources>
  <string name="app_name">Exemple-06</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>

Der obige Code entspricht der folgenden Darstellung (das Menü befindet sich oben rechts im Android-Laufzeitfenster):

 

Dieses Menü wird in der Aktivität [MainActivity] wie folgt behandelt:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      return true;
    }
 
    return super.onOptionsItemSelected(item);
}
  • Zeilen 1–6: Diese Methode wird aufgerufen, wenn das System bereit ist, das Anwendungsmenü zu erstellen. Der Eingabeparameter [Menu menu] ist ein leeres Menü, das noch keine Optionen enthält;
  • Zeile 4: Die Datei [res/menu/menu_main.xml] wird verwendet. Dem als Parameter übergebenen Objekt [Menu menu] werden die in dieser Datei definierten Menüoptionen zugewiesen;
  • Zeile 5: Es wird angegeben, dass das Menü erstellt wurde;
  • Zeilen 8–21: Die Methode [onOptionsItemSelected] wird ausgeführt, sobald eine Menüoption angeklickt wird;
  • Zeile 13: Die Referenz der angeklickten Menüoption;
  • Zeilen 16–18: Wenn die angeklickte Option diejenige mit der Kennung [action_settings] ist, wird nichts unternommen und es wird angezeigt, dass das Ereignis verarbeitet wurde (Zeile 17);
  • Zeile 20: Das Ereignis wird an die übergeordnete Klasse weitergeleitet;

Um besser zu verstehen, was mit diesem Menü geschieht, fügen wir dem vorherigen Code Log-Einträge hinzu:


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "création menu en cours");
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }
 
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}

1.7.4.4. Die schwebende Schaltfläche

Die generierte Ansicht verfügt über eine schwebende Schaltfläche:

  

Diese Komponente ist in der Hauptansicht [activity-main.xml] definiert:


  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
android:src="@android:drawable/ic_dialog_email"/>

Zeile 7 verweist auf ein vom Android-Framework bereitgestelltes Bild, genauer gesagt auf einen Briefumschlag.

Diese Komponente wird in der Klasse [MainActivity] wie folgt behandelt:


    // floating button
    FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
});
  • Zeile 2: Abrufen der Referenz des schwebenden Buttons in der Ansicht, die mit der Aktivität (activity_main) verknüpft ist;
  • Zeilen 3–9: Wir weisen ihm einen Handler zu, um Klicks darauf zu verarbeiten;
  • Zeile 6: Die Klasse [Snackbar] ermöglicht es Ihnen, mithilfe der Methode [Snackbar.make] temporäre Meldungen in der Ansicht anzuzeigen. Das erste Argument ist eine Ansicht, in der [Snackbar] nach einer übergeordneten Ansicht sucht, in der die Meldung angezeigt werden soll. Hier ist [view] die Ansicht des angeklickten Umschlags (Zeile 5). Die gefundene übergeordnete Ansicht ist die Ansicht [activity_main]. Das zweite Argument ist die anzuzeigende Meldung. Das dritte Argument ist die Anzeigedauer (SHORT oder LONG);
  • Zeile 7: Sie können auf die angezeigte Meldung klicken, um eine Aktion auszulösen. Hier ist dem Klicken auf die Meldung keine Aktion zugeordnet. Schließlich zeigt die Methode [show] die Meldung an;

Ein Klick auf die schwebende Schaltfläche führt zu folgendem visuellen Ergebnis:

 

1.7.5. Ausführen des Projekts

Nachdem wir nun die Details des generierten Codes erläutert haben, können wir dessen Ausführung besser verstehen:

Image

Wenn Sie auf die Registerkarte #i klicken, wird das Fragment #i im Ansichts-Container angezeigt. Dies geht aus dem in [4] angezeigten Text hervor. Sie können außerdem sehen, dass Sie zwischen den Registerkarten wechseln können, indem Sie die Ansicht mit der Maus nach rechts oder links wischen. Wir werden sehen, dass dieses Verhalten gesteuert werden kann.

Wenn Sie auf die Menüoption unter [6] klicken, erhalten Sie die folgenden Protokolle:

 

1.7.6. Lebenszyklus eines Fragments

  • In [1] sehen wir, dass die Methode [onCreateView] und die nachfolgenden Methoden ausgeführt werden, wenn das Fragment zum ersten Mal angezeigt wird und jedes Mal, wenn die Aktivität es neu zeichnen muss;

Um den Lebenszyklus der Aktivität und der Fragmente zu verfolgen, fügen wir dem [MainActivity]-Code die folgenden Log-Einträge hinzu:


// manufacturer
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // parent
    super.onCreate(savedInstanceState);
...
  }
 
  // a fragment
  public static class PlaceholderFragment extends Fragment {
    // a text displayed in the fragment
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }
 
    // renders a fragment with one piece of information: the fragment number passed as a parameter
    public static PlaceholderFragment newInstance(int sectionNumber) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", sectionNumber));
      // fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      ...
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
      Log.d("PlaceholderFragment", String.format("newInstance %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      ...
    }
  }
 
 
}

Wir führen das Projekt erneut aus. Die ersten Protokolle lauten wie folgt:

1
2
3
4
5
6
7
8
9
05-28 10:44:32.622 29371-29371/exemples.android D/MainActivity: constructor
05-28 10:44:32.626 29371-29371/exemples.android D/MainActivity: onCreate
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 1
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: newInstance 2
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:44:32.759 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 10:44:32.760 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 10:44:33.349 29371-29371/exemples.android D/menu: création menu en cours
  • Zeile 1: Erstellen der Aktivität;
  • Zeile 2: Ausführung der Methode [onCreate];
  • Zeilen 3–4: Instanziierung von Fragment Nr. 1;
  • Zeilen 5–6: Instanziierung von Fragment Nr. 2;
  • Zeile 7: Initialisierung von Fragment Nr. 2;
  • Zeile 8: Initialisierung von Fragment Nr. 1;
  • Zeile 9: Erstellung des Aktivitätsmenüs;

Hier müssen wir uns den Code in Erinnerung rufen, der für die Erstellung der Fragmente zuständig ist:


  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
      // instantiate a fragment [PlaceHolder] and render it
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • Zeilen 11–15: Ein Fragment wird jedes Mal, wenn der Fragment-Container eines anfordert, durch [newInstance] instanziiert;

Die obigen Protokolle zeigen, dass die ersten beiden Fragmente instanziiert und initialisiert wurden.

Klicken wir nun auf Registerkarte Nr. 2. Die neuen Protokolle lauten wie folgt:

1
2
3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: newInstance 3
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: constructor
05-28 10:47:15.566 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 3
  • Zeilen 1–3: Fragment Nr. 3 wird instanziiert und initialisiert. Beachten Sie, dass Fragment Nr. 2 dasjenige ist, das angezeigt wird;

Klicken wir nun auf Registerkarte Nr. 3. Hier sind keine Protokolle zu sehen. Das liegt wahrscheinlich daran, dass Fragment Nr. 3, das angezeigt werden soll, bereits instanziiert wurde. Kehren wir nun zu Registerkarte Nr. 1 zurück. Die Protokolle lauten wie folgt:

05-28 10:48:26.630 29371-29371/exemples.android D/PlaceholderFragment: onCreateView 1

Fragment Nr. 1 wird nicht erneut instanziiert, aber seine [onCreateView]-Methode wird erneut ausgeführt. Dieses Verhalten tritt auch bei den beiden anderen Fragmenten auf.

Aus diesen Protokollen lässt sich folgern, dass:

  • die Aktivität wurde einmal instanziiert und initialisiert;
  • jedes Fragment wurde einmal instanziiert;
  • die [onCreateView]-Methode jedes Fragments wurde mehrfach ausgeführt;

Was Sie wissen müssen – und was die Protokolle bestätigen – ist, dass standardmäßig, wenn Fragment #i angezeigt wird, die Fragmente i-1 und i+1 instanziiert werden, sofern sie nicht bereits instanziiert sind. Dies erklärt beispielsweise, warum beim Start, obwohl Fragment #1 angezeigt werden sollte, die Fragmente 1 und 2 instanziiert und initialisiert wurden. Die Protokolle zeigen auch, dass die Methode [getItem(i)] nur einmal aufgerufen wird, selbst wenn Fragment #i mehrfach angezeigt wird. Es scheint also, dass der Fragment-Container [ViewPager], der das [SectionsPagerAdapter]-Fragment #i anzeigen soll, dieses einmal vom Fragment-Manager [ ] anfordert. Danach fordert er es nicht erneut an und verwendet weiterhin das bereits erhaltene.

Schließlich liefern die Protokolle Informationen über die [onCreateView]-Methode der Fragmente:

  • Beim Start wurden die Fragmente 1 und 2 instanziiert und ihre [onCreateView]-Methode ausgeführt;
  • Beim Wechsel von Fragment 1 zu Fragment 2 wird die [onCreateView]-Methode von Fragment 2 nicht erneut ausgeführt. Daher kann sie nicht zur Aktualisierung von Fragment 2 verwendet werden. Es kann jedoch sein, dass der Benutzer in Fragment 1 eine Operation durchgeführt hat, deren Ergebnis von Fragment 2 angezeigt werden sollte. Wir sehen, dass die [onCreateView]-Methode nicht zur Aktualisierung von Fragment 2 verwendet werden kann. Wir müssen eine andere Lösung finden;

1.8. Beispiel-07: Beispiel-06, umgeschrieben unter Verwendung der [AA]-Bibliothek

1.8.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-06] in [Beispiel-07], um in letzterem Android-Annotationen einzuführen. Befolgen Sie dazu die Vorgehensweise in Abschnitt 1.4. Wir erhalten das folgende Ergebnis:

1.8.2. Gradle-Konfiguration

 

Wir aktualisieren die Datei [build.gradle] wie folgt:


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 {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }
}
 
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
}

Wir haben die für die Verwendung der [Android Annotations]-Bibliothek erforderliche Konfiguration hinzugefügt (siehe Abschnitt 1.4).

1.8.3. Hinzufügen der ersten AA-Annotationen

Wir erstellen AA-Annotationen in [MainActivity]:

  

Die Klasse [MainActivity] ändert sich wie folgt:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;
 
 
  // manufacturer
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    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 issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
  }
  • Zeile 1: Die Annotation [@EActivity] macht [MainActivity] zu einer von AA verwalteten Klasse. Ihr Parameter [R.layout.activity_main] ist der Bezeichner der Ansicht [activity_main.xml], die der Aktivität zugeordnet ist;
  • Zeilen 11–12: Die durch [R.id.tabs] identifizierte Komponente wird in das Feld [tabLayout] injiziert. Dies ist der Tab-Manager;
  • Zeilen 14–15: Die durch [R.id.fab] identifizierte Komponente wird in das Feld [fab] injiziert. Dies ist die schwebende Schaltfläche;
  • Zeilen 23–50: Der Code, der zuvor in der Methode [onCreate] stand, wird in eine Methode mit einem beliebigen Namen verschoben, die jedoch mit [@AfterViews] annotiert ist (Zeile 23). In der so annotierten Methode können wir sicher sein, dass alle mit [@ViewById] annotierten visuellen Schnittstellenkomponenten initialisiert wurden;
  • Wir haben außerdem Logs hinzugefügt, um den Lebenszyklus der Aktivität anzuzeigen;

Denken Sie daran, dass die Annotation [@EActivity] eine Klasse [MainActivity_] generiert, die die eigentliche Aktivität des Projekts darstellt. Daher müssen Sie die Datei [AndroidManifest.xml] wie folgt ändern:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:label="@string/app_name"
      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 12: die neue Aktivität.

Führen Sie das Projekt nun erneut aus und überprüfen Sie, ob die Oberfläche mit den Registerkarten weiterhin angezeigt wird.

1.8.4. Umschreiben der Fragmente

Wir werden uns ansehen, wie Fragmente im Projekt verwaltet werden. Derzeit ist die Klasse [PlaceholderFragment] eine statische innere Klasse der Aktivität [MainActivity]. Wir werden zu einem gängigeren Anwendungsfall zurückkehren, bei dem Fragmente in externen Klassen definiert werden. Außerdem führen wir AA-Annotationen für Fragmente ein.

Das Projekt [Example-07] entwickelt sich wie folgt weiter:

  

Oben sehen wir die Klasse [PlaceholderFragment], die aus der Klasse [MainActivity] herausgelöst wurde. Sie wurde wie folgt umgeschrieben:


package exemples.android;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
  @AfterViews
  protected void afterViews() {
    Log.d("PlaceholderFragment", String.format("afterViews %s", getArguments().getInt(ARG_SECTION_NUMBER)));
  }
 
 
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    Log.d("PlaceholderFragment", String.format("onCreateView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    return super.onCreateView(inflater, container, savedInstanceState);
  }
 
  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    if (textViewInfo != null) {
      Log.d("PlaceholderFragment", String.format("onResume setText %s", getArguments().getInt(ARG_SECTION_NUMBER)));
      textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
    }
  }
}
  • Zeile 15: Das Fragment ist mit der Annotation [@EFragment] versehen, deren Parameter die Kennung der mit dem Fragment verknüpften XML-Ansicht ist, in diesem Fall die Ansicht [fragment_main.xml];
  • Zeilen 19–20: Fügen Sie in das Feld [textViewInfo] den Verweis auf die Komponente in [fragment_main.xml] ein, die durch [R.id.section_label] identifiziert wird und vom Typ [TextView] ist (Zeile (l) unten):

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context="exemples.android.MainActivity$PlaceholderFragment">
 
  <TextView
    android:id="@+id/section_label"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
 
</RelativeLayout>
  • Zeilen 42–52: Die Methode [onResume] wird ausgeführt, bevor die mit dem Fragment verknüpfte Ansicht angezeigt wird. Sie kann verwendet werden, um die anzuzeigende Benutzeroberfläche zu aktualisieren;
  • Zeile 47: Sie müssen die gleichnamige Methode in der übergeordneten Klasse aufrufen;
  • Zeile 49: Es ist unklar, ob die Methode [onResume] ausgeführt werden kann, bevor das Feld in Zeile 20 initialisiert wurde. Die Logs, die zur Verfolgung des Lebenszyklus des Fragments eingerichtet wurden, werden uns dies zeigen. Vorläufig führen wir vorsichtshalber eine Nullprüfung durch;
  • Zeile 51: Wir aktualisieren die Informationen im Feld [textViewInfo] mit dem Integer-Argument, das dem Fragment bei seiner Erstellung übergeben wurde;

Die Klasse [MainActivity] verliert ihre innere Klasse [PlaceholderFragment] und sieht, wie sich ihr Fragment-Manager wie folgt entwickelt:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // number of fragments
    private static final int FRAGMENTS_COUNT = 3;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; 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);
      }
    }
 
    // fragment n° position
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
 
    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
    }
  }
  • Zeile 4: Die Fragmente werden in einem Array abgelegt;
  • Zeilen 16–23: Das Fragment-Array wird im Konstruktor initialisiert. Sie sind vom Typ [PlaceholderFragment_] (Zeile 18) und nicht vom Typ [PlaceholderFragment]. Die Klasse [PlaceholderFragment] wurde tatsächlich mit einer AA-Annotation versehen und generiert eine von [PlaceholderFragment] abgeleitete Klasse [PlaceholderFragment_], und genau diese Klasse muss die Aktivität verwenden. Jedem erstellten Fragment wird ein ganzzahliges Argument übergeben, das vom Fragment angezeigt wird;
  • Zeilen 42–45: Wir haben die Fragmenttitel geändert. Da dies auch die Registerkartentitel sind, sollten wir eine Änderung in der Registerkartenleiste sehen;

Kompilieren wir [Make] [1] dieses Projekt:

 
  • In [2] sehen wir, dass sich die von der AA-Bibliothek generierten Klassen im Ordner [app / build / generated / source / apt / debug] befinden (Sie müssen sich in der [Projekt]-Perspektive befinden, um [2] zu sehen);

Führen Sie das Projekt [Example-07] aus und überprüfen Sie, ob es noch funktioniert.

1.8.5. Überprüfen der Protokolle

Beim Start der Anwendung sehen die Protokolle wie folgt aus:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours
  • Zeile 1: Aufbau der einzelnen Aktivität;
  • Zeile 2: die Methode [afterViews] der Aktivität: ihre mit [@ViewById] annotierten Felder werden initialisiert;
  • Zeilen 3–5: Erstellung der drei Fragmente;
  • Zeilen 6–7: Der Fragment-Container [ViewPager] fordert die ersten beiden Fragmente an;
  • Zeilen 8–9: Methoden von Fragment 2;
  • Zeilen 10–11: Methoden von Fragment 1;
  • Zeilen 12–13: [onResume]-Methode von Fragment 1;
  • Zeilen 14–15: [onResume]-Methode von Fragment 2;
  • Zeile 16: Erstellung des Aktivitätsmenüs;

Beachten Sie, dass dies eine zuvor gestellte Frage beantwortet: Die [onResume]-Methode von Fragment 1 (Zeile 12) wird beispielsweise nach der [afterViews]-Methode des Fragments (Zeile 11) ausgeführt. Wenn die [onResume]-Methode ausgeführt wird, kann sie daher die mit [@ViewById] annotierten Felder verwenden. Wir können die [onResume]-Methode nun wie folgt schreiben:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onResume();
    // display
    textViewInfo.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
}

Wechseln wir nun von Registerkarte 1 zu Registerkarte 2. Die neuen Protokolle lauten wie folgt:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3
  • Zeile 1: Der Fragment-Container [ViewPager] fordert Fragment Nr. 3 an;
  • Zeilen 2–3: Methoden von Fragment Nr. 3. Beachten Sie, dass dieses Fragment beim Start der Anwendung instanziiert wurde;
  • Zeilen 4–5: Die Methode [onResume] von Fragment Nr. 3 wird ausgeführt. Beachten Sie, dass derzeit Fragment Nr. 2 angezeigt wird;

Wechseln wir nun von Registerkarte 2 zu Registerkarte 3. Es gibt keine Protokolleinträge. Daher wird keine der Methoden [onCreateView, afterViews, onResume] von Fragment Nr. 3 ausgeführt. Der Text [Hello World from section:3] wird nur deshalb korrekt angezeigt, weil dieser Text bereits im vorherigen Schritt erstellt wurde, als Fragment Nr. 2 angezeigt wurde. Erinnern Sie sich daran, dass in diesem Schritt die Methode [onResume] von Fragment Nr. 3 ausgeführt wurde. Wir sehen hier, dass die Methode [onResume] ebenso wie die Methode [onCreateView] nicht zur Aktualisierung von Fragment 3 verwendet werden kann. Hätten wir den vom Fragment angezeigten Text ändern müssen, hätte keine dieser beiden Methoden dies bewerkstelligen können.

Kehren wir nun von Registerkarte Nr. 3 zu Registerkarte Nr. 1 zurück. Die Protokolle lauten dann wie folgt:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Wir sehen, dass alle Methoden in Fragment 1 ausgeführt wurden. Wir sehen, dass die Methode getItem nicht aufgerufen wurde. Wie bereits erwähnt, wird diese Methode für jedes Fragment nur einmal aufgerufen;

Wechseln wir nun von Registerkarte 1 zur benachbarten Registerkarte 2. Wir erhalten die folgenden Protokolleinträge:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Überraschend, nicht wahr? Alle Methoden von Fragment Nr. 3 werden erneut ausgeführt.

Um diese Phänomene zu verstehen, denken Sie daran, dass der Fragment-Container standardmäßig, wenn er Fragment i anzeigt, die Fragmente i-1, i und i+1 initialisiert. Sehen wir uns die Protokolle vor diesem Hintergrund noch einmal an.

Zunächst die Protokolle beim Start der App:

05-28 13:54:54.801 8809-8809/exemples.android D/MainActivity: constructor
05-28 13:54:54.901 8809-8809/exemples.android D/MainActivity: afterViews
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.919 8809-8809/exemples.android D/PlaceholderFragment: constructor
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[0]
05-28 13:54:54.963 8809-8809/exemples.android D/MainActivity: getItem[1]
05-28 13:54:54.963 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 2
05-28 13:54:54.965 8809-8809/exemples.android D/PlaceholderFragment: afterViews 2
05-28 13:54:54.966 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume 2
05-28 13:54:54.968 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 2
05-28 13:54:55.536 8809-8809/exemples.android D/menu: création menu en cours

Da der Fragment-Container Fragment 1 anzeigt, werden die Fragmente 1 und 2 initialisiert (Zeilen 8–15).

Wir wechseln nun von Registerkarte 1 zu Registerkarte 2:

1
2
3
4
5
05-28 14:01:42.786 8809-8809/exemples.android D/MainActivity: getItem[2]
05-28 14:01:42.786 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:01:42.789 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Da der Fragment-Container Fragment 2 anzeigt, müssen die Fragmente 1, 2 und 3 initialisiert werden. Die Fragmente 1 und 2 wurden bereits im vorherigen Schritt initialisiert. Fragment 3 wird in den Zeilen 2–5 initialisiert.

Wir wechseln von Registerkarte 2 zu Registerkarte 3. Es gibt keine Protokolleinträge. Da der Fragment-Container Fragment 3 anzeigen wird, müssen die Fragmente 2 und 3 initialisiert werden. Da dies jedoch bereits im vorherigen Schritt geschehen ist, sind sie bereits initialisiert. Was wir hier nicht sehen, ist, dass Fragment 1, das nicht an Fragment 3 angrenzt, seinen Zustand verliert, der nicht im Speicher beibehalten wird.

Wir wechseln von Registerkarte 3 zu Registerkarte 1. Die Protokolle lauten wie folgt:

1
2
3
4
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: afterViews 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume 1
05-28 14:11:18.353 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 1

Da der Fragment-Container Fragment 1 anzeigt, muss auch Fragment 2 initialisiert werden. Es wurde bereits im vorherigen Schritt initialisiert. Im selben Schritt ging der Zustand von Fragment 1 verloren. Er wird daher in den Zeilen 1–4 zurückgesetzt. Was wir hier nicht sehen, ist, dass Fragment 3, das nicht an Fragment 1 angrenzt, seinen Zustand verliert, der dann nicht im Speicher beibehalten wird.

Beim Wechsel von Registerkarte 1 zur benachbarten Registerkarte 2 erhalten wir die folgenden Protokolleinträge:

1
2
3
4
05-28 14:12:59.526 8809-8809/exemples.android D/PlaceholderFragment: onCreateView 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: afterViews 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume 3
05-28 14:12:59.527 8809-8809/exemples.android D/PlaceholderFragment: onResume setText 3

Da der Fragment-Container Fragment 2 anzeigt, müssen die Fragmente 1, 2 und 3 initialisiert werden. Die Fragmente 1 und 2 wurden bereits im vorherigen Schritt initialisiert. Fragment 3 wird in den Zeilen 1–4 initialisiert.

Was haben wir gelernt?

  • Dass die Standard-Fragmentverwaltung sehr spezifisch ist und dass man sie verstehen muss, wenn man sich nicht die Haare raufen will. Wir können diesen Verwaltungsmodus ändern, und das werden wir etwas später tun;
  • dass bei dieser Standardbehandlung keine der Methoden [onCreateView, onResume] zur Aktualisierung des anzuzeigenden Fragments verwendet werden kann, da wir nicht sicher sein können, dass sie ausgeführt werden;

1.8.6. onDestroyView

Die Methode [onDestroyView] ist Teil des Fragment-Lebenszyklus (siehe Abschnitt 1.7.6):

Wir sehen, dass im Lebenszyklus eines Fragments:

  • die Methode [onCreateView] möglicherweise mehrmals ausgeführt wird;
  • bevor später zur Methode [onCreateView] zurückgekehrt wird, erfolgt zwangsläufig ein Aufruf der Methode [onDestroyView] [2];

Wir werden diese Methoden in die Fragmente einfügen, um deren Lebenszyklus besser nachverfolgen zu können. Der Fragment-Code sieht dann wie folgt aus:


package exemples.android;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
...
 
  @Override
  public void onDestroyView() {
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // parent
    super.onDestroyView();
  }
 
}

Starten wir die App. Die ersten Protokolleinträge lauten wie folgt:

06-03 02:45:42.163 2346-2346/exemples.android D/MainActivity: constructor
06-03 02:45:42.331 2346-2346/exemples.android D/MainActivity: afterViews
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.341 2346-2346/exemples.android D/PlaceholderFragment: constructor
06-03 02:45:42.515 2346-2346/exemples.android D/MainActivity: getItem[0]
06-03 02:45:42.516 2346-2346/exemples.android D/MainActivity: getItem[1]
06-03 02:45:42.517 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 02:45:42.520 2346-2346/exemples.android D/PlaceholderFragment: afterViews 2
06-03 02:45:42.523 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:45:42.524 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume 2
06-03 02:45:42.525 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 02:45:44.596 2346-2346/exemples.android D/menu: création menu en cours
  • Zeile 1: Aufbau der einzelnen Aktivität;
  • Zeile 2: die Methode [afterViews] der Aktivität: ihre mit [@ViewById] annotierten Felder werden initialisiert;
  • Zeilen 3–5: Erstellung der drei Fragmente;
  • Zeilen 6–7: Der Fragment-Container [ViewPager] fordert die ersten beiden Fragmente an;
  • Zeilen 8–9: Die Ansicht für Fragment 2 wird erstellt (nicht unbedingt sichtbar gemacht);
  • Zeilen 10–11: Die Ansicht für Fragment 1 wird erstellt (nicht unbedingt sichtbar gemacht);
  • Zeilen 12–13: Methode [onResume] von Fragment 1;
  • Zeilen 14–15: [onResume]-Methode von Fragment 2;
  • Zeile 16: Das Aktivitätsmenü wird erstellt;

Wechsel von Registerkarte 1 zu Registerkarte 3:


06-03 02:50:02.685 2346-2346/exemples.android D/MainActivity: getItem[2]
06-03 02:50:02.685 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: afterViews 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume 3
06-03 02:50:02.686 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 02:50:03.024 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 1
  • Zeile 1: Der Fragment-Container fordert das dritte Fragment an;
  • Zeilen 2–3: Die Ansicht für Fragment 3 wird erstellt (nicht unbedingt angezeigt);
  • Zeilen 4–5: Die Methode [onResume] von Fragment 3 wird ausgeführt;
  • Zeile 6: Die Methode [onDestroyView] von Fragment 1 wird ausgeführt. Das bedeutet, dass der Lebenszyklus dieses Fragments erneut durchlaufen wird, wenn der Benutzer zu Fragment 1 oder einem benachbarten Fragment zurückkehrt;

Zurückkehren von Registerkarte 3 zu Registerkarte 1:


06-03 02:53:46.255 2346-2346/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: afterViews 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume 1
06-03 02:53:46.256 2346-2346/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 02:53:46.604 2346-2346/exemples.android D/PlaceholderFragment: onDestroyView 3
  • Zeilen 1–4: Der Lebenszyklus von Fragment 1 wird erneut ausgeführt, da es einen [onDestroyView]-Aufruf durchlaufen hat;
  • Zeile 5: Die Methode [onDestroyView] von Fragment 3 wird nun ausgeführt. Auch hier gilt: Wenn der Benutzer zu Fragment 3 oder einem benachbarten Fragment zurückkehrt, wird der Lebenszyklus dieses Fragments erneut durchlaufen;

1.8.7. setUserVisibleHint

Die Methode [onCreateView] des Lebenszyklus instanziiert die mit dem Fragment verbundene Ansicht, macht sie jedoch nicht unbedingt sichtbar. Das werden wir nun sehen. Die Methode [Fragment.setUserVisibleHint] wird jedes Mal ausgeführt, wenn sich die Sichtbarkeit des Fragments ändert. Wir fügen diese Methode zum Code des Fragments hinzu:


package exemples.android;
 
....
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  ...
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

Beim Start sehen die Protokolle wie folgt aus:


06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
  • Die Protokolleinträge für die Zeilen 7, 9–10 zeigen, dass nur Fragment 1 sichtbar wird. Wir können auch sehen, dass es sichtbar wird, bevor seine [onCreateView]-Methode ausgeführt wird;

Wechseln wir von Registerkarte 1 zu Registerkarte 2:


06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3
  • Fragment 1 wird ausgeblendet (Zeile 3), Fragment 2 wird angezeigt (Zeile 4);

Wechseln wir von Registerkarte 2 zu Registerkarte 3:


06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1
  • Fragment 2 ist ausgeblendet (Zeile 1), Fragment 3 wird angezeigt (Zeile 2);

Kehren wir zu Registerkarte 1 zurück:


06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3
  • Fragment 3 wird ausgeblendet (Zeile 2), Fragment 1 wird angezeigt (Zeile 3);

Was haben wir gelernt?

  • Die Methode [setUserVisibleHint] wird einmal ausgeführt, wobei die Eigenschaft [isVisibleToUser] für das Fragment, das angezeigt werden soll, auf „true“ gesetzt ist;
  • Wir können nicht bestimmen, wann diese Methode im Verhältnis zum Lebenszyklus des Fragments ausgeführt wird. So wurde für Fragment 1 die Methode [setUserVisibleHint, true] vor der Methode [onCreateView] zu Beginn des Lebenszyklus dieses Fragments ausgeführt, während für die Fragmente 2 und 3 das Gegenteil der Fall war;

1.8.8. setOffscreenPageLimit

Die vorherigen Protokolle zeigen, dass der Fragment-Container [ViewPager], wenn er im Begriff ist, Fragment #i anzuzeigen, den Lebenszyklus der benachbarten Fragmente i-1 und i+1 ausführt, sofern dies nicht bereits geschehen ist. Dieses Verhalten kann durch die Methode [ViewPager].setOffscreenPageLimit gesteuert werden:

// fragment offset
    [ViewPager].setOffscreenPageLimit(n);

Mit der obigen Anweisung,

  1. wird, wenn der Fragment-Container [ViewPager] im Begriff ist, Fragment #i anzuzeigen, der Lebenszyklus der benachbarten Fragmente im Bereich [i-n, i+n] ausgeführt, sofern dies nicht bereits geschehen ist;
  2. Wenn dann das Fragment j angezeigt wird:
    • tritt dasselbe Phänomen für die benachbarten Fragmente im Intervall [j-n, j+n] auf;
    • können die in Schritt 1 initialisierten Fragmente, die innerhalb des Bereichs [j-n, j+n] nicht mehr an das neue Fragment angrenzen, anschließend eine [onDestroyView]-Operation durchlaufen. Ich habe jedoch in anderen Anwendungen, insbesondere in der aus Kapitel 3, beobachtet, dass dies nicht immer der Fall war;

Wir ändern die Methode [MainActivity.afterViews] wie folgt:


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
    // the fragment manager
    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.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);
 
    // the tab bar is also associated with the fragment container
    // i.e. tab n° i displays fragment n° i of the container
    tabLayout.setupWithViewPager(mViewPager);
 
    // floating button
    fab.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
          .setAction("Action", null).show();
      }
    });
}
  • Zeile 20: Wir setzen die Anzahl der zu initialisierenden benachbarten Fragmente auf die Gesamtzahl der Fragmente minus 1. Wenn der Fragment-Container beim Start also Fragment Nr. 1 anzeigt, initialisiert er gleichzeitig die Fragmente 2, 3, ..., n, wobei n = 1 + mSectionsPagerAdapter.getCount() - 1 = mSectionsPagerAdapter.getCount() ist. Das bedeutet, dass alle Fragmente initialisiert werden. Wenn der Viewport zu einem anderen Fragment wechselt, erkennt der Fragment-Container:
    • erkennen, dass alle an das neue Fragment angrenzenden Fragmente bereits initialisiert sind, und wird sie daher nicht initialisieren;
    • da die Nachbarschaft des neuen Fragments auch alle Fragmente umfasst, wird keines vom Fragment-Container „deinitialisiert“;

Insgesamt sollten wir sehen, dass alle Fragmente beim Start der Anwendung instanziiert und initialisiert werden und danach nie wieder. Dies werden wir nun durch Überprüfung der Protokolle verifizieren.

Beim Start haben wir die folgenden Protokolle:

06-03 03:30:55.411 10344-10344/exemples.android W/System: ClassLoader referenced unknown path: /data/app/exemples.android-1/lib/x86
06-03 03:30:55.417 10344-10344/exemples.android D/MainActivity: constructor
06-03 03:30:55.460 10344-10344/exemples.android D/MainActivity: afterViews
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.474 10344-10344/exemples.android D/PlaceholderFragment: constructor
06-03 03:30:55.559 10344-10344/exemples.android D/MainActivity: getItem[0]
06-03 03:30:55.559 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[1]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/MainActivity: getItem[2]
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:30:55.560 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:30:55.564 10344-10344/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:30:55.565 10344-10344/exemples.android D/PlaceholderFragment: onResume setText 3
06-03 03:30:56.798 10344-10344/exemples.android D/menu: création menu en cours
  • Zeilen 4–6: Erstellung der drei Fragmente;
  • Zeilen 7, 9, 11: Der Fragment-Container fordert die drei Fragmente an. In der vorherigen Version wurden zwei angefordert;
  • Zeilen 14–25: Der Lebenszyklus der drei Fragmente läuft ab;

Wechseln wir nun von Registerkarte 1 zu Registerkarte 2:

06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:34:03.388 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true

Wechseln wir von Registerkarte 2 zu Registerkarte 3:

06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:34:43.292 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true

Dann von Registerkarte 3 zu Registerkarte 1:

06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:35:32.666 10344-10344/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true

Die Protokolle bestätigen die Theorie. Alle Fragmente wurden beim Start instanziiert und initialisiert. Danach werden ihre Lebenszyklusmethoden nicht mehr ausgeführt. Dies ist ein sehr vorhersehbares Verhalten von Fragmenten, was ihre Verwendung erheblich vereinfacht.

Was wir finden wollen, ist eine Möglichkeit, ein Fragment zu aktualisieren, das gerade angezeigt werden soll, unabhängig von der vom Entwickler gewählten Fragment-Anordnung. Die Protokolle haben uns zwei Dinge gezeigt:

  • Die Methode [setUserVisibleHint, true] wird immer für das Fragment ausgeführt, das gerade angezeigt werden soll, nicht jedoch für die anderen;
  • dieses Ereignis kann vor oder nach dem Lebenszyklus des Fragments auftreten. Dies hängt von der vom Entwickler gewählten Fragment-Adjazenz ab. Dies ist ein Problem, denn wenn der Lebenszyklus noch nicht stattgefunden hat, bedeutet dies, dass das Fragment nicht durch die Methode [setUserVisibleHint, true] aktualisiert werden kann;

Die Protokolle beim Start der Anwendung, als die Fragment-Adjazenz 1 war, sahen wie folgt aus:


06-03 03:06:13.263 20586-20586/exemples.android D/MainActivity: constructor
06-03 03:06:13.291 20586-20586/exemples.android D/MainActivity: afterViews
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.324 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.329 20586-20586/exemples.android D/PlaceholderFragment: constructor
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[0]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/MainActivity: getItem[1]
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:06:13.504 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:06:13.511 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:06:13.519 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:06:13.520 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: afterViews 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume 2
06-03 03:06:13.527 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 2
06-03 03:06:15.075 20586-20586/exemples.android D/menu: création menu en cours
  • Wir sehen, dass, wenn Fragment 1 sichtbar wird, seine Ansicht noch nicht erstellt wurde. Daher können wir nicht mit ihm interagieren. Dies kann während des Lebenszyklus des Fragments erfolgen, beispielsweise in der Methode [onCreateView] (Zeile 11) oder der Methode [onResume] (Zeilen 13–14). Da wir AA-Annotationen verwenden, müssen wir die [onCreateView]-Methode normalerweise nicht schreiben. Daher scheint die [onResume]-Methode hier am besten geeignet zu sein, um Fragment 1 zu aktualisieren;

Als wir von Registerkarte 1 zu Registerkarte 2 gewechselt haben, sahen die Protokolle wie folgt aus:


06-03 03:10:15.215 20586-20586/exemples.android D/MainActivity: getItem[2]
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=true
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 3
06-03 03:10:15.215 20586-20586/exemples.android D/PlaceholderFragment: afterViews 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume 3
06-03 03:10:15.216 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 3

Diesmal haben wir nur die Methode [setUserVisibleHint, true] in Zeile 4, um Fragment 2 zu aktualisieren;

Als wir von Registerkarte 2 zu Registerkarte 3 wechselten, sahen die Protokolle wie folgt aus:


06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 isVisibleToUser=false
06-03 03:12:06.238 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=true
06-03 03:12:06.239 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 1

Hier haben wir nur die Methode [setUserVisibleHint, true] in Zeile 2, um Fragment 3 zu aktualisieren;

Als wir von Registerkarte 3 zu Registerkarte 1 gewechselt haben, sahen die Protokolle wie folgt aus:


06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 isVisibleToUser=false
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 isVisibleToUser=true
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onCreateView 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: afterViews 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume 1
06-03 03:13:10.427 20586-20586/exemples.android D/PlaceholderFragment: onResume setText 1
06-03 03:13:10.789 20586-20586/exemples.android D/PlaceholderFragment: onDestroyView 3

Hier müssen Sie die [onResume]-Methode von Fragment 1 (Zeilen 6–7) verwenden, um Fragment 1 zu aktualisieren.

In diesem Beispiel sehen wir also, dass wir zwei Methoden haben, um ein Fragment zu aktualisieren, das gerade angezeigt werden soll: [setUserVisibleHint] und [onResume].

Wir werden diese Lösung in einem neuen Projekt umsetzen, in dem jedes Fragment die Anzahl seiner Aufrufe anzeigen muss, die wir als „Besuch“ bezeichnen. Daher müssen wir seine Anzeige bei jedem Aufruf aktualisieren. Dies ist genau das Problem, das wir zu lösen versuchen.

Zuvor wollen wir uns jedoch die letzte Phase im Lebenszyklus einer Aktivität oder eines Fragments ansehen: die Zerstörung. Das System kann beschließen, eine Aktivität zu zerstören, wenn andere Aktivitäten mit höherer Priorität Ressourcen benötigen, die derzeit nicht verfügbar sind. Um diese Ressourcen freizugeben, wird das System bestimmte Aktivitäten von sich aus zerstören. Die Methode [onDestroy] der Aktivität und der Fragmente wird dann aufgerufen.

1.8.9. OnDestroy

Wir ermöglichen es dem Benutzer, die Aktivität über eine Menüoption [5] zu löschen. Dazu fügen wir der Datei [menu_main.xml] [1] eine neue Menüoption hinzu:


<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/action_terminate"
        android:title="@string/action_terminate"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>

Kopieren Sie einfach die erste Menüoption und passen Sie das Ergebnis an (Zeilen 9 und 10). Die Bezeichnung für diese neue Option wird der Datei [strings.xml] hinzugefügt [2]:


<resources>
  <string name="app_name">Exemple-07</string>
  <string name="action_settings">Settings</string>
  <string name="action_terminate">Terminate</string>
  <string name="section_format">Hello World from section: %1$d</string>
</resources>

Schließlich behandeln wir in der Klasse [MainActivity] den Klick auf die Option [Beenden]:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();
 
    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    if (id == R.id.action_terminate) {
      Log.d("menu", "action_terminate selected");
      //we finish the activity
      finish();
      return true;
    }
    // parent
    return super.onOptionsItemSelected(item);
}
  • Zeilen 14–19: Kopiere die Zeilen 10–13 und füge sie ein, passe den Code an die neue Option an;
  • Zeile 17: Die Aktivität wird durch eine Softwareaktion beendet;

Führen wir nun diese neue Version aus, und sobald die erste Ansicht angezeigt wird, klicken Sie auf die Menüoption [Beenden]. Die Protokolle sehen dann wie folgt aus:

1
2
3
4
5
6
7
8
9
06-04 12:35:32.996 15994-15994/exemples.android D/menu: onOptionsItemSelected
06-04 12:35:32.996 15994-15994/exemples.android D/menu: action_terminate selected
06-04 12:35:33.561 15994-15994/exemples.android D/MainActivity: onDestroy
06-04 12:35:33.561 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 1
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 2
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroyView 3
06-04 12:35:33.562 15994-15994/exemples.android D/PlaceholderFragment: onDestroy 3
  • Zeilen 1–2: Klicken Sie auf die Option [Beenden];
  • Zeile 4: Die Methode [onDestroy] der Aktivität wird aufgerufen;
  • Zeilen 4–5: Die Methode [onDestroyView] von Fragment 1 wird aufgerufen, gefolgt von dessen Methode [onDestroy];
  • Zeilen 6–9: Dieser Vorgang wiederholt sich für die beiden anderen Fragmente;

Es ist wichtig zu beachten, dass die Methode [onDestroy] der Aktivität und der Fragmente aufgerufen wird, wenn die Aktivität vom System, vom Entwickler oder vom Benutzer beendet werden soll. Diese Methode kann verwendet werden, um Informationen zu speichern – beispielsweise lokal auf dem Tablet –, damit sie abgerufen werden können, wenn der Benutzer die Anwendung neu startet.

1.9. Beispiel-08: Aktualisieren eines Fragments mit variabler Fragment-Nachbarschaft

1.9.1. Erstellen des Projekts

Duplizieren Sie das Projekt [Beispiel-07] in [Beispiel-08]. Befolgen Sie dazu die in Abschnitt 1.4 beschriebene Vorgehensweise zum Duplizieren von [Beispiel-02] in [Beispiel-03].

1.9.2. Das Fragment [PlaceholderFragment] umschreiben

Der neue Code für das Fragment [PlaceholderFragment] lautet wie folgt. Er funktioniert unabhängig von der den Fragmenten zugewiesenen Nachbarschaft (1, teilweise, vollständig):


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }
 
  @Override
  public void onDestroyView() {
...
  }
 
  @Override
  public void onResume() {
...
  }
 
  // update fragment
  public void update() {
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  private String getInfos() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s", numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone);
  }
}
  • Zeilen 34–48: Die Methode [@AfterViews] kann mehrfach ausgeführt werden. Früher haben wir sie verwendet, um den Text des Fragments zu initialisieren (Zeile 42). Das tun wir immer noch, aber um sicherzustellen, dass dies nur einmal geschieht, verwalten wir eine boolesche Variable [initDone] (Zeile 44), um anzuzeigen, dass die Initialisierung abgeschlossen ist und nicht wiederholt werden muss;
  • Zeilen 56–59: Wir führen die Methode [onDestroyView] ein, um der Tatsache Rechnung zu tragen, dass beim nächsten Anzeigen des Fragments dessen Lebenszyklus erneut durchlaufen wird;
  • Die Protokolle zeigten, dass nach der Methode [@AfterViews] zwei Methoden ausgeführt werden können: die Methoden [setUserVisibleHint] und [onResume]. Die Methode [onResume] wird nur ausgeführt, wenn der Lebenszyklus des Fragments durchlaufen wird. Die Methode [setUserVisibleHint] wird jedoch nicht immer nach der Methode [@AfterViews] ausgeführt. Die Protokolle zeigten, dass mindestens eine der beiden Methoden nach der [@AfterViews]-Methode ausgeführt wird. Die Protokolle haben nie gezeigt, dass beide gemeinsam nach der [@AfterViews]-Methode ausgeführt werden könnten. Es ist entweder die eine oder die andere. Als Vorsichtsmaßnahme setzen wir einen booleschen Wert [updateDone], wenn eine Aktualisierung vorgenommen wurde;

Die Methoden [setUserVisibleHint] und [onResume] lauten wie folgt:


  // data
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;
 
@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // increment
      numVisit++;
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // update
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • Zeile 14: Der Sichtbarkeitsstatus des Fragments wird gespeichert;
  • Zeilen 22–25: Wenn das Fragment sichtbar ist und die Methode [@AfterViews] ausgeführt wurde, wird die Methode [update] ausgeführt und die boolesche Variable [updateDone] auf „true“ gesetzt;
  • Zeilen 26–28: Wenn das Fragment ausgeblendet werden soll, wird der Boolesche Wert [updateDone] auf „false“ zurückgesetzt. Wir benötigen ein Ereignis, um den booleschen Wert [updateDone] – der auf „true“ gesetzt wird, sobald die Methode [update] aufgerufen wird – auf „false“ zurückzusetzen, damit neue Aktualisierungen vorgenommen werden können. Dazu nutzen wir die Tatsache, dass das Fragment nicht mehr sichtbar ist. Wenn es wieder sichtbar wird, muss das Fragment erneut aktualisiert werden;
  • Zeilen 32–42: Die Protokolle zeigen, dass je nach der für die Fragmente gewählten Nachbarschaft die Methode [onResume] möglicherweise ausgeführt wird, obwohl das Fragment nicht sichtbar ist. Ist es nicht sichtbar, führen wir die Aktualisierung nicht durch (Zeile 39) und verwalten, wie bei [setMenuVisibility], die boolesche Variable [updateDone].

Schließlich sieht die Methode [onDestroyView] wie folgt aus:


  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // indicator update
    afterViewsDone = false;
    // log
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

Die Methode [onDestroyView] wird ausgeführt, wenn der Lebenszyklus eines Fragments endet. Ein weiterer Lebenszyklus kann später fortgesetzt werden.

  • Zeile 6: Die Methode [onDestroyView] entfernt jegliche Verbindung zu der dem Fragment zugeordneten Ansicht. Diese wird während des nächsten Lebenszyklus des Fragments neu erstellt. Vorerst müssen wir den booleschen Wert [afterViews] auf „false“ setzen, um anzuzeigen, dass die Verbindung zur Ansicht nicht mehr besteht;

Wir führen die Anwendung mit 5 Fragmenten und einer Adjazenz von 2 aus. Die Änderungen werden in [MainActivity] vorgenommen:


    // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT=2;
 
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
 
    ....
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
...
}

Die Startprotokolle lauten wie folgt:


05-31 06:23:07.015 32551-32551/exemples.android D/MainActivity: constructor
05-31 06:23:07.041 32551-32551/exemples.android D/MainActivity: afterViews
05-31 06:23:07.050 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.053 32551-32551/exemples.android D/PlaceholderFragment: constructor
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[0]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[1]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/MainActivity: getItem[2]
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.278 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.280 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.291 32551-32551/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:23:07.294 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.295 32551-32551/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 06:23:07.798 32551-32551/exemples.android D/menu: création menu en cours
  • Zeilen 8, 10, 12: Der Fragment-Container fordert alle an Fragment 1 angrenzenden Fragmente an;
  • Zeilen 9, 11, 13: Die Methode [setUserVisibleHint] dieser Fragmente wird ausgeführt, wobei [visibleToUser] auf false gesetzt ist;
  • Zeile 14: Die Methode [setUserVisibleHint] von Fragment 1 wird aufgerufen, wobei [visibleToUser] auf „true“ gesetzt ist;
  • Zeilen 15–17: Die Methode [afterViews] der drei benachbarten Fragmente wird aufgerufen. Hier sehen wir einen Fall, in dem diese Methode aufgerufen wird, nachdem ein Fragment sichtbar geworden ist (Fragment 1, Zeile 14);
  • Zeilen 18–20: Die Methode [onResume] der drei benachbarten Segmente wird aufgerufen;

Wechsel von Registerkarte 1 zu Registerkarte 2:


05-31 06:52:36.132 32551-32551/exemples.android D/MainActivity: getItem[3]
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:52:36.132 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:52:36.134 32551-32551/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • Da das Fragment-Layout um eine Position nach rechts verschoben ist, wird Fragment 4 vom Fragment-Container beansprucht;
  • Zeile 2: Die Methode [setUserVisibleHint] von Fragment 4 wird aufgerufen, wobei [visibleToUser] auf false gesetzt ist;
  • Zeile 3: Die Methode [setUserVisibleHint] von Fragment 1 wird aufgerufen, wobei [visibleToUser] auf false gesetzt ist. Infolgedessen ist Fragment 1 nun ausgeblendet;
  • Zeile 4: Die Methode [setUserVisibleHint] von Fragment 2 wird aufgerufen, wobei [visibleToUser] auf „true“ gesetzt ist. Fragment 2 ist nun sichtbar;
  • Zeilen 5–6: Der Lebenszyklus von Fragment 4 wird fortgesetzt;

Wir wechseln von Registerkarte 2 zu Registerkarte 3:


05-31 06:58:16.228 32551-32551/exemples.android D/MainActivity: getItem[4]
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 06:58:16.228 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 06:58:16.229 32551-32551/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
  • Da das Fragment-Layout um eine Position nach rechts verschoben ist, wird Fragment 5 vom Fragment-Container beansprucht;
  • Zeile 2: Die Methode [setUserVisibleHint] von Fragment 5 wird aufgerufen, wobei [visibleToUser] auf false gesetzt ist;
  • Zeile 3: Die Methode [setUserVisibleHint] von Fragment 2 wird aufgerufen, wobei [visibleToUser] auf false gesetzt ist. Infolgedessen ist Fragment 2 nun ausgeblendet;
  • Zeile 4: Die Methode [setUserVisibleHint] von Fragment 3 wird aufgerufen, wobei [visibleToUser] auf „true“ gesetzt ist. Fragment 3 ist nun sichtbar;
  • Zeilen 5–6: Der Lebenszyklus von Fragment 5 wird fortgesetzt;

Wir wechseln von Registerkarte 3 zu Registerkarte 4:


05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:00:17.762 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • Zeile 1: Fragment 3 ist nun ausgeblendet;
  • Zeile 2: Fragment 4 ist nun sichtbar. Beachten Sie, dass der Lebenszyklus von Fragment 4 nicht ausgeführt wird. Dies geschah bereits zwei Schritte zuvor;
  • Zeile 3: Fragment 1 verlässt den Bereich des angezeigten Fragments 4. Seine Methode [onDestroyView] wird ausgeführt. Bei der nächsten Anzeige wird sein View-Lebenszyklus [onCreateView, afterViews, onResume] erneut ausgeführt;

Wir wechseln von Registerkarte 4 zu Registerkarte 5:


05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:04:19.004 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • Zeile 1: Fragment 4 ist nun ausgeblendet;
  • Zeile 2: Fragment 5 ist nun sichtbar. Beachten Sie, dass der Lebenszyklus von Fragment 5 nicht ausgeführt wird. Dies geschah bereits zwei Schritte zuvor;
  • Zeile 3: Fragment 2 verlässt den Bereich des angezeigten Fragments 5. Seine [onDestroyView]-Methode wird ausgeführt;

Wir wechseln von Registerkarte 5 zu Registerkarte 1:


05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=false, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.246 32551-32551/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.247 32551-32551/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 4 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:06:17.819 32551-32551/exemples.android D/PlaceholderFragment: onDestroyView 5 : numVisit=1, afterViewsDone=false, isVisibleToUser=false, initDone=true, updateDone=false
  • Zeilen 1, 4, 5, 6: Der Lebenszyklus von Fragment 1 wird erneut ausgeführt. Dies liegt daran, dass die Verbindung zu seiner Ansicht unterbrochen wurde;
  • Zeilen 2, 5, 8, 9: Aus demselben Grund wird der Lebenszyklus von Fragment 2 erneut ausgeführt;
  • Zeilen 10–11: Die Fragmente 4 und 5 werden aus der Umgebung des angezeigten Fragments entfernt;
  • Zeile 7: Fragment 1 wird aktualisiert;
 

Die Protokolle zeigten nie, dass sowohl die Methode [setUserVisibleHint] als auch die Methode [onResume] versuchten, das Fragment zu aktualisieren. Es ist entweder das eine oder das andere. Der Leser ist eingeladen, weitere Tests durchzuführen und die Protokolle zu beobachten, um die Konzepte der Fragment-Nachbarschaft und des Lebenszyklus vollständig zu verstehen.

Lassen Sie uns nun die vollständige Adjazenz festlegen und dieselben Tests durchführen.

In [MainActivity]:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Die Startprotokolle lauten wie folgt:


05-31 07:34:44.717 28908-28908/exemples.android D/MainActivity: constructor
05-31 07:34:44.844 28908-28908/exemples.android D/MainActivity: afterViews
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:44.887 28908-28908/exemples.android D/PlaceholderFragment: constructor
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[0]
05-31 07:34:45.201 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.201 28908-28908/exemples.android D/MainActivity: getItem[1]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[2]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.204 28908-28908/exemples.android D/MainActivity: getItem[3]
05-31 07:34:45.204 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/MainActivity: getItem[4]
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.205 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.207 28908-28908/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.208 28908-28908/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.209 28908-28908/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:45.210 28908-28908/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
05-31 07:34:46.548 28908-28908/exemples.android D/menu: création menu en cours
  • Die Protokolle zeigen, dass der Lebenszyklus der 5 Fragmente ausgeführt wird;
  • Fragment 1 wird in Zeile 18 angezeigt;

Wechsel von Registerkarte 1 zu Registerkarte 2:


05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:38:27.780 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 1 ist ausgeblendet;
  • Zeile 2: Fragment 2 wird angezeigt;

Wechsel von Registerkarte 2 zu Registerkarte 3:


05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:39:33.059 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 2 ist ausgeblendet;
  • Zeile 2: Fragment 3 wird angezeigt;

Wechsel von Registerkarte 3 zu Registerkarte 4:


05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:40:30.362 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 3 ist ausgeblendet;
  • Zeile 2: Fragment 4 wird angezeigt;

Wechsel von Registerkarte 4 zu Registerkarte 5:


05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:41:23.479 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 4 ist ausgeblendet;
  • Zeile 2: Fragment 5 wird angezeigt;

Wir wechseln von Registerkarte 5 zu Registerkarte 1:


05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=1, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:42:22.549 28908-28908/exemples.android D/PlaceholderFragment: update 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 5 ist ausgeblendet;
  • Zeile 2: Fragment 1 wird angezeigt;
  • Zeile 3: Fragment 1 wird aktualisiert;

Wechsel von Registerkarte 1 zu Registerkarte 4:


05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=2, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=true
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
05-31 07:44:13.129 28908-28908/exemples.android D/PlaceholderFragment: update 4 : numVisit=2, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
  • Zeile 1: Fragment 1 ist ausgeblendet;
  • Zeile 2: Fragment 4 wird angezeigt;
  • Zeile 3: Fragment 4 wird aktualisiert;

Wir sehen, dass das Verhalten der Fragmente bei vollständiger Adjazenz viel vorhersehbarer ist.

Setzen wir nun die Adjazenz auf Null und schauen wir, was passiert. Die Klasse [MainActivity] entwickelt sich wie folgt:


  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Die Startprotokolle lauten wie folgt:


06-01 03:11:52.068 5679-5679/exemples.android D/MainActivity: constructor
06-01 03:11:52.353 5679-5679/exemples.android D/MainActivity: afterViews
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.433 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.434 5679-5679/exemples.android D/PlaceholderFragment: constructor
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[0]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/MainActivity: getItem[1]
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.566 5679-5679/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.571 5679-5679/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false
06-01 03:11:52.574 5679-5679/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false
06-01 03:11:54.597 5679-5679/exemples.android D/menu: création menu en cours
  • In den Zeilen 8 und 10 sehen wir, dass der Fragment-Container zwei Fragmente angefordert hat, die Nummern 1 und 2. Alles verläuft daher so, als gäbe es eine Adjazenz von 1. Die Adjazenz von 0 wurde somit ignoriert.

1.9.3. Kommunikation zwischen Fragmenten

In der bisherigen Architektur haben wir eine Aktivität und n Fragmente. Der Benutzer interagiert mit den verschiedenen Fragmenten. Diese Interaktionen verändern den Zustand der Anwendung. Der Zustand der Anwendung bezieht sich hier auf die Gesamtheit der Informationen, die sie während ihrer gesamten Lebensdauer speichert. Dabei ergibt sich folgendes Problem:

  • Wenn der Benutzer mit Fragment i interagiert, wechselt die Anwendung vom Zustand E1 in den Zustand E2;
  • Eine Benutzeraktion auf Fragment i bewirkt, dass Fragment j angezeigt wird;
  • wie aktualisieren wir Fragment j mit dem aktuellen Zustand E2 der Anwendung?

Aus früheren Beispielen wissen wir, wie wir Fragment j aktualisieren. Aber wo finden wir den Zustand E2 der Anwendung, um es zu aktualisieren?

Es gibt verschiedene Lösungen für dieses Problem. Eine davon haben wir bereits gesehen: Fragment i kann den Anwendungsstatus E2 über Argumente an Fragment j übergeben. Auf diese Methode sind wir in der Klasse [MainActivity] gestoßen, als wir die Fragmente erstellt haben:


      for (int i = 0; i < fragments.length; 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);
}

Diese Lösung ist hier nicht sofort einsetzbar. Wenn der Benutzer auf die Registerkarte j klickt, wodurch das Fragment j angezeigt wird, wird unser Code nämlich nicht aufgerufen. Es wird nur Systemcode ausgeführt. In einem zukünftigen Projekt werden wir sehen, wie man einen Klick auf eine Registerkarte abfängt, aber vorerst verfolgen wir einen anderen Ansatz.

Wir haben den Zustand der Anwendung besprochen: die Menge an Daten, die von der Anwendung im Laufe der Zeit verwaltet wird. Hier besteht die Anwendung aus einer Aktivität und n Fragmenten, die alle beim Start der Anwendung einmal instanziiert werden und deren Lebensdauer der der Anwendung entspricht. Daher kann jedes dieser Elemente – oder mehrere davon zusammen – als Kandidat für die Speicherung des Anwendungszustands dienen. Jedes Fragment hat über die Methode [Fragment.getActivity()] Zugriff auf die Aktivität, die es erstellt hat. Da alle Fragmente Zugriff auf die Aktivität haben, erscheint es naheliegend, den Anwendungszustand darin zu speichern.

Das Ergebnis der Methode [Fragment.getActivity()] hängt jedoch davon ab, zu welchem Zeitpunkt im Lebenszyklus sie aufgerufen wird. Wir veranschaulichen diesen Punkt, indem wir der Klasse [PlaceholderFragment] einige Log-Einträge hinzufügen:


  // update fragment
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // the work to be done depends on the visit number
    if (numVisit > 1) {
      // log
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // modified text
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }
 
  // local info for logs
  private String getInfos() {
    return String.format("numVisit=%s, afterViewsDone=%s, isVisibleToUser=%s, initDone=%s, updateDone=%s, getActivity()==null:%s",
      numVisit, afterViewsDone, isVisibleToUser, initDone, updateDone, getActivity() == null);
}
  • Zeilen 14–16: Die Methode [getInfo] zeigt einen Teil des App-Status an;

Wir starten die App mit einer Fragment-Adjazenz von 2. Die Protokolle beim Start der App:


06-01 03:26:13.769 10931-10931/exemples.android D/MainActivity: constructor
06-01 03:26:13.856 10931-10931/exemples.android D/MainActivity: afterViews
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:13.864 10931-10931/exemples.android D/PlaceholderFragment: constructor
06-01 03:26:14.535 10931-10931/exemples.android D/MainActivity: getItem[0]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[1]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/MainActivity: getItem[2]
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:26:14.538 10931-10931/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.541 10931-10931/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.545 10931-10931/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:14.547 10931-10931/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:26:15.967 10931-10931/exemples.android D/menu: création menu en cours
  • Zeilen 9, 10, 13, 14: Wir sehen, dass in den [setUserVisibleHint]-Methoden [getActivity()==null] gilt, wenn das Fragment noch nicht sichtbar ist (isVisibleToUser==false);
  • Zeile 19: Wir sehen, dass die Methode [getActivity] die Aktivität korrekt zurückgibt, wenn der Ausführungsablauf die Methode [update] von Fragment 1 erreicht;

Wenn die Fragment-Adjazenz auf 4 (volle Adjazenz) gesetzt ist, lauten die Logs wie folgt:


06-01 03:35:23.553 2814-2814/exemples.android D/MainActivity: constructor
06-01 03:35:23.751 2814-2819/exemples.android I/art: Ignoring second debugger -- accepting and dropping
06-01 03:35:23.900 2814-2814/exemples.android D/MainActivity: afterViews
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:23.991 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.002 2814-2814/exemples.android D/PlaceholderFragment: constructor
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[0]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[1]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 2 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[2]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 3 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[3]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 4 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/MainActivity: getItem[4]
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 5 : numVisit=0, afterViewsDone=false, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:true
06-01 03:35:24.207 2814-2814/exemples.android D/PlaceholderFragment: setUserVisibleHint 1 : numVisit=0, afterViewsDone=false, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.210 2814-2814/exemples.android D/PlaceholderFragment: afterViews 2 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.211 2814-2814/exemples.android D/PlaceholderFragment: afterViews 3 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.214 2814-2814/exemples.android D/PlaceholderFragment: afterViews 4 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 5 numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: afterViews 1 numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=false, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: onResume 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.215 2814-2814/exemples.android D/PlaceholderFragment: update 1 : numVisit=1, afterViewsDone=true, isVisibleToUser=true, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 2 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 3 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 4 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:24.216 2814-2814/exemples.android D/PlaceholderFragment: onResume 5 : numVisit=0, afterViewsDone=true, isVisibleToUser=false, initDone=true, updateDone=false, getActivity()==null:false
06-01 03:35:26.602 2814-2814/exemples.android D/menu: création menu en cours

Wir erhalten die gleichen Ergebnisse. Daraus lässt sich schließen, dass die Methode [getActivity] die Aktivität des Fragments zurückgibt, sobald das Fragment sichtbar ist. Wir stellen außerdem fest, dass die Methode [getActivity] tatsächlich einen Wert zurückgibt, sobald die Ausführung die Methode [update] des Fragments erreicht, das gerade angezeigt werden soll.

Um die Kommunikation zwischen Fragmenten zu veranschaulichen, erstellen wir ein neues Projekt.

1.10. Beispiel-09: Kommunikation zwischen Fragmenten, Wischen und Scrollen

1.10.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-07] als [Beispiel-08]. Dazu gehen wir genauso vor wie beim Duplizieren von [Beispiel-02] als [Beispiel-03] in Abschnitt 1.4.

1.10.2. Die Sitzung

In diesem neuen Projekt sollen die Fragmente die Gesamtzahl der vom Benutzer angezeigten Fragmente anzeigen. Hierfür müssen wir einen Zähler führen, auf den alle Fragmente zugreifen können. Wir bezeichnen das Objekt, das die von den Fragmenten gemeinsam genutzten Daten kapselt, als „Sitzung“. Diese Terminologie stammt aus der Webentwicklung, wo Daten, die über verschiedene, vom selben Benutzer angeforderte Ansichten hinweg gemeinsam genutzt werden sollen, in einer Sitzung abgelegt werden. Die Kapselung der von den verschiedenen Fragmenten gemeinsam genutzten Informationen in einem einzigen Objekt macht den Code lesbarer.

Die Klasse [Session] sieht wie folgt aus:

  

package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
 
  // getters and setters
 
  public int getNumVisit() {
    return numVisit;
  }
 
  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
}
  • Zeile 8: Die Sitzung kapselt die Anzahl der besuchten Fragmente;
  • Zeile 5: Die Annotation [EBean] ist eine AA-Annotation. Das Attribut [scope] legt den Geltungsbereich (oder die Lebensdauer) der annotierten Klasse fest. Hier macht das Attribut [scope = EBean.Scope.Singleton] die Klasse [Session] zu einem Singleton: Sie wird genau einmal instanziiert, und zwar beim Start der Anwendung. Eine Referenz auf eine mit [EBean] annotierte Klasse kann dann in eine andere Klasse injiziert werden. Dies ist das Konzept der Abhängigkeitsinjektion;

1.10.3. Die [MainActivity]

Die [MainActivity]-Aktivität entwickelt sich wie folgt:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");
 
    // session initialization
    session.setNumVisit(0);
  }
 
...
  • Zeilen 7–8: Injektion der Referenz auf das Session-Singleton mithilfe der Annotation [@Bean]. Der Parameter der Annotation ist die Klasse des zu injizierenden Beans. Das auf diese Weise annotierte Feld darf keinen [private]-Gültigkeitsbereich haben;
  • Zeile 15: Die Annotation [@AfterInject] wird verwendet, um eine Methode zu kennzeichnen, die aufgerufen werden soll, sobald alle Injektionen für die Klasse abgeschlossen sind. Wenn also die Methode [afterInject] in Zeile 16 aufgerufen wird, ist die Referenz aus Zeile 8 bereits initialisiert;
  • Zeile 20: Der Besuchszähler wird auf Null zurückgesetzt;

1.10.4. Das Fragment [PlaceholderFragment]

Das [PlaceholderFragment]-Fragment entwickelt sich wie folgt:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {
 
....
 
  // session
  protected Session session;
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // number of visits
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // the fragment will be hidden
      updateDone = false;
    }
  }
 
  // update fragment
  public void update() {
    // log
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // session
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • Zeile 7: die Sitzung;
  • Zeilen 35–37: Wir wissen, dass beim Aufruf der Methode [update] die Methode [getActivity] die Aktivität korrekt zurückgibt. Wir nutzen diese Gelegenheit, um die Sitzung abzurufen und lokal zu speichern (Zeile 36);
  • Zeilen 39–41: Um die Besuchsanzahl zu erhöhen, rufen wir sie aus der Sitzung ab. Wir hätten diesen Code in die Methode [setUserVisibleHint] ab Zeile 19 einfügen können, da wir wissen, dass die Methode [getActivity] an dieser Stelle die Aktivität zurückgibt. Hier entscheiden wir uns jedoch, dieser Methode keine spezifische Rolle zuzuweisen und den fragment-spezifischen Code in die Methode [update] des Fragments zu verschieben, die für diesen Zweck vorgesehen ist;
  • Zeile 43: Zeigt die Besuchsanzahl an;

Wenn diese Anwendung mit 5 Fragmenten ausgeführt wird, wobei 2 Fragmente nebeneinander liegen, lauten die ersten Protokolleinträge wie folgt:


05-31 08:38:47.305 20114-20114/exemples.android D/MainActivity: constructor
05-31 08:38:47.307 20114-20114/exemples.android D/MainActivity: afterInject
05-31 08:38:47.351 20114-20114/exemples.android D/MainActivity: afterViews
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
05-31 08:38:47.354 20114-20114/exemples.android D/PlaceholderFragment: constructor
...
  • Zeilen 2–3: Wir sehen, dass die [afterInject]-Methode der Aktivität vor ihrer [afterViews]-Methode ausgeführt wird;

Leser sind eingeladen, diese neue Anwendung zu testen.

1.10.5. Wischen deaktivieren

In der vorherigen App wird, wenn Sie mit der Maus im Android-Emulator nach links oder rechts wischen, die aktuelle Ansicht durch die Ansicht auf der rechten bzw. linken Seite ersetzt. Dieses Standardverhalten ist nicht immer erwünscht. Wir werden lernen, wie man das Wischen zwischen Ansichten deaktiviert.

Kehren wir zur Haupt-XML-Ansicht [activity_main] zurück:

  

Im XML-Code der Ansicht finden wir den Fragment-Container:


  <android.support.v4.view.ViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

Zeile 1 gibt die Klasse an, die die Seiten der Aktivität verwaltet. Diese Klasse befindet sich in der [MainActivity]:


import android.support.v4.view.ViewPager;
...
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

Zeile 12: Der Fragment-Container ist vom Typ [android.support.v4.view.ViewPager] (Zeile 1). Um das Wischen zu deaktivieren, müssen wir diese Klasse wie folgt erweitern:

  

package exemples.android;
 
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;
 
  // 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;
    }
  }
 
  // setter
  public void setSwipeEnabled(boolean isSwipeEnabled) {
    this.isSwipeEnabled = isSwipeEnabled;
  }
 
}
  • Zeile 8: Die Klasse [MyPager] erweitert die Android-Klasse [ViewPager] (Zeile 4);
  • beim Wischen mit dem Finger können die Ereignisbehandler in den Zeilen 24 und 34 aufgerufen werden. Beide geben einen booleschen Wert zurück. Sie müssen lediglich den booleschen Wert [false] zurückgeben, um das Wischen zu deaktivieren;
  • Zeile 11: Der boolesche Wert, der angibt, ob die Wischgeste zugelassen werden soll oder nicht.

Sobald dies erledigt ist, müssen wir nun unseren neuen Seitenmanager verwenden. Dies geschieht in der XML-Ansicht [activity_main.xml] und in der Hauptaktivität [MainActivity]. In [activity_main.xml] schreiben wir:

  

  <exemples.android.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

Zeile 1: Wir verwenden die neue Klasse. In [MainActivity] ändert sich der Code wie folgt:


package exemples.android;
 
...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
 
  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // the fragment container is associated with the fragment manager
    // i.e. fragment no. i in the fragment container is fragment no. i issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
    // the tab bar is also associated with the fragment container
...
  • Zeile 12: Der Seitenmanager ist nun vom Typ [MyPager];
  • Zeile 23: Wir aktivieren oder deaktivieren das Wischen.

Testen Sie diese neue Version. Aktivieren oder deaktivieren Sie das Wischen und beobachten Sie den Unterschied im Verhalten der Ansichten, wenn Sie sie mit der Maus nach rechts oder links ziehen. In allen zukünftigen Anwendungen wird das Wischen deaktiviert sein. Wir werden es nicht mehr erwähnen.

1.10.6. Scrollen zwischen Fragmenten deaktivieren

Fahren wir mit einer Verbesserung des Tab-Managers fort. Beim Wechsel von Tab 1 zu Tab 4 sehen Sie, wie die beiden dazwischenliegenden Tabs, 2 und 3, vorbeiscrollen. Im Android-Jargon wird dies als smoothScrolling bezeichnet. Dieses Verhalten kann bei vielen Tabs störend sein. Es lässt sich deaktivieren, indem Sie den folgenden Code zum Fragment-Manager [MyPager] hinzufügen:


// swipe control
  private boolean isSwipeEnabled;
  // controls scrolling
  private boolean isScrollingEnabled;
 
 ...
  // scrolling
  @Override
  public void setCurrentItem(int position){
    super.setCurrentItem(position,isScrollingEnabled);
  }
 
  // setters
...
 
  public void setScrollingEnabled(boolean scrollingEnabled) {
    isScrollingEnabled = scrollingEnabled;
  }

Da der Tab-Manager mit dem Fragment-Manager [MyPager] verknüpft wurde, wird beim Klicken auf Tab #i das Fragment #i vom Fragment-Container mithilfe der oben genannten Methode [setCurrentItem] (Zeile 9) angezeigt. [position] ist die Nummer des anzuzeigenden Fragments;

  • Zeile 10: Die Methode [setCurrentItem] der übergeordneten Klasse wird aufgerufen. Das zweite Argument, das auf [false] gesetzt ist, fordert einen sofortigen Übergang zwischen dem alten und dem neuen Fragment an (kein Scrollen); auf [true] gesetzt, fordert es einen Übergang per Scrollen an. Hier ist das zweite Argument der Wert des Feldes in Zeile 4, ein Feld, das der Entwickler mithilfe der Methode in den Zeilen 16–18 setzen kann;

Wenn Sie das Scrollen deaktivieren möchten, sieht die Klasse [MainActivity] wie folgt aus:


...
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
...

Führen Sie das Projekt erneut aus und überprüfen Sie, ob beispielsweise zwischen den Registerkarten 1 und 4 kein Scrollen mehr stattfindet. Von nun an werden wir das Scrollen immer deaktivieren. Wir werden darauf nicht mehr zurückkommen.

1.10.7. Ein neues Fragment

In unserem Beispiel sind alle Fragmente vom gleichen Typ [PlaceHolderFragment]. Wir werden nun lernen, wie man ein neues Fragment erstellt und anzeigt.

Kopieren Sie zunächst die Ansicht [vue1.xml] aus dem Projekt [Example-04] in das Projekt [Example-09] [1]:

 
  • in [1] die Ansicht [vue1.xml];
  • In [3] werden in der Ansicht Fehler angezeigt, die auf fehlenden Text in der Datei [res/values/strings.xml] zurückzuführen sind;

In [2] fügen wir den fehlenden Text hinzu, indem wir ihn aus der Datei [res/values/strings.xml] im Projekt [Example-04] übernehmen


<resources>
  <string name="app_name">Exemple-07</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
</resources>
  • Oben haben wir die Zeilen 6–9 hinzugefügt;

Nun erstellen wir die Klasse [Vue1Fragment], die als Fragment für die Anzeige der Ansicht [vue1.xml] zuständig sein wird:

  

Die Klasse [Vue1Fragment] sieht wie folgt aus:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends Fragment {
 
  // 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(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
}
 
  • Zeile 10: Die Annotation [@EFragment] stellt sicher, dass das von der Aktivität verwendete Fragment tatsächlich die Klasse [Vue1Fragment_] ist. Behalten Sie dies im Hinterkopf. Das Fragment ist mit der Ansicht [vue1.xml] verknüpft;
  • Zeilen 14–15: Die durch [R.id.editTextNom] identifizierte Komponente wird in Zeile 15 in das Feld [editTextNom] eingefügt;
  • Zeilen 18–20: Die Methode [doValider] verarbeitet das „Click“-Ereignis auf der Schaltfläche, die durch [R.id.buttonValider] identifiziert wird;
  • Zeile 21: Der erste Parameter von [Toast.makeText] ist vom Typ [Activity]. Die Methode [Fragment.getActivity()] ruft die Aktivität ab, in der sich das Fragment befindet. Dies ist [MainActivity], da wir in dieser Architektur nur eine einzige Aktivität haben, die verschiedene Ansichten oder Fragmente anzeigt;

In der Klasse [MainActivity] ist der Fragment-Manager wie folgt implementiert:


public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private Fragment[] fragments;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int 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[fragments.length - 1] = new Vue1Fragment_();
    }
 
 ...
  }
  • Zeile 13: Es gibt [FRAGMENTS_COUNT] Fragmente: [FRAGMENTS_COUNT-1] Fragmente vom Typ [PlaceholderFragment] (Zeilen 14–21) und ein Fragment vom Typ [Vue1Fragment_], Zeile 23 (beachte den Unterstrich);

Kompilieren Sie das Projekt [Example-09] und führen Sie es anschließend aus. Registerkarte 5 sollte nun anders aussehen:

1.10.8. Leiten Sie alle Fragmente von derselben abstrakten Klasse ab

Das neue [Vue1Fragment]-Fragment muss sich ebenfalls aktualisieren, wenn es angezeigt wird. Dazu müssen wir Code erstellen, der dem für das [PlaceholderFragment]-Fragment erstellten Code ähnelt. Um Wiederholungen zu vermeiden, werden wir alles, was möglich ist, in eine abstrakte Klasse auslagern, von der alle Fragmente in der Anwendung erben werden.

Dazu erstellen wir ein neues Projekt.

1.11. Beispiel 10: Ableitung aller Fragmente von einer abstrakten Klasse

1.11.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-09] in [Beispiel-10]:

1.11.2. Verwaltung des Debug-Modus

Wir fügen dem Projekt die Option hinzu, Debug-Modus-Protokolle ein- oder auszublenden. Dazu fügen wir der Klasse [MainActivity] eine statische Konstante hinzu:


  // mode debug
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. Die abstrakte Oberklasse aller Fragmente

  

Die Klasse [AbstractFragment] sieht wie folgt aus:


package exemples.android;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
 
public abstract class AbstractFragment extends Fragment {
 
  // private data
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;
 
  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;
 
  // activity
  protected MainActivity activity;
 
  // session
  protected Session session;
 
  // manufacturer
  public AbstractFragment() {
    // init
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    ...
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    ...
  }
 
  // local news
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }
 
  // update fragment
  protected void update() {
    ...
    // the daughter class is asked to update itself
    updateFragment();
  }
 
  protected abstract void updateFragment();
}
  • Zeile 7: Die Klasse [AbstractFragment] erweitert die Android-Klasse [Fragment];
  • Jedes Fragment muss in der Lage sein, sich selbst zu aktualisieren. Deshalb verlangt die übergeordnete Klasse [AbstractFragment] von ihren untergeordneten Klassen, dass sie über eine [updateFragment]-Methode verfügen (Zeile 68), die sie aufruft (Zeile 65);
  • Zeile 19: Die Klasse speichert einen Verweis auf die Aktivität der Anwendung;
  • Zeile 22: Die Klasse speichert einen Verweis auf die Sitzung, in der die von den Fragmenten und der Aktivität gemeinsam genutzten Daten gesammelt werden;
  • Zeilen 25–33: Der Konstruktor der abstrakten Klasse;
  • Zeile 27: Erstellung einer Kopie der Konstante [MainActivity.IS_DEBUG_ENABLED] im Feld in Zeile 16;
  • Zeile 28: Der Name der instanziierten Klasse wird gespeichert, d. h. der Name einer Unterklasse;
  • Zeilen 15–22: Diese Felder haben das Attribut [protected], damit Unterklassen darauf zugreifen können. Beachten Sie, dass die Unterklassen nichts von der Existenz der Booleschen Werte [isVisibleToUser] und [updateDone] wissen (Zeilen 10–11);
  • Zeile 57: Die Methode [getParentInfos] hat das Attribut [protected], damit Unterklassen sie aufrufen können;

Die Methoden [setUserVisibleHint, onDestroyView, onResume] bleiben unverändert gegenüber der Klasse [PlaceholderFragment] aus dem vorherigen Projekt:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // memory
    this.isVisibleToUser = isVisibleToUser;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // when the fragment becomes visible
    if (isVisibleToUser) {
      // update fragment
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // we leave the fragment
      updateDone = false;
    }
  }
 
  @Override
  public void onDestroyView() {
    // parent
    super.onDestroyView();
    // indicator update
    afterViewsDone = false;
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }
 
  @Override
  public void onResume() {
    // parent
    super.onResume();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // update
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

Die [update]-Methode lautet wie folgt:


  // update fragment
  protected void update() {
    // recover activity and session
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}

Gemäß dem obigen Code ist das Fragment sichtbar, wenn die [update]-Methode eines Fragments ausgeführt wird. Dies ist wichtig, da es bedeutet, dass die [Fragment.getActivity]-Methode dann eine Referenz auf die Aktivität der Anwendung zurückgibt (siehe Abschnitt 1.10.8), die wiederum Zugriff auf die Sitzung gewährt.

  • Zeilen 4–10: Initialisieren Sie die Aktivität und die Sitzung, falls diese noch nicht initialisiert wurden;
  • Zeile 12: Die Methode [updateFragment] der untergeordneten Klasse wird aufgerufen. Wenn diese Methode ausgeführt wird, sind die Felder [activity] und [session], auf die sie Zugriff hat, bereits initialisiert;

1.11.4. Die Klasse [PlaceholderFragment]

  

Die Klasse [PlaceholderFragment] ist wie folgt aufgebaut:


package exemples.android;
 
import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {
 
  // visual interface component
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
 
  // data
  private boolean initDone;
 
  // data
  private String text;
  private int numVisit;
 
  // fragment no
  private static final String ARG_SECTION_NUMBER = "section_number";
 
  // manufacturer
  public PlaceholderFragment() {
    super();
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }
 
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
 ...
  }
 
  // update fragment
  public void updateFragment() {
  ...
  }
 
}
  • Zeile 10: Die Klasse [PlaceholderFragment] erweitert die Klasse [AbstractFragment]. Bei dieser Architektur umfasst das Schreiben eines Fragments:
    • das Schreiben der Methode [@AfterViews], die dazu dient, das Fragment während seines ersten Lebenszyklus zu initialisieren oder es zurückzusetzen, falls zuvor ein [onDestroyView] aufgetreten ist. Zeile 39 ist erforderlich, um den Lebenszyklus des Fragments ordnungsgemäß zu verwalten;
    • das Schreiben der Methode [updateFragment], die das Fragment unmittelbar vor seiner Anzeige aktualisiert. Diese Methode kann die Sitzung ihrer übergeordneten Klasse nutzen;
    • Schreiben der Ereignisbehandler des Fragments. Dies werden wir in zukünftigen Projekten tun;

Die Methoden [@AfterViews] und [updateFragment] bleiben unverändert gegenüber dem vorherigen Projekt:


@AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // initial text
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // init done
      initDone = true;
    }
    // current text display
    textViewInfo.setText(text);
  }
 
  // update fragment
  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • Zeilen 7 und 23: In den Protokollen zeigen wir Informationen aus der übergeordneten Klasse mithilfe der geerbten Methode [getParentInfos] an;

1.11.5. Die Klasse [Vue1Fragment]

  

Die Klasse [Vue1Fragment] hat denselben Aufbau wie die Klasse [PlaceholderFragment]:


package exemples.android;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import org.androidannotations.annotations.*;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  // data
  private int numVisit;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s - %s", getParentInfos(), getLocalInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
  // local info for logs
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // the visit number is displayed
    Toast.makeText(getActivity(), String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
  }
}
  • Zeile 9: Die Klasse [Vue1Fragment] erweitert die Klasse [AbstractFragment];
  • Zeilen 18–26: Die Methode [@AfterViews] hat keine nennenswerten Aufgaben. Sie muss dennoch geschrieben werden, um die boolesche Variable [afterViewsDone] auf „true“ zu setzen, da diese Information von der übergeordneten Klasse verwendet wird;
  • Zeilen 42–49: Die Methode [updateFragment] besteht darin, eine kurze Meldung mit der Besuchsnummer anzuzeigen (Zeile 48) und diese Nummer in der Sitzung zu erhöhen (Zeilen 44–46);

Leser sind eingeladen, dieses neue Projekt zu testen.

Wir werden diese Architektur in allen zukünftigen Projekten verwenden:

  • eine Aktivität und n Fragmente;
  • alle Fragmente erweitern die Klasse [AbstractFragment];
  • Daten, die zwischen Fragmenten sowie zwischen Fragmenten und der Aktivität ausgetauscht werden sollen, werden in der Klasse [Session] abgelegt;

1.11.6. Zuordnung von Registerkarten und Fragmenten

In der Klasse [MainActivity], die die Tabs verwaltet, steht Folgendes geschrieben:


// the tab bar is also associated with the fragment container
// i.e. tab n° i displays fragment n° i of the container
tabLayout.setupWithViewPager(mViewPager);

Zeile 3 verknüpft den Tab-Manager mit dem Fragment-Container. Eine Folge dieser Verknüpfung haben wir bereits gesehen: Wenn der Benutzer auf Tab Nr. i klickt, zeigt der Fragment-Container Fragment Nr. i an. Das Gegenteil haben wir noch nicht gesehen: Wenn wir den Fragment-Container auffordern, Fragment Nr. i anzuzeigen, wird automatisch Tab Nr. i ausgewählt.

Um dieses Verhalten zu veranschaulichen, fügen wir dem aktuellen Menü die Optionen [Fragment 1, Fragment 2, ...] hinzu. Wenn der Benutzer auf die Option [Fragment i] klickt, weisen wir den Fragment-Container an, Fragment #i anzuzeigen. Anschließend prüfen wir, ob Registerkarte #i ausgewählt wurde oder nicht.

Dieser Schritt beginnt mit der Anpassung des Anwendungsmenüs:

 

Der Inhalt der Datei [res/menu/menu_main.xml] ändert sich wie folgt:


<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"/>
  <item android:id="@+id/fragment5"
        android:title="@string/fragment5"
        android:orderInCategory="100"
        app:showAsAction="never"/>
</menu>
  • Zeilen 9–28: die fünf neuen Menüoptionen;
  • die Bezeichnungen der Optionen (Zeilen 10, 14, 18, 22, 26) sind in der Datei [res/values/strings.xml] [2] definiert:

<resources>
  <string name="app_name">Exemple-10</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <string name="btn_vue2">Vue n° 2</string>
  <!-- menu -->
  <string name="fragment1">Fragment 1</string>
  <string name="fragment2">Fragment 2</string>
  <string name="fragment3">Fragment 3</string>
  <string name="fragment4">Fragment 4</string>
  <string name="fragment5">Fragment 5</string>
</resources>

Das visuelle Ergebnis sieht wie folgt aus:

  

Die Klickverarbeitung für diese Menüoptionen wird in der Klasse [MainActivity] abgewickelt:


@Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "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;
      }
      case R.id.fragment5: {
        showFragment(4);
        break;
      }
    }
    // item processed
    return true;
  }
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
    }
  }
  • Zeile 2: Die Methode [onOptionsItemSelected] wird aufgerufen, wenn eine der Menüoptionen angeklickt wird;
  • Zeile 8: Wir rufen die ID der angeklickten Option ab;
  • Zeilen 9–36: Die verschiedenen Fälle werden durch eine switch-Anweisung behandelt;
  • Zeilen 16–36: Durch Klicken auf die Option [Fragment i] wird die Methode [showFragment(i-1)] in den Zeilen 41–45 aufgerufen;
  • Zeile 43: Der Fragment-Container wird aufgefordert, das angeforderte Fragment anzuzeigen;
  • Zeile 42: Wir prüfen zunächst, ob dies möglich ist (Bedingung 1) und ob es notwendig ist (Bedingung 2);

Leser sind eingeladen, diese neue Version zu testen. Wir stellen fest, dass, wenn wir die Anzeige von Fragment #i anfordern, dieses tatsächlich angezeigt wird und die Registerkarte #i selbst ausgewählt ist.

Nachdem wir nun gesehen haben, wie die Zuordnung zwischen Registerkarte und Fragment funktioniert, betrachten wir einen anderen Fall: einen, bei dem die Verwaltung der Registerkarten von der Verwaltung der Fragmente entkoppelt ist. Dies ist beispielsweise der Fall, wenn es weniger Registerkarten als Fragmente gibt. Um diesen neuen Anwendungsfall zu veranschaulichen, erstellen wir ein neues Projekt.

1.12. Beispiel 11: Von Fragmenten getrennte Registerkarten

1.12.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-10] in [Beispiel-11]:

1.12.2. Ziele

Die neue Anwendung wird zwei Registerkarten haben:

  • Der erste Reiter zeigt immer das Fragment [View1] an;
  • die zweite Registerkarte zeigt ein aus dem Menü ausgewähltes Fragment an;

Image

  • in [1] das Fragment [View1];
  • in [2] das vom Benutzer ausgewählte Fragment [PlaceholderFragment];
  • in [3] werden die Besuche weiterhin gezählt;

1.12.3. Die Sitzung

  

Die neue Sitzung wird wie folgt ablaufen:


package exemples.android;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment;
 
  // getters and setters
...
}
  • Zeile 10: Wir werden die Klicks auf die Registerkarten selbst verarbeiten. Wenn auf eine Registerkarte geklickt wird, müssen wir das Fragment laden, das bei der letzten Auswahl angezeigt wurde. Das Feld [numFragment] speichert die Fragmentnummer für Registerkarte Nr. 2, eine Zahl im Bereich [0, Fragments_COUNT-2]. Wenn auf Registerkarte Nr. 2 geklickt wird, rufen wir die anzuzeigende Fragmentnummer aus der Sitzung ab;

1.12.4. Das Menü

  

Das Menü [res / menu / menu_main.xml] ändert sich wie folgt:


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

Registerkarte 2 zeigt eines der vier Fragmente aus den Zeilen 9–24 an. Das fünfte Fragment ist das [Vue1Fragment], das immer auf Registerkarte 1 angezeigt wird.

1.12.5. Die Klasse [MainActivity]

Die Klasse [MainActivity] muss nun die Registerkarten und die Navigation zwischen ihnen verwalten, was sie zuvor nicht tat. Ihr Code ändert sich wie folgt:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);
 
    // at the start there is only one tab
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
 
    // event manager
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // a tab has been selected - change the fragment displayed by the fragment container
        ...
      }
 
      @Override
      public void onTabUnselected(TabLayout.Tab tab) {
 
      }
 
      @Override
      public void onTabReselected(TabLayout.Tab tab) {
 
      }
    });
 
...
 
}
  • Zeile 17: Das erste vom Fragment-Container angezeigte Fragment ist das [Vue1Fragment]. Laut Design ist dies das letzte Fragment im Container;
  • Zeilen 20–22: Da wir keine Verbindung zwischen den Registerkarten und dem Fragment-Container hergestellt haben, müssen wir die Registerkarten selbst verwalten. Zu Beginn enthält die Registerkartenleiste [tabLayout] in Zeile 3 keine Registerkarten;
  • Zeile 20: Wir erstellen die erste Registerkarte;
  • Zeile 21: Wir geben ihr einen Titel. In den vorherigen Beispielen waren die Registerkartentitel identisch mit den Fragmenttiteln. Das ist nun nicht mehr der Fall. Daher entfernen wir die Methode [getPageTitle] aus dem Fragment-Manager. Wir benötigen sie nicht mehr:

    // optional - gives a title to managed fragments
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
}
  • Zeile 22: Die erstellte Registerkarte wird zur Registerkartenleiste hinzugefügt. Unsere Registerkartenleiste verfügt nun über eine Registerkarte. Was zeigt diese Registerkarte an? Es ist wichtig zu verstehen, dass Registerkarten und Fragmente zwei getrennte Konzepte sind. Das angezeigte Fragment ist immer dasjenige, das vom Fragment-Container ausgewählt wurde. Wenn Sie die Registerkarte wechseln und den Container nicht auffordern, das angezeigte Fragment zu ändern, passiert nichts: Es wird weiterhin dasselbe Fragment angezeigt, aber die ausgewählte Registerkarte hat sich geändert. Hier ist das angezeigte Fragment also dasjenige, das in Zeile 17 ausgewählt wurde: das Fragment [Vue1Fragment];
  • Zeilen 26–30: die Methode, die geschrieben werden muss, um den Tab-Wechsel des Benutzers zu verarbeiten;

Die Methode [onTabSelected] in den Zeilen 26–30 wird bei jedem Tab-Wechsel ausgelöst (wenn der Benutzer auf einen bereits ausgewählten Tab klickt, passiert nichts). Ihr Code lautet wie folgt:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // a tab has been selected - change the fragment displayed by the fragment container
        // miter position
        int position = tab.getPosition();
        // fragment number to display
        int numFragment;
        switch (position) {
          case 0:
            // fragment no. [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // fragment no. [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // fragment display
        mViewPager.setCurrentItem(numFragment);
}
  • Zeile 8: Wir ermitteln die Position der angeklickten Registerkarte. Hier erhalten wir die Zahl 0 oder 1;
  • Zeilen 12–15: Wenn die erste Registerkarte angeklickt wurde, bereiten wir die Anzeige des Fragments [Vue1Fragment] vor;
  • Zeilen 16–18: In anderen Fällen (Registerkarte Nr. 2 angeklickt) bereiten wir die erneute Anzeige des Fragments vor, das beim letzten Mal angezeigt wurde, als Registerkarte Nr. 2 ausgewählt wurde. Dessen ID wurde damals in der Sitzung der App gespeichert;
  • Zeile 21: Wir weisen den Fragment-Container an, das gewünschte Fragment anzuzeigen;

Sehen wir uns nun die Verwaltung der Menüoptionen an (immer noch in [MainActivity]):


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // processing menu options
    int id = item.getItemId();
    switch (id) {
      case R.id.action_settings: {
        if (IS_DEBUG_ENABLED) {
          Log.d("menu", "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;
}
  • Zeilen 16–31: Behandlung der 4 Menüoptionen. Jeder Handler ruft die Methode [showFragment] mit der Nummer des anzuzeigenden Fragments auf;

Die Methode [showFragment] sieht wie folgt aus:


  // tab n° 2
  private TabLayout.Tab tab2 = null;
 
  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // if the 2nd tab doesn't yet exist, we create it
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // set the title of the second tab
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // change the displayed fragment
      mViewPager.setCurrentItem(i);
      // the fragment number displayed is set to session
      session.setNumFragment(i);
      // tab 2 is selected - does nothing if already selected
      tab2.select();
    }
}
  • Beachten Sie, dass beim Start der Anwendung nur eine Registerkarte vorhanden ist;
  • Zeile 2: ein Verweis auf Registerkarte Nr. 2, zunächst null;
  • Zeile 5: Die Anzeigebedingungen haben sich gegenüber der vorherigen Version nicht geändert;
  • Zeilen 7–10: Wenn Registerkarte Nr. 2 noch nicht existiert, wird sie erstellt (Zeile 8) und zur Registerkartenleiste hinzugefügt (Zeile 9);
  • Zeile 12: Die Nummer des anzuzeigenden Fragments wird in den Titel der zweiten Registerkarte gesetzt, wobei die Nummerierung bei 1 beginnt;
  • Zeile 14: Das gewünschte Fragment wird angezeigt;
  • Zeile 16: Seine Nummer wird in der Sitzung gespeichert;
  • Zeile 18: Registerkarte Nr. 2 wird ausgewählt. War sie bereits ausgewählt, geschieht nichts: Die Methode [onTabSelected] wird nicht ausgeführt. War sie noch nicht ausgewählt, wird die Methode [onTabSelected] ausgelöst. Diese Methode weist dann den Fragment-Container an, das bereits in Zeile 14 angezeigte Fragment anzuzeigen. Eine einfache Überprüfung in der Methode [onTabSelected] verhindert dieses Szenario:

        // display fragment only if necessary
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Leser sind eingeladen, diese neue Version zu testen.

1.12.6. Verbesserungen

Wir verfügen nun über ein solides Verständnis von Fragmenten, ihrem Lebenszyklus, dem Konzept der Fragment-Nachbarschaft und ihrer Beziehung zur Tab-Leiste. Außerdem verfügen wir über eine robuste Architektur, die gerade den Test in Beispiel 11 bestanden hat:

  • eine Aktivität und n Fragmente;
  • alle Fragmente erweitern die Klasse [AbstractFragment];
  • Daten, die zwischen Fragmenten sowie zwischen Fragmenten und der Aktivität ausgetauscht werden sollen, werden in der Klasse [Session] abgelegt;

In einem neuen Projekt werden wir die Beziehungen zwischen der Aktivität und den Fragmenten durch Hinzufügen einer Schnittstelle verdeutlichen.

1.13. Beispiel 12: Definieren der Beziehungen zwischen der Aktivität und den Fragmenten

In diesem Beispiel möchten wir die minimalen Beziehungen zwischen der Aktivität und den Fragmenten definieren. Dazu verwenden wir:

  • eine Schnittstelle [IMainActivity], die definiert, was Fragmente von der Aktivität anfordern können;
  • eine abstrakte Klasse [AbstractFragment], die den Zustand und die Methoden definiert, über die jedes Fragment verfügen sollte;

1.13.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-11] in [Beispiel-12], indem wir die Vorgehensweise in Abschnitt 1.4 befolgen. Wir erhalten das folgende Ergebnis:

1.13.2. Die Schnittstelle [IMainActivity]

Aus den vorherigen Beispielen geht klar hervor, dass Fragmente Zugriff auf die von der Aktivität instanziierte Sitzung benötigen. Darüber hinaus ist zu erwarten, dass Ereignisbehandler in Fragmenten manchmal zu einer Änderung der Ansicht führen, auch wenn dies in diesen Beispielen nicht sichtbar ist. Die Aktivität wird aufgefordert, diese Änderung durchzuführen. Die Schnittstelle [IMainActivity] könnte dann wie folgt aussehen:

  

package exemples.android;
 
public interface IMainActivity {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
}

Zeile 12: Beachten Sie das Vorhandensein einer Konstante, die zuvor in der Klasse [MainActivity] enthalten war. Wir möchten die Kopplung zwischen den Fragmenten und der Aktivität reduzieren und auf eine Kopplung zwischen [AbstractFragment] und [IMainActivity] beschränken. Die Aktivität kann dann einen anderen Namen als [MainActivity] erhalten. Da die Konstante [IS_DEBUG_ENABLED] in den Fragmenten verwendet wird, wird sie in die Schnittstelle [IMainActivity] verschoben.

1.13.3. Die abstrakte Klasse [AbstractFragment]

Die abstrakte Klasse [AbstractFragment] ändert sich nur geringfügig:


  // data accessible to daughter classes
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
...
  // update fragment
  protected void update() {
    // recover activity and session
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // the daughter class is asked to update itself
    updateFragment();
}
  • Zeilen 6 und 7: Wir verwalten zwei Arten von Verweisen auf die Aktivität:
    • Zeile 6: eine Referenz auf die Aktivität, die die Schnittstelle [IMainActivity] implementiert;
    • Zeile 7: eine Referenz auf die Aktivität, die von der Android-Klasse [Activity] erbt. Dies gilt für alle Aktivitäten;

Diese beiden Verweise verweisen natürlich auf dasselbe Objekt. Dieses Objekt wird jedoch als zwei verschiedene Typen angesehen. Dies verhindert eine Typumwandlung zur Laufzeit;

  • Zeile 14: Wir rufen eine Referenz auf die Aktivität mithilfe der Methode [getActivity] ab;
  • Zeile 15: Ist diese Referenz nicht null, können wir auf die Sitzung zugreifen;
  • Zeilen 16–17: Wir speichern die Aktivität als Implementierung der Schnittstelle [IMainActivity] und die Sitzung;

1.13.4. Änderung des Fragment-Managers

Der Fragment-Adapter [SectionsPagerAdapter] in der Klasse [MainActivity] wird an einer Stelle geändert: Anstatt Fragmente vom Typ [Fragment] zu verwalten, verwaltet er nun Fragmente vom Typ [AbstractFragment]:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
    // fragment no
    private static final String ARG_SECTION_NUMBER = "section_number";
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
      // initialization of fragment table
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // a fragment of +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. Ändern der Klasse [MainActivity]

Die Klasse [MainActivity] muss die Schnittstelle [IMainActivity] implementieren:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{
 
...
  // injection session
  @Bean(Session.class)
  protected Session session;
...
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if(mViewPager.getCurrentItem()!=position){
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  • Zeilen 10–12: Die Methode [getSession] existierte bereits;
  • Zeilen 15–22: Die Methode [navigateToView] zeigt das Fragment #[position] an;
  • Zeile 17: Wir prüfen, ob etwas zu tun ist;
  • Zeile 19: Fragment #[position] wird angezeigt;

Führen Sie nun die Anwendung aus. Sie sollte funktionieren.

1.13.6. Anpassen der Anzeige von Fragmenten in [MainActivity]

Derzeit zeigt die Klasse [MainActivity] ein Fragment mithilfe der Anweisung an:


    // view1 display
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

Da die Methode [navigateToView] dasselbe bewirkt, ersetzen Sie diese Art von Anweisung an allen Stellen (2 Stellen) durch:

navigateToView(...);

Führen Sie dann die App aus. Sie sollte weiterhin funktionieren.

1.13.7. Fazit

Von nun an werden wir immer die bisherige Architektur verwenden:

  • eine Aktivität, die die Schnittstelle [IMainActivity] implementiert;
  • Fragmente, die die Klasse [AbstractFragment] erweitern, was erfordert, dass sie die Methode [updateFragment] implementieren. Diese müssen außerdem über eine Methode [@AfterViews] verfügen, in der sie den booleschen Wert [afterViewsDone] auf true setzen;
  • eine Session, die die Daten kapselt, die zwischen den Fragmenten und der Aktivität ausgetauscht werden sollen;

1.14. Beispiel-13: Beispiel-05 mit Fragmenten

Im Projekt [Beispiel-05] haben wir die Navigation zwischen Ansichten eingeführt. Damals handelte es sich um die Navigation zwischen Aktivitäten: 1 Ansicht = 1 Aktivität. Hier schlagen wir vor, eine einzige Aktivität mit mehreren Ansichten vom Typ [AbstractFragment] zu verwenden.

1.14.1. Erstellen des Projekts

Wir duplizieren das vorherige Projekt [Beispiel-12] in [Beispiel-13], indem wir die Vorgehensweise in Abschnitt 1.4 befolgen. Wir erhalten das folgende Ergebnis:

1.14.2. Projektstruktur

Wir beginnen damit, den Code mithilfe von Paketen zu organisieren. Vorerst lassen sich zwei unterschiedliche Bereiche unterscheiden:

  • Aktivitätsverwaltung;
  • Fragmentverwaltung;

Wir erstellen dafür zwei Pakete: [examples.android.activity] und [examples.android.fragments]:

 

Wir gehen genauso vor, um das Paket [examples.android.fragments] zu erstellen:

In [8] erstellen wir ein drittes Paket namens [architecture], in dem wir die Entitäten [IMainActivity, AbstractFragment, Session, MyPager] ablegen, die die Bausteine der Architektur unserer App bilden. Dies dient als Erinnerung daran, dass wir eine bestimmte architektonische Entscheidung getroffen haben. Verschieben Sie als Nächstes die vorhandenen Projektelemente wie in [9] gezeigt. Jeder Verschiebungsvorgang muss durch Klicken auf die Schaltfläche [Refactor] bestätigt werden.

Kompilieren Sie nun die Anwendung. In [MainActivity] treten folgende Fehler auf:

 

Beim Verschieben von Klassen in Pakete hat Android Studio die erforderlichen Änderungen am Anwendungscode vorgenommen (zum Beispiel in den Zeilen 18–21). Die in den Zeilen 15 und 17 referenzierten Klassen wurden nicht verschoben. Sie werden von der Android-Annotations-Bibliothek generiert. Für diese Klassen müssen Sie die Importe manuell ändern. Diese Zeilen lauten daher:

 

Sobald dies erledigt ist, treten keine Kompilierungsfehler mehr auf. Führen Sie die Anwendung aus. Sie erhalten dann folgende Fehlermeldung:

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{exemples.android/exemples.android.MainActivity_}: 

Dieser Fehler ist auf das Anwendungsmanifest zurückzuführen:

  

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.android">
 
  <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
      android:name=".MainActivity_"
      android:label="@string/app_name"
      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>

In den Zeilen 3 und 12 wird angegeben, dass die benannte Aktivität [examples.android.MainActivity_] ist. Da die Aktivität jedoch in das Paket [activity] verschoben wurde, muss Zeile 12 nun wie folgt lauten:


      android:name=".activity.MainActivity_"

Beachten Sie das „.“ vor [activity]. Auch hier konnte Android Studio das Manifest nicht aktualisieren, da es auf eine Android-Annotations-Klasse verweist, die nicht verschoben wurde. Die Verwendung der AA-Bibliothek bringt daher eine Reihe von Unannehmlichkeiten mit sich.

1.14.3. Das Projekt bereinigen

Im neuen Projekt:

  • gibt es keine Registerkarten, schwebenden Schaltflächen oder Menüs mehr;
  • die Fragmente [PlaceholderFragment] verschwinden. Die App verwaltet zwei Fragmente: [Vue1Fragment], das wir bereits haben, und [Vue2Fragment], das wir erstellen müssen;
  • die Sitzung ist nicht mehr dieselbe;

1.14.3.1. Bereinigung der Fragmente

Löschen Sie die Klasse [PlaceHolderFragment] [1]:

 

Löschen Sie ebenfalls die diesem Fragment zugeordnete Ansicht [res/layout/fragment_main.xml] [2].

1.14.3.2. Bereinigung der Sitzung

Die Sitzung sieht derzeit wie folgt aus:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in second tab
  private int numFragment;
 
  // getters and setters
 
  public int getNumVisit() {
    return numVisit;
  }
 
  public void setNumVisit(int numVisit) {
    this.numVisit = numVisit;
  }
 
  public int getNumFragment() {
    return numFragment;
  }
 
  public void setNumFragment(int numFragment) {
    this.numFragment = numFragment;
  }
}

Wir speichern nichts aus dieser Sitzung.

Kompilieren Sie das Projekt. Die Zeilen, die Fehler verursachen, sind diejenigen, die den Inhalt der Session verwendet haben. Entfernen Sie diese. In der Klasse [Vue1Fragment] entfernen wir außerdem die Variable [numVisit] aus dem Code, der nun wie folgt aussieht:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event manager
  @Click(R.id.buttonValider)
  protected void doValider() {
    // the name entered is displayed
    Toast.makeText(getActivity(), String.format("Bonjour %s", editTextNom.getText().toString()), Toast.LENGTH_LONG).show();
  }
 
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}

1.14.3.3. Entfernen der Registerkarten, der schwebenden Schaltfläche und des Menüs

Das Entfernen der Registerkarten und der schwebenden Schaltfläche erfolgt an zwei Stellen:

  • in der Ansicht [res/layout/activity-main.xml], die diese Elemente und ihre Platzierung in der Ansicht definiert;
  • im Aktivitätscode [MainActivity];

Das Menü wird ebenfalls an zwei Stellen entfernt:

  • in der Ansicht [res/menu/menu-main.xml], die die Menüoptionen definiert;
  • im Aktivitätscode [MainActivity];

Der Code für die Ansicht [res/layout/activity-main.xml] lautet derzeit wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">
 
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">
 
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
 
    </android.support.v7.widget.Toolbar>
 
    <android.support.design.widget.TabLayout
      android:id="@+id/tabs"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>
 
  </android.support.design.widget.AppBarLayout>
 
  <exemples.android.architecture.MyPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
 
  <android.support.design.widget.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="end|bottom"
    android:layout_margin="@dimen/fab_margin"
    android:src="@android:drawable/ic_dialog_email"/>
 
</android.support.design.widget.CoordinatorLayout>
  • Entferne die Zeilen [28–31, 41–47];
  • entferne außerdem die Symbolleiste aus den Zeilen 18–24;

Der Menücode [res / menu / menu_main.xml] lautet derzeit wie folgt:


<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/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>
  • Wir entfernen die Zeilen 9–24. Damit bleibt eine Option übrig, die wir nicht verwenden werden. Dies dient lediglich als Beispiel für eine Menüoptionsdeklaration, die per Kopieren/Einfügen repliziert werden kann;

Entfernen Sie in der Klasse [MainActivity] alles, was sich auf die Registerkarten, die schwebende Schaltfläche, die Symbolleiste und das Menü bezieht. Am einfachsten finden Sie diese Verweise, indem Sie deren Deklarationen entfernen:


  // the tab manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // the floating button
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

und kompiliere die App neu. Die Zeilen mit Fehlern sind diejenigen, die auf die fehlenden Elemente verweisen. Lösche also alle diese Zeilen. Passe außerdem den Fragment-Manager so an, dass er nicht mehr auf das [PlaceholderFragment]-Fragment verweist, das wir gelöscht haben:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • Zeilen 7–10: Wir haben die gesamte Fragmentgenerierung entfernt;

An dieser Stelle sollten keine Kompilierungsfehler mehr auftreten. In der Klasse [MainActivity] haben wir nun den folgenden Zwischencode erhalten:


package exemples.android.activity;
 
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.IMainActivity;
import exemples.android.architecture.MyPager;
import exemples.android.architecture.Session;
import exemples.android.fragments.Vue1Fragment_;
import org.androidannotations.annotations.*;
 
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  // the fragment container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// the toolbar
@ViewById(R.id.toolbar)
protected Toolbar toolbar;
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // number of fragments
  private final int FRAGMENTS_COUNT = 5;
  // fragment adjacency
  private final int OFF_SCREEN_PAGE_LIMIT = 2;
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
  // the fragment manager
  private SectionsPagerAdapter mSectionsPagerAdapter;
 
  // manufacturer
  public MainActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }
 
  @AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
 
    // toolbar - this is where the application name is displayed
    setSupportActionBar(toolbar);
 
    // the fragment manager
    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 issued by the fragment manager
    mViewPager.setAdapter(mSectionsPagerAdapter);
 
    // fragment offset
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);
 
    // inhibit swiping between fragments
    mViewPager.setSwipeEnabled(false);
 
    // no scrolling
    mViewPager.setScrollingEnabled(false);
 
    // view1 display
    navigateToView(FRAGMENTS_COUNT - 1);
 
  }
 
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }
 
  // getter session
  public Session getSession() {
    return session;
  }
 
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // the fragment manager
  // it is used to request fragments to be displayed in the main view
  // must define methods [getItem] and [getCount] - the others are optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments;
 
    // manufacturer
    public SectionsPagerAdapter(FragmentManager fm) {
      // parent
      super(fm);
    }
 
    // fragment n° position
    @Override
    public AbstractFragment getItem(int position) {
      // log
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }
 
    // makes the number of fragments managed
    @Override
    public int getCount() {
      return fragments.length;
    }
  }
}

Es sind noch ein paar Änderungen vorzunehmen:

  • Löschen Sie Zeile 31, die nicht mehr benötigt wird;
  • Zeile 33: Setze die Fragment-Adjazenz auf 1;
  • Zeile 76: Navigiere zu Ansicht 0. Diese wird als erste angezeigt;
  • Zeile 108: Initialisiere das Array mit dem Fragment [Vue1Fragment_]:

    // fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Wir haben also nur ein Fragment. Führen Sie die Anwendung aus. Sie sollten das folgende Ergebnis erhalten:

Image

Die Schaltfläche [Validate] sollte funktionieren.

1.14.4. Erstellen von Fragmenten und zugehörigen Ansichten

Die Anwendung wird zwei Ansichten haben, nämlich die aus dem Projekt [Example-05]. Die Ansicht [vue1.xml] ist im aktuellen Projekt bereits vorhanden. Wir werden nun [vue2.xml] aus [Example-05] nach [Example-12] kopieren (öffne beide Projekte und kopiere die Datei zwischen ihnen hin und her).

 
  • In [1] die neue Ansicht. Wenn wir versuchen, sie zu bearbeiten, treten Fehler auf [2]. Wir müssen die Datei [strings.xml] [3] ändern, um die von dieser neuen Ansicht referenzierten Strings hinzuzufügen:

<resources>
  <string name="app_name">Exemple-13</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- vue 2 -->
  <string name="btn_vue2">Vue n° 2</string>
  <string name="titre_vue2">Vue n° 2</string>
  <string name="btn_vue1">Vue n° 1</string>
</resources>

Wir duplizieren die Klasse [View1Fragment] in [View2Fragment]:

  

und ändern Sie den kopierten Code wie folgt:


package exemples.android.fragments;
 
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.EFragment;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Zeile 9: Das Fragment ist mit der Ansicht [res/layout/view2.xml] verknüpft;
  • Zeile 10: Die Klasse erweitert die abstrakte Klasse [AbstractFragment];
  • Zeilen 12–20: die erforderliche Methode [@AfterViews];
  • Zeilen 23–25: die erforderliche Methode [updateFragment];

1.14.5. Implementierung von Fragmenten und Navigation zwischen ihnen

Die Aktivität verwaltet nun zwei Fragmente. Ihre Klasse [SectionsPagerAdapter] wird wie folgt aktualisiert:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
 
    ...
}

Die Schnittstelle [IMainActivity] steuert die Navigation zwischen den Ansichten mithilfe ihrer Methode [navigateToView]. Wir werden den Klick auf die Schaltfläche [View 2] im Fragment [Vue1Fragment] behandeln:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // event managers ----------------------------------
  @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();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Zeilen 37–40: Die Methode [showVue2] verarbeitet das „Click“-Ereignis der Schaltfläche [View #2];
  • Zeile 39: Die Navigation erfolgt über die Methode [navigateToView] der Aktivität. Erinnern Sie sich daran, dass die Aktivität in der übergeordneten Klasse wie folgt gespeichert wurde:

  // activity
protected IMainActivity mainActivity;

und dass diese Aktivität bereits initialisiert wurde, wenn ein beliebiger Ereignis-Handler aufgerufen wird.

  • Zeile 34: Die Anweisung verwendet die Variable [activity] der übergeordneten Klasse, die eine Referenz auf die Aktivität als Instanz des Android-Typs [Activity] darstellt;

protected Activity activity;

Ähnlichen Code finden wir für das Fragment [Vue2Fragment]:


package exemples.android.fragments;
 
import android.util.Log;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
  }
}
  • Zeilen 24–27: Die Methode [showVue1] verarbeitet das „click“-Ereignis der Schaltfläche [View 1];

Führen Sie das Projekt aus und überprüfen Sie, ob die Navigation zwischen den Ansichten funktioniert.

1.14.6. Definition der Sitzung

Die Anwendung funktioniert wie folgt:

  • Geben Sie einen Namen in Ansicht 1 ein;
  • Zeigen Sie diesen Namen in Ansicht 2 an;

Damit Ansicht 1 den eingegebenen Namen an Ansicht 2 weitergeben kann, verwenden wir die folgende Sitzung;


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // name
  private String nom;
 
  // getters and setters
...
}
  • Zeile 8: der eingegebene Name;

Die Klasse [MainActivity] initialisiert die Sitzung wie folgt:


  // injection session
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // init session
    session.setNom("");
}

1.14.7. Endgültiger Code für die Fragmente

Im Fragment [Vue1Fragment] ändern wir den Code für den Klick-Handler der Schaltfläche [Validate]:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.EditText;
import android.widget.Toast;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextNom)
  protected EditText editTextNom;
 
...
  // event managers ----------------------------------
 
  @Click(R.id.buttonValider)
  protected void doValider() {
    // memorizes the name entered
    String nom = editTextNom.getText().toString();
    // we display it
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }
 
  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // enter the name entered in the session
    session.setNom(editTextNom.getText().toString());
    // navigate to view no. 2
    mainActivity.navigateToView(1);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
 
  }
}
  • Zeilen: 31–37: Verarbeiten des Klicks auf die Schaltfläche [Ansicht Nr. 2];
  • Zeile 34: Bevor wir zu Ansicht 2 navigieren, speichern wir den eingegebenen Namen in der Sitzung, damit die neue Ansicht darauf zugreifen kann;

Die Ansicht [View2Fragment] entwickelt sich wie folgt:


package exemples.android.fragments;
 
import android.util.Log;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // gestionnaires d'évts ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Wenn Ansicht Nr. 2 angezeigt wird, muss der in Ansicht Nr. 1 eingegebene Name angezeigt werden. Wir wissen, dass unmittelbar nach der Anzeige die Methode [updateFragment] ausgeführt wird. Daher können wir den Code zur Anzeige des Namens in dieser Methode (Zeilen 36–42) platzieren.

  • Zeilen 16–17: Deklaration der einzigen visuellen Komponente der Ansicht;
  • Zeile 39: Der in Ansicht 1 eingegebene Name wird aus der Sitzung abgerufen;
  • Zeile 41: Das Label [textViewBonjour] wird aktualisiert;

Führen Sie das Projekt aus und überprüfen Sie, ob es funktioniert.

1.14.8. Verwaltung des Fragment-Lebenszyklus

Im Fragment [Vue1Fragment] lautet die Methode [@AfterViews] wie folgt:


  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
}

Diese Methode ist unvollständig. Tatsächlich müssen wir immer den Fall berücksichtigen, in dem das Fragment nach einem [onDestroyView]-Vorgang wiederverwendet wird. In diesem Fall wird die Ansicht von Fragment 1 neu generiert, und jeder zuvor eingegebene Name verschwindet aus der Ansicht. Das wollen wir nicht. Derzeit bleibt der eingegebene Name weiterhin angezeigt, da die Nähe der Fragmente von Fragment 1 dazu führt, dass der Lebenszyklus des [Vue1Fragment]-Fragments nur einmal ausgeführt wird. Es ist jedoch vorzuziehen, die Wiederverwendung des Fragments zu berücksichtigen.

Es gibt mehrere Möglichkeiten, dieses Problem zu lösen:

  • Wir können die Tatsache nutzen, dass die [update]-Methode bei jeder Anzeige des Fragments systematisch ausgeführt wird, um den eingegebenen Namen zu aktualisieren;
  • Sie können diese Aktualisierung nur dann durchführen, wenn die Methode [@AfterViews] erneut ausgeführt wird. Wir werden den letzteren Ansatz wählen;

Wir ändern den Code in [View1Fragment] wie folgt:


    // visual interface elements
    @ViewById(R.id.editTextNom)
    protected EditText editTextNom;
 
    // data
    private String nom;
 
    @AfterViews
    protected void afterViews() {
        // memory
        afterViewsDone = true;
        // log
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // the text displayed is (re)initialized
        editTextNom.setText(nom);
    }
 
    // event managers ----------------------------------
 
...
 
    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // the name entered is noted so that it can be retrieved if the fragment is recycled
        nom = editTextNom.getText().toString();
        // enter the name entered in the session
        session.setNom(nom);
        // navigate to view no. 2
        activity.navigateToView(1);
}
  • Zeile 27: Da wir im Begriff sind, von Ansicht 1 zu Ansicht 2 zu wechseln, speichern wir den eingegebenen Namen;
  • Zeile 17: Jedes Mal, wenn der Lebenszyklus des Fragments ausgeführt wird, wird der zuletzt eingegebene Name erneut angezeigt;

Für das Fragment [View2Fragment] reicht der vorhandene Code aus:


  // visual interface components
  @ViewById(R.id.textViewBonjour)
  protected TextView textViewBonjour;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }
 
  // update fragment
  @Override
  protected void updateFragment() {
    // retrieve the name entered in the session
    String nom = session.getNom();
    // we display it
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • Die einzige visuelle Komponente der Ansicht (Zeile 3) wird bei jeder Anzeige der Ansicht (Zeile 21) aktualisiert. Die Methode [@AfterViews] hat daher nichts hinzuzufügen;

1.14.9. Fazit

An dieser Stelle haben wir erneut die Relevanz unserer Architektur demonstriert:

  • eine Aktivität, die die Schnittstelle [IMainActivity] implementiert;
  • Fragmente, die die Klasse [AbstractFragment] erweitern, was erfordert, dass sie die Methode [updateFragment] implementieren. Diese müssen außerdem über eine [@AfterViews]-Methode verfügen, in der sie den booleschen Wert [afterViewsDone] auf „true“ setzen;
  • eine Session, die die zwischen den Fragmenten und der Aktivität auszutauschenden Daten kapselt;

1.15. Beispiel 14: Eine zweischichtige Architektur

Wir werden eine Single-View-Anwendung mit der folgenden Architektur erstellen:

1.15.1. Erstellen des Projekts

Wir duplizieren das vorherige Projekt [Beispiel-12] in [Beispiel-13], indem wir die Vorgehensweise in Abschnitt 1.4 befolgen. Wir erhalten das folgende Ergebnis:

1.15.2. Die Ansicht [view1]

Die Anwendung wird nur eine Ansicht [view1.xml] haben. Daher löschen wir die andere Ansicht [view2.xml] zusammen mit dem zugehörigen Fragment:

 

Kompilieren Sie die Anwendung. In [MainActivity] treten Fehler auf:

 

Korrigieren Sie Zeile 4 unten im Fragment-Manager [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

Zeile 4 oben wird zu:


    // fragments
private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_()};

Entfernen Sie die nicht mehr benötigten Importe [Strg-Umschalt-O]. Es sollten nun keine Kompilierungsfehler mehr auftreten. Führen Sie das Projekt aus: Ansicht Nr. 1 sollte erscheinen. Wir werden sie nun anpassen.

Wir erstellen die Ansicht [vue1.xml], die Zufallszahlen generiert:

 

Ihre Komponenten sind wie folgt:

Nr.
ID
Typ
Rolle
1
edtNbAleas
EditText
Anzahl der zu generierenden Zufallszahlen im ganzzahligen Intervall [a,b]
2
edtA
EditText
Wert von a
2
edtB
EditText
Wert von b
4
btnExecute
Schaltfläche
startet die Nummerngenerierung
5
ListView
lstAnswers
Liste der generierten Zahlen in umgekehrter Reihenfolge ihrer Generierung. Die zuletzt generierte Zahl wird zuerst angezeigt;

Der dazugehörige XML-Code lautet wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/RelativeLayout1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="20dp"
                android:orientation="vertical" >
 
  <TextView
    android:id="@+id/txt_Titre2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="20dp"
    android:text="@string/aleas"
    android:textAppearance="?android:attr/textAppearanceLarge" />
 
  <TextView
    android:id="@+id/txt_nbaleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/txt_Titre2"
    android:layout_marginTop="20dp"
    android:text="@string/txt_nbaleas" />
 
  <EditText
    android:id="@+id/edt_nbaleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_nbaleas"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_nbaleas"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_errorNbAleas"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/edt_nbaleas"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_nbaleas"
    android:text="@string/txt_errorNbAleas"
    android:textColor="@color/red" />
 
  <TextView
    android:id="@+id/txt_a"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/txt_nbaleas"
    android:layout_marginTop="20dp"
    android:text="@string/txt_a" />
 
  <EditText
    android:id="@+id/edt_a"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_a"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_b"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_a"
    android:text="@string/txt_b" />
 
  <EditText
    android:id="@+id/edt_b"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/txt_a"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/txt_b"
    android:inputType="number" />
 
  <TextView
    android:id="@+id/txt_errorIntervalle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBaseline="@+id/edt_b"
    android:layout_marginLeft="20dp"
    android:layout_toRightOf="@+id/edt_b"
    android:text="@string/txt_errorIntervalle"
    android:textColor="@color/red" />
 
 
  <Button
    android:id="@+id/btn_Executer"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/txt_a"
    android:layout_marginTop="20dp"
    android:text="@string/btn_executer" />
 
 
  <TextView
    android:id="@+id/txt_Reponses"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_below="@+id/btn_Executer"
    android:layout_marginTop="30dp"
    android:text="@string/list_reponses"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:textColor="@color/blue" />
 
  <ListView
    android:id="@+id/lst_reponses"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/txt_Reponses"
    android:layout_marginTop="40dp"
    android:background="@color/wheat"
    android:clickable="true"
    tools:listitem="@android:layout/simple_list_item_1" >
  </ListView>
 
</RelativeLayout>

Die vorherige Ansicht verwendet Beschriftungen, die in der Datei [res/values/strings.xml] definiert sind:


<resources>
  <string name="app_name">Exemple-14</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="list_reponses">Liste des réponses</string>
  <string name="btn_executer">Exécuter</string>
  <string name="aleas">Génération de N nombres aléatoires</string>
  <string name="txt_nbaleas">Valeur de N :</string>
  <string name="txt_a">"Intervalle [a,b] de génération, a : "</string>
  <string name="txt_b">"b : "</string>
  <string name="txt_dummy">Dummy</string>
  <string name="txt_errorNbAleas">Tapez un nombre entier >=1</string>
  <string name="txt_errorIntervalle">Les bornes de l\'intervalle doivent être entières et b>=a</string>
</resources>

Die in [vue1.xml] verwendeten Farben sind in der Datei [res/values/colors.xml] definiert:


<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <!-- colors applied -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
  <color name="floral_white">#FFFAF0</color>
</resources>

1.15.3. Die Sitzung

  

Da es hier nur ein Fragment gibt, muss keine Kommunikation zwischen Fragmenten geplant werden. Die Sitzung ist daher leer:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
}

Kompilieren Sie nun die Anwendung. Es werden Fehler in den Zeilen angezeigt, die Elemente aus der nun leeren Session verwendet haben. Entfernen Sie diese Zeilen und überprüfen Sie, ob die Kompilierung nun fehlerfrei verläuft.

1.15.4. Das Fragment [Vue1Fragment]

  

Wir ändern das vorhandene [Vue1Fragment]-Fragment wie folgt:


package exemples.android.fragments;
 
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.lst_reponses)
  protected ListView listReponses;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  protected TextView txtErrorIntervalle;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
    // memory
    afterViewsDone = true;
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // hide error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Es gibt hier nur ein Fragment, dessen Lebenszyklus nur einmal ausgeführt wird, und zwar beim Start der Anwendung. Aus diesem Grund werden die Methode [@AfterViews] (Zeilen 46–57) und die Methode [updateFragment] (Zeilen 75–81) nur einmal beim Start der Anwendung ausgeführt;
  • Zeilen 55–56: Wir blenden die beiden Fehlermeldungen aus der Ansicht aus (siehe unten) [1–2];
 
  • Zeilen 59–60: Die Methode, die beim Klicken auf die Schaltfläche [Execute] ausgeführt wird;
  • Zeilen 71–73: Die Gültigkeit der Eingaben wird überprüft;

Die Methode [isPageValid] lautet wie folgt:


  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
...
 
// check the validity of the data entered
  private boolean isPageValid() {
    // enter the number of random numbers
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // enter a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // b input
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // mistake?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // return
    return (nbErreurs == 0);
  }
 
  • Zeilen 2–4: Diese drei Felder werden durch die Methode [isPageValid] initialisiert. Außerdem gibt diese Methode „true“ zurück, wenn alle Einträge gültig sind, andernfalls „false“. Sind Einträge ungültig, werden die entsprechenden Fehlermeldungen angezeigt;

Zu diesem Zeitpunkt ist die Anwendung lauffähig. Überprüfen Sie die Funktionalität der Methode [isPageValid], indem Sie falsche Daten eingeben.

1.15.5. Die [business]-Schicht

  

Die [Business]-Schicht stellt die folgende [IMetier]-Schnittstelle bereit:


package exemples.android.metier;
 
import java.util.List;
 
public interface IMetier {
 
    List<Object> getAleas(int a, int b, int n);
}

Die Methode [getAleas(a,b,n)] gibt normalerweise n zufällige Ganzzahlen im Bereich [a,b] zurück. Sie ist außerdem so konzipiert, dass sie in jedem dritten Fall eine Ausnahme auslöst, und diese Ausnahme ist in den von der Methode zurückgegebenen Ergebnissen enthalten. Letztendlich gibt die Methode eine Liste von Objekten des Typs [Exception] oder [Integer] zurück.

Die [Metier]-Implementierung dieser Schnittstelle lautet wie folgt:


package exemples.android.metier;
 
import org.androidannotations.annotations.EBean;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
@EBean(scope = EBean.Scope.Singleton)
public class Metier implements IMetier {
 
    public List<Object> getAleas(int a, int b, int n) {
        // object list
        List<Object> réponses = new ArrayList<Object>();
        // some checks
        if (n < 1) {
            réponses.add(new AleaException("Le nombre d'entier aléatoires demandé doit être supérieur ou égal à 1"));
        }
        if (a < 0) {
            réponses.add(new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0"));
        }
        if (b < 0) {
            réponses.add(new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0"));
        }
        if (a >= b) {
            réponses.add(new AleaException("Dans l'intervalle [a,b], on doit avoir a< b"));
        }
        // mistake?
        if (réponses.size() != 0) {
            return réponses;
        }
        // random numbers are generated
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // generate a random exception 1 time / 3
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // otherwise a random number is returned between two bounds [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // result
        return réponses;
    }
}
  • Zeile 9: Wir verwenden die AA-Annotation [@EBean] für die Klasse [Business], damit wir Referenzen darauf in die [Presentation]-Schicht injizieren können. Das Attribut (scope = EBean.Scope.Singleton) stellt sicher, dass nur eine einzige Instanz der Klasse [Business] erstellt wird. Daher wird immer dieselbe Referenz injiziert, wenn sie mehrfach in die [Presentation]-Schicht injiziert wird;
  • der Rest des Codes ist Standard;

Der von der Klasse [Metier] verwendete Typ [AleaException] lautet wie folgt:


package exemples.android.metier;
 
public class AleaException extends RuntimeException {
 
    private static final long serialVersionUID = 1L;
 
    public AleaException() {
    }
 
    public AleaException(String detailMessage) {
        super(detailMessage);
    }
 
    public AleaException(Throwable throwable) {
        super(throwable);
    }
 
    public AleaException(String detailMessage, Throwable throwable) {
        super(detailMessage, throwable);
    }
 
}
  • Zeile 3: Die Klasse [AleaException] erweitert die Systemklasse [RuntimeException] und ist somit eine nicht behandelte Ausnahme: Sie muss weder in einem try/catch-Block behandelt noch in Methodensignaturen angegeben werden;

1.15.6. Ein erneuter Blick auf [MainActivity]

  

[Business] LayerActivityUserView

Die Aktivität implementiert die [IMetier]-Schnittstelle der [Business]-Schicht. Somit hat das Fragment/die Ansicht nur die Aktivität als Gegenstück.

Die [MainActivity]-Aktivität implementiert bereits die [IMainActivity]-Schnittstelle. Damit sie auch die [IMetier]-Schnittstelle implementiert, können wir:

  • die Schnittstelle [IMetier] zu den von der Aktivität implementierten Schnittstellen hinzufügen;
  • sicherstellen, dass die Schnittstelle [IMainActivity] selbst die Schnittstelle [IMetier] erweitert. Dies ist der Ansatz, den wir verfolgen;

Die Schnittstelle [IMainActivity] sieht dann wie folgt aus:

  

package exemples.android.architecture;
 
import exemples.android.metier.IMetier;
 
public interface IMainActivity extends IMetier {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // debug mode
  public static final boolean IS_DEBUG_ENABLED = true;
 
}
  • Zeile 5: Die Schnittstelle [IMainActivity] erweitert die Schnittstelle [IMetier]

Die Klasse [MainActivity] entwickelt sich wie folgt:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity {
 
  ...
 
  // injection session
  @Bean(Session.class)
  protected Session session;
 
  // injection molding
  @Bean(Metier.class)
  protected IMetier metier;
 
...
  // implémentation IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • Zeilen 11–12: Die [business]-Schicht wird in die Aktivität injiziert. Dazu verwenden wir die Annotation [@Bean], deren Parameter die Klasse mit der Annotation [@EBean] ist;
  • Zeile 2: Die Aktivität implementiert die Schnittstelle [IMainActivity] und damit die Schnittstelle [IMetier] der [Business]-Schicht;
  • Zeilen 16–19: Implementierung der einzigen Methode der Schnittstelle [IMetier]. Wir delegieren den Aufruf einfach an die [business]-Schicht;

1.15.7. Das Fragment [Vue1Fragment] noch einmal betrachtet

  

Der Code für die Klasse [Vue1Fragment] entwickelt sich wie folgt:


package exemples.android.fragments;
 
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.lst_reponses)
  protected ListView listReponses;
  @ViewById(R.id.edt_nbaleas)
  protected EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  protected EditText edtA;
  @ViewById(R.id.edt_b)
  protected EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  protected TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  protected TextView txtErrorIntervalle;
 
  // list of order responses
  private List<String> reponses = new ArrayList<>();
  // listview adapter
  private ArrayAdapter<String> adapterReponses;
 
  // seizures
  private int nbAleas;
  private int a;
  private int b;
 
  @AfterViews
  protected void afterViews() {
   ...
  }
 
  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }
 
  // check the validity of the data entered
  private boolean isPageValid() {
   ...
  }
 
  @Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // will only be executed once when the application is started
    // create the ListView adapter - this requires the [activity] variable to have been initialized
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • Zeilen 69–70: Den Adapter für die [ListView]-Komponente festlegen;

Die [ListView]-Komponente dient zur Anzeige einer Liste von Elementen. Dazu verwendet sie einen [ListAdapter]-Adapter, der wiederum mit der Datenquelle verbunden ist, die die [ListView] versorgt. Um den Adapter für eine [ListView] zu definieren, verwenden Sie die folgende [ListView.setAdapter]-Methode:


public void setAdapter (ListAdapter adapter)

[ListAdapter] ist eine Schnittstelle. Die Klasse [ArrayAdapter] ist eine Klasse, die diese Schnittstelle implementiert. Der in Zeile 69 oben verwendete Konstruktor lautet wie folgt:


public ArrayAdapter (Context context, int resource, int textViewResourceId, List<T> objects)
  • [context] ist die Aktivität, die die [ListView] anzeigt;
  • [resource] ist die Ganzzahl, die die Ansicht identifiziert, die zur Anzeige eines Elements in der [ListView] verwendet wird. Diese Ansicht kann beliebig komplex sein. Der Entwickler erstellt sie entsprechend seinen Anforderungen;
  • [textViewResourceId] ist die Ganzzahl, die eine [TextView]-Komponente in der [resource]-Ansicht identifiziert. Die angezeigte Zeichenfolge wird von dieser Komponente dargestellt;
  • [objects]: die Liste der Objekte, die von der [ListView] angezeigt werden. Die [toString]-Methode der Objekte wird verwendet, um das Objekt in der durch [textViewResourceId] identifizierten [TextView] innerhalb der durch [resource] identifizierten Ansicht anzuzeigen.

Die Aufgabe des Entwicklers besteht darin, die [resource]-Ansicht zu erstellen, die jedes Element in der [ListView] anzeigt. Für den einfachen Fall, in dem wir wie hier nur eine einzelne Zeichenfolge anzeigen möchten, stellt Android die durch [android.R.layout.simple_list_item_1] identifizierte Ansicht bereit. Diese enthält eine [TextView]-Komponente, die durch [android.R.id.text1] identifiziert wird. Dies ist die Methode, die in Zeile 69 verwendet wird, um den [ListView]-Adapter zu erstellen. Dieser Adapter muss nur einmal definiert werden. Um seine Wiederverwendung zu ermöglichen, wurde er als Instanzvariable der Klasse definiert (Zeile 39). Schauen wir uns noch einmal Zeile 69 an:


adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);

Der erste Parameter des [ArrayAdapter]-Konstruktors ist die Aktivität, die über [getActivity] von einem Fragment abgerufen und hier in der Variablen [activity] der übergeordneten Klasse gespeichert wurde. Dieses Feld hat nicht immer einen Wert. Die Logs zeigen daher, dass es bei Erreichen der Methode [@AfterViews] noch nicht initialisiert ist, sodass wir die Zeilen 69–70 nicht in diese Methode einfügen können. In der Methode [updateFragment] ist dies möglich, da wir wissen, dass [activity] zum Zeitpunkt der Ausführung dieser Methode zwangsläufig nicht null ist. Der Adapter ist hier mit der in Zeile 37 definierten Datenquelle [reponses] verknüpft;

Die Methode [doExecute] verarbeitet den Klick auf die Schaltfläche [Execute]. Ihr Code lautet wie folgt:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // hide any previous error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // we ask for the random numbers in the activity
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // we create a list of Strings from this data
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // refresh listview
    adapterReponses.notifyDataSetChanged();
  }
  • Zeilen 7–8: Wir möchten die ListView löschen. Dazu löschen wir die Datenquelle [reponses] und fordern den mit der ListView verknüpften Adapter auf, die Daten zu aktualisieren;
  • Zeilen 10–12: Bevor wir die angeforderte Aktion ausführen, überprüfen wir, ob die eingegebenen Werte korrekt sind;
  • Zeile 14: Die Liste der Zufallszahlen wird von der Aktivität angefordert. Wir erhalten eine Liste von Objekten, wobei jedes Objekt vom Typ [Integer] oder [AleaException] ist;
  • Zeilen 16–22: Anhand der erhaltenen Objektliste wird die von der ListView angezeigte Datenquelle [reponses] aktualisiert;
  • Zeile 24: Der ListView-Adapter wird zur Aktualisierung aufgefordert;

1.15.8. Ausführung

Führen Sie das Projekt aus und überprüfen Sie, ob es korrekt funktioniert.

1.16. Beispiel 15: Client-Server-Architektur

Wir werden uns nun eine gängige Architektur für eine Android-App ansehen, bei der die Android-App mit externen Webdiensten kommuniziert. Wir werden nun die folgende Architektur haben:

Wir haben der Android-App eine [DAO]-Schicht hinzugefügt, um mit dem Remote-Server zu kommunizieren. Diese kommuniziert mit dem Server, der die auf dem Android-Tablet angezeigten Zufallszahlen generiert. Dieser Server verfügt über die folgende zweistufige Architektur:

Clients fragen bestimmte URLs in der [Web/JSON]-Schicht ab und erhalten eine Textantwort im JSON-Format (JavaScript Object Notation). Hier verarbeitet unser Webservice eine einzelne URL der Form [/a/b], die eine Zufallszahl im Bereich [a,b] zurückgibt. Wir werden die Anwendung in der folgenden Reihenfolge beschreiben:

Der Server

  • seine [Business]-Schicht;
  • seinen mit Spring MVC implementierten [Web/JSON]-Dienst;

Der Client

  • seine [DAO]-Schicht. Es wird keine [Business]-Schicht geben;

1.16.1. Der [Web/JSON]-Server

Wir möchten die folgende Architektur aufbauen:

1.16.1.1. Projekterstellung

Wir werden den Webservice unter Verwendung des Spring-Ökosystems [http://spring.io/] erstellen. Wir rufen die Website [http://start.spring.io/] (Stand: Juni 2016) auf, über die wir ein Gradle-Projekt mit den für unser Projekt erforderlichen Abhängigkeiten generieren können – es handelt sich dabei nicht um ein Android-Projekt, für das Android Studio keine Unterstützung bietet:

  • in [1]: Wählen Sie ein Gradle-Projekt aus;
  • in [2-3]: die Eigenschaften der vom Projekt generierten JAR-Abhängigkeit (siehe unten);
  • in [4]: Wählen Sie die Web-Abhängigkeit [5] aus, damit die für unseren Webdienst erforderlichen Binärdateien verfügbar sind;
  • in [6]: Erstellen Sie das Projekt. Anschließend wird eine ZIP-Datei eines Gradle-Skelettprojekts erstellt und zum Download bereitgestellt;

Was sollten Sie in [2-3] eingeben? Wir haben bereits Gradle-Abhängigkeiten verwendet. Die aus dem vorherigen Projekt lautete beispielsweise wie folgt:

 

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
  ...
}
def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile fileTree(dir: 'libs', include: ['*.jar'])
  testCompile 'junit:junit:4.12'
}
  • Zeile 22: Eine Abhängigkeit wird im Format [groupId:artifactId:version] angegeben. Was im Formular unter [http://start.spring.io/] abgefragt wird:
    • in [2] ist [groupId];
    • in [3] steht [artifactId];

Entpacken Sie die erhaltene ZIP-Datei in den Ordner, der die anderen Projekte enthält:

Öffnen Sie mit Android Studio das Gradle-Projekt [server-01] [1-2]. Das geöffnete Projekt befindet sich in [3] (Projektansicht).

1.16.1.2. Gradle-Konfiguration

  

Die generierte Gradle-Datei (Juni 2016) sieht wie folgt aus:


buildscript {
 ext {
  springBootVersion = '1.3.5.RELEASE'
 }
 repositories {
  mavenCentral()
 }
 dependencies {
  classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
 }
}
 
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'spring-boot' 
 
jar {
 baseName = 'server-01'
 version = '0.0.1-SNAPSHOT'
}
 
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
repositories {
 mavenCentral()
}
 
dependencies {
 compile('org.springframework.boot:spring-boot-starter-web')
 testCompile('org.springframework.boot:spring-boot-starter-test') 
}
 
eclipse {
 classpath {
   containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
   containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
 }
}
  • Die Zeilen 14 und 34–38 sind für die Eclipse-IDE vorgesehen. Wir entfernen sie;
  • Die Zeilen 1–11 und 15 dienen dazu, ein Plugin namens [spring-boot] zu unserem Gradle-Projekt hinzuzufügen. Spring Boot ist ein Projekt innerhalb des Spring-Ökosystems [http://projects.spring.io/spring-boot/]. Dieses Plugin definiert die Versionen der Abhängigkeiten, die am häufigsten mit Spring verwendet werden. Dadurch können wir die Angabe ihrer Versionen weglassen (Zeilen 30 und 31). Die Version ist dann diejenige, die durch die verwendete Spring-Boot-Version definiert ist (Zeile 3);
  • Zeilen 22–23: die zu verwendende Java-Version, hier Version 1.8;
  • Zeilen 25–27: die Binär-Repositorys, die zum Herunterladen von Abhängigkeiten verwendet werden sollen;
  • Zeile 26: gibt das Maven Central Repository an. Dies ist derzeit das größte verfügbare Open-Source-Binär-Repository;
  • Zeilen 29–32: die für das Projekt erforderlichen Abhängigkeiten:
  • Zeile 30: Diese Abhängigkeit umfasst alle Binärdateien, die zum Erstellen eines Spring-Webdienstes benötigt werden;
  • Zeile 31: Diese Abhängigkeit umfasst alle Binärdateien, die für Tests benötigt werden, insbesondere JUnit-Tests;
  • Eine [compile]-Abhängigkeit gibt an, dass die Abhängigkeit zum Kompilieren des Projekts benötigt wird. Eine [testCompile]-Abhängigkeit gibt an, dass die Abhängigkeit nur zum Ausführen von Tests benötigt wird. Sie ist daher nicht im Projekt-Binärdatei enthalten;

Wir beginnen damit, die Gradle-Datei zu bereinigen:


// spring boot
buildscript {
  ext {
    springBootVersion = '1.3.5.RELEASE'
  }
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
  }
}
 
// plugins
apply plugin: 'java'
apply plugin: 'spring-boot'
 
// binaire du projet
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}
 
// versions Java
sourceCompatibility = 1.8
targetCompatibility = 1.8
 
// dépôts Maven
repositories {
  mavenLocal()
  mavenCentral()
}
 
// dépendances
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
  • Zeile 30: Wir haben das lokale Maven-Repository für den Entwicklungsrechner hinzugefügt. Dieses wird bei der Installation von Maven erstellt (siehe Abschnitt 6.10). Befindet sich die angeforderte Abhängigkeit bereits im lokalen Maven-Repository, wird sie nicht aus dem zentralen Maven-Repository abgerufen;
  • Zeilen 19–22: Eine Gradle-Aufgabe zum Erstellen der Binärdatei des Projekts. Wir werden sie verwenden, um zu sehen, was dabei geschieht;
  • Führen Sie in [1–4] die in der Datei [build.gradle] definierte [jar]-Aufgabe aus ([1] befindet sich oben rechts und an der Seite der IDE);

Der vorherige Schritt erstellt das JAR-Archiv des Projekts und legt es im Ordner [build/libs] ab [5]:

  

Der Archivname wird direkt aus den Informationen abgeleitet, die der [jar]-Task in der [build.gradle]-Datei (Zeilen 19–22) übergeben werden.

Alle Abhängigkeiten des Projekts können wie folgt eingesehen werden:

 

In [1] sehen wir, dass die einzige Abhängigkeit des Projekts [compile('org.springframework.boot:spring-boot-starter-web')] Dutzende von Binärdateien mit sich gebracht hat. Spring Boot für das Web enthält die Abhängigkeiten, die eine Spring-MVC-Webanwendung wahrscheinlich benötigt. Das bedeutet, dass einige davon möglicherweise unnötig sind. Spring Boot ist ideal für ein Tutorial:

  • es enthält die Abhängigkeiten, die wir wahrscheinlich benötigen;
  • es enthält einen eingebetteten Tomcat-Server [1], wodurch wir uns die Bereitstellung der Anwendung auf einem externen Webserver sparen;

Auf der Website des Spring-Ökosystems [http://spring.io/guides] finden Sie viele Beispiele für die Verwendung von Spring Boot.

Wir werden nun die Datei [build.gradle] wie folgt vervollständigen:


// spring boot
...
// dépendances
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
 
// plugin pour créer un binaire aux normes Maven dans le dépôt Maven local
apply plugin: 'maven-publish'
publishing {
  publications {
    maven(MavenPublication) {
      groupId 'istia.st.exemples.android'
      artifactId 'server-01'
      version '0.0.1-SNAPSHOT'
      from components.java
    }
  }
  repositories {
    maven {
      // change to point to your repo, e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • Zeile 10: Wir importieren ein Gradle-Plugin namens [maven-publish], mit dem wir die Binärdatei des Projekts gemäß den Maven-Standards in einem Maven-Repository veröffentlichen können;
  • Zeile 11: eine Gradle-Aufgabe namens [publishing];
  • Zeilen 14–15: die Eigenschaften der zu erstellenden Maven-Binärdatei;
  • Zeile 23: das Maven-Repository, in dem die Datei veröffentlicht wird, in diesem Fall ein lokales Maven-Repository;

Durch das Hinzufügen des [maven-publish]-Plugins wurden neue Aufgaben im Gradle-Projekt erstellt:

Wenn wir in [2] die Aufgabe [publish] ausführen, wird die Projekt-Binärdatei erstellt und in dem Ordner installiert, der in Zeile 23 der Datei [build.gradle] angegeben ist:

 

Die [jar]-Aufgabe generiert die Binärdatei des Projekts. Diese Binärdatei enthält keine Abhängigkeiten und ist daher nicht ausführbar. Es ist möglich, eine ausführbare Binärdatei zu generieren, die alle Abhängigkeiten enthält. Dazu fügen wir den folgenden Code in die [build.gradle]-Datei ein:


// créer un binaire avec toutes ses dépendances
version = '1.0'
task fatJar(type: Jar) {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    attributes 'Main-Class': 'istia.st.exemples.android.Server01Application'
  }
  baseName = project.name + '-all'
  from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
  with jar
}
  • Zeile 6: Geben Sie den vollständigen Namen der ausführbaren Klasse des Projekts ein:
  

Der Code für diese Klasse lautet wie folgt:


package istia.st.exemples.android;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class Server01Application {
 
    public static void main(String[] args) {
        System.out.println("Server01Application running");
        //SpringApplication.run(Server01Application.class, args);
    }
}

Aktualisieren Sie das Gradle-Projekt und führen Sie anschließend die Aufgabe [fatJar] aus:

 

Die Binärdatei wird im Ordner [build/libs] erstellt und kann ausgeführt werden [1-7]:

1.16.1.3. Projektkonfiguration

Die Gradle-Konfiguration reicht nicht aus. Wir müssen auch das Projekt konfigurieren. Da es sich hierbei nicht um ein von der IDE generiertes Android-Projekt handelt, muss diese Konfiguration – die wir bisher noch nicht vorgenommen haben – hier durchgeführt werden.

 
  • In [3-4]: Verwenden Sie JDK 1.8;

Um das Projekt zu kompilieren, ist die für Android-Projekte verfügbare Schaltfläche nicht mehr vorhanden. Wir verwenden eine Menüoption [1-2]:

Als Nächstes wird der Leser aufgefordert, das folgende Projekt zu erstellen. Wir werden den endgültigen Projektcode kommentieren [3].

1.16.1.4. Die [Business]-Schicht

  

Die [Business]-Schicht folgt dem gleichen Ansatz wie die [Business]-Schicht im vorherigen Beispiel. Sie verfügt über die folgende [IMetier]-Schnittstelle:


package exemples.android.server.metier;
 
public interface IMetier {
  // random number in [a,b]
    int getAlea(int a, int b);
}
  • Zeile 5: Die Methode, die 1 Zufallszahl im Bereich [a,b] generiert

Der Code für die Klasse [Metier], die diese Schnittstelle implementiert, lautet wie folgt:


package exemples.android.server.metier;
 
import org.springframework.stereotype.Service;
 
import java.util.Date;
import java.util.Random;
 
@Service
public class Metier implements IMetier {
 
  @Override
  public int getAlea(int a, int b) {
    // some checks
    if (a < 0) {
      throw new AleaException("Le nombre a de l'intervalle [a,b] doit être supérieur à 0", 2);
    }
    if (b < 0) {
      throw new AleaException("Le nombre b de l'intervalle [a,b] doit être supérieur à 0", 3);
    }
    if (a >= b) {
      throw new AleaException("Dans l'intervalle [a,b], on doit avoir a< b", 4);
    }
    // result generation
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Wir werden die Klasse nicht näher erläutern: Sie ähnelt der im vorherigen Beispiel, außer dass sie keine Ausnahmen zufällig auslöst. Beachten Sie die Spring-Annotation [@Service] in Zeile 8, die bewirkt, dass Spring die Klasse als Einzelinstanz (Singleton) instanziiert und ihre Referenz anderen Spring-Komponenten zur Verfügung stellt. Hier hätten auch andere Spring-Annotationen verwendet werden können, um denselben Effekt zu erzielen. Spring-Komponenten haben Standardnamen, die als Attribut der verwendeten Annotation angegeben werden können. Ohne dieses Attribut, wie hier, übernimmt die Spring-Komponente den Namen der Klasse, wobei der erste Buchstabe klein geschrieben wird. Somit heißt die Spring-Komponente hier standardmäßig [metier];

Die Klasse [Metier] löst Ausnahmen vom Typ [AleaException] aus:


package exemples.android.server.metier;
 
public class AleaException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  public AleaException() {
  }
 
  public AleaException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public AleaException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  public AleaException(String detailMessage, Throwable throwable, int code) {
    super(detailMessage, throwable);
    this.code = code;
  }
 
  // getters and setters
....
}
  • Zeile 3: [AleaException] erweitert die Klasse [RuntimeException]. Es handelt sich daher um eine unbehandelte Ausnahme (es ist nicht erforderlich, sie mit try/catch zu behandeln);
  • Zeile 6: Der Klasse [RuntimeException] wird ein Fehlercode hinzugefügt;

1.16.1.5. Der Webdienst / JSON

 
  

Der Webdienst / JSON wird von Spring MVC implementiert. Spring MVC implementiert das MVC-Architekturmuster (Model–View–Controller) wie folgt:

Die Verarbeitung einer Client-Anfrage verläuft wie folgt:

  1. Anfrage – die angeforderten URLs haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Das [Dispatcher-Servlet] ist die Spring-Klasse, die eingehende URLs verarbeitet. Es „leitet“ die URL an die Aktion weiter, die sie bearbeiten soll. Diese Aktionen sind Methoden bestimmter Klassen, die als [Controller] bezeichnet werden. Das C in MVC bezieht sich hier auf die Kette [Dispatcher-Servlet, Controller, Aktion]. Wenn keine Aktion für die Bearbeitung der eingehenden URL konfiguriert wurde, antwortet das [Dispatcher-Servlet], dass die angeforderte URL nicht gefunden wurde (404 NOT FOUND-Fehler);
  1. Bei der Verarbeitung
  • Die ausgewählte Aktion kann die Parameter verwenden, die ihr vom [Dispatcher Servlet] übergeben wurden. Diese können aus verschiedenen Quellen stammen:
    • dem Pfad [/param1/param2/...] der URL,
    • die URL-Parameter [p1=v1&p2=v2],
    • aus Parametern, die der Browser mit seiner Anfrage übermittelt hat;
  • Bei der Bearbeitung der Benutzeranfrage wird möglicherweise die [Business]-Schicht [2b] benötigt. Sobald die Anfrage des Clients bearbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
  • die Aktion weist an, eine bestimmte Ansicht anzuzeigen [3]. Diese Ansicht zeigt Daten an, die als View-Modell bezeichnet werden. Dies ist das M in MVC. Die Aktion erstellt dieses M-Modell [2c] und weist an, eine V-Ansicht anzuzeigen [3];
  1. Antwort – die ausgewählte Ansicht V verwendet das von der Aktion erstellte Modell M, um die dynamischen Teile der HTML-Antwort zu initialisieren, die sie an den Client senden muss, und sendet dann diese Antwort.

Bei einem Webdienst / JSON wird die vorstehende Architektur leicht modifiziert:

  • In [4a] wird das Modell, bei dem es sich um eine Java-Klasse handelt, durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
  • in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;

Ein Beispiel für die Serialisierung eines Java-Objekts in eine JSON-Zeichenkette und die Deserialisierung einer JSON-Zeichenkette in ein Java-Objekt findet sich in den Anhängen in Abschnitt 6.14.

Kehren wir nun zur [Web]-Schicht unserer Anwendung zurück:

In unserer Anwendung gibt es nur einen Controller:

  

Der Web-/JSON-Dienst sendet seinen Clients eine Antwort vom Typ [Response] wie folgt:


package exemples.android.server.web;
 
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
...
}
  • Zeile 13: Das Feld [T body] enthält die vom Client erwartete Antwort. Wir haben uns hier für eine generische Antwort vom Typ T entschieden, anstatt den Integer-Typ der erwarteten Zufallszahl zu verwenden. Wir möchten diese Klasse in anderen Situationen wiederverwenden können. Bei der Verarbeitung der Client-Anfrage kann der Server auf ein Problem stoßen, das dann in den beiden anderen Feldern zusammengefasst wird;
    • Zeile 8: ein Statuscode (0, wenn kein Fehler vorliegt);
    • Zeile 9: Wenn status != 0, eine Liste von Fehlermeldungen – in der Regel diejenigen aus dem Ausnahmestapel, falls eine Ausnahme aufgetreten ist – null, wenn keine Fehler vorliegen;

Der Controller [WebController] sieht wie folgt aus:


package exemples.android.server.web;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.server.metier.AleaException;
import exemples.android.server.metier.IMetier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
 
import java.util.ArrayList;
import java.util.List;
 
@Controller
public class WebController {
 
  // business layer
  @Autowired
  private IMetier metier;
  // mapper JSON
  @Autowired
  private ObjectMapper mapper;
 
  // random numbers
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
  public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
 
    // the answer
    Response<Integer> response = new Response<>();
    // we use the business layer
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // we return the answer
    return mapper.writeValueAsString(response);
  }
 
  private List<String> getMessagesFromException(Throwable e) {
    // message list
    List<String> messages = new ArrayList<String>();
    // browse the exception stack
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // we return the result
    return messages;
  }
 
}
  • Zeile 17: Die Annotation [@Controller] gibt an, dass es sich bei der Klasse um einen MVC-Controller handelt, dessen Methoden Anfragen für bestimmte URLs in der Webanwendung bearbeiten;
  • Zeilen 21–22: Die Annotation [@Autowired] weist Spring an, eine Komponente vom Typ [IMetier] in das Feld zu injizieren. Dabei handelt es sich um die zuvor definierte Klasse [Metier]. Da wir ihr die Annotation [@Service] hinzugefügt haben, wird sie als Spring-Komponente behandelt;
  • Zeilen 24–25: Wir verfahren ebenso mit einem JSON-Mapper, den wir später definieren werden. Unser Webservice wird seine Antwort als JSON-String senden. Dieser Mapper übernimmt die Serialisierung der Antwort in JSON;
  • Zeile 30: Die Methode, die die Zufallszahl generiert. Ihr Name spielt keine Rolle. Bei ihrer Ausführung werden ihre Parameter von Spring MVC initialisiert. Wir werden sehen, wie das geschieht. Wenn sie ausgeführt wird, liegt das außerdem daran, dass der Webserver eine HTTP-GET-Anfrage für die URL in Zeile 28 erhalten hat;
  • Zeile 28: Die Annotation [@RequestMapping] definiert bestimmte Eigenschaften der annotierten Methode:
    • [value]: die von der Methode akzeptierte URL;
    • [method]: die von der Methode akzeptierte HTTP-Methode. Es gibt hauptsächlich zwei: GET und POST. Die [POST]-Methode wird verwendet, wenn der Client ein Dokument an seine HTTP-Anfrage anhängen möchte;
    • [produces]: Legt einen der Header der HTTP-Antwort fest, die an den Client gesendet wird. Unter den HTTP-Headern, die mit der Antwort an den Client gesendet werden, befindet sich hier einer, der den Client darüber informiert, dass die Antwort in Form einer JSON-Zeichenkette gesendet wird. Dieser Header ist nicht obligatorisch. Er wird dem Client zu Informationszwecken bereitgestellt, falls der Client Antworten erwartet, die verschiedene Formen annehmen können;
    • [consumes]: hier nicht vorhanden. Er gibt die HTTP-Header an, die der HTTP-Anfrage des Clients beigefügt sein müssen, damit diese akzeptiert wird;
  • Zeile 29: Die Annotation [@ResponseBody] gibt an, dass das von der Methode erzeugte Ergebnis an den Client gesendet werden muss. Ohne diese Annotation wird die Antwort der Methode als Schlüssel behandelt, der zur Auswahl der an den Client zu sendenden HTML-Seite dient. In einem Webservice / JSON gibt es keine HTML-Seiten;
  • Zeile 28: Die verarbeitete URL hat die Form /{a}/{b}, wobei {x} eine Variable darstellt. Die Variablen {a} und {b} werden in Zeile 30 den Parametern der Methode zugewiesen. Dies geschieht über die Annotation @PathVariable("x"). Beachten Sie, dass {a} und {b} Bestandteile einer URL sind und daher vom Typ String sind. Die Konvertierung von String in den Parametertyp kann fehlschlagen. Spring MVC löst dann eine Ausnahme aus. Zusammenfassend: Wenn ich die URL /100/200 in einem Browser aufrufe, wird die Methode getAlea in Zeile 30 mit den ganzzahligen Parametern a=100, b=200 ausgeführt;
  • Zeile 36: Die [business]-Schicht wird um eine Zufallszahl im Bereich [a,b] gebeten. Beachten Sie, dass die Methode [business].getAlea eine Ausnahme auslösen kann;
  • Zeile 37: kein Fehler;
  • Zeile 39: Fehlercode;
  • Zeile 40: Die Liste der Antwortmeldungen entspricht der des Ausnahmestapels (Zeilen 46–57). Hier wissen wir, dass der Stapel nur eine Ausnahme enthält, aber wir wollten eine allgemeinere Methode demonstrieren;
  • Zeile 43: Die Antwort vom Typ [Response<Integer>] wird als JSON-Zeichenkette zurückgegeben;

1.16.1.6. Spring-Projektkonfiguration

  

Es gibt verschiedene Möglichkeiten, Spring zu konfigurieren:

  • mithilfe von XML-Dateien;
  • mit Java-Code;
  • durch eine Kombination aus beidem;

Wir entscheiden uns dafür, unsere Webanwendung mithilfe von Java-Code zu konfigurieren. Die folgende [Config]-Klasse übernimmt diese Konfiguration:


package exemples.android.server.config;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
@ComponentScan(basePackages = { "exemples.android.server.metier", "exemples.android.server.web" })
@EnableWebMvc
public class Config {
  // web configuration ------------------------------------
  @Bean
  public DispatcherServlet dispatcherServlet() {
    DispatcherServlet servlet = new DispatcherServlet();
    return servlet;
  }
 
  @Bean
  public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
    return new ServletRegistrationBean(dispatcherServlet, "/*");
  }
 
  @Bean
  public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
    return new TomcatEmbeddedServletContainerFactory("", 8080);
  }
 
  // mapper jSON
  @Bean
  public ObjectMapper jsonMapper() {
    return new ObjectMapper();
  }
 
}
  • Zeile 12: Wir teilen Spring mit, in welchen Paketen es die beiden Komponenten findet, die es verwalten soll:
    • die mit [@Service] annotierte [Metier]-Komponente im Paket [exemples.android.server.metier];
    • die mit [@Controller] annotierte [WebController]-Komponente im Paket [examples.android.server.web];
  • Zeile 13: Die Annotation [@EnableWebMvc] ermöglicht es Spring Boot, eine Reihe von Standardkonfigurationen für eine Spring-MVC-Anwendung automatisch zu übernehmen. Dies reduziert den Arbeitsaufwand für den Entwickler erheblich;
  • Zeilen 16, 22, 27 und 33: Die Annotation [@Bean] definiert ebenfalls Spring-Komponenten (Beans) auf die gleiche Weise wie die beiden zuvor behandelten Annotationen (@Service, @Controller). Hier wird die Annotation [@Bean] nicht auf eine Klasse, sondern auf eine Methode angewendet, und das Ergebnis der Methode ist die Spring-Komponente. Fehlt ein Namensattribut innerhalb der Annotation [@Bean], erhält die erstellte Spring-Komponente den Namen der annotierten Methode;
  • Zeilen 16–20: definieren die [dispatcherServlet]-Bean. Dies ist ein vordefinierter Name in Spring MVC, der den Front-Controller der MVC-Anwendung definiert, ein Objekt, durch das alle Client-Anfragen laufen und das diese (daher der Name) an die verschiedenen [@Controller]s in der Spring MVC-Anwendung weiterleitet;
  • Zeile 18: Die [dispatcherServlet]-Bean ist eine Instanz der von Spring MVC bereitgestellten [DispatcherServlet]-Klasse;
  • Zeilen 22–25: Die [servletRegistrationBean]-Bean wird verwendet, um zu definieren, welche URLs von der Anwendung akzeptiert werden. In Zeile 24 werden alle URLs akzeptiert;
  • Zeilen 27–30: Die Bean [embeddedServletContainerFactory] dient dazu, den eingebetteten Server in den Projektabhängigkeiten zu definieren, der die Webanwendung hosten wird. Zeile 29 gibt an, dass es sich um einen Tomcat-Server handelt, der auf Port 8080 läuft. Standardmäßig werden die Binärdateien für diesen Webserver durch die Abhängigkeit [org.springframework.boot:spring-boot-starter-web] in der Gradle-Datei bereitgestellt;

1.16.1.7. Ausführen des Webdienstes / JSON

  

Das Projekt wird über die folgende ausführbare [Boot]-Klasse ausgeführt:


package exemples.android.server.boot;
 
import exemples.android.server.config.Config;
import org.springframework.boot.SpringApplication;
 
public class Boot {
  public static void main(String[] args) {
    // application execution
    SpringApplication.run(Config.class, args);
  }
 
}
  • Die Klasse [Boot] ist eine ausführbare Klasse (Zeilen 7–10);
  • Zeile 9: Die statische Methode [SpringApplication.run] ist eine Methode von [Spring Boot] (Zeile 4), die die Anwendung startet. Ihr erster Parameter ist die Java-Klasse, die das Projekt konfiguriert. Hier ist es die soeben beschriebene [Config]-Klasse. Der zweite Parameter ist das Array der Argumente, die an die [main]-Methode übergeben werden (Zeile 7);

Die Webanwendung kann auf verschiedene Arten gestartet werden, darunter die folgenden:

 

In der Konsole werden dann mehrere Protokolleinträge angezeigt:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.1.1.RELEASE)

2014-10-07 09:13:42.194  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Starting Application on Gportpers3 with PID 7408 (D:\data\istia-1415\android\dvp\exemples\exemple-10-server\target\classes started by ST in D:\data\istia-1415\android\dvp\exemples)
2014-10-07 09:13:42.294  INFO 7408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@23edbbb9: startup date [Tue Oct 07 09:13:42 CEST 2014]; root of context hierarchy
2014-10-07 09:13:42.996  INFO 7408 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-10-07 09:13:44.251  INFO 7408 --- [           main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-10-07 09:13:44.665  INFO 7408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-10-07 09:13:44.666  INFO 7408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.54
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-10-07 09:13:44.866  INFO 7408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2575 ms
2014-10-07 09:13:45.748  INFO 7408 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-10-07 09:13:45.750  INFO 7408 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-10-07 09:13:46.802  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.946  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/{a}/{b}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public android.exemples.server.web.AleaResponse android.exemples.server.web.AleaController.getAlea(int,int)
2014-10-07 09:13:46.950  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.951  INFO 7408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:46.979  INFO 7408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-10-07 09:13:47.294  INFO 7408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2014-10-07 09:13:47.335  INFO 7408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-10-07 09:13:47.337  INFO 7408 --- [           main] a.exemples.server.boot.Application       : Started Application in 6.081 seconds (JVM running for 6.897)
  • Zeilen 12–14: Der eingebettete Tomcat-Server wird gestartet;
  • Zeilen 15–19: Das Spring-MVC-Servlet [DispatcherServlet] wird geladen und konfiguriert;
  • Zeile 20: Die Webserver-URL [/{a}/{b}] wird erkannt;

Öffnen wir nun einen Browser und testen wir die JSON-URL des Webdienstes:

Jedes Mal erhalten wir die JSON-Darstellung eines Objekts vom Typ [Response<Integer>].

Anstelle eines Standardbrowsers verwenden wir nun die Erweiterung [ Advanced Rest Client] für den Chrome-Browser (siehe Anhang, Abschnitt 6.13):

Image

  • in [1] die angeforderte URL;
  • in [2] die Verwendung einer GET-Anfrage;
  • in [3] wird die Anfrage gesendet;

Image

  • in [4] die HTTP-Header der Serverantwort. Beachten Sie, dass dies darauf hinweist, dass es sich bei dem gesendeten Dokument um eine JSON-Zeichenkette handelt;
  • in [5] die empfangene JSON-Zeichenkette;

1.16.1.8. Erstellen der ausführbaren JAR-Datei des Projekts

In Abschnitt 1.16.1.2 haben wir gezeigt, wie die Gradle-Datei konfiguriert wird, um eine ausführbare Datei für die Anwendung mit all ihren Abhängigkeiten zu generieren. An die aktuelle Anwendung angepasst sieht diese Konfiguration wie folgt aus:


// créer un binaire avec toutes ses dépendances
version = '1.0'
task fatJar(type: Jar) {
  manifest {
    attributes 'Implementation-Title': 'Gradle Quickstart', 'Implementation-Version': version
    attributes 'Main-Class': 'exemples.android.server.boot.Boot'
  }
  baseName = project.name + '-all'
  from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
  with jar
}

Um diese ausführbare Datei zu erstellen, führen Sie die folgenden Schritte [1–5] aus:

Um sie auszuführen, stoppen Sie den Webdienst, falls er läuft [1], und führen Sie dann das Archiv aus [2-4]:

 

Öffnen Sie einen Browser und rufen Sie die URL [localhost:8080/100/200] auf. Sie sollten die gleichen Ergebnisse wie zuvor erhalten.

1.16.1.9. Protokollverwaltung

Wenn Sie das ausführbare Archiv ausführen, werden Sie feststellen, dass sich die Protokolle von denen unterscheiden, die bei der Ausführung des Projekts über die IDE angezeigt werden. Sie sehen Protokolle im [DEBUG]-Modus:


...
09:32:03.741 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletConfigInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [servletContextInitParams]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemProperties]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Searching for key 'spring.liveBeansView.mbeanDomain' in [systemEnvironment]
09:32:03.742 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Could not find key 'spring.liveBeansView.mbeanDomain' in any property source. Returning [null]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:32:03 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:32:03.810 [main] INFO org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:32:03.813 [main] INFO exemples.android.server.boot.Boot - Started Boot in 1.984 seconds (JVM running for 2.206)

Sie können die Protokollstufe verwalten, indem Sie eine [logback.xml]-Datei zum [resources]-Ordner des Projekts hinzufügen:

  

Diese Datei könnte folgenden Inhalt haben:


<configuration>
 
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <root level="info"> <!-- info, debug, warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Die Protokollstufe wird in Zeile 12 gesteuert. Wenn wir nun das ausführbare Archiv neu erstellen und ausführen, erhalten wir nur Protokolle der Stufe [info]:


...
09:36:52.433 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.2.4.Final
09:36:52.762 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@7085bdee: startup date [Tue Jun 07 09:36:51 CEST 2016]; root of context hierarchy
09:36:52.811 [main] INFO  o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/{a}/{b}],methods=[GET],produces=[application/json;charset=UTF-8]}" onto public java.lang.String exemples.android.server.web.WebController.getAlea(int,int) throws com.fasterxml.jackson.core.JsonProcessingException
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol init
INFOS: Initializing ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.coyote.AbstractProtocol start
INFOS: Starting ProtocolHandler ["http-nio-8080"]
juin 07, 2016 9:36:52 AM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFOS: Using a shared selector for servlet write/read
09:36:52.923 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
09:36:52.926 [main] INFO  exemples.android.server.boot.Boot - Started Boot in 1.865 seconds (JVM running for 2.203)

1.16.2. Der Android-Client für den Webserver / JSON

Der Android-Client wird die folgende Architektur aufweisen:

Der Client wird aus zwei Komponenten bestehen:

  1. eine [Präsentationsschicht] (View + Activity), ähnlich der, die wir in Beispiel [Beispiel-14] behandelt haben;
  2. die [DAO]-Schicht, die mit dem zuvor behandelten [Web-/JSON]-Dienst interagiert.

1.16.2.1. Erstellen des Projekts

Wir duplizieren das vorherige Projekt [Beispiel-14] in [Beispiel-15], indem wir die Vorgehensweise in Abschnitt 1.4 befolgen. Wir erhalten das folgende Ergebnis:

Als Nächstes ist der Leser aufgefordert, das folgende Projekt zu erstellen.

1.16.2.2. Gradle-Konfiguration

 

Die Datei [build.gradle] sieht wie folgt aus:


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 {
    applicationId "exemples.android"
    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 fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}
 
repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}

Wir werden nur auf das eingehen, was noch nicht behandelt wurde:

  • Zeilen 46–47: Einfügen eines AA-Plugins. Das [rest-spring-api]-Plugin ermöglicht es, die Client-Server-Kommunikation an die AA-Bibliothek zu delegieren;
  • Zeile 50: Die Bibliothek [spring-android-rest-template] ist die von AA zur Abwicklung der Client-Server-Kommunikation verwendete Bibliothek. Die Version [2.0.0.M3] ist eine sogenannte „Milestone“-Version, die in den üblichen Maven-Repositorys nicht zu finden ist. Daher müssen wir in den Zeilen 56–59 das zu verwendende Repository (Zeile 58) angeben, um die Bibliothek zu finden;
  • Zeile 51: eine JSON-Bibliothek;
  • Zeilen 33–39: Ohne diese Eigenschaft treten Fehler bei der Erstellung der APK-Binärdatei des Projekts auf;

1.16.2.3. Das Android-Anwendungsmanifest

  

Die Datei [AndroidManifest.xml] muss aktualisiert werden. Standardmäßig ist der Internetzugang deaktiviert. Er muss mithilfe einer speziellen Anweisung aktiviert werden:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="exemples.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 5: Internetzugang ist erlaubt;

1.16.2.4. Die [DAO]-Schicht

  

1.16.2.4.1. Die [IDao]-Schnittstelle der [DAO]-Schicht

Die Schnittstelle der [DAO]-Schicht sieht wie folgt aus:


package exemples.android.dao;
 
public interface IDao {
 
  // random number
  int getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
}
  • Zeile 6: die Webservice-/JSON-Methode zum Abrufen einer Zufallszahl im Bereich [a,b] von diesem Webservice;
  • Zeile 9: die URL des Webdienstes / JSON zur Generierung von Zufallszahlen;
  • Zeile 12: Wir legen eine maximale Wartezeit für die Antwort des Servers fest;
  • Zeile 15: Wir möchten vor der Ausführung der Anfrage an den Server ein Timeout festlegen, um dem Benutzer Zeit zu geben, seine Anfrage abzubrechen;

1.16.2.4.2. Die [WebClient]-Schnittstelle
  

Die [WebClient]-Schnittstelle übernimmt die Kommunikation mit dem Webdienst. Der Code lautet wie folgt:


package exemples.android.dao;
 
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.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 
@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
 
  // 1 random number in the range [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • Zeile 12: [WebClient] ist eine Schnittstelle, die die AA-Bibliothek mithilfe der Annotationen, die wir hinzufügen werden, selbst implementieren wird. Diese Schnittstelle muss Aufrufe an die vom Webservice / JSON bereitgestellten URLs implementieren:

  // random number
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
  @ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b) throws JsonProcessingException {
  • Zeile 11: Die Annotation [@Rest] ist eine AA-Annotation. Der Wert des Attributs [converters] ist ein Array von Konvertern. Hier sorgt der Konverter [MappingJackson2HttpMessageConverter.class] dafür, dass eine vom Server gesendete JSON-Zeichenkette automatisch deserialisiert wird. So sehen wir in Zeile (d), dass die URL [/{a}/{b}] einen String-Typ zurückgibt, bei dem es sich tatsächlich um eine JSON-Zeichenkette handelt (Zeile b). Mit dieser Information und der Angabe des erwarteten Typs in Zeile 16 deserialisiert die [WebClient]-Instanz des Clients die empfangene Zeichenkette in einen [Response<Integer>]-Typ;
  • Zeile 15: Eine @Get-Annotation, die angibt, dass die URL mit einer HTTP-GET-Methode aufgerufen werden muss. Der Parameter der @Get-Annotation ist das vom Webdienst erwartete URL-Format. Verwenden Sie einfach den Parameter [value] aus der @RequestMapping-Annotation (Zeile b) der Methode, die im [WebController] des Servers aufgerufen wird. Die geschweiften Klammern {} umschließen die URL-Parameter, die in Zeile 16 an die Parameter der Methode übergeben werden müssen. Die Syntax [@Path("a") int a] bewirkt, dass dem Parameter [a] der Methode der Wert {a} aus der URL zugewiesen wird. Wenn der URL-Parameter und der Methodenparameter denselben Namen haben, wie hier, können wir einfacher [@Path int a] schreiben;

Im Falle einer HTTP-POST-Anfrage hätte die Methode „call“ die folgende Signatur:


  @Post("/{a}/{b}")
  Response<Integer> getAlea(@Body T body, @Path("a") int a, @Path("b") int b);

Die Annotation [@Body] bezeichnet den übermittelten Wert. Dieser wird automatisch in JSON serialisiert. Auf der Serverseite haben wir die folgende Signatur:


  // random numbers
  @RequestMapping(value = "/{a}/{b}", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAlea(@PathVariable("a") int a, @PathVariable("b") int b, @RequestBody T body) {
  • Zeile 2: gibt an, dass eine HTTP-POST-Anfrage erwartet wird und dass der Anfragetext (das gesendete Objekt) als JSON-Zeichenkette übertragen werden muss (Attribut „consumes“);
  • Zeile 4: Der gesendete Wert wird im Parameter [@RequestBody T body] der Methode abgerufen;

Kehren wir zum Code für die Klasse [WebClient] zurück:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • Wir müssen in der Lage sein, die URL des anzurufenden Webdienstes anzugeben. Dies wird durch die Erweiterung der von AA bereitgestellten [RestClientRootUrl]-Schnittstelle erreicht. Diese Schnittstelle stellt eine [setRootUrl(urlServiceWeb)]-Methode bereit, mit der wir die URL des anzurufenden Webdienstes festlegen können;
  • Darüber hinaus möchten wir den Aufruf des Webdienstes steuern, da wir die Wartezeit auf die Antwort begrenzen wollen. Dazu erweitern wir die Schnittstelle [RestClientSupport], die die Methode [setRestTemplate] bereitstellt, mit der wir:
    • das [RestTemplate]-Objekt selbst erstellen, das zur Verwaltung des Datenaustauschs zwischen Client und Server dient;
    • dieses Objekt so zu konfigurieren, dass das maximale Zeitlimit für die Antwort festgelegt wird;

1.16.2.4.3. Die Klasse [Response]

Die Methode [getAlea] der Schnittstelle [IDao] gibt eine Antwort vom Typ [Response] wie folgt zurück:


package exemples.android.dao;
 
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
...
}

Dies ist die [Response]-Klasse, die bereits auf der Serverseite verwendet wird (Abschnitt 1.16.1.5). Aus programmiertechnischer Sicht ist es tatsächlich so, als würde die [DAO]-Schicht des Clients direkt mit dem [WebController] des Webdienstes kommunizieren:

Die Netzwerkkommunikation zwischen Client und Server sowie die Serialisierung/Deserialisierung von Java-Objekten auf der Client-Seite sind für den Programmierer transparent.

1.16.2.4.4. Implementierung der [DAO]-Schicht
  

Die [IDao]-Schnittstelle wird mit der folgenden [Dao]-Klasse implementiert:


package exemples.android.dao;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean
public class Dao implements IDao {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // timeout before request execution
  private int delay;
 
// interface IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    ...
  }
 
  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
   ...
  }
 
  @Override
  public void setTimeout(int timeout) {
   ...
  }
 
  @Override
  public void setDelay(int delay) {
    this.delay = delay;
  }
 
}
  • Zeile 15: Wir versehen die Klasse [Dao] mit der Annotation [@EBean], um sie in ein AA-Bean umzuwandeln, das wir an anderer Stelle injizieren können;
  • Zeilen 19–20: Wir injizieren die Implementierung der [WebClient]-Schnittstelle, die wir beschrieben haben. Die [@RestService]-Annotation übernimmt diese Injektion;
  • die anderen Methoden implementieren die Schnittstelle [IDao] (Zeilen 27–46);

[setTimeout]-Methode

Die [setTimeout]-Methode sieht wie folgt aus:


  @Override
  public void setTimeout(int timeout) {
    // set the client request timeout REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // we build the restTemplate
    RestTemplate restTemplate = new RestTemplate(factory);
    // set the jSON converter
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // set the restTemplate of the web client
    webClient.setRestTemplate(restTemplate);
}
  • Die [WebClient]-Schnittstelle wird von einer Klasse AA unter Verwendung der Gradle-Abhängigkeit [org.springframework.android:spring-android-rest-template] implementiert. [spring-android-rest-template] implementiert die Kommunikation des Clients mit dem Web-/JSON-Server mithilfe einer [RestTemplate]-Klasse;
  • Zeile 4: Die Klasse [SimpleClientHttpRequestFactory] wird von der Abhängigkeit [spring-android-rest-template] bereitgestellt. Sie ermöglicht es uns, das maximale Timeout für die Serverantwort festzulegen (Zeilen 5–6);
  • Zeile 8: Wir erstellen das [RestTemplate]-Objekt, das als Kommunikationskanal zum Webservice dient. Wir übergeben das soeben erstellte [factory]-Objekt als Parameter an dieses;
  • Zeile 10: Der Client-Server-Dialog kann verschiedene Formen annehmen. Der Austausch erfolgt über Textzeilen, und wir müssen dem [RestTemplate]-Objekt mitteilen, was mit jeder Textzeile geschehen soll. Dazu stellen wir ihm Konverter zur Verfügung – Klassen, die Textzeilen verarbeiten können. Die Auswahl des Konverters erfolgt in der Regel über die HTTP-Header, die die Textzeile begleiten. Hier wissen wir, dass wir ausschließlich Textzeilen im JSON-Format empfangen. Außerdem hat der Server, wie wir in Abschnitt 1.16.1.7 gesehen haben, den HTTP-Header gesendet:

Content-Type: application/json;charset=UTF-8 

Zeile 10: Der einzige Konverter für das [RestTemplate] wird ein JSON-Konverter sein, der mithilfe der [Jackson]-Bibliothek implementiert wird. Bei diesen Konvertern gibt es eine Besonderheit: AA verlangt, dass wir ihn auch in die [WebClient]-Annotation aufnehmen:


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

Zeile 1: Wir müssen einen Konverter angeben, obwohl wir ihn bereits programmgesteuert festlegen.

  • Zeile 12: Das auf diese Weise erstellte [RestTemplate]-Objekt wird in die Implementierung der [WebClient]-Schnittstelle injiziert, und dieses Objekt übernimmt die Client-Server-Kommunikation;

Methode [getAlea]

Die Methode [getAlea] sieht wie folgt aus:


  @Override
  public int getAlea(int a, int b) {
    // service execution
    Response<Integer> info;
    DaoException ex;
    try {
      // waiting
      waitSomeTime(delay);
      // service execution
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // we return the result
        return info.getBody();
      } else {
        // we note the exception
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // we note the exception
      ex = new DaoException(e, 100);
    }
    // we launch the exception
    throw ex;
  }
...
  // private methods -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • Zeile 8: warte [delay] Millisekunden;
  • Zeile 10: Wir rufen einfach die Methode mit derselben Signatur in der Klasse auf, die die [WebClient]-Schnittstelle implementiert;
  • Zeile 11: Wir analysieren die vom Server empfangene Antwort, indem wir ihren [Status] überprüfen;
  • Zeilen 12–14: Wenn kein serverseitiger Fehler vorliegt (status = 0), geben wir das Ergebnis der Methode zurück;
  • Zeile 17: Wenn ein serverseitiger Fehler aufgetreten ist (status!=0), bereiten wir eine Ausnahme vor, ohne sie auszulösen. Der Server hat eine Liste mit Fehlermeldungen gesendet. Wir erstellen eine Ausnahme, deren einzige Meldung die JSON-Zeichenkette der Meldungsliste des Servers ist;
  • Zeilen 19–22: weitere Ausnahmefälle;
  • Zeile 24: Wenn wir diesen Punkt erreichen, ist zwangsläufig eine Ausnahme aufgetreten. Also lösen wir sie aus;

Die in diesem Code verwendete [DaoException] lautet wie folgt:


package exemples.android.dao;
 
import java.util.ArrayList;
import java.util.List;
 
public class DaoException extends RuntimeException {
 
  // error code
  private int code;
 
  // manufacturers
  public DaoException() {
  }
 
  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }
 
  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }
 
  // getters and setters
...
}
  • Zeile 6: Die [DaoException] ist eine nicht behandelte Ausnahme;

Methode [setUrlServiceWebJson]

Die Methode [setUrlServiceWebJson] lautet wie folgt:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // we set the URL of the REST service
    webClient.setRootUrl(urlServiceWebJson);
}
  • Zeile 4: Wir legen die URL des Webdienstes mithilfe der Methode [setRootUrl] der Schnittstelle [WebClient] fest. Diese Methode existiert, da diese Schnittstelle die Schnittstelle [RestClientRootUrl] erweitert;

1.16.2.5. Das [architecture]-Paket

Das [architecture]-Paket enthält die Elemente, die die Anwendung strukturieren:

1.16.2.5.1. Die Schnittstelle [IMainActivity]

Die Schnittstelle [IMainActivity] listet die Methoden auf, die die Aktivität der Anwendung implementieren muss:


package exemples.android.architecture;
 
import exemples.android.dao.IDao;
 
public interface IMainActivity extends IDao {
 
  // session access
  Session getSession();
 
  // change of view
  void navigateToView(int position);
 
  // waiting
  void beginWaiting();
 
  void cancelWaiting();
 
  // debug mode
  boolean IS_DEBUG_ENABLED = true;
  // response time
  int TIMEOUT = 1000;
  // fragment adjacency
  int OFF_SCREEN_PAGE_LIMIT = 1;
 
}
  • Zeile 5: Die Schnittstelle [IMainActivity] erweitert die Schnittstelle [IDao];
  • Zeilen 13–16: Zu den bereits in den vorherigen Beispielen vorhandenen Methoden (Zeilen 7–11) haben wir zwei Methoden zur Verwaltung des Ladebildschirms der Anwendung hinzugefügt (Zeilen 14, 16);
  • Zeile 21: Wir legen eine maximale Zeitüberschreitung für die Serverantwort auf 1 Sekunde fest;

1.16.2.5.2. Die Klasse [Utils]

Wir haben statische Hilfsmethoden in der Klasse [Utils] zusammengefasst, die von verschiedenen Teilen der Anwendungsarchitektur aus aufgerufen werden können:


package exemples.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 getMessagesForAlert(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();
  }
 
}
  • Zeilen 9–18: Erstellt eine Liste der in einem Throwable enthaltenen Fehlermeldungen;
  • Zeilen 21–32: verwendet die vorherige Methode, um aus der Liste der erhaltenen Meldungen den Text zu erstellen, der in einer Android-Warnmeldung angezeigt werden soll;
  • Zeilen 27–28: Die Meldungen werden nummeriert. Die kleinste Nummer (1) entspricht der ersten Ausnahme, die höchste Nummer der jüngsten Ausnahme im Ausnahmestapel;

1.16.2.5.3. Die abstrakte Klasse [AbstractFragment]

Die Klasse [AbstractFragment] hat zwei Zwecke:

  1. sicherzustellen, dass die Methode [updateFragments] der untergeordneten Klassen immer und nur einmal aufgerufen wird, wenn das Fragment angezeigt wird;
  2. den Status und die Methoden der Unterklassen herauszulösen, die herausgelöst werden können;

Es ist Zweck 2, der uns dazu veranlasst, Operationen zur Verwaltung von Wartebildern in diese Klasse aufzunehmen: Alle Komponenten einer asynchronen Android-Anwendung müssen diese Art von Problem behandeln:


  // wait management
  protected void beginWaiting() {
    // we set the hourglass
    mainActivity.beginWaiting();
  }
 
  protected void cancelWaiting() {
    // the hourglass is removed
    mainActivity.cancelWaiting();
}

1.16.2.6. Die Ansicht

1.16.2.6.1. Die Ansicht [view1.xml]
  

Im Vergleich zum vorherigen Beispiel hat sich die Ansicht [view1.xml] wie folgt geändert:

 
 
  • In [1] muss der Benutzer vor jedem Aufruf des Webdienstes die URL des Webdienstes und die Zeitüberschreitung [2] angeben;
  • in [3] werden die Antworten gezählt;
  • in [4] kann der Benutzer seine Anfrage abbrechen;
  • in [5] erscheint eine Ladeanzeige, wenn die Nummern angefordert werden. Sie verschwindet, sobald alle Nummern empfangen wurden oder der Vorgang abgebrochen wurde;

Image

  • In [6] wird die Gültigkeit der Eingaben überprüft;

Der Leser wird gebeten, die Datei [vue1.xml] aus den Beispielen zu laden. Für den Rest dieses Abschnitts geben wir die IDs der neuen Komponenten an:

Image

Nr.
Typ
ID
1
EditText
edt_nbaleas
2
TextView
txt_FehlerAnzahlEpisoden
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorInterval
6
EditText
editTextWebServiceUrl
7
TextView
textViewErrorUrl
8
EditText
editTextDelay
9
TextView
textViewErrorDelay
10
Schaltfläche
btn_Execute
11
Schaltfläche
btn_Abbrechen
12
TextView
txt_Answers
13
ListView
lst_answers

Die Schaltflächen [10-11] liegen physisch übereinander. Zu jedem Zeitpunkt ist nur eine der beiden sichtbar.

1.16.2.6.2. Das [Vue1Fragment]-Fragment
  

Das Grundgerüst des [Vue1Fragment]-Fragments sieht wie folgt aus:


package exemples.android.fragments;
 
import android.app.AlertDialog;
import android.support.annotation.*;
import android.support.v4.app.Fragment;
import android.view.View;
import android.widget.*;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Utils;
import org.androidannotations.annotations.*;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.api.BackgroundExecutor;
 
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // visual interface elements
  @ViewById(R.id.editTextUrlServiceWeb)
  EditText edtUrlServiceRest;
  @ViewById(R.id.textViewErreurUrl)
  TextView txtMsgErreurUrlServiceWeb;
  @ViewById(R.id.editTextDelay)
  EditText edtDelay;
  @ViewById(R.id.textViewErreurDelay)
  TextView textViewErreurDelay;
  @ViewById(R.id.lst_reponses)
  ListView listReponses;
  @ViewById(R.id.txt_Reponses)
  TextView infoReponses;
  @ViewById(R.id.edt_nbaleas)
  EditText edtNbAleas;
  @ViewById(R.id.edt_a)
  EditText edtA;
  @ViewById(R.id.edt_b)
  EditText edtB;
  @ViewById(R.id.txt_errorNbAleas)
  TextView txtErrorAleas;
  @ViewById(R.id.txt_errorIntervalle)
  TextView txtErrorIntervalle;
  @ViewById(R.id.btn_Executer)
  Button btnExecuter;
  @ViewById(R.id.btn_Annuler)
  Button btnAnnuler;
...
  // local data
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone=true;
    // initially no error messages
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // hidden [Cancel] button
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // list of answers
    reponses = new ArrayList<>();
  }
...
  • Zeilen 24–49: Verweise auf die Ansichtskomponenten [view1.xml] (Zeile 20);
  • Zeilen 55–69: die Methode [@AfterViews], die ausgeführt wird, wenn die Verweise in den Zeilen 24–49 initialisiert wurden;
  • Zeile 58: Nicht vergessen – notwendig für den Lebenszyklus des Fragments;
  • Zeilen 60–63: Fehlermeldungen werden ausgeblendet;
  • Zeilen 65–66: Die Schaltfläche [Cancel] wird ausgeblendet (Zeile 65) und die Schaltfläche [Execute] wird angezeigt (Zeile 66). Beachten Sie, dass sie physisch übereinander liegen;
  • Zeile 68: Das Feld in Zeile 52 enthält die Liste der Zeichenfolgen, die von der ListView der Antworten angezeigt werden sollen;

Unmittelbar nach der Methode [@AfterViews] wird die folgende Methode [updateFragment] ausgeführt:


  @Override
  protected void updateFragment() {
    // create the response list adapter
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • Zeilen 4–5: Erstellen Sie den ListView-Adapter für die Antworten. Er wird in einer Instanzvariablen gespeichert, sodass er für andere Methoden in der Klasse verfügbar ist;

Ein Klick auf die Schaltfläche [Execute] löst die Ausführung der folgenden Methode aus:


// seizures
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;
 
  // local data
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;
 
  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // we start waiting
    beginWaiting();
  }
 
  @Background(id = "alea")
  void getAlea(int a, int b) {
    // do as little as possible here
    // in any case no display - these must take place in the UiThead
    try {
      // the result is displayed in the UiThread
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
  }
  • Zeilen 17–18: Wir löschen die vorherige Liste der Antworten vom Server. Dazu löschen wir in Zeile 17 die mit dem ListView-Adapter verknüpfte Datenquelle [reponses];
  • Zeile 19: Ein boolescher Wert, der angibt, ob der Benutzer seine Anfrage abgebrochen hat oder nicht;
  • Zeilen 21–22: Wir zeigen einen auf Null gesetzten Zähler für die Anzahl der Antworten an;
  • Zeilen 24–26: Wir rufen die Einträge aus den Zeilen [2–6] ab und überprüfen ihre Gültigkeit. Ist einer davon ungültig, wird die Methode abgebrochen (Zeile 25) und der Benutzer kehrt zur visuellen Oberfläche zurück;
  • Zeilen 28–29: Sind alle eingegebenen Daten gültig, werden die Webservice-URL (Zeile 28) und die Wartezeit vor jedem Serviceaufruf (Zeile 29) an die Aktivität übergeben. Diese Informationen werden von der [DAO]-Schicht benötigt; beachten Sie, dass die Kommunikation mit dieser Schicht über die Aktivität erfolgt;
  • Zeilen 31–33: Zufallszahlen werden nacheinander von der Methode [getAlea] in Zeile 39 angefordert;
  • Zeile 38: Die Methode [getAlea] ist mit der AA-Annotation [@Background] versehen, was bedeutet, dass sie in einem anderen Thread (Ausführungsablauf, Prozess) ausgeführt wird als dem, in dem die visuelle Schnittstelle läuft. Es ist in der Tat zwingend erforderlich, jeden Internetaufruf in einem anderen Thread als dem der visuellen Schnittstelle auszuführen. Somit kann es zu jedem Zeitpunkt mehrere Threads geben:
    • derjenige, der die Benutzeroberfläche (UI) anzeigt und deren Ereignisse verwaltet,
    • die [nbAleas]-Threads, von denen jeder eine Zufallszahl vom Webdienst anfordert. Diese Threads werden asynchron gestartet: Der UI-Thread startet einen [getAlea]-Thread (Zeile 32), der eine Zufallszahl vom Webdienst anfordert und nicht auf dessen Beendigung wartet. Er wird über ein Ereignis über den Abschluss benachrichtigt. Somit werden die [nbAleas]-Threads parallel gestartet. Es ist möglich, die Anwendung so zu konfigurieren, dass jeweils nur ein Thread gestartet wird. In diesem Fall gibt es eine Warteschlange mit auszuführenden Threads;

Zeile 38: Der Parameter [id] weist dem generierten Thread einen Namen zu. Hier haben alle [nbAleas]-Threads denselben Namen [alea]. Dadurch können wir sie alle gleichzeitig abbrechen. Dieser Parameter ist optional, wenn das Abbrechen von Threads nicht unterstützt wird;

  • Zeile 44: Die Methode [getAlea] der Aktivität wird aufgerufen. Sie wird daher in einem vom UI getrennten Thread ausgeführt. Dieser Thread ruft den Webdienst auf und wartet nicht auf die Antwort. Er wird später über ein Ereignis benachrichtigt, dass die Antwort verfügbar ist. An dieser Stelle, in Zeile 44, wird die Methode [showInfo] mit der empfangenen Antwort als Parameter aufgerufen;
  • Zeilen 45–47: Die Ausführung der Webanfrage kann eine Ausnahme auslösen. Wir fordern dann an, dass die Fehlermeldungen der Ausnahme in einer Warnmeldung angezeigt werden;
  • Zeile 35: Wir warten auf die Ergebnisse:
    • Es wird eine Ladeanzeige eingeblendet;
    • die Schaltfläche [Abbrechen] ersetzt die Schaltfläche [Ausführen]. Da die gestarteten Threads asynchron sind, wartet der UI-Thread nicht auf sie, und Zeile 35 wird ausgeführt, bevor sie beendet sind. Sobald die Methode [beginWaiting] beendet ist, kann die Benutzeroberfläche wieder auf Benutzereingaben reagieren, beispielsweise auf einen Klick auf die Schaltfläche [Abbrechen]. Wären die gestarteten Threads synchron, würde Zeile 35 erst erreicht werden, wenn alle Threads beendet sind. Ein Abbrechen wäre dann nicht mehr sinnvoll;

Die Methode [showInfo] lautet wie folgt:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // are we done?
      if (nbInfos == nbAleas) {
        // we end the wait
        cancelWaiting();
      }
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // we display the answers
      adapterReponses.notifyDataSetChanged();
    }
}
  • Die Methode [showInfo] wird innerhalb des mit [@Background] annotierten Threads [getAlea] aufgerufen. Diese Methode aktualisiert die Benutzeroberfläche. Dies ist nur möglich, wenn sie im UI-Thread ausgeführt wird. Das ist die Bedeutung der Annotation [@UiThread] in Zeile 1;
  • Zeile 2: Die Methode erhält eine Zufallszahl;
  • Zeile 3: Der Hauptteil der Methode wird nur ausgeführt, wenn der Benutzer seine Anfrage nicht abgebrochen hat;
  • Zeilen 5–6: Der Antwortzähler wird erhöht und angezeigt;
  • Zeilen 8–11: Wenn alle erwarteten Antworten eingegangen sind, wird das Warten beendet (Ende des Wartesignals; die Schaltfläche [Execute] ersetzt die Schaltfläche [Cancel]);
  • Zeilen 12–15: Die empfangene Zufallszahl wird der Liste der Antworten hinzugefügt, die von der Komponente [ListView listReponses] angezeigt wird, und die Liste wird aktualisiert;

Die Methode [showAlert] lautet wie folgt:


  @UiThread
  protected void showAlert(Throwable th) {
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}

Die Logik hier ähnelt der der [showInfo]-Methode:

  • Zeile 1: Die Annotation [@UiThread] ist erforderlich;
  • Zeile 2: Die Methode empfängt die aufgetretene Ausnahme;
  • Zeile 3: Die Methode wird nur ausgeführt, wenn der Benutzer seine Anfrage nicht abgebrochen hat;
  • Zeile 5: Die Anfrage des Benutzers wird abgebrochen, als hätte er selbst auf die Schaltfläche [Abbrechen] geklickt;
  • Zeile 7: Die Warnmeldung wird mithilfe der Android-Klasse [AlertDialog] angezeigt:
    • [activity]: ist die Aktivität vom Typ [Activity], die in der übergeordneten Klasse [AbstractFragment] gespeichert ist;
    • [setTitle]: legt den Titel des Warnfensters fest [1];
    • [setMessage]: Legt die im Warnfenster angezeigte Meldung fest [2];
    • [setNeutral]: Legt die Schaltfläche fest, mit der das Warnfenster geschlossen wird [3];
    • [show]: fordert die Anzeige des Warnfensters an;
 

Das Klicken auf die Schaltfläche [Abbrechen] wird mit der folgenden Methode verarbeitet:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // memory
    hasBeenCanceled=true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea", true);
    // end of wait
    cancelWaiting();
}
  • Zeile 4: Beachten Sie, dass der Benutzer seine Anfrage abgebrochen hat;
  • Zeile 6: bricht alle Aufgaben ab, die durch die Zeichenfolge [alea] gekennzeichnet sind. Der zweite Parameter [true] bedeutet, dass sie auch dann abgebrochen werden müssen, wenn sie bereits gestartet wurden. Die Kennung [alea] ist diejenige, die zur Qualifizierung der Methode [getAlea] des Fragments verwendet wird (Zeile 1 unten):

  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

Hinweis: Es stellte sich heraus, dass sich Zeile 6 des Codes der Methode [doAnnuler] fehlerhaft verhielt. Aus diesem Grund haben wir die boolesche Variable [hasBeenCanceled] hinzugefügt. Tatsächlich würde im Falle einer Ausnahme (Serverausfall) das Warnfenster n-mal erscheinen, wenn wir n Zufallszahlen angefordert hätten.

1.16.2.7. Die Aktivität [MainActivity]

1.16.2.7.1. Die Ansicht [activity-main.xml]
  

Im Vergleich zum vorherigen Beispiel haben wir der Ansicht, die mit der [MainActivity] verknüpft ist, ein Ladebild hinzugefügt:


...
  <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">
      <!-- image d'waiting -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>
 
    </android.support.v7.widget.Toolbar>
    <!-- image d'waiting -->
  </android.support.design.widget.AppBarLayout>
...
  • Zeilen 17–21: das Platzhalterbild;

1.16.2.7.2. Die [MainActivity]-Aktivität

Die [MainActivity] hat sich gegenüber [Beispiel-14] kaum verändert. Zunächst injizieren wir die [DAO]-Schicht in sie:


  // dao injection
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // set the [DAO] layer
    setTimeout(TIMEOUT);
}
  • Zeilen 2–3: Einfügen der [DAO]-Ebene über eine AA-Annotation;
  • Zeilen 5–13: Code, der nach dieser Einbindung ausgeführt wird;
  • Zeile 12: Festlegen des Timeouts für die [DAO]-Schicht

Außerdem muss die Aktivität [MainActivity] die Schnittstelle [IMainActivity] implementieren, die ihrerseits die Schnittstelle [IDao] erweitert:


  // implémentation IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // the position view is displayed
    if (mViewPager.getCurrentItem() != position) {
      // fragment display
      mViewPager.setCurrentItem(position);
    }
  }
 
  // hold image management
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }
 
  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }
 
  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public int getAlea(int a, int b) {
    // execution
    return dao.getAlea(a, b);
  }
 
  @Override
  public void setDelay(int delay) {
    dao.setDelay(delay);
  }
 
  @Override
  public void setUrlServiceWebJson(String url) {
    dao.setUrlServiceWebJson(url);
  }
 
  @Override
  public void setTimeout(int timeout) {
    dao.setTimeout(timeout);
}

1.16.2.8. Ausführen des Projekts

Starten Sie den Webdienst (Abschnitt 1.16.1.7) und starten Sie anschließend den Android-Client:

Image

Um herauszufinden, was Sie in [1] eingeben müssen, führen Sie die folgenden Schritte aus. Öffnen Sie eine Eingabeaufforderung und geben Sie den folgenden Befehl ein:


C:\Program Files\Console2>ipconfig
 
Configuration IP de Windows
 
Carte réseau sans fil Connexion au réseau local* 3 :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . :
 
Carte Ethernet VirtualBox Host-Only Network :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::e481:1583:cd2a:c47%27
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.82.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte Ethernet VirtualBox Host-Only Network #2 :
 
   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::8191:14ad:407d:b840%54
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.64.2
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . :
 
Carte Ethernet Ethernet :
 
   Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
   Adresse IPv6 de liaison locale. . . . .: fe80::d972:ad53:3b8a:263f%28
   Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
   Masque de sous-réseau. . . . . . . . . : 255.255.0.0
   Passerelle par défaut. . . . . . . . . : 172.19.0.254
 
Carte réseau sans fil Wi-Fi :
 
   Statut du média. . . . . . . . . . . . : Média déconnecté
   Suffixe DNS propre à la connexion. . . : uang ad.univ-angers.fr univ-angers.fr

Wenn Sie [GenyMotion] installiert haben, hat die VirtualBox-Virtualisierungsmaschine Ihrem Computer IP-Adressen hinzugefügt (Zeilen 10 und 18). Diese Adressen sind besonders praktisch, da sie nicht von der Windows-Firewall blockiert werden. Zeile 30 zeigt die IP-Adresse Ihres Computers in einem lokalen Netzwerk an. Um diese Adresse zu verwenden, müssen Sie in der Regel die Windows-Firewall deaktivieren. Wenn Sie mit einem WLAN-Netzwerk verbunden sind, verwenden Sie die WLAN-Adresse und deaktivieren Sie auch in diesem Fall die Firewall, falls vorhanden.

Testen Sie die Anwendung in den folgenden Fällen:

  • 100 Zufallszahlen im Bereich [1000, 2000] ohne Zeitlimit;
  • 2000 Zufallszahlen im Bereich [10000, 20000] ohne Zeitlimit, und brechen Sie die Wartezeit ab, bevor die Generierung abgeschlossen ist;
  • 5 Zufallszahlen im Bereich [100, 200] mit einer Wartezeit von 5000 ms, und brechen Sie die Wartezeit ab, bevor die Generierung abgeschlossen ist;

1.16.2.9. Abbruchbehandlung

Um zu verfolgen, was geschieht, wenn der Benutzer eine Abbruchanforderung stellt oder wenn der Abbruch durch eine Ausnahme ausgelöst wird, fügen wir der Schnittstelle [IDao] die folgende Methode hinzu (siehe Abschnitt 1.16.2.4.1):


package exemples.android.dao;
 
public interface IDao {
 
  ...
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}

In der Klasse [Dao] fügen wir den folgenden Code hinzu:


  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
..
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
...
  // interface IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // service execution
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • Zeile 9: Wir notieren den Klassennamen;
  • Zeilen 16–18: Wir schreiben jedes Mal, wenn die Methode [getAlea] aufgerufen wird, einen Log-Eintrag;

Zusätzlich fügen wir im Fragment [Vue1Fragment] die folgenden Log-Einträge hinzu:


  @UiThread
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }
 
  @UiThread
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}
 
  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Jedes Mal, wenn das Fragment [Vue1Fragment] Informationen von der [DAO]-Schicht erhält, wird ein Protokoll erstellt. Außerdem wird das Ereignis protokolliert, wenn die Methode [doAnnuler] aufgerufen wird.

Test 1

Wir fordern 5 Zahlen an, obwohl der Server noch nicht gestartet wurde. Wir erhalten die folgenden Protokolleinträge:

06-06 08:48:51.571 15317-16201/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.576 15317-16202/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.585 15317-16204/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.586 15317-16203/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 08:48:51.593 15317-16205/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.568 15317-15317/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
06-06 08:48:53.587 15317-15317/exemples.android D/Vue1Fragment_: Exception reçue
  • Zeilen 1–5: Die Methode [getAlea] der Klasse [Dao] wird fünfmal aufgerufen. Beachten Sie, dass es sich hierbei um asynchrone Aufrufe handelt, die vom Fragment [VueFragment] getätigt werden, und dass das Fragment nicht auf das Ergebnis seines Aufrufs wartet;
  • Zeile 7: Die erste HTTP-Anfrage wurde gestellt, und das [VueFragment]-Fragment hat seine erste Ausnahme erhalten;
  • Zeile 8: Es fordert daraufhin die Abbruch aller Anfragen an;
  • Zeilen 9–12: Wir sehen jedoch, dass es die folgenden vier Ausnahmen erhält. Daher wurden alle ausstehenden asynchronen Anfragen ausgeführt;

Test 2

Starten wir nun den Server und fordern wir 5 Zahlen mit einer Verzögerung von 5 Sekunden an, dann klicken wir auf [Abbrechen], bevor die Verzögerung endet. Die Protokolle lauten wie folgt:

06-06 09:12:38.360 4640-5054/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.360 4640-5055/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.361 4640-5056/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.362 4640-5057/exemples.android D/Dao_: getAlea [100, 200] en cours
06-06 09:12:38.363 4640-5058/exemples.android D/Dao_: getAlea [100, 200] en cours
...
06-06 09:12:39.895 4640-4640/exemples.android D/Vue1Fragment_: Annulation demandée
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:29:56.313 1616-1616/exemples.android D/Vue1Fragment_: showInfo(185)
06-06 09:30:00.150 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
06-06 09:30:00.151 1616-1616/exemples.android D/Vue1Fragment_: showInfo(157)
  • Zeilen 1–5: Die Methode [getAlea] der Klasse [Dao] wird fünfmal aufgerufen;
  • Zeile 7: Der Benutzer hat die Abbruch der Anfragen angefordert;
  • Zeile 8: Wir sehen, dass [Vue1_Fragment] 5 Werte erhält. Erneut wurden alle ausstehenden asynchronen Anfragen ausgeführt;

Deshalb mussten wir eine boolesche Variable [hasBeenCanceled] verwalten, um zu vermeiden, dass etwas angezeigt wird, wenn eine Stornierung angefordert wurde. Im Stornierungscode:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // the asynchronous task is cancelled
    BackgroundExecutor.cancelAll("alea",true);
    // end of wait
    cancelWaiting();
}

Der Code in Zeile 10 verhält sich nicht wie erwartet. Dies kann daran liegen, dass die asynchronen Aufgaben dieselbe Methode nutzen, die mit [@Background] annotiert ist:


  @Background(id = "alea")
  void getAlea(int a, int b) {
    ...
}

1.17. Beispiel 16: Umgang mit Asynchronität mit RxAndroid

Wir werden nun die für Android-Anwendungen erforderliche Asynchronität mithilfe einer Bibliothek namens RxJava [http://reactivex.io/] und ihrer für die Android-Umgebung abgeleiteten Version [RxAndroid] verwalten. Dazu nutzen wir den Kurs [Einführung in RxJava. Anwendung in Swing- und Android-Umgebungen].

1.17.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-1] in [Beispiel-16]:

1.17.2. Gradle-Konfiguration

  

In [build.gradle] fügen wir die Abhängigkeit zur [RxAndroid]-Bibliothek hinzu:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. Die [DAO]-Schicht

  

1.17.4. Die [IDao]-Schnittstelle

Die [IDao]-Schnittstelle sieht nun wie folgt aus:


package exemples.android.dao;
 
import rx.Observable;
 
public interface IDao {
 
  // random number
  Observable<Integer> getAlea(int a, int b);
 
  // URL of the web service
  void setUrlServiceWebJson(String url);
 
  // max wait time (ms) for server response
  void setTimeout(int timeout);
 
  // client wait time in milliseconds before request
  void setDelay(int delay);
 
  // debug mode
  void setDebugMode(boolean isDebugEnabled);
}
  • Zeile 8: Die Methode [getAlea] gibt nun einen Typ [Observable] aus der RxJava-Bibliothek zurück (Zeile 3). Das Prinzip ist wie folgt:

Ein Stream von Elementen des Typs Observable<T> wird von einem oder mehreren Abonnenten (Beobachtern, Konsumenten) des Typs Subscriber<T> beobachtet. Die RxJava-Bibliothek ermöglicht es, den Observable<T>-Stream im Thread T1 und seinen Subscriber<T>-Beobachter im Thread T2 auszuführen, ohne dass sich der Entwickler um die Verwaltung des Lebenszyklus dieser Threads und um naturgemäß schwierige Probleme wie den Datenaustausch zwischen Threads und die Thread-Synchronisation zur Ausführung einer globalen Aufgabe kümmern muss. Sie erleichtert somit die asynchrone Programmierung.

1.17.5. Die Klasse [AbstractDao]

Wir werden die Klasse [Dao] von der folgenden Klasse [AbstractDao] ableiten:


package exemples.android.dao;
 
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();
 
  // méthodes protégées ----------------------------------------------------------
  // generic interface
  protected interface IRequest<T> {
    Response<T> getResponse();
  }
 
  // generic request
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // service execution
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // service execution
        try {
          // make the synchronous request and forward the response to the subscriber
          Response<T> response = request.getResponse();
          // mistake?
          int status = response.getStatus();
          if (status != 0) {
            // we note the exception
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // we issue the answer
            subscriber.onNext(response.getBody());
            // we signal the end of the observable
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // we note the exception
          ex = new DaoException(e, 100);
        }
        // exception?
        if (ex != null) {
          // we issue the exception
          subscriber.onError(ex);
        }
      }
    });
  }
 
}
  • Die Klasse [AbstractDao] enthält als Hauptelement eine generische Methode [getResponse], die dazu dient, eine [Response<T>] vom Server abzurufen, wobei T der vom HTTP-Client gewünschte Ergebnistyp ist (hier Integer);
  • Zeile 20: Der einzige Parameter der generischen Methode [getResponse] ist eine Instanz der generischen Schnittstelle [IRequest<T>] aus den Zeilen 15–17. Diese Schnittstelle verfügt nur über eine einzige Methode [getResponse], und genau diese Methode gibt die gewünschte [Response<T>] zurück;
  • Dank der beiden vorangegangenen Punkte kann die Klasse [AbstractDao] als Oberklasse für jede clientseitige [Dao]-Schicht eines Servers dienen, der Antworten vom Typ [Response<T>] sendet;
  • Zeile 20: Die generische Methode [getResponse] gibt einen Typ [Observable<T>] zurück, der das vom HTTP-Client tatsächlich erwartete Ergebnis darstellt (hier einen Typ Observable<Integer>);
  • Zeilen 22–51: Die statische Methode [rx.Observable.create] erstellt einen Typ [Observable];
  • Zeile 22: Der einzige Parameter dieser Methode ist eine Instanz vom Typ [rx.Observable.OnSubscribe<T>], eine Schnittstelle, die über die folgenden Methoden verfügt:
    • [onNext(T element)]: ermöglicht es, ein Element vom Typ T an einen Beobachter zu senden;
    • [onError(Throwable th)]: ermöglicht es, eine Ausnahme an einen Beobachter zu senden;
    • [onCompleted]: ermöglicht es Ihnen, einem Beobachter mitzuteilen, dass die Emissionen beendet sind;

Ein Typ [Observable<T>] unterliegt bestimmten Einschränkungen:

  • Er sendet seine Elemente mithilfe der Methode [onNext(T element)];
  • die Methode [onCompleted] muss genau einmal aufgerufen werden, sobald keine Elemente mehr an den Beobachter ausgegeben werden;
  • die Methode [onCompleted] wird nicht aufgerufen, wenn die Methode [onError(Throwable th)] aufgerufen wurde;

In unserem Beispiel:

  • ist der Beobachter das Fragment [Vue1Fragment]. Es ist der Beobachter, der die von [Observable<T>] ausgegebenen Elemente (Element oder Ausnahme) verarbeitet;
  • Der erstellte Typ [Observable<T>] gibt nur ein einziges Element aus (Zeile 37);
  • Zeile 29: führt eine synchrone HTTP-Anfrage an den Server durch und erhält den Typ [Response<T>]. Diese HTTP-Anfrage wird vom Typ [IRequest] verarbeitet, der als Parameter an die generische Methode [getResponse] übergeben wird;
  • Zeile 31: Ruft den Antwortstatus ab;
  • Zeilen 32–34: Wenn dieser Status einen Fehler anzeigt, wird eine Ausnahme vorbereitet;
  • Zeilen 36–39: Wenn der Status kein Fehler ist, wird die vom Client tatsächlich erwartete Antwort gesendet (Zeile 37), und der Beobachter wird darüber informiert, dass keine weiteren Emissionen erfolgen werden (Zeile 39);
  • Zeilen 41–44: Wenn die HTTP-Anfrage mit einer Ausnahme endet, wird diese protokolliert;
  • Zeilen 46–49: Wenn die Ausnahme [ex] nicht null ist, wird sie an den Beobachter ausgegeben. Es ist hier nicht erforderlich, die Methode [onCompleted] aufzurufen, um dem Beobachter mitzuteilen, dass keine weiteren Elemente ausgegeben werden. Dies ist implizit;

Die wichtigste Erkenntnis aus diesen Erläuterungen ist:

  • Die generische Methode [<T> Observable<T> getResponse(final IRequest<T> request)] gibt einen Typ [Observable<T>] zurück, der entweder ein einzelnes Element vom Typ T oder eine Ausnahme ausgibt;
  • diese Methode akzeptiert als einzigen Parameter einen Typ [IRequest<T>], dessen einzige Methode [getResponse()] die HTTP-Anfrage ausführt, die den Typ [Response<T>] zurückgibt;

1.17.6. Die Klasse [Dao]

Die Klasse [Dao] entwickelt sich wie folgt:


@EBean
public class Dao extends AbstractDao implements IDao {
 
  // service customer REST
  @RestService
  protected WebClient webClient;
 
  // timeout before request execution
  private int delay;
  // debug mode
  private boolean isDebugEnabled;
  // class name
  private String className;
 
  // manufacturer
  public Dao() {
    // class name
    className = getClass().getSimpleName();
  }
 
 
  // interface IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        return webClient.getAlea(a, b);
      }
    });
}
...
  • Zeile 2: Die Klasse [Dao] erweitert die Klasse [AbstractDao];
  • Zeile 24: Die Methode [getAlea] gibt nun einen Typ [Observable<Integer>] zurück;
  • Zeile 30: Aufruf der generischen Methode [getResponse] der übergeordneten Klasse. Ihr wird ein Parameter vom Typ [IRequest<Integer>] übergeben;
  • Zeilen 32–37: Implementierung der Schnittstelle [IRequest<Integer>];
  • Zeile 36: Die HTTP-Anfrage wird wie zuvor über die AA-Schnittstelle [webClient] gestellt. Wir wissen, dass wir einen Typ [Response<Integer>] erhalten werden, was tatsächlich der Typ ist, den die Methode [IRequest<Integer>.getResponse()] zurückgeben muss;
  • Zeile 36: Hier nutzen wir eine Funktion namens „Closure“: die Möglichkeit, Werte, die außerhalb einer Instanz liegen, bei deren Erstellung in diese zu kapseln, in diesem Fall die Werte von [a, b] aus Zeile 24. Dadurch kann die Methode [IRequest<Integer>.getResponse()] ohne Parameter auskommen. Diese Werte wurden in den Methodenkörper eingebettet. Und wo wir normalerweise die Parameter der Methode (a, b) -> (x, y) ändern würden, erstellen wir hier eine neue Instanz von [IRequest<Integer>], die die Werte von x und y kapselt;

1.17.7. Die Klasse [MainActivity]

Die Klasse [MainActivity], die die Schnittstelle [IDao] implementiert, entwickelt sich wie folgt:


  // implémentation IDao --------------------------------------------------------------------
 
  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // execution
    return dao.getAlea(a, b);
}

1.17.8. Die Klasse [Vue1Fragment]

Die Klasse [Vue1Fragment] entwickelt sich wie folgt:


  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // delete previous answers
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // reset the response counter to 0
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // test the validity of entries
    if (!isPageValid()) {
      return;
    }
    // activity initialization
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // we ask for the random numbers
    getAleasInBackground(a, b);
    // we start waiting
    beginWaiting();
}
  • Zeile 18: Zufallszahlen von der Methode [getAleasInBackground] anfordern, die so benannt ist, weil die Zahlen in einem anderen Thread als dem UI-Thread angefordert werden;

  private int nbReponses = 0;
  // subscriptions to observables
  private List<Subscription> abonnements;
 
// annotation [Background] unnecessary
  void getAleasInBackground(int a, int b) {
    // initially no response and no subscriptions
    nbReponses = 0;
    abonnements.clear();
    // prepare the observable
    Observable<Integer> response = Observable.empty();
    // merge the results of the various HTTP calls
    // they are executed on an I/O thread
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // the cumulative observable will be observed on the UI thread
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // the observable is executed
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // we add the information to the list of answers
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // error message
          showAlert(th);
          // end waiting
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // end waiting
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // the exception is displayed in the UiThread
      showAlert(e);
    }
}
  • Zeile 3: Ein Observable hat Abonnenten. Die Verbindung zwischen einem Abonnenten und dem von ihm beobachteten Prozess wird als Abonnement bezeichnet. Hier haben wir nur einen beobachteten Prozess und einen Abonnenten. Daher haben wir nur ein Abonnement. Der Einfachheit halber behandeln wir es so, als könnten wir mehrere beobachtete Prozesse haben, die von verschiedenen Beobachtern überwacht werden, was zu mehreren Abonnements führen würde;
  • Zeilen 11–18: Wir konfigurieren den beobachteten Prozess (Observable). Es ist wichtig zu verstehen, dass es sich hierbei nur um eine Konfiguration handelt: Der Prozess wird nicht ausgeführt;
  • Zeile 11: Wir beginnen mit einem leeren Observable, einem Observable, das nichts ausgibt;
  • Zeilen 14–16: Zu diesem leeren Observable fügen wir [nbAleas] Observables hinzu, bei denen es sich um [nbAleas] HTTP-Anfragen handelt, die [nbAleas] Zufallszahlen zurückgeben;
  • Zeile 15: Wie zuvor wird die Zufallszahl i von der Klasse [MainActivity] angefordert. Es ist wichtig zu verstehen, dass noch keine HTTP-Anfrage ausgeführt wurde. Die Methode [mainActivity.getRandom(a, b)] wird ausgeführt und gibt ein [Observable<Integer>] zurück. Dies ist ein Prozess, der beobachtet wird, sobald er gestartet ist;
  • Zeile 15: Die Methode [subscribeOn(Schedulers.io())] fordert an, dass der Prozess (sobald er ausgeführt wird) auf einem I/O-Thread ausgeführt wird. Die RxJava-Bibliothek bietet verschiedene Arten von Threads an. Der I/O-Thread eignet sich für HTTP-Anfragen;
  • Zeile 15: Das Observable #i wird mit dem ursprünglichen Observable aus Zeile 11 zusammengeführt: Aus [nbAleas] Observables, die jeweils ein Element ausgeben, erstellen wir ein Observable, das [nbAleas] Elemente ausgibt. Dies ist das Observable, das beobachtet wird. Dieses Observable gibt die Benachrichtigung [onCompleted] aus, wenn alle Observables, aus denen es besteht, ihre eigenen [onCompleted]-Benachrichtigungen ausgegeben haben. Dadurch müssen wir nicht mehr wie in der vorherigen Version die Antworten zählen, um festzustellen, ob wir alle erwarteten Zahlen erhalten haben;
  • Zeile 18: Zu diesem Zeitpunkt haben wir ein Observable konfiguriert, das aus [nbAleas] Observables besteht, die jeweils auf einem I/O-Thread laufen;
  • Zeile 18: Die Methode [observeOn(AndroidSchedulers.mainThread())] legt fest, auf welchem Thread die vom Observable ausgesendeten Werte beobachtet werden sollen. Hier gehört der Thread [AndroidSchedulers.mainThread())] zur RxAndroid-Bibliothek, nicht zu RxJava. Er bezieht sich auf den UI-Thread, auch bekannt als Event-Loop. Dieser Punkt ist wichtig: In einer Android-App kann die Änderung einer UI-Komponente nur auf dem UI-Thread erfolgen; andernfalls tritt eine Ausnahme auf;
  • Zeilen 19–45: Nachdem der zu beobachtende Prozess nun konfiguriert wurde, führen wir ihn aus;
  • Zeile 21: Die Operation [Observable.subscribe] initiiert die Ausführung des beobachteten Prozesses. Diese Operation startet die zuvor konfigurierten asynchronen Prozesse [nbAleas]. Die Ergebnisse dieser Prozesse werden dem Beobachter automatisch auf dem UI-Thread zur Verfügung gestellt;
  • Zur Erinnerung: Das Observable sendet drei Arten von Ereignissen aus:
    • [onNext]: wenn es ein Element ausgibt;
    • [onError]: wenn eine Ausnahme auftritt;
    • [onCompleted]: wenn es signalisiert, dass es keine Ereignisse mehr ausgibt;

Die Methode [Observable.subscribe] nimmt drei Objekte als Parameter entgegen: [Action1<Integer>, Action1<Throwable>, Action0], deren [call]-Methoden zur Behandlung dieser drei Ereignisse verwendet werden;

  • Zeilen 21–27: Der erste Parameter vom Typ [Action1<Integer>] wird zur Behandlung des [onNext]-Ereignisses verwendet. Seine [call]-Methode empfängt das vom Observable emittierte Element (Zeile 23);
  • Zeile 25: Wir verwenden die Methode [showInfo] aus dem vorherigen Beispiel erneut;
  • Zeilen 27–35: Der zweite Parameter vom Typ [Action1<Throwable>] wird zur Behandlung des [onError]-Ereignisses verwendet. Seine [call]-Methode empfängt die vom Observable ausgegebene Ausnahme (Zeile 29);
  • Zeile 31: Wir verwenden die Methode [showAlert] aus dem vorherigen Beispiel wieder;
  • Zeile 33: Wir leiten den Vorgang zum Abbrechen der Benutzeranfrage ein. Dazu müssen alle derzeit ausgeführten Observables abgebrochen werden;
  • Zeilen 35–41: Der dritte Parameter vom Typ [Action0] wird zur Behandlung des [onCompleted]-Ereignisses verwendet. Seine [call]-Methode nimmt keine Parameter entgegen;
  • Zeile 39: Die Wartezeit wird abgebrochen;

Die Methode [showInfo] entwickelt sich wie folgt:


  // annotation [UiThread] unnecessary
  protected void showInfo(int alea) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // one more piece of information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // we add the information to the list of answers
      reponses.add(0, String.valueOf(alea));
      // display answers
      adapterReponses.notifyDataSetChanged();
    }
}

Die Methode weist zwei Änderungen auf:

  • Zeile 1: Wir haben die AA-Annotation [@UiThread] entfernt;
  • Wir zählen die Antworten nicht mehr, um zu entscheiden, ob das Warten beendet werden soll oder nicht. Diese Information wird nun durch das [onCompleted]-Ereignis des Observables bereitgestellt;

Die Methode [showAlert] ändert sich wie folgt:


  // annotation [UiThread] unnecessary
  protected void showAlert(Throwable th) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // we cancel everything
      doAnnuler();
      // we display it
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • Die einzige Änderung befindet sich in Zeile 1: Wir haben die AA-Annotation [@UiThread] entfernt;

Schließlich ändert sich die Methode [doAnnuler] wie folgt:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // memory
    hasBeenCanceled = true;
    // asynchronous tasks are cancelled
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // end of wait
    cancelWaiting();
}
  • Zeile 12: bricht ein Abonnement und damit die Beobachtung des zugehörigen Prozesses ab;

1.17.9. Ausführung

Starten Sie den Webdienst (Abschnitt 1.16.1.7), starten Sie den Android-Client und wiederholen Sie die Tests, die Sie im vorherigen Beispiel durchgeführt haben (Abschnitt 1.16.2.8).

1.17.10. Behandlung der Kündigung

Wir wiederholen die gleichen Tests wie im vorherigen Beispiel (Abschnitt 1.16.2.9).

Test 1

Wir fordern 5 Zahlen an, obwohl der Server noch nicht gestartet wurde. Wir erhalten die folgenden Protokolle:

1
2
3
4
5
6
7
06-07 05:48:09.790 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:09.791 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Exception reçue
06-07 05:48:11.789 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Nach Zeile 7 gibt es keine weiteren Protokolleinträge mehr, was zeigt, dass der Beobachter (Vue1Fragment) keine Benachrichtigungen mehr vom beobachteten Prozess erhält.

Test 2

Starten wir nun den Server und fordern wir 5 Zahlen mit einer Verzögerung von 5 Sekunden an, dann klicken wir auf [Abbrechen], bevor die Verzögerung abgelaufen ist. Die Logs lauten wie folgt:

1
2
3
4
5
6
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:22.675 28272-28272/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 05:52:23.485 28272-28272/exemples.android D/Vue1Fragment_: Annulation demandée

Nach Zeile 6 gibt es keine weiteren Protokolleinträge mehr, was zeigt, dass der Beobachter (Vue1Fragment) keine Benachrichtigungen mehr vom beobachteten Prozess erhält.

Dies ist das erwartete Verhalten bei einer Abbruchaktion. Wir können daher die boolesche Variable [hasBeenCanceled] aus dem [Vue1Fragment]-Code entfernen, die wir im vorherigen Beispiel eingeführt haben, da sich der Abbruch nicht wie erwartet verhielt.

Die Tatsache, dass der Beobachter nach der Abbruch des Beobachtbaren keine Benachrichtigungen mehr erhält, bedeutet nicht, dass die HTTP-Anfragen selbst abgebrochen werden. Um dies zu verdeutlichen, ändern wir die [Dao]-Klasse wie folgt:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // log
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // web client execution
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // waiting
        waitSomeTime(delay);
        // synchronous HTTP call
        Response<Integer> response= webClient.getAlea(a, b);
        if (isDebugEnabled) {
          try {
            Log.d(String.format("%s", className), String.format("response [%s]", new ObjectMapper().writeValueAsString(response)));
          } catch (JsonProcessingException e) {
            Log.d(String.format("%s", className),"erreur désérialisation jSON");
          }
        }
        return response;
      }
    });
}
  • Zeilen 15–21: Wir protokollieren das Ergebnis der HTTP-Anfrage aus Zeile 14;

Die Protokolle für Test Nr. 2 lauten wie folgt:

06-07 06:03:20.778 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.784 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:20.785 27085-27085/exemples.android D/Dao_: getAlea [100, 200] en cours
06-07 06:03:21.493 27085-27085/exemples.android D/Vue1Fragment_: Annulation demandée
06-07 06:03:21.636 27085-27440/exemples.android D/Dao_: response [{"body":176,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27442/exemples.android D/Dao_: response [{"body":145,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27439/exemples.android D/Dao_: response [{"body":197,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27438/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
06-07 06:03:21.636 27085-27441/exemples.android D/Dao_: response [{"body":136,"messages":null,"status":0}]
  • Zeilen 1–5: Die 5 Anfragen wurden gestellt;
  • Zeile 6: Der Benutzer hat abgebrochen;
  • Zeilen 7–11: Wir erhalten erfolgreich die Antworten auf die fünf HTTP-Anfragen. Da das Observable jedoch abgebrochen wurde, werden diese Elemente nicht an den Observer weitergeleitet;

1.17.11. Fazit

Im weiteren Verlauf dieses Dokuments werden Client-Server-Anwendungen aus den folgenden Gründen mit der RxAndroid-Bibliothek anstelle der AA-Bibliothek implementiert:

  1. RxAndroid kann in einer Android-Anwendung verwendet werden, die AA nicht nutzt;
  2. RxAndroid leistet mehr als nur die Erleichterung asynchroner Operationen. Es bietet zahlreiche Methoden, um aus einem anderen Observable ein neues zu erstellen. Diese Methoden haben kein AA-Äquivalent;
  3. Sobald man versucht, eine mit AA annotierte Klasse, wie beispielsweise ein Fragment, abzuleiten, treten schwerwiegende Probleme auf. Man ist dann gezwungen, AA aufzugeben und Lösung 1 für die asynchrone Programmierung zu verwenden;

Leser, die sich näher mit den Möglichkeiten der RxAndroid-Bibliothek befassen möchten, können das Dokument [Einführung in RxJava. Anwendung in Swing- und Android-Umgebungen] zu Rate ziehen. Darin wird RxAndroid ohne die AA-Bibliothek verwendet.

1.18. Beispiel 17: Komponenten zur Dateneingabe

Wir erstellen ein neues Projekt, um einige gängige Komponenten zu demonstrieren, die in Dateneingabeformularen verwendet werden.

1.18.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-13] in [Beispiel-17]:

Das neue Projekt wird nur eine Ansicht [view1.xml] enthalten. Daher löschen wir die Ansicht [view2.xml] und das zugehörige Fragment [View2Fragment] [2]. Diese Änderung tragen wir im Fragment-Manager von [MainActivity] ein:


  // our fragment manager to be redefined for each application
  // must define the following methods: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
 
    // fragments
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Führen Sie das Projekt erneut aus. Es sollte wie zuvor Ansicht Nr. 1 anzeigen. Wir werden mit diesem Projekt weiterarbeiten.

1.18.2. Die XML-Ansicht des Formulars

  

Die durch die Datei [vue1.xml] generierte Ansicht sieht wie folgt aus:

Image

Der XML-Text der Ansicht lautet wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginBottom="30dp">
 
  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
 
    <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="30dp"
      android:text="@string/titre_vue1"
      android:textSize="30sp"/>
 
    <Button
      android:id="@+id/formulaireButtonValider"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/TextViewFormulaireCombo"
      android:layout_below="@+id/TextViewFormulaireCombo"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_valider"/>
 
    <TextView
      android:id="@+id/textViewFormulaireCheckBox"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireTitre"
      android:layout_below="@+id/textViewFormulaireTitre"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_checkbox"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireRadioButton"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireCheckBox"
      android:layout_below="@+id/textViewFormulaireCheckBox"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_radioButton"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireSeekBar"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireRadioButton"
      android:layout_below="@+id/textViewFormulaireRadioButton"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_seekBar"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireEdtText"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireSeekBar"
      android:layout_below="@+id/textViewFormulaireSeekBar"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_saisie"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireBool"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireEdtText"
      android:layout_below="@+id/textViewFormulaireEdtText"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_bool"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireDate"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:layout_alignLeft="@+id/textViewFormulaireBool"
      android:layout_below="@+id/textViewFormulaireBool"
      android:layout_marginTop="50dp"
      android:gravity="center"
      android:text="@string/formulaire_date"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireMultilignes"
      android:layout_width="150dp"
      android:layout_height="100dp"
      android:gravity="center"
      android:layout_alignBaseline="@+id/textViewFormulaireTitre"
      android:layout_alignParentTop="true"
      android:layout_marginLeft="400dp"
      android:layout_toRightOf="@+id/textViewFormulaireTitre"
      android:text="@string/formulaire_multilignes"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/textViewFormulaireTime"
      android:layout_width="wrap_content"
      android:layout_height="200dp"
      android:gravity="center"
      android:layout_alignLeft="@+id/textViewFormulaireMultilignes"
      android:layout_below="@+id/textViewFormulaireMultilignes"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_time"
      android:textSize="20sp"/>
 
    <TextView
      android:id="@+id/TextViewFormulaireCombo"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignLeft="@+id/textViewFormulaireTime"
      android:layout_below="@+id/textViewFormulaireTime"
      android:layout_marginTop="30dp"
      android:text="@string/formulaire_combo"
      android:textSize="20sp"/>
 
    <CheckBox
      android:id="@+id/formulaireCheckBox1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireCheckBox"
      android:layout_marginLeft="100dp"
      android:layout_toRightOf="@+id/textViewFormulaireCheckBox"
      android:text="@string/formulaire_checkbox1"/>
 
    <RadioGroup
      android:id="@+id/formulaireRadioGroup"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireRadioButton"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:orientation="horizontal">
 
      <RadioButton
        android:id="@+id/formulaireRadioButton1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radiobutton1"/>
 
      <RadioButton
        android:id="@+id/formulaireRadioButton2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radionbutton2"/>
 
      <RadioButton
        android:id="@+id/formulaireRadionButton3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/formulaire_radiobutton3"/>
    </RadioGroup>
 
    <SeekBar
      android:id="@+id/formulaireSeekBar"
      android:layout_width="200dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
      android:layout_alignLeft="@+id/formulaireCheckBox1"/>
 
    <EditText
      android:id="@+id/formulaireEditText1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireEdtText"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:ems="10"
      android:inputType="text">
    </EditText>
 
    <Switch
      android:id="@+id/formulaireSwitch1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireBool"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:text="@string/formulaire_switch"
      android:textOff="Non"
      android:textOn="Oui"/>
 
    <TimePicker
      android:id="@+id/formulaireTimePicker1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormulaireTime"
      android:layout_alignLeft="@+id/formulaireEditTextMultiLignes"
      android:timePickerMode="spinner"
    />
 
    <EditText
      android:id="@+id/formulaireEditTextMultiLignes"
      android:layout_width="wrap_content"
      android:layout_height="100dp"
      android:layout_alignBaseline="@+id/textViewFormulaireMultilignes"
      android:layout_alignBottom="@+id/textViewFormulaireMultilignes"
      android:layout_marginLeft="50dp"
      android:layout_toRightOf="@+id/textViewFormulaireMultilignes"
      android:ems="10"
      android:inputType="textMultiLine">
    </EditText>
 
    <Spinner
      android:id="@+id/formulaireDropDownList"
      android:layout_width="200dp"
      android:layout_height="50dp"
      android:layout_alignBottom="@+id/TextViewFormulaireCombo"
      android:layout_alignLeft="@+id/formulaireEditTextMultiLignes">
    </Spinner>
 
    <DatePicker
      android:id="@+id/formulaireDatePicker1"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBottom="@+id/textViewFormulaireDate"
      android:layout_alignLeft="@+id/formulaireCheckBox1"
      android:datePickerMode="spinner"
      android:calendarViewShown="false">
    </DatePicker>
 
    <TextView
      android:id="@+id/textViewSeekBarValue"
      android:layout_width="30dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/textViewFormulaireSeekBar"
      android:layout_marginLeft="30dp"
      android:layout_toRightOf="@+id/formulaireSeekBar"
      android:text=""/>
  </RelativeLayout>

Die Hauptkomponenten des Formulars sind wie folgt:

  • Zeile 2: ein vertikales [ScrollView]-Layout. Damit können Sie
  • ein Formular anzuzeigen, das größer ist als der Bildschirm des Tablets
  • . Sie können das gesamte Formular durch
  • Scrollen anzeigen;
 
  • Zeilen 125–132: ein Kontrollkästchen
  • Zeilen 134–159: eine Gruppe von drei Optionsfeldern
  • Zeilen 161–166: eine Suchleiste
  • Zeilen 16–176: ein Texteingabefeld
  • Zeilen 178–186: ein Ja/Nein-Schalter
  • Zeilen 188–195: ein Zeit-Eingabefeld
  • Zeilen 197–207: ein mehrzeiliges Textfeld
  • Zeilen 209–215: eine Dropdown-Liste
  • Zeilen 217–225: ein Datums-Eingabefeld
  • Alle anderen Komponenten sind [TextView]s, die Text anzeigen.
 

1.18.3. Die Zeichenfolgen des Formulars

Die Zeichenfolgen des Formulars sind in der folgenden Datei [res/values/strings.xml] definiert:

  

<resources>
  <string name="app_name">Exemple-17</string>
  <string name="action_settings">Settings</string>
  <string name="section_format">Hello World from section: %1$d</string>
  <!-- vue 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="formulaire_checkbox">Cases à cocher</string>
  <string name="formulaire_radioButton">Boutons Radio</string>
  <string name="formulaire_seekBar">Seek Bar</string>
  <string name="formulaire_saisie">Champ de saisie</string>
  <string name="formulaire_bool">Booléen</string>
  <string name="formulaire_date">Date</string>
  <string name="formulaire_time">Heure</string>
  <string name="formulaire_multilignes">Champ de saisie multilignes</string>
  <string name="formulaire_listview">Liste</string>
  <string name="formulaire_combo">Liste déroulante</string>
  <string name="formulaire_checkbox1">1</string>
  <string name="formulaire_checkbox2">2</string>
  <string name="formulaire_radiobutton1">1</string>
  <string name="formulaire_radionbutton2">2</string>
  <string name="formulaire_radiobutton3">3</string>
  <string name="formulaire_switch"></string>
  <string name="formulaire_valider">Valider</string>
</resources>

1.18.4. Das Formularfragment

  

Die Klasse [View1Fragment] sieht wie folgt aus:


package exemples.android.fragments;
 
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.widget.*;
import android.widget.SeekBar.OnSeekBarChangeListener;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.ArrayList;
import java.util.List;
 
// a fragment is a view displayed by a fragment container
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // the fields of the view displayed by the fragment
  @ViewById(R.id.formulaireDropDownList)
  Spinner dropDownList;
  @ViewById(R.id.formulaireButtonValider)
  Button buttonValider;
  @ViewById(R.id.formulaireCheckBox1)
  CheckBox checkBox1;
  @ViewById(R.id.formulaireRadioGroup)
  RadioGroup radioGroup;
  @ViewById(R.id.formulaireSeekBar)
  SeekBar seekBar;
  @ViewById(R.id.formulaireEditText1)
  EditText saisie;
  @ViewById(R.id.formulaireSwitch1)
  Switch switch1;
  @ViewById(R.id.formulaireDatePicker1)
  DatePicker datePicker1;
  @ViewById(R.id.formulaireTimePicker1)
  TimePicker timePicker1;
  @ViewById(R.id.formulaireEditTextMultiLignes)
  EditText multiLignes;
  @ViewById(R.id.formulaireRadioButton1)
  RadioButton radioButton1;
  @ViewById(R.id.formulaireRadioButton2)
  RadioButton radioButton2;
  @ViewById(R.id.formulaireRadionButton3)
  RadioButton radioButton3;
  @ViewById(R.id.textViewSeekBarValue)
  TextView seekBarValue;
 
  // 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");
  }
 
 
  @SuppressLint("DefaultLocale")
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    ...
  }
 
@Override
  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);
  }
}
  • Zeilen 22–49: Wir rufen die Referenzen für alle Komponenten des XML-Formulars [view1] ab (Zeile 18);
  • Zeile 58: Mit der Methode [setChecked] können Sie ein Optionsfeld oder ein Kontrollkästchen aktivieren;
  • Zeile 60: Standardmäßig zeigt die [DatePicker]-Komponente sowohl ein Datums-Eingabefeld als auch einen Kalender an. In Zeile 60 wird der Kalender entfernt;
  • Zeile 62: [SeekBar].setMax() legt den Maximalwert des Schiebereglers fest. Der Minimalwert ist 0;
  • Zeilen 63–74: Wir verarbeiten die Ereignisse des Schiebereglers. Bei jeder vom Benutzer vorgenommenen Änderung möchten wir den Wert des Schiebereglers in der [TextView] in Zeile 49 anzeigen;
  • Zeile 71: Der Parameter [progress] repräsentiert den Wert des Schiebereglers;
  • Zeilen 76–79: eine Liste von [String]s, die der Dropdown-Liste zugeordnet werden;
  • Zeile 90: Die [updateFragment]-Methode des Fragments. Bei ihrer Ausführung ist die Variable [activity] der übergeordneten Klasse initialisiert;
  • Zeile 92: Die Datenquelle [list] wird an den Adapter der Dropdown-Liste gebunden;
  • Zeilen 93–94: Der [dataAdapter] ist an die Dropdown-Liste [dropDownList] gebunden;
  • Zeile 84: Die Methode [doValider] ist mit einem Klick auf die Schaltfläche [Valider] verknüpft;

Der Zweck der Methode [doValider] besteht darin, die vom Benutzer eingegebenen Werte anzuzeigen. Ihr Code lautet wie folgt:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // list of messages to display
    List<String> messages = new ArrayList<>();
    // checkbox
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // radio buttons
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // on SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // the input field
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // the switch
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // the date
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // multi-line text
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // by the hour
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // drop-down list
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // display
    doAfficher(messages);
}
  • Zeile 4: Die eingegebenen Werte werden einer Liste von Nachrichten hinzugefügt;
  • Zeile 6: Die Methode [CheckBox].isChecked() ermittelt, ob ein Kontrollkästchen aktiviert ist oder nicht;
  • Zeile 9: Die Methode [RadioGroup].getCheckedButtonId() gibt die ID des ausgewählten Optionsfelds zurück oder -1, wenn keines ausgewählt ist;
  • Zeile 10: Der Code [activity.findViewById(id)] ruft das aktivierte Optionsfeld und damit dessen Beschriftung ab;
  • Zeile 13: Die Methode [SeekBar].getProgress() gibt den Wert eines Schiebereglers zurück;
  • Zeile 19: Die Methode [Switch].isChecked() ermittelt, ob ein Schalter auf „Ein“ (true) oder „Aus“ (false) steht;
  • Zeile 22: Die Methode [DatePicker].getYear() ruft das ausgewählte Jahr mithilfe eines [DatePicker]-Objekts ab;
  • Zeile 23: Die Methode [DatePicker].getMonth() gibt den ausgewählten Monat aus einem [DatePicker]-Objekt im Bereich [0,11] zurück;
  • Zeile 24: Die Methode [DatePicker].getDayOfMonth() gibt den ausgewählten Tag des Monats mithilfe eines [DatePicker]-Objekts im Bereich [1,31] zurück;
  • Zeile 30: Die Methode [TimePicker].getHour() gibt die ausgewählte Stunde mithilfe eines [TimePicker]-Objekts zurück;
  • Zeile 31: Die Methode [TimePicker].getMinute() gibt die ausgewählten Minuten unter Verwendung eines [TimePicker]-Objekts zurück;
  • Zeile 34: Die Methode [Spinner].getSelectedItemPosition() gibt die Position des ausgewählten Elements in einer Dropdown-Liste zurück;
  • Zeile 35: Die Methode [Spinner].getSelectedItem() gibt das ausgewählte Element in einer Dropdown-Liste zurück;

Die Methode [doAfficher], die die Liste der eingegebenen Werte anzeigt, lautet wie folgt:


    private void doAfficher(List<String> messages) {
        // we build the poster text
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // we display it
        new AlertDialog.Builder(activité).setTitle("Valeurs saisies").setMessage(texte).setNeutralButton("Fermer", null).show();
}
  • Zeile 1: Die Methode erhält eine Liste der anzuzeigenden Meldungen;
  • Zeilen 3–6: Aus diesen Meldungen wird ein [StringBuilder]-Objekt erstellt. Für die Verkettung von Zeichenfolgen ist der Typ [StringBuilder] effizienter als der Typ [String];
  • Zeile 8: Ein Dialogfeld zeigt den Text aus Zeile 3 an:

Image

1.18.5. Ausführen des Projekts

Führen Sie das Projekt aus und testen Sie die verschiedenen Eingabekomponenten.

1.19. Beispiel 18: Verwendung eines View-Musters

1.19.1. Erstellen des Projekts

Wir erstellen ein neues Projekt [Beispiel-18], indem wir das Projekt [Beispiel-13] kopieren.

1.19.2. Die Ansichtsvorlage

Wir möchten die beiden Ansichten aus dem Projekt wiederverwenden und in eine Vorlage einbinden:

  

Image

Beide Ansichten sind auf die gleiche Weise aufgebaut:

  • in [1] eine Kopfzeile;
  • in [2] eine linke Spalte, die Links enthalten könnte;
  • in [3] eine Fußzeile;
  • in [4] der Inhalt.

Dies wird durch eine Änderung der Basisansicht der Aktivität [activity_main.xml] erreicht;

Der XML-Code für die [main]-Ansicht lautet wie folgt:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:tools="http://schemas.android.com/tools"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/main_content"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent"
                                                 android:fitsSystemWindows="true"
                                                 tools:context=".activity.MainActivity">
 
  <android.support.design.widget.AppBarLayout
    android:id="@+id/appbar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingTop="@dimen/appbar_padding_top"
    android:theme="@style/AppTheme.AppBarOverlay">
 
    <android.support.v7.widget.Toolbar
      android:id="@+id/toolbar"
      android:layout_width="match_parent"
      android:layout_height="?attr/actionBarSize"
      android:background="?attr/colorPrimary"
      app:popupTheme="@style/AppTheme.PopupOverlay"
      app:layout_scrollFlags="scroll|enterAlways">
 
    </android.support.v7.widget.Toolbar>
 
  </android.support.design.widget.AppBarLayout>
 
  <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:layout_marginTop="75dp"
                android:orientation="vertical">
 
    <LinearLayout
      android:id="@+id/header"
      android:layout_width="match_parent"
      android:layout_height="100dp"
      android:layout_weight="0.1"
      android:background="@color/lavenderblushh2">
 
      <TextView
        android:id="@+id/textViewHeader"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center_horizontal"
        android:text="@string/txt_header"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@color/red"/>
    </LinearLayout>
 
    <LinearLayout
      android:layout_width="match_parent"
      android:layout_height="fill_parent"
      android:layout_weight="0.8"
      android:orientation="horizontal">
 
      <LinearLayout
        android:id="@+id/left"
        android:layout_width="100dp"
        android:layout_height="match_parent"
        android:background="@color/lightcyan2">
 
        <TextView
          android:id="@+id/txt_left"
          android:layout_width="fill_parent"
          android:layout_height="fill_parent"
          android:gravity="center_vertical|center_horizontal"
          android:text="@string/txt_left"
          android:textAppearance="?android:attr/textAppearanceLarge"
          android:textColor="@color/red"/>
      </LinearLayout>
 
      <exemples.android.architecture.MyPager
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/floral_white"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
    </LinearLayout>
 
    <LinearLayout
      android:id="@+id/bottom"
      android:layout_width="match_parent"
      android:layout_height="100dp"
      android:layout_weight="0.1"
      android:background="@color/wheat1">
 
      <TextView
        android:id="@+id/textViewBottom"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center_vertical|center_horizontal"
        android:text="@string/txt_bottom"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="@color/red"/>
    </LinearLayout>
 
  </LinearLayout>
</android.support.design.widget.CoordinatorLayout>
  • Die Kopfzeile [1] wird durch die Zeilen 38–54 generiert;
  • das linke Panel [2] wird durch die Zeilen 56–84 generiert;
  • die Fußzeile [3] wird durch die Zeilen 86–101 erstellt;
  • der Inhalt [4] wird durch die Zeilen 78–84 generiert;

Die [Haupt-]XML-Ansicht verwendet Informationen aus den Dateien [res/values/colors.xml] und [res/values/strings.xml]:

  

Die Datei [colors.xml] sieht wie folgt aus:


<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>
    <color name="lavenderblushh2">#EEE0E5</color>
    <color name="lightcyan2">#D1EEEE</color>
    <color name="wheat1">#FFE7BA</color>
 
</resources>

und die folgende [strings.xml]-Datei:


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">exemple-12</string>
    <string name="action_settings">Settings</string>
    <string name="titre_vue1">Vue n° 1</string>
    <string name="textView_nom">Quel est votre nom :</string>
    <string name="btn_Valider">Validez</string>
    <string name="btn_vue2">Vue n° 2</string>
    <string name="titre_vue2">Vue n° 2</string>
    <string name="btn_vue1">Vue n° 1</string>
    <string name="textView_bonjour">"Bonjour "</string>
    <string name="txt_header">Header</string>
    <string name="txt_left">Left</string>
    <string name="txt_bottom">Bottom</string>
 
</resources>

Erstellen Sie einen Laufzeitkontext für dieses Projekt und führen Sie es aus.

1.20. Beispiel 19: Die [ListView]-Komponente

Mit der [ListView]-Komponente können Sie eine bestimmte Ansicht für jedes Element in einer Liste wiederholen. Die wiederholte Ansicht kann beliebig komplex sein, von einer einfachen Zeichenfolge bis hin zu einer Ansicht, in der Sie Informationen für jedes Element der Liste eingeben können. Wir erstellen die folgende [ListView]:

Image

Jede Ansicht in der Liste besteht aus drei Komponenten:

  • ein [TextView] für Informationen;
  • ein [CheckBox];
  • ein anklickbares [TextView];

1.20.1. Erstellen des Projekts

Wir erstellen ein neues Projekt [Beispiel-19], indem wir das Projekt [Beispiel-18] klonen.

  

Wir werden das Projekt wie in [3] beschrieben entwickeln.

1.20.2. Die Sitzung

  

Die Sitzung speichert Daten, die zwischen der Aktivität und den Fragmenten gemeinsam genutzt werden:


package exemples.android.architecture;
 
import org.androidannotations.annotations.EBean;
 
import java.util.ArrayList;
import java.util.List;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // a list of data
  private List<Data> liste=new ArrayList<>();
 
  // getters and setters
...
}
  • Zeile 11: die von beiden Ansichten verwendete Datenliste;

Die Klasse [Data] sieht wie folgt aus:


package exemples.android.architecture;
 
public class Data {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
 
    // getters and setters
    ...
}
  • Zeile 6: Der Text, der das erste [TextView] jedes Listenelements füllt;
  • Zeile 7: Der boolesche Wert, der verwendet wird, um das [checkBox] für jedes Element in der Liste zu aktivieren oder zu deaktivieren;

1.20.3. Die [MainActivity]

Der Code für die [@AfterInject]-Methode sieht nun wie folgt aus:


  // injection session
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<Data> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new Data("Texte n° " + i, false));
    }
}
  • Zeilen 12–15: Initialisierung der Liste der in der Sitzung vorhandenen Daten;

1.20.4. Die anfängliche Ansicht [View1]

Die XML-Ansicht [view1.xml] zeigt den oben genannten Bereich [1] an. Ihr Code lautet wie folgt:


<?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:id="@+id/textView_titre"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentLeft="true"
    android:layout_alignParentTop="true"
    android:layout_marginLeft="30dp"
    android:layout_marginTop="20dp"
    android:text="@string/titre_vue1"
    android:textSize="50sp" />
 
  <Button
    android:id="@+id/button_vue2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignLeft="@+id/textView_titre"
    android:layout_below="@+id/textView_titre"
    android:layout_marginTop="50dp"
    android:text="@string/btn_vue2" />
 
  <ListView
    android:id="@+id/listView1"
    android:layout_width="600dp"
    android:layout_height="200dp"
    android:layout_alignParentLeft="true"
    android:layout_below="@+id/button_vue2"
    android:layout_marginLeft="30dp"
    android:layout_marginTop="50dp" >
  </ListView>
 
</RelativeLayout>
  • Zeilen 7–16: die [TextView]-Komponente [2];
  • Zeilen 27–35: die [ListView]-Komponente [4];
  • Zeilen 18–25: die [Button]-Komponente [3];

1.20.5. Die von der [ListView] wiederholte Ansicht

Die von der [ListView] wiederholte Ansicht ist die folgende [list_data]-Ansicht:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat" >
 
    <TextView
        android:id="@+id/txt_Libellé"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginTop="20dp"
        android:text="@string/txt_dummy" />
 
    <CheckBox
        android:id="@+id/checkBox1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/txt_Libellé"
        android:layout_marginLeft="37dp"
        android:layout_toRightOf="@+id/txt_Libellé"
        android:text="@string/txt_dummy" />
 
    <TextView
        android:id="@+id/textViewRetirer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/txt_Libellé"
        android:layout_alignBottom="@+id/txt_Libellé"
        android:layout_marginLeft="68dp"
        android:layout_toRightOf="@+id/checkBox1"
        android:text="@string/txt_retirer"
        android:textColor="@color/blue"
        android:textSize="20sp" />
 
</RelativeLayout>
  • Zeilen 8–14: die [TextView]-Komponente [1];
  • Zeilen 16–23: die [CheckBox]-Komponente [2];
  • Zeilen 25–35: die [TextView]-Komponente [3];

1.20.6. Das [Vue1Fragment]-Fragment

  

Das [Vue1Fragment]-Fragment verwaltet die [vue1]-XML-Ansicht. Sein Code lautet wie folgt:


package exemples.android.fragments;
 
import android.view.View;
import android.widget.ListView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
import java.util.List;
 
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {
 
  // the fields of the view displayed by the fragment
  @ViewById(R.id.listView1)
  protected ListView listView;
  // list adapter
  private ListAdapter adapter;
  // init done
  private boolean initDone = false;
 
  @AfterViews
  void afterViews() {
    // memory
    afterViewsDone = true;
  }
 
  @Click(R.id.button_vue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  public void doRetirer(int position) {
   ...
  }
 
  @Override
  protected void updateFragment() {
    if (!initDone) {
      // associate data with [ListView]
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // if the fragment has been (re)generated - in this case the ListView must be reconnected to its adapter
    listView.setAdapter(adapter);
    // if other fragments have changed the data source - in this case, refresh the ListView
    adapter.notifyDataSetChanged();
  }
}
  • Zeile 15: Die XML-Ansicht [view1] ist dem Fragment zugeordnet;
  • Zeilen 26–30: Die Methode [@AfterViews] führt keine Aktion aus. Es ist jedoch erforderlich, die Variable [afterViewsDone] auf „true“ zu setzen, da sie von der übergeordneten Klasse [AbstractFragment] verwendet wird;
  • Zeilen 42–53: Die Methode [updateFragment], die jedes Mal aufgerufen wird, wenn das Fragment sichtbar wird. Die Methode wurde hier so geschrieben, als ob das Fragment die Nachbarschaft des angezeigten Fragments verlassen und damit seinen Lebenszyklus zurücksetzen könnte. Dies ist hier nicht der Fall, wäre es aber, wenn die Anwendung drei Fragmente mit einer Nachbarschaft von 1 hätte;
  • Zeile 44: Der [ListView]-Adapter muss nur einmal initialisiert werden;
  • Zeile 46: Wir verknüpfen einen [ListAdapter] mit dieser [ListView]. Wir werden diese Klasse erstellen. Sie leitet sich von der Klasse [ArrayAdapter] ab, die wir bereits verwendet haben, um Daten mit einer [ListView] zu verknüpfen. Wir übergeben verschiedene Informationen an den Konstruktor von [ListAdapter]:
    • einen Verweis auf die aktuelle Aktivität,
    • die Kennung der Ansicht, die für jedes Element in der Liste instanziiert wird,
    • eine Datenquelle zum Befüllen der Liste,
    • eine Referenz auf das Fragment. Diese wird verwendet, um einen Klick auf einen [Remove]-Link in der [ListView] über die Methode [doRemove] in Zeile 38 zu verarbeiten;
  • Zeile 50: Der Adapter wird an die [ListView] gebunden. Gleichzeitig wird die Datenquelle [lists] an die [ListView] gebunden. Dieser Vorgang wird hier jedes Mal ausgeführt, wenn Ansicht Nr. 1 angezeigt wird. Tatsächlich muss dies jedoch erst erfolgen, nachdem die Methode [@AfterViews] ausgeführt wurde. Hier wird die Anweisung zu oft ausgeführt. Wir benötigen eine boolesche Variable, die uns mitteilt, dass die Methode [@AfterViews] gerade ausgeführt wurde und dass die [ListView] daher wieder mit ihrem Adapter verknüpft werden muss;
  • Zeile 52: Wir aktualisieren die [ListView]. In diesem Beispiel hat dies keinen Zweck, da nur Ansicht Nr. 1 die Datenquelle der [ListView] ändern kann. Betrachten wir einen allgemeineren Fall, in dem auch Ansicht Nr. 2 die Datenquelle der [ListView] ändern könnte. Auf solche Beispiele werden wir später in diesem Dokument stoßen. In diesem Fall muss beim Wechsel von Ansicht Nr. 2 zu Ansicht Nr. 1 die [ListView] in Ansicht Nr. 1 aktualisiert werden;

1.20.7. Der [ListAdapter] der [ListView]

Die Klasse [ListAdapter]

  • konfiguriert die Datenquelle der [ListView];
  • verwaltet die Anzeige der verschiedenen Elemente in der [ListView];
  • behandelt die Ereignisse dieser Elemente;

Der Code lautet wie folgt:


package exemples.android.fragments;
 
import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {
 
    // execution context
    private Context context;
    // the id of the layout displaying a line in the list
    private int layoutResourceId;
    // list data
    private List<Data> data;
    // the fragment that displays the [ListView]
    private Vue1Fragment fragment;
    // the adapter
    final ListAdapter adapter = this;
 
    // manufacturer
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // memorize information
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.data = data;
        this.fragment = fragment;
    }
 
    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • Zeile 5: Die Klasse [ListAdapter] erweitert die Klasse [ArrayAdapter];
  • Zeile 19: der Konstruktor;
  • Zeile 20: Vergiss nicht, den Konstruktor der übergeordneten Klasse [ArrayAdapter] mit den ersten drei Parametern aufzurufen;
  • Zeilen 22–25: Wir speichern die Informationen des Konstruktors;
  • Zeile 29: Die Methode [getView] wird wiederholt von [ListView] aufgerufen, um die Ansicht für das Element mit der Position #[position] zu generieren. Das zurückgegebene [View]-Ergebnis ist eine Referenz auf die erstellte Ansicht.

Der Code für die Methode [getView] lautet wie folgt:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // create the current ListView line
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // the text
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // the checkbox
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // the [Remove] link
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {
 
            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // manage the click on the checkbox
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // we return the line
        return row;
}
  • Zeile 2: Die Methode nimmt drei Parameter entgegen. Wir verwenden nur den ersten;
  • Zeile 4: Wir erstellen die Ansicht für Element #[position]. Dies ist die Ansicht [list_data], deren ID als zweiter Parameter an den Konstruktor übergeben wurde. Anschließend rufen wir die Referenzen auf die Komponenten der soeben instanziierten Ansicht ab;
  • Zeile 6: Wir rufen die Referenz auf [TextView] Nr. 1 ab;
  • Zeile 7: Wir weisen ihr Text aus der Datenquelle zu, die als dritter Parameter an den Konstruktor übergeben wurde;
  • Zeile 9: Wir rufen die Referenz auf [CheckBox] #2 ab;
  • Zeile 10: Wir aktivieren oder deaktivieren sie anhand eines Werts aus der Datenquelle der [ListView];
  • Zeile 12: Rufen Sie die Referenz auf [TextView] Nr. 3 ab;
  • Zeilen 13–18: Wir verarbeiten den Klick auf den Link [Remove];
  • Zeile 16: Die Methode [Vue1Fragment].doRetirer verarbeitet diesen Klick. Es ist sinnvoller, dass das Fragment, das die [ListView] anzeigt, dieses Ereignis verarbeitet. Es verfügt über einen Überblick, den die Klasse [ListAdapter] nicht hat. Die Referenz auf das Fragment [Vue1Fragment] wurde als vierter Parameter an den Klassenkonstruktor übergeben;
  • Zeilen 20–25: Verarbeitet den Klick auf das Kontrollkästchen. Die darauf ausgeführte Aktion spiegelt sich in den angezeigten Daten wider. Dies hat folgenden Grund: Die [ListView] ist eine Liste, die nur einen Teil ihrer Elemente anzeigt. Daher ist ein Listenelement manchmal ausgeblendet und manchmal angezeigt. Wenn Element #i angezeigt werden soll, wird die Methode [getView] aus Zeile 2 oben für Position #i aufgerufen. Zeile 10 berechnet den Status des Kontrollkästchens auf der Grundlage der Daten, mit denen es verknüpft ist, neu. Daher muss sie den Status des Kontrollkästchens im Laufe der Zeit speichern;

1.20.8. Ein Element aus der Liste entfernen

Das Klicken auf den Link [Remove] wird im Fragment [Vue1Fragment] durch die folgende Methode [doRetirer] verarbeitet:


  public void doRetirer(int position) {
    // remove element n° [position] from the list
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // note the scroll position to return to it
    // read
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // position of 1st element fully visible or not
    int firstPosition = listView.getFirstVisiblePosition();
    // y offset of this element relative to the top of the ListView
    // measures the height of any hidden part
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // refresh the [ListView]
    adapter.notifyDataSetChanged();
    // we position ourselves at the right spot on the ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • Zeile 1: Ermitteln der Position des angeklickten [Remove]-Links in der [ListView];
  • Zeile 3: Rufe die Datenliste ab;
  • Zeile 4: Entferne das Element an Position [position];
  • Zeile 15: Wir aktualisieren die [ListView]. Ohne dies ändert sich optisch nichts.
  • Zeilen 5–13, 17: ein recht komplexer Vorgang. Ohne ihn geschieht Folgendes:
    • Die [ListView] zeigt die Zeilen 15–18 der Datenliste an,
    • Zeile 16 wird gelöscht,
    • Zeile 15 darüber setzt sie komplett zurück, und die [ListView] zeigt dann die Zeilen 0–3 der Datenliste an;

Mit den obigen Zeilen erfolgt die Löschung und die [ListView] bleibt auf der Zeile positioniert, die auf die gelöschte Zeile folgt.

1.20.9. Die XML-Ansicht [View2]

Der XML-Code für die Ansicht lautet wie folgt:


<?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:id="@+id/textView_titre"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="30dp"
        android:layout_marginTop="20dp"
        android:text="@string/titre_vue2"
        android:textSize="50sp" />
 
    <Button
        android:id="@+id/button_vue1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textViewResultats"
        android:layout_marginTop="25dp"
        android:layout_alignLeft="@+id/textView_titre"
        android:text="@string/btn_vue1" />
 
    <TextView
        android:id="@+id/textViewResultats"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView_titre"
        android:layout_marginTop="50dp"
        android:layout_alignLeft="@+id/textView_titre"
        android:text="" />
 
</RelativeLayout>
  • Zeilen 6–15: [TextView]-Komponente Nr. 1;
  • Zeilen 26–33: [TextView]-Komponente Nr. 2;
  • Zeilen 17–24: [Button]-Komponente Nr. 3;

1.20.10. Das [Vue2Fragment]-Fragment

123

Das [Vue2Fragment]-Fragment verwaltet die [vue2]-XML-Ansicht. Sein Code lautet wie folgt:


package exemples.android.fragments;
 
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.architecture.Data;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.ViewById;
 
@EFragment(R.layout.vue2)
public class Vue2Fragment extends AbstractFragment {
 
    // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
    @AfterViews
    void initFragment(){
        // memory
        afterViewsDone=true;
    }
 
  @Click(R.id.button_vue1)
    void navigateToView1() {
        // navigate to view 1
        mainActivity.navigateToView(0);
    }
 
    @Override
    protected void updateFragment() {
        // displays list items selected in view 1
        StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
        for (Data data : mainActivity.getListe()) {
            if (data.isChecked()) {
                texte.append(String.format("(%s)", data.getTexte()));
            }
        }
        texte.append("]");
        txtResultats.setText(texte);
    }
}

Der wichtige Code befindet sich in der Methode [updateFragment] in Zeile 32:

  • Zeile 34: Wir berechnen den Text, der in [TextView] Nr. 2 angezeigt werden soll;
  • Zeilen 35–39: Wir durchlaufen die Liste der von der [ListView] angezeigten Daten. Diese ist in der Aktivität gespeichert;
  • Zeile 36: Wenn das Datenelement i markiert wurde, wird das zugehörige Label zu einem [StringBuilder] hinzugefügt;
  • Zeile 41: Die [TextView] zeigt den berechneten Text an;

1.20.11. Ausführung

Erstellen Sie eine Ausführungskonfiguration für dieses Projekt und führen Sie es aus.

1.20.12. Verbesserung

Im vorherigen Beispiel haben wir eine Datenquelle vom Typ List<Data> verwendet, wobei die [Data]-Klasse wie folgt aussah:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String texte;
    private boolean isChecked;
 
    // manufacturer
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...
 
}

In Zeile 7 haben wir eine boolesche Variable verwendet, um die Kontrollkästchen für die Elemente in der [ListView] zu verwalten. Häufig muss die [ListView] Daten anzeigen, die durch Aktivieren eines Kontrollkästchens ausgewählt werden können, obwohl das Element in der Datenquelle kein boolesches Feld besitzt, das diesem Kontrollkästchen entspricht. In diesem Fall können Sie wie folgt vorgehen:

Die [Data]-Klasse sieht dann wie folgt aus:


package exemples.android.fragments;
 
public class Data {
 
    // data
    private String texte;
 
    // manufacturer
    public Data(String texte) {
        this.texte = texte;
    }
 
    // getters and setters
...
}

Wir erstellen eine Klasse [CheckedData], die von der vorherigen abgeleitet ist:


package exemples.android.fragments;
 
public class CheckedData extends Data {
 
    // checked item
    private boolean isChecked;
 
    // manufacturer
    public CheckedData(String text, boolean isChecked) {
        // parent
        super(text);
        // local
        this.isChecked = isChecked;
    }
 
    // getters and setters
...
}

Ersetzen Sie dann einfach den Typ [Data] durch den Typ [CheckedData] im gesamten Code (MainActivity, ListAdapter, View1Fragment, View2Fragment). Zum Beispiel in [MainActivity]:


  @AfterInject
  protected void afterInject() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // create a list of data
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

Das Projekt für diese Version wird unter dem Namen [Beispiel-19B] bereitgestellt.

1.21. Beispiel-20: Verwendung eines Menüs

1.21.1. Erstellen des Projekts

Wir duplizieren das Projekt [Beispiel-19B] in das Projekt [Beispiel-20]:

3

Wir werden die Schaltflächen aus den Ansichten 1 und 2 entfernen und durch Menüoptionen [1-2] ersetzen.

1.21.2. Die XML-Definition der Menüs

  

Die Datei [res/menu/menu_vue1] definiert das Menü für Ansicht Nr. 1:


<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/menuOptions"
    app:showAsAction="ifRoom"
    android:title="@string/menuOptions">
    <menu>
      <item
        android:id="@+id/actionCacherMontrerTout"
        android:title="@string/actionCacherMontrerTout"/>
      <item
        android:id="@+id/actionCacherMontrerActions"
        android:title="@string/actionCacherMontrerActions"/>
      <item
        android:id="@+id/actionCacherMontrerActionsValider"
        android:title="@string/actionCacherMontrerActionsValider"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuActions"
    app:showAsAction="ifRoom"
    android:title="@string/menuActions">
    <menu>
      <item
        android:id="@+id/actionValider"
        android:title="@string/actionValider"/>
    </menu>
  </item>
  <item
    android:id="@+id/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationVue2"
        android:title="@string/navigationVue2"/>
    </menu>
  </item>
</menu>

Menüelemente werden durch die folgenden Informationen definiert:

  • android:id: die Kennung des Elements;
  • android:title: die Bezeichnung des Eintrags;
  • app:showsAsAction: gibt an, ob der Menüpunkt in der Aktionsleiste der Aktivität platziert werden kann. [ifRoom] gibt an, dass der Menüpunkt in der Aktionsleiste platziert werden soll, sofern dort Platz dafür ist;
  • ein Menüpunkt kann selbst ein Untermenü sein (das Tag <menu>, Zeilen 25, 29);

Die Datei [res / menu / menu_vue2] definiert das Menü für Ansicht Nr. 2:


<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/menuNavigation"
    app:showAsAction="ifRoom"
    android:title="@string/menuNavigation">
    <menu>
      <item
        android:id="@+id/navigationVue1"
        android:title="@string/navigationVue1"/>
    </menu>
  </item>
</menu>

1.21.3. Menüverwaltung in der abstrakten Klasse [AbstractFragment]

Wir werden die Menüverwaltung in die übergeordnete Klasse [AbstractFragment] der beiden Ansichten auslagern:


package exemples.android.architecture;
 
import android.app.Activity;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
 
import java.util.ArrayList;
import java.util.List;
 
public abstract class AbstractFragment extends Fragment {
 
  // data accessible to daughter classes
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;
 
  // activity
  protected IMainActivity mainActivity;
  protected Activity activity;
 
  // session
  protected Session session;
 
  // menu
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;
 
  // manufacturer
  public AbstractFragment() {
    // init
    className = getClass().getSimpleName();
    // log
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }
 
@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // retrieve # menu options if not already done
    if (!initDone) {
      // retrieve the # menu options
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // transfer the list of options to a table
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // activity
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // memory
      initDone = true;
    }
 
    // the girl fragment is asked to stand
    updateFragment();
  }
 
 
  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }
 
  // display menu options -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }
 
  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }
 
  // update girl class
  protected abstract void updateFragment();
}
  • Zeile 42: Die Protokolle zeigen, dass die Methode [onCreateOptionsMenu] jedes Mal aufgerufen wird, wenn das Fragment angezeigt wird. Sie wird sehr spät aufgerufen, nämlich erst nachdem die Methode [updateFragment] aufgerufen wurde. Dies legt nahe, dass sie zur Aktualisierung des Fragments verwendet werden könnte. Genau das werden wir hier tun (Zeile 63);
  • Zeile 42: Die Methode hat zwei Parameter:
    • [menu]: ein leeres Menü;
    • [inflater]: ein Tool, mit dem wir das Menü anhand seiner ursprünglichen Beschreibung erstellen können. Wir werden diese Option hier nicht verwenden, da wir eine AA-Annotation nutzen, die dies für uns übernimmt;
  • Zeile 44: Wir speichern das Menü. Wir werden es später benötigen;
  • Zeilen 52–53: Wir speichern die IDs aller Menüpunkte im Array aus Zeile 28;
  • Zeilen 55–57: Die Protokolle zeigen, dass beim Aufruf der Methode [onCreateOptionsMenu] die Methode [Fragment.getActivity()] die mit dem Fragment verknüpfte Aktivität zurückgibt;
  • Zeile 55: Wir speichern die Aktivität als Instanz der Android-Klasse [Activity];
  • Zeile 56: Wir speichern die Aktivität als Instanz der Schnittstelle [IMainActivity];
  • Zeile 57: Wir speichern die Sitzung;
  • Zeile 59: Wir stellen fest, dass die Klasse bereits initialisiert wurde, sodass wir dies nicht erneut tun müssen (Zeile 50);
  • Zeile 63: Wir fordern das untergeordnete Fragment auf, sich selbst zu aktualisieren. Dies ist möglich, da das Fragment sowohl sichtbar ist als auch mit seiner Ansicht und seinem Menü verknüpft ist;

Die Methode [getMenuOptions], die die IDs der Menüelemente abruft, lautet wie folgt:


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

Mit der Methode [setAllMenuOptions] können Sie alle Menüoptionen ausblenden oder einblenden;


  protected void setAllMenuOptions(boolean isVisible) {
    // update all menu options
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

Mit der Methode [setMenuOptions] können Sie bestimmte Menüoptionen ausblenden oder anzeigen;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // update certain menu options
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

Die Klasse [MenuItemState] sieht wie folgt aus:

  

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

1.21.4. Menüverwaltung im Fragment [View1Fragment]

Die Klasse [Vue1Fragment] sieht nun wie folgt aus:


@EFragment(R.layout.vue1)
@OptionsMenu(R.menu.menu_vue1)
public class Vue1Fragment extends AbstractFragment {
 
...
 
  @OptionsItem(R.id.navigationVue2)
  void navigateToView2() {
    // navigate to view 2
    mainActivity.navigateToView(1);
  }
 
  @OptionsItem(R.id.actionValider)
  void valider() {
    // a message is displayed
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }
 
  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // we change state
    actionCacherMontrerTout = !actionCacherMontrerTout;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuNavigation, actionCacherMontrerTout), new MenuItemState(R.id.menuActions, actionCacherMontrerTout)});
  }
 
  private boolean actionCacherMontrerActions = true;
  @OptionsItem(R.id.actionCacherMontrerActions)
  void actionCacherMontrerActions() {
    // we change state
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }
 
  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // we change state
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...
 
  @Override
  protected void updateFragment() {
    ....
    // update the menu
    //setMenuOptions(...)
  }
}
  • Zeile 2: Das Menü [res/menu/menu_vue1.xml] ist mit dem Fragment verknüpft;
  • Zeile 48: Wenn die Methode [updateFragment] ausgeführt wird, kann auch das Menü aktualisiert werden, um den neuen Zustand des Fragments widerzuspiegeln;
  • Zeile 7: Die Annotation [@OptionsItem(R.id.navigationVue2)] kennzeichnet die Methode, die ausgeführt werden muss, wenn die Menüoption [Navigation / Ansicht 2] angeklickt wird;
  • Zeilen 19–25: Um einen Zweig des Menüs auszublenden, blenden Sie einfach dessen Stammoption aus;
  • Zeile 24: Die Stammoptionen [menuNavigation, menuActions] werden ein- oder ausgeblendet;
  • Zeile 40: Um eine Option in einem Menüzweig anzuzeigen, müssen Sie nicht nur diese Option anzeigen, sondern auch alle Optionen, auf die man stößt, wenn man von der Endoption zurück zur Menüwurzel navigiert;

1.21.5. Menüverwaltung im Fragment [Vue2Fragment]

Ähnlicher Code findet sich im Fragment von Ansicht 2:


package exemples.android.fragments;
 
import android.widget.TextView;
import exemples.android.R;
import exemples.android.architecture.AbstractFragment;
import exemples.android.models.CheckedData;
import org.androidannotations.annotations.*;
 
@EFragment(R.layout.vue2)
@OptionsMenu(R.menu.menu_vue2)
public class Vue2Fragment extends AbstractFragment {
 
  // fields of view
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;
 
  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // navigate to view 1
    mainActivity.navigateToView(0);
  }
 
  @Override
  protected void updateFragment() {
    // displays list items selected in view 1
    StringBuilder texte = new StringBuilder("Eléments sélectionnés [");
    for (CheckedData data : session.getListe()) {
      if (data.isChecked()) {
        texte.append(String.format("(%s)", data.getTexte()));
      }
    }
    texte.append("]");
    txtResultats.setText(texte);
    // update the menu
    // setMenuOptions(...)
  }
}
  • Zeile 35: Zeige die Option [Navigation / Ansicht 1] an;
  • Zeilen 17–20: Wenn die Option [Navigation / Ansicht 1] angeklickt wird, wird die Methode [navigateToView1] aufgerufen;

1.21.6. Ausführung

Erstellen Sie einen Laufzeitkontext für dieses Projekt und führen Sie es aus.

1.22. Beispiel 21: Refactoring der Klasse [AbstractFragment]

Das vorherige Beispiel hat uns gezeigt, dass, wenn das Fragment über ein Menü verfügt, seine Methode [onCreateOptionsMenu] ein guter Ort ist, um das Fragment aufzufordern, sich selbst zu aktualisieren:

  • Sie wird genau einmal aufgerufen, wenn das Fragment angezeigt werden soll;
  • wenn sie aufgerufen wird, sind die Verknüpfungen des Fragments mit seiner Aktivität, seiner Ansicht und seinem Menü bereits hergestellt;

Um dies zu veranschaulichen, greifen wir auf Beispiel 12 zurück, das viele Fragmente enthält, deren Anordnung geändert werden kann. In diesem Beispiel hatten die Fragmente kein Menü. Wir werden ihnen ein leeres Menü zuweisen.

1.22.1. Erstellen des Projekts

Wir duplizieren das Projekt [Example-12] in das Projekt [Example-21]:

1.22.2. Das Fragment-Menü

  

Das für die Fragmente hinzugefügte Menü ist leer:


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

Was Sie hier verstehen müssen, ist, dass die Aktivität bereits über ein eigenes Menü [menu_main] verfügt:


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

Wenn eine Aktivität bereits über ein Menü verfügt, wird das mit den Fragmenten verbundene Menü zum Menü der Aktivität hinzugefügt: Sie haben somit die Optionen aus beiden Menüs. In diesem Fall ist das Menü der Fragmente leer. Sie sehen also nur das Menü der Aktivität.

1.22.3. Die Fragmente

  

Wir verwenden die abstrakte Klasse [AbstractFragment] aus dem vorherigen Beispiel (siehe Abschnitt 1.21.3). Wir verknüpfen das Menü [menu_fragment] mit den beiden Fragmenten:


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

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

In beiden Fragmenten [PlaceholderFragment] und [Vue1Fragment] entfernen wir alle Verweise auf die alte abstrakte Klasse [AbstractFragment].

1.22.4. Ausführung

Führen Sie die App aus und überprüfen Sie, ob sie funktioniert. Sehen Sie in den Protokollen nach, wann die Methode [onCreateOptionsMenu] der Klasse [AbstractFragment] ausgeführt wird. Es ist nun diese Methode, die die Methode [updateFragment] der untergeordneten Fragmente aufruft.

1.23. Beispiel 22: Speichern/Wiederherstellen des Zustands der Aktivität und der Fragmente

1.23.1. Das Problem

Hier befassen wir uns mit dem Problem der Drehung des Android-Geräts (Hochformat <--> Querformat). Um dies zu veranschaulichen, greifen wir auf das vorherige Beispiel 21 zurück:

Image

Wenn wir das Gerät drehen [1], erhalten wir die folgende neue Ansicht:

Image

Wir sehen, dass:

  • in [1] die Registerkarte [Fragment Nr. 3] verschwunden ist;
  • in [2] ist der angezeigte Text zwar der von Fragment Nr. 3, aber der Besucherzähler ist falsch;

Während dieser Rotation lauten die Protokolle wie folgt:

07-13 04:08:27.188 1677-1677/exemples.android D/MainActivity: constructor
07-13 04:08:27.189 1677-1677/exemples.android D/MainActivity: afterInject
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.190 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.194 1677-1677/exemples.android D/MainActivity: afterViews
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor PlaceholderFragment_
07-13 04:08:27.195 1677-1677/exemples.android D/AbstractFragment: constructor Vue1Fragment_
07-13 04:08:27.203 1677-1677/exemples.android D/PlaceholderFragment: afterViews 4 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.204 1677-1677/exemples.android D/PlaceholderFragment: afterViews 3 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.208 1677-1677/exemples.android D/Vue1Fragment: afterViews Vue1Fragment_ - numVisit=0
07-13 04:08:27.208 1677-1677/exemples.android D/PlaceholderFragment: afterViews 2 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.209 1677-1677/exemples.android D/PlaceholderFragment: afterViews 1 - PlaceholderFragment_ - numVisit=0, initDone=false, getActivity()==null:false
07-13 04:08:27.351 1677-1677/exemples.android D/menu: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment_: création menu en cours
07-13 04:08:27.351 1677-1677/exemples.android D/PlaceholderFragment: update 3 - PlaceholderFragment_ - numVisit=0, initDone=true, getActivity()==null:false
  • Zeile 1: Wir sehen, dass die Aktivität vollständig neu aufgebaut wird;
  • Zeilen 3–7: Das Gleiche gilt für die fünf Fragmente, die von der Aktivität verwaltet werden;
  • Zeile 21: Fragment Nr. 3 wird gleich angezeigt. Wir sehen, dass die Besucherzahl vor der Erhöhung 0 beträgt;

Das nach der Rotation erzielte Ergebnis lässt sich dann wie folgt erklären:

  • Die Klasse [MainActivity] erstellt zunächst eine Tab-Leiste mit einem einzigen Tab namens [View 1]. Dies ist der Tab, der sichtbar ist;
  • Nachdem das Gerät gedreht wurde, zeigt der Seitenmanager [mViewPager] dasselbe Fragment erneut an, in diesem Fall Fragment Nr. 3. Es ist wichtig, sich hier vor Augen zu halten, dass Tabs und Fragmente unterschiedliche Konzepte sind und unterschiedliche Lebenszyklen haben. Die Methode [updateFragment] von Fragment Nr. 3 wird ausgeführt:

  public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // increment visit no
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // modified text
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • Zeile 7: Die ID des letzten Besuchs wird aus der Sitzung gelesen. Die Sitzung wurde jedoch – wie alles andere auch – zurückgesetzt, und die Besuchs-ID wurde auf Null zurückgesetzt. Dies erklärt das in Fragment Nr. 3 angezeigte Ergebnis;

1.23.2. Methoden zum Speichern/Wiederherstellen der Aktivität und der Fragmente

1.23.2.1. Lösung 1: Manuelle Sicherung

Wenn das Gerät gedreht wird, werden zwei Methoden der Aktivität aufgerufen:


// backup / restore management ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // backup activity status
    // ....
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
     // restoring activity
    // ...
  }
  • Zeilen 2–8: Die Methode [onSaveInstanceState] wird vom System während der Drehung aufgerufen. Hier kann die Aktivität gespeichert werden. Wenn nichts unternommen wird, wird nichts gespeichert. Der Zustand der Aktivität muss im Parameter [Bundle outState] gespeichert werden, der an die Methode übergeben wird. Die Klasse [Bundle] ähnelt einem Wörterbuch. Sie verfügt über Methoden [putString, putInt, putLong, putBoolean, putChar, ...] mit zwei Parametern: void putT(String key, T value);
  • Zeilen 10–16: Die Methode [onCreate] wird aufgerufen, wenn die Aktivität erstellt wird. Wenn der Zustand der Aktivität gespeichert wurde, wird dieser gespeicherte Zustand im Parameter [Bundle savedInstanceState] an sie übergeben. Um die gespeicherten Werte abzurufen, stehen Methoden wie [getString, getInt, getLong, getBoolean, getChar, ...] mit einem einzigen Parameter zur Verfügung: T getT(String key);

Fragmente verfügen über dieselben beiden Methoden, um ihren Zustand zu speichern.

Wir werden diese Informationen nutzen, um den Zustand von Beispiel 21 zu speichern und wiederherzustellen. Dazu duplizieren wir das Projekt [Example-21] in [Example-22].

1.23.2.2. Lösung 2: Automatisches Speichern

In der Android-Dokumentation heißt es, dass man beim Drehen des Geräts die Zerstörung eines Fragments verhindern kann, indem man die Anweisung [Fragment].setRetainInstance(true) verwendet. Mehrere Artikel auf [StackOverflow] empfehlen, diese Anweisung nur für Fragmente ohne visuelle Benutzeroberfläche zu verwenden [http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean, http://stackoverflow.com/questions/12640316/further-understanding-setretaininstancetrue, http://stackoverflow.com/questions/21203948/setretaininstancetrue-in-oncreate-fragment-in-android]. Ich habe diese Aussage anhand von zwei Beispielen getestet: Beispiel-17 (Abschnitt 1.18 – eine Anwendung mit einem einzigen Fragment, die ein Formular anzeigt) und Beispiel-21 (Abschnitt 1.22 – eine Anwendung mit fünf Fragmenten). In beiden Fällen erwies sich die Anwendung dieser einzigen Anweisung auf alle Fragmente der Anwendung als unzureichend, um die beim Drehen des Geräts angezeigte Ansicht korrekt wiederherzustellen. Anstatt zwei Modelle zu erstellen, eines basierend auf [setRetainInstance(true)] und ein anderes basierend auf [setRetainInstance(false)] – was der Standardwert ist –, habe ich mich entschieden, den Empfehlungen von [StackOverflow] zu folgen und den Standardwert false für die Methode [setRetainInstance(boolean)] beizubehalten. Die Anweisung: [Fragment].setRetainInstance(true) wurde im weiteren Verlauf dieses Dokuments nie verwendet.

1.23.3. Die Backup-/Wiederherstellungsmethode für das Projekt [Beispiel-22]

Das Projekt [Beispiel-22] entwickelt sich wie folgt:

  

Es erscheinen zwei neue Klassen:

  • [PlaceHolderFragmentState], die den Zustand eines Fragments vom Typ [PlaceHolderFragment] speichert;
  • [Vue1FragmentState], das den Zustand eines Fragments vom Typ [Vue1Fragment] speichert;

Diese Klassen lauten wie folgt:


package exemples.android;
 
public class Vue1FragmentState {
  // status Vue1Fragment
  private boolean hasBeenVisited=false;
  // getters and setters
...
}
  • Zeile 5: Der boolesche Wert [hasBeenVisited] ist wahr, wenn das Fragment [Vue1Fragment] mindestens einmal aufgerufen (angezeigt) wurde. Dieses Feld wurde für das Beispiel erstellt, da das Fragment [Vue1Fragment] nichts zu speichern hat;

Die Klasse [PlaceHolderFragmentState] sieht wie folgt aus:


package exemples.android;
 
public class PlaceHolderFragmentState {
  // whether visited or not
  private boolean hasBeenVisited;
  // display text
  private String text;
 
  // getters and setters
...
}
  • Zeile 5: Wir sehen den booleschen Wert [hasBeenVisited];
  • Zeile 7: Der Text, der vom Fragment in dem Moment angezeigt wird, in dem es gespeichert werden muss. Wir haben gesehen, dass dieser Text während der Drehung verloren ging;

Der Zustand der Fragmente wird in der Sitzung gespeichert, und die Aktivität ist für das Speichern und Wiederherstellen dieser Sitzung verantwortlich. Die Sitzung verläuft wie folgt:


package exemples.android;
 
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.androidannotations.annotations.EBean;
 
@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // number of fragments visited
  private int numVisit;
  // n° fragment type [PlaceholderFragment] displayed in the second tab
  private int numFragment = -1;
  // selected tab no
  private int selectedTab = 0;
  // n° current view
  private int currentView;
 
  // fragment backups ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];
 
  // manufacturer
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // getters and setters
...
}
  • Zeile 18: der Zustand des Fragments [Vue1Fragment];
  • Zeile 19: der Zustand von Fragmenten vom Typ [PlaceHolderFragment];
  • Zeilen 22–27: Im Konstruktor der Session werden die Felder aus den Zeilen 18 und 19 initialisiert;
  • Zeilen 12–15: Es erscheinen zwei neue Felder:
    • Zeile 13: die Nummer der zuletzt ausgewählten Registerkarte;
    • Zeile 15: die Nummer des zuletzt angezeigten Fragments;

Die Aktivität speichert/stellt die Sitzung wie folgt wieder her:


  // backup / restore management ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // parent
    super.onSaveInstanceState(outState);
    // save session
    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();
      }
    }
  }
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // parent
    super.onCreate(savedInstanceState);
    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();
        }
      }
    }
}
  • Zeile 8: Die Sitzung wird als JSON-Zeichenkette gespeichert;
  • Zeile 29: Die Sitzung wird aus der JSON-Zeichenkette wiederhergestellt;

Um das Speichern und Wiederherstellen von Fragmenten zu verwalten, entwickelt sich die abstrakte Klasse [AbstractFragment] wie folgt:


// backup / restore management -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // parent
    super.setUserVisibleHint(isVisibleToUser);
    // backup?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // the fragment will be hidden - save it
      saveFragment();
      saveFragmentDone = true;
    }
    // memory
    this.isVisibleToUser = isVisibleToUser;
  }
 
  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // parent
    super.onActivityCreated(savedInstanceState);
    // log
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // the fragment must be restored
    fragmentHasToBeInitialized = true;
  }
 
 
  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // log
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // parent
    super.onSaveInstanceState(outState);
    // save fragment only if visible
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }
 
  // girls' classes
  protected abstract void updateFragment();
 
  protected abstract void saveFragment();
  • Wir beschließen, den Zustand der Fragmente an zwei Stellen in der Sitzung zu speichern:
    • Zeilen 2–14: wenn das Fragment von sichtbar auf ausgeblendet wechselt;
    • Zeilen 29–42: wenn das System anzeigt, dass das Fragment gespeichert werden soll und das Fragment sichtbar ist (Zeile 38);

Dieser Mechanismus verhindert, dass öfter als nötig gespeichert wird. Da wir den Zustand von Fragment i gespeichert haben, als es von sichtbar auf ausgeblendet wechselte, besteht keine Notwendigkeit, Fragment i erneut zu speichern, wenn Fragment j angezeigt wird und eine Drehung stattfindet. Wenn es seit dem letzten Speichern nicht erneut angezeigt wurde, hat sich sein Zustand nicht geändert. Nur der Zustand von Fragment j muss gespeichert werden. Dieser Mechanismus hat noch einen weiteren Vorteil: Nicht nur bei einer Gerätedrehung müssen wir den Zustand eines Fragments speichern. Es gibt auch den Fall der reinen Navigation zwischen Fragmenten, zum Beispiel in einem System mit Registerkarten. In solchen Fällen möchten wir ein Fragment in dem Zustand abrufen, in dem es sich bei seiner letzten Anzeige befand. Dieser Zustand kann teilweise verloren gegangen sein, wenn das Fragment zu einem bestimmten Zeitpunkt aus der Nähe der angezeigten Fragmente entfernt wurde. Das Fragment wird dann nicht vollständig rekonstruiert, wohl aber die ihm zugeordnete Ansicht. Die Speicherung, die durchgeführt wurde, als das Fragment ausgeblendet wurde, wird verwendet, um den letzten Zustand dieser Ansicht wiederherzustellen;

  • Zeilen 10, 40: Um zwei aufeinanderfolgende Speichervorgänge zu vermeiden, wird die boolesche Variable [saveFragmentDone] verwendet, um anzuzeigen, dass ein Speichervorgang durchgeführt wurde;
  • Zeilen 9, 39: Das untergeordnete Fragment wird aufgefordert, seinen Zustand zu speichern. Die Methode [saveFragment] ist abstrakt (Zeile 47). Es ist daher Aufgabe der untergeordneten Klassen, sie zu implementieren;
  • Zeilen 16–26: Die Methode [onActivityCreated] wird verwendet, um die boolesche Variable [fragmentHasToBeInitialized] auf „true“ zu setzen. Dies geschieht, weil das untergeordnete Fragment wissen muss, dass es den Zustand des Fragments vollständig neu initialisieren muss, ausgehend von einem Zustand, den es in der Sitzung vorfindet;

Ebenfalls in der Klasse [AbstractFragment] ändert sich die Methode [onCreateOptionsMenu] wie folgt:


// fragment update
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // memory
    this.menu = menu;
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // the girl fragment is asked to update itself
    updateFragment();
    // backup to do
    saveFragmentDone = false;
  }
  • Zeile 14: Wir haben gesehen, dass die boolesche Variable [saveFragmentDone] bei einer Speicherung auf „true“ gesetzt wurde. Irgendwann muss es auf „false“ zurückgesetzt werden. Wenn die Methode [updateFragment] (Zeile 12) des untergeordneten Fragments ausgeführt wird, wird es sichtbar. Ein Fragment muss jedoch gespeichert werden, wenn es sichtbar ist, genauer gesagt in dem Moment, in dem es vom sichtbaren in den ausgeblendeten Zustand wechselt. Wir setzen dann die boolesche Variable [saveFragmentDone] auf „false“, damit die Speicherung stattfinden kann;

1.23.4. Speichern des Fragments [Vue1Fragment]

Fragmente werden in der Methode [saveFragment] gespeichert, die von der übergeordneten Klasse [AbstractFragment] aufgerufen wird:


// save fragment status
  @Override
  public void saveFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // in-session saving of fragment status
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment 2 state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • Zeilen 9–11: Speichern des Fragmentzustands in der Sitzung. Wenn die Methode [saveFragment] aufgerufen wird, ist das Fragment sichtbar. Daher muss der boolesche Wert [hasBeenVisited] auf true gesetzt werden (Zeile 10);

1.23.5. Speichern des Fragments [PlaceHolderFragment]

Fragmente werden in der Methode [saveFragment] gespeichert, die von der übergeordneten Klasse [AbstractFragment] aufgerufen wird:


  @Override
  public void saveFragment() {
    // save fragment state in session
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // log
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • Zeilen 4–7: Speichern des Fragmentzustands in der Sitzung;
  • Zeile 5: Der aktuell von [TextView] textViewInfo angezeigte Text wird gespeichert;
  • Zeile 6: Der boolesche Wert [hasBeenVisited] des Fragments wird auf true gesetzt;
  • Zeile 7: Der Zustand des Fragments wird in der Session im Array [placeHolderFragmentStates] gespeichert. Der Index des zu initialisierenden Elements ist die Abschnittsnummer des Fragments minus eins;

1.23.6. Wiederherstellen des [Vue1Fragment]-Fragments

Fragmente werden in der Methode [updateFragment] wiederhergestellt:


@Override
  protected void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // restoration?
    if (fragmentHasToBeInitialized) {
      // restoration condition
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // log
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // navigation?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // increment visit no
      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();
    }
    // change n° current view
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • Zeilen 8–12: Wiederherstellung des Fragmentzustands. Die boolesche Variable [fragmentHasToBeInitialized] wurde von der übergeordneten Klasse [AbstractFragment] initialisiert. Wenn sie wahr ist, wurde das Fragment gerade rekonstruiert und muss neu initialisiert werden. Hier geschieht dies. In diesem konkreten Beispiel gibt es nichts zu tun. Wir haben lediglich gezeigt, dass wir den Wert des Booleschen Werts [hasBeenVisited] aus dem gespeicherten Zustand des Fragments abrufen können (Zeile 10);
  • Zeile 11: Vergiss nicht, [fragmentHasToBeInitialized] wieder auf „false“ zu setzen, damit wir später, wenn wir zu diesem Fragment zurückkehren, ohne dass sich das Gerät gedreht hat, keine unnötige Initialisierung des Fragments durchführen;
  • Zeilen 18–26: Erhöhen Sie den Besuchszähler. Hier gibt es eine Herausforderung: Beim Wiederherstellen des Fragments möchten wir diesen Zähler nicht erhöhen. Wir müssen hier unterscheiden zwischen:
    • einfacher Navigation, die den Benutzer zurück zur Registerkarte [Ansicht 1] bringt;
    • einer Wiederherstellung, wenn der Benutzer sein Gerät dreht, während die Registerkarte [View 1] angezeigt wird;

Wir unterscheiden diese beiden Fälle anhand der in der Sitzung gespeicherten Ansichtsnummer. Diese Nummer ist die der zuletzt angezeigten Ansicht (Zeile 28).

  • Zeile 18: Es erfolgt eine Navigation statt einer Aktualisierung, wenn die Nummer der letzten Ansicht von der der aktuellen Ansicht abweicht;
  • Zeilen 21–25: Erhöhen des Besuchszählers und Anzeigen desselben;

1.23.7. Wiederherstellen des [PlaceHolderFragment]

Fragmente werden in der Methode [updateFragment] wiederhergestellt:


  // data
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...
 
public void updateFragment() {
    // log
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // which fragment is it?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // does the fragment need to be initialized?
    if (fragmentHasToBeInitialized) {
      // initial text
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // navigation?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // increment visit no
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // modified text
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // we are dealing with a restoration
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // text display
    textViewInfo.setText(newText);
    // current view
    session.setCurrentView(numView);
}
  • Zeilen 15–16: Ermittlung der Nummer der zu aktualisierenden Ansicht;
  • Zeilen 18–22: Fall, in dem sich das Fragment nach einer Änderung der Geräteausrichtung in einem Speicher-/Wiederherstellungszyklus befindet. Es muss hier wiederhergestellt werden. Dies beinhaltet in der Regel die Wiederherstellung bestimmter Felder des Fragments;
  • Zeile 20: Das Feld [text] in Zeile 2 muss den vom Fragment angezeigten Anfangstext enthalten: [Hello world from section i]. Dieser muss hier neu generiert werden;
  • Zeile 21: Beachten Sie, dass das Fragment initialisiert wurde;
  • Zeilen 24–36: Wie beim zuvor beschriebenen [Vue1Fragment]-Fragment darf der Besuchszähler während einer Wiederherstellung nicht erhöht werden. Wie zuvor müssen wir zwischen Navigation und Wiederherstellung unterscheiden;
  • Zeilen 32–36: Wiederherstellungsfall;
  • Zeile 34: Der Zustand des Fragments vor der Drehung des Geräts wird aus der Sitzung abgerufen;
  • Zeile 35: Der zu diesem Zeitpunkt angezeigte Text wird abgerufen;
  • Zeile 38: Dieser Text wird erneut angezeigt;
  • Zeile 40: Die Nummer der neu angezeigten Ansicht wird in der Sitzung vermerkt;

1.23.8. Tab-Verwaltung

In den vorangegangenen Abschnitten wurde die Tab-Verwaltung nicht behandelt. In Beispiel 21 stießen wir jedoch beim Drehen des Geräts auf ein Problem: Nur der erste Tab [Ansicht 1] blieb erhalten. Der zweite Tab ging verloren.

Wir lösen dieses Problem in der Klasse [MainActivity] wie folgt:


@AfterViews
  protected void afterViews() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // toolbar
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
 
 ...
 
    // 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);
    }
 
    // which tab to select?
    tabLayout.getTabAt(session.getSelectedTab()).select();
 
...
 
  }
  • Zeilen 14–16: Erstellung der ersten Registerkarte;
  • Zeilen 18–23: Erstellung der zweiten Registerkarte. Um festzustellen, ob sie erstellt werden soll, überprüfen wir die Sitzung auf die Nummer des in Registerkarte 2 angezeigten Fragments. Ist diese Nummer nicht -1 (ihr Anfangswert), wird die zweite Registerkarte erstellt. Zu diesem Zeitpunkt haben wir zwei Registerkarten, wobei die erste standardmäßig ausgewählt ist;
  • Zeile 26: Wir rufen aus der Sitzung die Nummer des Tabs ab, der vor dem Speichern/Wiederherstellen ausgewählt war, und wählen ihn erneut aus. Wenn das Feld [selectedTab] vom Code noch nicht initialisiert wurde, wird sein Anfangswert 0 verwendet;