Skip to content

1. Einführung in die Android-Programmierung

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

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

1.1. Introduction

1.1.1. Inhalt

Dieses Dokument ist eine Überarbeitung mehrerer bestehender Dokumente:

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

und führt folgende Neuerungen ein:

  • Dokument 1 stellte eine Architektur namens AVAT (Aktivitäten-Ansichten-Aktionen-Aufgaben) vor, um die asynchrone Programmierung in einer Android-Anwendung zu vereinfachen. In diesem Dokument wird die Standardbibliothek RxJava zur Verwaltung asynchroner Aktionen verwendet;
  • Dokument 2 verwendete IDE Eclipse mit einem Android-Plugin. Dieses Dokument verwendet Android Studio;
  • Dokument 3 wird unverändert übernommen;
  • Dokument 4 verwendete die Bibliothek [Android Annotations] (AA) mit der IntelliJ Community Edition IDE IDEA. Dieses Dokument übernimmt das gesamte Dokument 4 mit folgenden Unterschieden:
    • IDE ist nun Android Studio;
    • das Build-System ist Gradle für alle Client- oder Server-Projekte (in Dokument 4 wurde manchmal Maven verwendet);
    • die asynchrone Programmierung erfolgt mit der Bibliothek RxJava (in Dokument 4 wurde die Bibliothek AA verwendet);
  • Dieses Dokument befasst sich mit Bereichen, die in den vorherigen Dokumenten nicht oder nur am Rande behandelt wurden:
    • das Konzept der Fragment-Nachbarschaft;
    • das Speichern und Wiederherstellen der Aktivität und ihrer Fragmente;
    • den Lebenszyklus von Fragmenten;

Schließlich wird das Grundgerüst eines Android-Clients vorgestellt, der mit einem Webdienst / jSON kommuniziert, in dem eine Vielzahl von Elementen zusammengefasst sind, die in dieser Art von Clients regelmäßig vorkommen. Dieses Grundgerüst wird in allen Beispielen ab Kapitel 2 verwendet. Dies ist der wirklich innovative Teil des Dokuments.

Die folgenden Beispiele werden vorgestellt:

Exemple
Art
1
Import eines bestehenden Android-Projekts
2
Ein einfaches Android-Projekt
3
Ein einfaches [Android Annotations]-Projekt
4
Ansichten und Ereignisse
5
Navigation zwischen Ansichten
6
Navigation über Registerkarten
7
Verwendung der Bibliothek [Android Annotations] mit Gradle
8 à 12
Verwaltung von Fragmenten in einer Android-Anwendung
13
Überarbeitete Navigation zwischen Ansichten
14
Zweischichtige Architektur
15
Client-Server-Architektur
16
Asynchronität mit RxJava verwalten
17, 17B
Komponenten zur Dateneingabe
18
Verwendung einer Ansichtsvorlage
19
Die Komponente ListView
20
Verwendung eines Menüs
21
Verwendung einer übergeordneten Klasse für Fragmente
22, 22B
Speichern und Wiederherstellen des Status der Aktivität und der Fragmente
23
Wetter-Client
Chap 2
Grundgerüst eines Android-Clients, der mit einem Webdienst kommuniziert / jSON. Darin werden zahlreiche Elemente zusammengefasst, die in dieser Art von Android-Clients regelmäßig vorkommen.
Chap 3
Terminverwaltung einer Arztpraxis
Chap 4
Anwendungsübung – Verwaltung einer einfachen Gehaltsabrechnung
Chap 5
Anwendungsübung – Steuerung von Arduino-Karten

Dieses Dokument wurde im letzten Studienjahr der Ingenieursschule IstiA der Universität Angers [istia.univ-angers.fr] verwendet. Dies erklärt den manchmal etwas eigenwilligen Ton des Textes. Bei den beiden Anwendungsübungen handelt es sich um Texte aus TP, für die nur die Grundzüge der Lösung angegeben werden. Diese muss vom Leser selbst erarbeitet werden.

Der Quellcode der Beispiele ist unter |ICI| verfügbar. Um diese Beispiele auszuführen, müssen Sie die in Abschnitt 6.12 beschriebene Vorgehensweise befolgen.

Dieses Dokument dient als Einführung in die Android-Programmierung. Es erhebt keinen Anspruch auf Vollständigkeit und richtet sich in erster Linie an Anfänger.

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

1.1.2. Voraussetzungen

Voraussetzung für die optimale Nutzung dieses Dokuments ist eine gute Beherrschung der Programmiersprache Java.

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:

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

1.2. Beispiel-01: Importieren eines Android-Beispiels

1.2.1. Erstellung des Projekts

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

  

Anschließend erstellen wir mit Android Studio ein Projekt. Zunächst importieren wir eines der mitgelieferten Beispiele: IDE oder [1-5]:

 

Image

Beim Import des Projekts können Fehler auftreten, die darauf zurückzuführen sind, dass die bei der Erstellung des Projekts verwendete Umgebung nicht mit der hier für die Ausführung verwendeten übereinstimmt. Dies ist eine gute Gelegenheit, um zu sehen, wie sich solche Fehler beheben lassen. In diesem Fall haben wir folgenden Fehler:

Das importierte Projekt ist durch die folgenden Dateien konfiguriert: [build.gradle] und [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"
}

// Der Beispiel-Build verwendet mehrere Verzeichnisse, um
// den Standardcode und den gemeinsamen Code vom
// dem Hauptbeispielcode zu trennen.
List<String> dirs = [
    'main',     // Hauptbeispielcode; hier finden Sie die interessanten Inhalte.
    'common',   // Komponenten, die von mehreren Beispielen wiederverwendet werden
    'template'] // Standardcode, der durch den Beispielvorlagenprozess generiert wird

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 haben keine Version SDK 21. Wir ersetzen diese Version durch die uns vorliegende Version 23.

In der Datei [build.gradle] macht Android Studio folgende Vorschläge:

 

Um die Vorschläge zu übernehmen, wählen wir bei dem Vorschlag „[alt-entrée]“ aus:

 

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

 

Dieser Fehler rührt von einer Nichtübereinstimmung zwischen der von der Projektdatei [build.gradle] geforderten Gradle-Version (2.10, Zeile 6 unten) her:


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

und der in der Datei [<projet>/gradle/wrapper/gradle-wrapper.properties] angegebenen Version:


#Mi, 10. Apr. 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

In Zeile 6 oben muss die Zahl 2,8 durch 2,10 ersetzt werden.

Um auf die Datei „[<projet>/gradle/wrapper/gradle-wrapper.properties]“ zugreifen zu können, muss die Projektperspektive des Projekts verwendet werden:

Nachdem dies korrigiert wurde, kann man die Anwendung [1] kompilieren, den Genymotion-Emulator [2] starten und anschließend das Projekt [3] ausführen:

 

Image

Beenden wir die Anwendung:

  

Jetzt können wir das Projekt schließen. Wir werden ein neues erstellen.

  

1.2.2. Einige Anmerkungen zu IDE

1.2.2.1. Die Ansichten

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

  • die Perspektive [Android] [1]:
  • die Perspektive [Project] [4];
 
  

Meistens werden wir mit der Perspektive [Android] arbeiten. Wenn wir ein Projekt in ein anderes duplizieren, benötigen wir die Perspektive [Project].

1.2.2.2. Ausführungssteuerung

Es gibt mehrere Möglichkeiten, ein Projekt AS auszuführen, anzuhalten oder erneut auszuführen. Da wären zunächst einmal die Schaltflächen in der Symbolleiste:

Die Schaltfläche [Rerun] [3] stoppt die Ausführung des Projekts [2] und startet es anschließend erneut [1].

1.2.2.3. Cache-Verwaltung

Android Studio verwaltet einen Cache der von ihm verwalteten Projekte, um die Reaktionsgeschwindigkeit von IDE so hoch wie möglich zu halten. Bei der Version Android 2.1 (Mai 2016) wurden in diesem Cache häufig die gerade vorgenommenen Codeänderungen nicht berücksichtigt. In diesem Fall muss dieser Cache geleert werden:

Unter Android 2.1 (Mai 2016) musste der oben beschriebene Vorgang mehrfach durchgeführt werden, und manchmal reichte dies nicht aus, um den festgestellten Fehler zu beheben. Die Lösung bestand darin, die Technologie [Instant Run] zu deaktivieren:

  • in [3-4], wobei alles deaktiviert wurde;

Im weiteren Verlauf wurde mit dieser Cache-Konfiguration gearbeitet, und es traten keine Probleme auf.

1.2.2.4. Protokollverwaltung

Während der Ausführung eines Projekts werden 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 die Protokolle gelöscht werden. Diese Schaltfläche ist nützlich, wenn man die Protokolle einer bestimmten Aktion anzeigen möchte:

  • Man löscht die Protokolle;
  • auf dem Android-Gerät führt man die Aktion aus, deren Protokolle man anzeigen möchte;
  • die daraufhin angezeigten Protokolle beziehen sich auf die durchgeführte Aktion;

Es gibt mehrere Protokollstufen ([4]). Standardmäßig ist der Modus [Verbose] ausgewählt. Das bedeutet, dass die Protokolle aller Stufen angezeigt werden. Mit [4] kann man eine bestimmte Stufe auswählen.

Die Protokolle sind sehr nützlich, um zu erfahren, zu welchen Zeitpunkten während der Ausführung eines Projekts bestimmte Methoden aufgerufen werden. Wir werden häufig darauf zurückgreifen. Betrachten wir den Code der Klasse [MainActivity] aus dem Projekt [Exemple-01]:

 

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

Die oben genannten Methoden [onCreate, ligne 14] und [onCreateOptionsMenu, ligne 26] sind Methoden der übergeordneten Klasse [Activity] (Zeile 9). Sie werden zu verschiedenen Zeitpunkten im Lebenszyklus der Anwendung aufgerufen. Manchmal werden sie mehrmals ausgeführt. Selbst beim Lesen der Dokumentation ist es manchmal schwer zu sagen, ob eine solche Lebenszyklusmethode vor oder nach einer Methode ausgeführt wird, die man selbst geschrieben hat. Diese Information ist jedoch oft wichtig zu wissen. Man kann dann Logs wie unten gezeigt einfü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. Diese Klasse ermöglicht es, Logs auf der Android-Konsole [logcat] auszugeben. Die Logs werden in verschiedene Stufen unterteilt (Info, Warning, Debug, Verbose, Error). [Log.d] zeigt Protokolle der Stufe [debug] an. Sein erstes Argument ist die Quelle der Protokollmeldung. Tatsächlich können verschiedene Quellen Meldungen an die Protokollkonsole senden. Um diese unterscheiden zu können, wird dieses erste Argument verwendet. Das zweite Argument ist die Meldung, die an die Protokollkonsole geschrieben werden soll;

Wenn wir das Projekt [Exemple-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

So lässt sich feststellen, dass die Methode [onCreate], die die Android-Aktivität erstellt, vor der Methode [onCreateOptionsMenu] ausgeführt wird, die das Anwendungsmenü erstellt.

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

  

wird der folgende Eintrag in die Log-Konsole geschrieben:


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

Im weiteren Verlauf werden wir häufig Log-Anweisungen in den Android-Code einfügen. Meistens werden wir diese nicht auskommentieren. Sie dienen lediglich dazu, den Leser dazu anzuregen, einen Blick in die Log-Konsole zu werfen, um den Lebenszyklus einer Android-App nach und nach zu verstehen.

1.2.2.5. Verwaltung des Emulators [Genymotion]

Manchmal stürzt der Genymotion-Emulator ab und lässt sich nicht mehr neu starten. Das liegt daran, dass VirtualBox-Prozesse im Task-Manager weiterhin aktiv sind. Öffnen Sie diesen [Ctrl-Alt-Supp] und beenden Sie alle vorhandenen VirtualBox-Prozesse:

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

1.2.2.6. Verwaltung der erstellten Binärdatei „APK“

Die Kompilierung des Projekts erzeugt eine Binärdatei mit der Endung .apk:

Es gibt zwei Versionen: die mit der Bezeichnung [debug] und die mit der Bezeichnung [debug-unaligned]. Es muss die erste verwendet werden, da es sich bei der anderen um eine Zwischenversion handelt. Die mit [4] erstellte .pak-Datei kann direkt auf einen Emulator oder ein Android-Gerät übertragen werden. Um sie auf einen Emulator zu übertragen, reicht es aus, sie mit der Maus per Drag & Drop auf den Emulator zu ziehen.

1.3. Beispiel-02: Ein einfaches Android-Projekt

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

 

In [13] wird die Anwendung ausgeführt. Daraufhin wird die Ansicht [14] auf dem Genymotion-Emulator angezeigt.

1.3.1. Gradle-Konfiguration

Das erstellte Projekt wird durch die folgende Datei „[build.gradle]“ 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 vom IDE anhand der Elemente seiner Konfiguration generiert. Es handelt sich um eine minimale Datei, die wir nach und nach erweitern werden.

  • Zeilen 3–12: die Merkmale der Android-Anwendung;
  • Zeilen 22–25: ihre Abhängigkeiten. Vor allem hier werden wir entsprechend den untersuchten Beispielen Änderungen vornehmen;

1.3.2. Das Manifest der Anwendung

  

Die Datei [AndroidManifest.xml] [1] legt die Eigenschaften der Android-Anwendungsbinärdatei fest. 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 Paket des Android-Projekts;
  • Zeile 10: der Name der Aktivität;

Diese beiden Angaben stammen aus den Eingaben, die bei der Erstellung des Projekts vorgenommen wurden:

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

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 Symbol [2] der Anwendung. Es kann geändert werden;
  • Zeile 7: Der Name der Anwendung. Er 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 Name der Anwendung stammt aus der Eingabe, die beim Erstellen des Projekts [4] vorgenommen wurde:

 
  • Zeile 10: ein Aktivitäts-Tag. Eine Android-Anwendung kann mehrere Aktivitäten haben;
  • Zeile 12: Die Aktivität wird als Hauptaktivität gekennzeichnet;
  • Zeile 13: und sie muss in der Liste der Anwendungen 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] lautet wie folgt:


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 vor der Anzeige der der Aktivität zugeordneten Ansicht;
  • Zeile 10: Die Methode [onCreate] der übergeordneten Klasse wird aufgerufen. Dies muss immer erfolgen;
  • Zeile 11: Die Datei [activity_main.xml] [2] ist die der Aktivität zugeordnete Ansicht. Die Definition XML 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 Formatierungsmanager. Als Standard wurde der Typ [RelativeLayout] ausgewählt. In diesem Containertyp werden die Komponenten relativ zueinander positioniert (rechts von, links von, unter, über);
  • Zeilen m–p: eine Komponente vom Typ [TextView], die zur Anzeige von Text dient;
  • Zeile n: der angezeigte Text. Es wird davon abgeraten, Text fest in die Ansichten einzufügen. Es ist besser, diese Texte in die Datei [res/values/strings.xml] [3] zu verschieben:

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

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

 

Die Klasse [R] lautet wie folgt:


...............
    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 der Ansicht [res / layout / activity_main.xml];
  • Zeile 7: Das Attribut [R.string.app_name] ist die Kennung der Zeichenfolge [app_name] in der Datei [res / values / string.xml]:
  • Zeile 19: Das Attribut [R.mipmap.ic_launcher] ist die Kennung des Bildes [res / mipmap / ic_launcher];

Man sollte also bedenken, dass man, wenn man im Code auf [R.layout.activity_main] verweist, auf ein Attribut der Klasse [R] verweist. IDE hilft uns dabei, die verschiedenen Elemente dieser Klasse zu erkennen:

1.3.4. Ausführung der Anwendung

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

  • in [1] wählen wir [Edit Configurations];
  • Das Projekt wurde mit der Konfiguration [app] angelegt, die wir löschen werden. Erstellen Sie [2] neu;
  • in [3] eine neue Ausführungskonfiguration erstellen;
  
  • Wählen Sie unter [4] die Konfiguration [Android Application] aus;

Image

  • in [5], in der Dropdown-Liste das Modul [app] auswählen;
  • bei [6-8] die vorgeschlagenen Standardwerte beibehalten;
  • bei [7] ist die Standardaktivität diejenige, die in der Datei [AndroidManifest.xml] (Zeile 1 unten) definiert ist:

    <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
</activity>
  • In [8] wählen Sie [Show Chooser Dialog] aus, um das Ausführungsgerät der Anwendung (Emulator, Tablet) auszuwählen;
  • In [9] geben Sie an, dass diese Auswahl gespeichert werden soll;
  • Bestätigen Sie die Konfiguration;
  
  • Starten Sie in [11] den Emulator-Manager [Genymotion] (siehe Abschnitt 6.9);
  • in [12], wählen Sie einen Tablet-Emulator aus und starten Sie [13];
  • in [14], führen Sie die Ausführungskonfiguration [app] aus;
  • Bei [15] wird das Formular zur Auswahl des Ausführungsgeräts angezeigt. Hier steht nur eines zur Verfügung: der zuvor gestartete Emulator [Genymotion];

Der Software-Emulator zeigt nach kurzer Zeit die folgende Ansicht an:

Image

1.3.5. Der Lebenszyklus einer Aktivität

Werfen wir noch einmal einen Blick auf den Code der Aktivität [MainActivity]:


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 gehört zu den Methoden, die im Laufe des Lebenszyklus einer Aktivität aufgerufen werden können. Die Android-Dokumentation listet diese auf:

 
  • [1]: Die Methode [onCreate] wird beim Start der Aktivität aufgerufen. In dieser Methode wird die Aktivität einer Ansicht zugeordnet und es werden die Referenzen ihrer Komponenten abgerufen;
  • [2-3]: Anschließend werden die Methoden [onStart, onResume] aufgerufen. Man sieht, dass die Methode [onResume] die letzte Methode ist, die ausgeführt wird, bevor der Zustand [4] der aktuell ausgeführten Aktivität erreicht wird;

1.4. Beispiel-03: Überarbeitung des Projekts [Exemple-02] mit der Bibliothek [Android Annotations]

Wir werden nun die Bibliothek [Android Annotations] einführen, die die Entwicklung von Android-Anwendungen erleichtert. Dazu duplizieren wir das Beispiel [Exemple-02] in [Exemple-03] gemäß der Vorgehensweise in [1-16].

  • in [1]. Wählen Sie die Perspektive [Project], um das gesamte Android-Projekt anzuzeigen;

Hinweis: Zwischen [14] und [15] wurde von einer Perspektive [Android] zu einer Perspektive [Project] gewechselt (siehe Abschnitt 1.2.2.1).

Anschließend bearbeiten 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 die gesamte Konfiguration von [Exemple-02] übernommen hat:

 

Bei [19] erhalten wir dasselbe Ergebnis wie bei [Exemple-02], jedoch mit einem neuen Namen.

Wir werden nun die Bibliothek [Android Annotations] einführen, die wir der Einfachheit 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, die ebenso wie die vom Entwickler geschriebenen Klassen an der Kompilierung des Moduls beteiligt sind. Damit ergibt sich folgende Kompilierungskette:

Zunächst fügen wir in die Datei [build.gradle] die Abhängigkeiten zum Annotations-Compiler AA (der oben genannte Prozessor) ein:


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 Bibliothek AA besteht;

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

  • Verarbeitung der Android-Annotationen, wodurch neue Klassen entstehen;
  • Kompilierung aller Klassen des Projekts;

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    // Seit Android-Gradle-Plugin 0.11 muss android-apt >= 1.3 verwendet werden
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'
  • Zeile 8: Version des Plugins [android-apt], nach der im zentralen Maven-Repository (Zeile 3) gesucht wird;
  • Zeile 13: Aktivierung dieses Plugins;

Überprüfen Sie an dieser Stelle, ob die Ausführungskonfiguration [app] weiterhin funktioniert.

Wir fügen nun eine erste Annotation der Bibliothek AA in die Klasse [MainActivity] ein:

  

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

Diesen Code haben wir 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 Annotation AA (Zeile 3). Ihr Parameter ist die mit der Aktivität verknüpfte Ansicht;

Diese Annotation erzeugt eine Klasse [MainActivity_], die von der Klasse [MainActivity] abgeleitet ist, und genau diese Klasse stellt die eigentliche Aktivität dar. Wir müssen daher das Manifest des Projekts [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;

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

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

Die generierte Klasse [MainActivity_] lautet wie folgt:


//
// DO NOT EDIT THIS FILE.
// Erstellt mit AndroidAnnotations 4.0.0.
// 
// Sie können ein größeres Werk erstellen, das diese Datei enthält, und dieses Werk unter Bedingungen Ihrer Wahl verbreiten.
//


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 zu verbergen versuchen. Es kann jedoch manchmal sinnvoll sein, ihn zu untersuchen, wenn man verstehen möchte, wie die verwendeten Annotationen „übersetzt“ werden.

Nun kann die Konfiguration [app] erneut ausgeführt werden. Man erhält das gleiche Ergebnis wie zuvor. Wir werden nun von diesem Projekt ausgehen, das wir duplizieren werden, um die wichtigen Konzepte der Android-Programmierung vorzustellen.

1.5. Beispiel-04: Ansichten und Ereignisse

1.5.1. Erstellung des Projekts

Wir folgen der in Abschnitt 1.4 beschriebenen Vorgehensweise, um [Exemple-02] in [Exemple-03] zu duplizieren:

Wir:

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

1.5.2. Eine Ansicht erstellen

Wir werden nun mit dem grafischen Editor die vom Projekt [Exemple-04] angezeigte Ansicht bearbeiten:

  • in [1-4] erstellen Sie eine neue Ansicht XML;
  • in [5] benennen Sie die Ansicht;
  • Geben Sie bei [6] das Stamm-Tag der Ansicht an. Hier wählen wir einen Container „[RelativeLayout]“. In diesem Komponenten-Container werden die Komponenten relativ zueinander angeordnet: „rechts von“, „links von“, „unterhalb von“, „oberhalb von“;
  

Die generierte Datei [vue1.xml] (aus [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 Container [RelativeLayout], der die gesamte Breite des Tablets (Zeile 3) und dessen gesamte Höhe (Zeile 4) einnimmt;
  • Wählen Sie in [1] die Registerkarte [Design] in der angezeigten Ansicht [vue1.xml] aus;
  • Wechseln Sie bei [2-4] in den Tablet-Modus;
  • Wechseln Sie in [5] auf den Maßstab 1 des Tablets;
  • in [6] wählen Sie den Querformat-Modus für das Tablet;
  • Der Screenshot [7] fasst die getroffenen Einstellungen zusammen.
  • In [1] nehmen Sie ein [Large Text] und ziehen es auf die Ansicht [2];
  • in [3] auf die Komponente doppelklicken;
  • in [4] den angezeigten Text ändern. Anstatt ihn fest in die Ansicht XML einzufügen, lagern wir ihn in die Datei [res / values / string.xml] aus
  • in [5], fügen wir einen neuen Wert in die Datei [strings.xml] ein;
  • in [8] wird der Zeichenfolge eine Kennung zugewiesen;
  • In [9] wird der Wert der Zeichenfolge angegeben;
  • in [10] die neue Ansicht nach der Validierung des vorherigen Schritts;
  • Nach einem Doppelklick auf die Komponente wird deren Kennung geändert: [11];
  • in [12]; in den Eigenschaften der Komponente wird die Schriftgröße von [50sp]
  • 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 in der grafischen Benutzeroberfläche vorgenommenen Änderungen befinden sich in den Zeilen 10, 11 und 14. Die übrigen Attribute von [TextView] sind Standardwerte oder ergeben sich aus der Positionierung der Komponente in der Ansicht;
  • Zeilen 7–8: Die Größe der Komponente entspricht in Höhe und Breite der des darin enthaltenen Textes (wrap_content);
  • Zeile 13: Der obere Rand der Komponente ist am oberen Rand der Ansicht (Zeile 13) ausgerichtet, 50 Pixel darunter (Zeile 13);
  • Zeile 12: Die linke Seite der Komponente ist am linken Rand der Ansicht (Zeile 13) ausgerichtet und befindet sich 213 Pixel weiter rechts (Zeile 12);

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

Erstellen Sie auf die gleiche Weise die folgende Ansicht [1]:

 

Die Komponenten sind wie folgt:

Id
Type
Rôle
1
textViewTitreVue1
TextView
Titre de la vue
2
textView1
TextView
une question
3
editTextNom
EditText
saisie d'un nom
4
buttonValider
Button
pour valider la saisie
5
buttonVue2
Button
pour passer à la vue n° 2

Die Anordnung der Komponenten zueinander kann sich als frustrierend erweisen, da der grafische Editor manchmal überraschend reagiert. Es kann daher vorteilhaft sein, die Eigenschaften der Komponenten zu nutzen:

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

  • Bei [1] ist die Oberkante (top) der Komponente in einem Abstand von 50 Pixeln zur Unterkante (bottom) der Komponente [textViewTitreVue1] ausgerichtet ([3] (top));
  • In [2] wird die linke Kante (left) der Komponente in einem Abstand von 50 Pixeln an der linken Kante des Containers ausgerichtet ([3] (left));

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

 
  • Bei [1] ist die linke Kante (left) der Komponente an der rechten Kante (right) der Komponente [textView1] in einem Abstand von 60 Pixeln zu [2] (left) ausgerichtet. Sie ist an der Unterkante (bottom:bottom) der Komponente [textView1] [1] ausgerichtet;

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

 
  • Bei [1] ist die linke Kante (left) der Komponente an der rechten Kante (right) der Komponente [editTextNom] in einem Abstand von 60 Pixeln zu [2] (left) ausgerichtet. Sie ist an der Unterkante der Komponente (bottom:bottom) [editTextNom] [1] ausgerichtet;

Die Komponente [buttonVue2] muss 50 Pixel unterhalb der Komponente [textView1] platziert und linksbündig an dieser Komponente ausgerichtet werden;

 
  • Bei [1] ist die linke Kante (left) der Komponente an der linken Kante (left) der Komponente [textView1] ausgerichtet und wird darunter (top:bottom) in einem Abstand von 50 Pixeln zu [2] (top) platziert;

Die generierte Datei XML 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: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>

Darin findet sich alles wieder, was grafisch vorgenommen wurde. Eine weitere Möglichkeit, eine Ansicht zu erstellen, besteht darin, diese Datei direkt zu bearbeiten. Wenn man damit vertraut ist, kann dies schneller sein als die Verwendung des grafischen Editors.

  • In Zeile 38 findet sich eine Information, die wir nicht gezeigt haben. Sie wird über die Eigenschaften der Komponente [editTextNom] [1] angegeben:
 

Alle Texte stammen 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 Aktivität [MainActivity] so, dass diese Ansicht beim Start der Anwendung 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 Aktivität zeigt nun die Ansicht [vue1.xml] an;

Ä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 angezeigt wird, sobald die Ansicht [vue1] angezeigt wird. Diese Ansicht enthält nämlich ein Eingabefeld, das beim Anzeigen der Ansicht den Fokus hat. Dieser Fokus bewirkt standardmäßig, dass die virtuelle Tastatur angezeigt wird;

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

Image

1.5.3. Ereignisbehandlung

Behandeln wir nun den Klick auf die Schaltfläche [Valider] der Ansicht [Vue1]:

Image

Der Code von [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 {

  // Elemente der Benutzeroberfläche
  @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");
  }

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

}
  • Zeilen 17–18: Das Feld [protected EditText editTextNom] wird der Identifikationskomponente [R.id.editTextNom] der visuellen Schnittstelle zugeordnet. Das der Komponente zugeordnete Feld muss in der abgeleiteten Klasse [MainActivity_] zugänglich sein und darf daher nicht den Geltungsbereich [private] 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 den Bezeichnern [id]. AA verarbeitet diese nicht korrekt.

  • Zeile 32: Die Anmerkung [@Click(R.id.buttonValider)] bezeichnet die Methode, die das „Click“-Ereignis auf der Schaltfläche mit der Kennung [R.id.buttonValider] verarbeitet. Diese Kennung 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 einen 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 Feld erscheinen soll,
    • der dritte Parameter ist die Lebensdauer des angezeigten Fensters: Toast.LENGTH_LONG oder Toast.LENGTH_SHORT;
  • In Zeile 26 kennzeichnet die Anmerkung [@AfterViews] die Methode, die ausgeführt werden soll, sobald alle mit [@ViewById] gekennzeichneten Felder initialisiert sind. Es ist wichtig zu wissen, wann diese Felder initialisiert werden. Kann man beispielsweise in der Methode [onCreate] die Referenz aus Zeile 18 verwenden? Um diese Frage zu beantworten, haben wir Logs gesetzt;

Führen Sie das Projekt [Exemple-04] aus und überprüfen Sie, ob etwas passiert, wenn Sie auf die Schaltfläche [Valider] klicken. Wir erhalten folgende Protokolleinträ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

Daraus lässt sich schließen, dass bei der Ausführung der Methode [onCreate] die mit [@ViewById] gekennzeichneten Felder noch nicht initialisiert sind. Auch hier wird dem Anfänger empfohlen, solche Protokolle in die Methoden einzufügen, die den Lebenszyklus der Anwendung verwalten.

1.6. Beispiel-05: Navigation zwischen Ansichten

Im vorherigen Projekt wurde die Schaltfläche [Vue n° 2] nicht genutzt. Wir wollen sie nun nutzen, indem wir eine zweite Ansicht erstellen und zeigen, wie man von einer Ansicht zur anderen navigiert. Es gibt mehrere Möglichkeiten, dieses Problem zu lösen. Die hier vorgeschlagene besteht darin, jede Ansicht einer Aktivität zuzuordnen. Eine andere Methode besteht darin, eine einzige Aktivität vom Typ [AppCompatActivity] zu verwenden, die Ansichten vom Typ [Fragment] anzeigt. Diese Methode wird in zukünftigen Anwendungen verwendet werden.

1.6.1. Erstellung des Projekts

Das Projekt [Exemple-04] wird in [Exemple-05] dupliziert. Dazu befolgen wir die Vorgehensweise, die in Abschnitt 1.4 zum Duplizieren von [Exemple-02] in [Exemple-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 wird die Ansicht Nr. 2 verwalten. Wir befinden uns hier in einem Modell, bei dem eine Ansicht einer Aktivität entspricht. Es gibt auch andere mögliche Modelle.

123

Image

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

Image

  • in [5] den Namen der Klasse, die generiert werden soll;
  • in [6] der Name der Ansicht (vue2.xml), die der neuen Aktivität zugeordnet ist;
  
  • in [7-8] die von der vorherigen Konfiguration betroffenen Dateien;

Die Aktivität [SecondActivity] lautet wie folgt:


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 der Ansicht [vue2.xml] zugeordnet;

Die Ansicht [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">

</RelativeLayout>

Es handelt sich um eine derzeit leere Ansicht mit einem Layout-Manager vom Typ [RelativeLayout] (Zeile 2). In Zeile 11 ist zu sehen, dass sie der neuen Aktivität zugeordnet wurde.

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

In Zeile 20 wurde eine zweite Aktivität registriert.

1.6.3. Navigation von Ansicht Nr. 1 zu Ansicht Nr. 2

Kehren wir zum Code der Klasse [MainActivity] zurück, die Ansicht Nr. 1 anzeigt. Der Wechsel zu Ansicht Nr. 2 wird derzeit nicht unterstützt:

  

Wir implementieren dies wie folgt:


  // Zur Ansicht Nr. 2 wechseln
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
    // Wechseln zur Ansicht Nr. 2 unter Übergabe des in Ansicht Nr. 1 eingegebenen Namens
    // Es wird ein Intent erstellt
    Intent intent = new Intent();
    // Dieser Intent wird einer Aktivität zugeordnet
    intent.setClass(this, SecondActivity.class);
    // Man ordnet diesem Intent Informationen zu
    intent.putExtra("NOM", editTextNom.getText().toString().trim());
    // Die Aktivität vom Typ [SecondActivity] wird gestartet, wobei der Intent übergeben wird
    startActivity(intent);
}
  • Zeilen 2–3: Die Methode [navigateToView2] verarbeitet den „Klick“ auf die Schaltfläche, die durch [R.id.buttonVue2] identifiziert 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 zum Wechseln der Ansicht erforderlich sind:

  1. Zeile 6: Erstellen Sie ein Objekt vom Typ [Intent]. Mit diesem Objekt können Sie sowohl die zu startende Aktivität als auch die an diese zu übergebenden Informationen festlegen;
  2. Zeile 8: Ordnen Sie den Intent einer Aktivität zu, in diesem Fall einer Aktivität vom Typ [SecondActivity], die für die Anzeige der Ansicht Nr. 2 zuständig ist. Beachten Sie, dass die Aktivität [MainActivity] die Ansicht Nr. 1 anzeigt. Es gilt also: eine Ansicht entspricht einer Aktivität. Wir müssen den Typ [SecondActivity] definieren;
  3. Zeile 10: Optional können Informationen in das Objekt [Intent] eingegeben werden. Diese sind für die Aktivität [SecondActivity] bestimmt, die gestartet wird. Die Parameter von [Intent.putExtra] lauten (Schlüsselobjekt, Wertobjekt). Es ist zu beachten, dass die Methode [EditText.getText()], die den im Eingabefeld eingegebenen Text zurückgibt, nicht den Typ [String], sondern den Typ [Editable] zurückgibt. Um den eingegebenen Text zu erhalten, muss die Methode [toString] verwendet werden;
  4. Zeile 12: Starten Sie die durch das Objekt [Intent] definierte Aktivität.

Führen Sie das Projekt [Exemple-05] aus und überprüfen Sie, ob Sie tatsächlich die Ansicht Nr. 2 erhalten (die derzeit noch leer ist):

1.6.4. Erstellung der Ansicht Nr. 2

 
  • In [1-2] löschen wir die Ansicht [main.xml], die wir nicht mehr benötigen, und ändern anschließend die Ansicht [vue2.xml] wie folgt:
 

Die Komponenten lauten wie folgt:

Id
Type
Rôle
1
textViewTitreVue2
TextView
Titre de la vue
2
textViewBonjour
TextView
un texte
5
btn_vue1
Button
pour passer à la vue n° 1

Die Datei XML [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 [Exemple-05] aus und überprüfen Sie, ob Sie durch Klicken auf die Schaltfläche [Vue n° 2] tatsächlich die neue Ansicht erhalten.

1.6.5. Die Aktivität [SecondActivity]

In [MainActivity] haben wir den folgenden Code geschrieben:


    // Navigieren Sie zur Ansicht Nr. 2
    protected void navigateToView2() {
        // Es wird zur Ansicht Nr. 2 navigiert, wobei der in Ansicht Nr. 1 eingegebene Name übergeben wird
        // Es wird ein Intent erstellt
        Intent intent = new Intent();
        // Dieser Intent wird einer Aktivität zugeordnet
        intent.setClass(this, SecondActivity.class);
        // Man ordnet diesem Intent Informationen zu
        intent.putExtra("NOM", edtNom.getText().toString().trim());
        // Die Aktivität vom Typ [SecondActivity] wird gestartet, wobei der Intent übergeben wird
        startActivity(intent);
}

In Zeile 9 haben wir für [SecondActivity] Informationen eingefügt, die bisher nicht ausgewertet wurden. Diese werten wir nun aus, und zwar im Code von [SecondActivity]:

  

Der Code von [SecondActivity] entwickelt 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 {

  // Komponenten der visuellen Benutzeroberfläche
  @ViewById
  protected TextView textViewBonjour;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @AfterViews
  protected void afterViews() {
    // Der Intent wird abgerufen, sofern er vorhanden ist
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // Der Name wird abgerufen
        String nom = extras.getString("NOM");
        if (nom != null) {
          // man zeigt ihn an
          textViewBonjour.setText(String.format("Bonjour %s !", nom));
        }
      }
    }
  }

}
  • Zeile 11: Mit der Anmerkung [@EActivity] wird angegeben, dass die Klasse [SecondActivity] eine Aktivität ist, die mit der Ansicht [vue2.xml] verknüpft ist;
  • Zeilen 15–16: Es wird eine Referenz auf die Komponente [TextView] abgerufen, die durch [R.id.textViewBonjour] identifiziert wird. Hier wurde [@ViewById(R.id.textViewBonjour)] nicht angegeben. 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] annotiert 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 [Exemple-05] wird von einer Aktivität zur nächsten gewechselt, 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 Annahme zutraf;
  • Zeile 26: Die Klasse [AppCompatActivity] verfügt über eine Methode [getIntent], die das der Aktivität zugeordnete Objekt [Intent] zurückgibt;
  • Zeile 28: Die Methode [Intent.getExtras] gibt einen Typ [Bundle] zurück, bei dem es sich um eine Art Wörterbuch handelt, das die Informationen enthält, die dem Objekt [Intent] der Aktivität zugeordnet sind;
  • Zeile 31: Der im Objekt [Intent] der Aktivität gespeicherte Name wird abgerufen;
  • Zeile 34: Dieser wird angezeigt.

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

Kommen wir noch einmal auf die Klasse [SecondActivity] zurück. Da wir geschrieben haben:


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

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

[MainActivity]


  // Zur Ansicht Nr. 2 navigieren
  @Click(R.id.buttonVue2)
  protected void navigateToView2() {
..
    // Dieser Intent wird einer Aktivität zugeordnet
    intent.setClass(this, SecondActivity_.class);
    ...
}
  • In Zeile 6 muss [SecondActivity] durch [SecondActivity_] ersetzt werden;

[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>
  • In Zeile 20 muss [SecondActivity] durch [SecondActivity_] ersetzt werden;

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

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

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

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

Der Code von [SecondActivity] entwickelt sich wie folgt:


  @Click(R.id.buttonVue1)
  protected void navigateToView1() {
    // Es wird ein Intent für die Aktivität [MainActivity] erstellt
    Intent intent1 = new Intent();
    intent1.setClass(this, MainActivity_.class);
    // den Intent der aktuellen Aktivität abrufen: [SecondActivity]
    Intent intent2 = getIntent();
    if (intent2 != null) {
      Bundle extras2 = intent2.getExtras();
      if (extras2 != null) {
        // Der Name wird in den Intent von [MainActivity] eingefügt
        intent1.putExtra("NOM", extras2.getString("NOM"));
      }
      // [MainActivity] wird gestartet
      startActivity(intent1);
    }
}
  • Zeilen 1–2: Die Methode [navigateToView1] wird dem Klick auf die Schaltfläche [btn_vue1] zugeordnet;
  • Zeile 4: Es wird ein neuer [Intent] erstellt;
  • Zeile 5: Verknüpfung mit der Aktivität [MainActivity_];
  • Zeile 7: Der mit [SecondActivity] verknüpfte Intent wird abgerufen;
  • Zeile 9: Die Informationen dieses Intents werden abgerufen;
  • Zeile 12: Der Schlüssel [NOM] wird aus [intent2] abgerufen, um mit demselben zugehörigen Wert in [intent1] gesetzt zu werden;
  • Zeile 15: Die Aktivität [MainActivity_] wird gestartet.

Im Code von [MainActivity] wird die folgende Methode [@AfterViews] hinzugefügt:


  @AfterViews
  protected void afterViews() {
    // Man ruft den Intent ab, falls er vorhanden ist
    Intent intent = getIntent();
    if (intent != null) {
      Bundle extras = intent.getExtras();
      if (extras != null) {
        // Der Name wird abgerufen
        String nom = extras.getString("NOM");
        if (nom != null) {
          // Anzeige
          editTextNom.setText(nom);
        }
      }
    }
}

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

1.6.7. Lebenszyklus von Aktivitäten

In Abschnitt 1.3.5 haben wir den Lebenszyklus einer Aktivität vorgestellt. Wir haben hier zwei Aktivitäten, zwischen denen während der Ausführung gewechselt wird. Diese Aktivitäten enthalten zwei Methoden, bei denen nicht ganz klar ist, wann sie im Verhältnis zueinander aufgerufen werden: [onCreate] und [afterViews]. Es ist wichtig, dies zu wissen. Zu diesem Zweck fügen wir in beiden Aktivitäten Log-Einträge hinzu:

So schreiben wir in der Klasse [MainActivity]:


  // Hersteller
  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] ein- oder mehrmals instanziiert wird;
  • Zeile 8: Wir möchten wissen, ob die Methode [onCreate] ein- 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 Anwendung erhalten wir folgende 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 man auf die Schaltfläche [Vue n° 2] klickt, lauten die neuen Protokolle 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 man auf die Schaltfläche [Vue n° 1] klickt, lauten die neuen Protokolle 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 somit erneut instanziiert. Wenn man auf die Schaltfläche [Vue n° 2] klickt, lauten die neuen Protokolle 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 somit erneut instanziiert.

Beide Aktivitäten werden also systematisch neu erstellt, wenn man die Aktivität wechselt.

Wir werden nun eine Architektur mit einer einzigen Aktivität kennenlernen, 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: Navigation über Registerkarten

Wir werden hier die Benutzeroberflächen mit Registerkarten untersuchen. Das Beispiel ist komplex, führt jedoch alle Elemente ein, die wir im weiteren Verlauf verwenden werden: eine einzige Aktivität, Fragment-Manager (Ansichten), Fragment-Container, Navigation zwischen Fragmenten. Das Konzept der Registerkarten unterscheidet sich von dem der Fragmente und spielt in dem, was wir in diesem Beispiel zeigen wollen, nur eine untergeordnete Rolle.

1.7.1. Erstellung des Projekts

Wir erstellen ein neues Projekt:

 
  • In [7] wählen wir eine Aktivität mit Registerkarten (Tabbed Activity) aus;
  • In [10-14] behält man die vorgeschlagenen Standardwerte bei;
  • In [15] wählt man Registerkarten mit einer Titelleiste aus;

Das erstellte Projekt sieht dann wie folgt aus:

 
  • in [1] die Aktivität;
  • in [2] die Sichten;

Eine Ausführungskonfiguration [app], benannt nach dem Modul, wurde automatisch erstellt: [2b]:

 

Sie kann ausgeführt werden. Daraufhin wird ein Fenster mit drei Registerkarten angezeigt: [3-6]:

Image

1.7.2. Gradle-Konfiguration

Das Projekt [Exemple-06] wurde mit der folgenden Datei [build.gradle] 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 bereits bekannt ist, gibt es eine Neuerung: 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 zur Aktivität [MainActivity] des Projekts gehörende Ansicht. Im Modus [design] sieht die Ansicht wie folgt aus:

Image

Sie enthält die folgenden Komponenten:

  
  • [main_content] ist die gesamte Ansicht;
  • [appbar] (roter Rahmen, 1) ist die Anwendungsleiste. Sie enthält zwei Komponenten:
    • [toolbar] (gelber Rahmen 4) ist die Symbolleiste;
    • [tabs] (orangefarbener Rahmen 5) ist die Titelleiste der Registerkarten;
  • [container] (grüner Rahmen, 2) kann verschiedene Fragmente aufnehmen. 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 Modus [text] 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>

Hier finden sich die zuvor beschriebenen Elemente wieder:

  • Zeilen 2–49: die Definition der Komponente [main_content] (Zeile 5), die die gesamte Ansicht umfasst. Man sieht, dass es sich um einen layout (Komponenten-Layout-Manager) vom Typ [CoordinatorLayout] (Zeile 2) handelt;
  • Zeilen 11–33: Der Container [appbar] (Zeile 12). Es handelt sich um einen layout vom Typ [AppBarLayout] (Zeile 11);
  • Zeilen 18–24: die Komponente [toolbar] (Zeile 19) vom Typ [Toolbar] (Zeile 18);
  • Zeilen 28–31: der Container [tabs] (Zeile 29). Es handelt sich um einen layout vom Typ [TabLayout] (Zeile 28). Er zeigt die Titel der Registerkarten an;
  • Zeilen 35–39: die Komponente [container] (Zeile 36). Dieser Container zeigt die verschiedenen Ansichten der Aktivität an;
  • Zeilen 41–47: die Komponente [fab] (Zeile 42) vom Typ [FloatingActionButton] (Zeile 41). Es handelt sich um eine Schaltfläche, auf die man klicken kann. Sie wird standardmäßig unten rechts in der Gesamtansicht platziert;

Wir werden nicht versuchen, die Bedeutung aller Attribute dieser Komponenten zu verstehen. Wir werden sie so verwenden, wie sie sind. Mit der Zeit und oft im Modus [design] entdeckt man ihre Rolle. In diesem Modus stellt man fest, dass die Komponenten mehrere Dutzend Attribute haben. In der Regel werden nur einige davon initialisiert, während die anderen einen Standardwert beibehalten.

Lassen Sie uns dennoch einige Punkte näher erläutern. Die meisten Werte, die die verschiedenen Ansichten konfigurieren, sind im Ordner [res / values] zusammengefasst:

  

Diese Werte werden in den Zeilen 15–16, 23, 39 und 46 der Datei [activity_main.xml] referenziert. Nehmen wir ein Beispiel:

  • Zeile 15:

    android:paddingTop="@dimen/appbar_padding_top"

Der Eintrag [@dimen] verweist auf die Datei [res / values / dimens.xml]:


<resources>
  <!-- Standard-Bildschirmränder gemäß den Android-Designrichtlinien. -->
  <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] verweist auf die oben genannte Zeile (f);

Analog dazu verweist die Anmerkung:

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

1.7.4. Die Aktivität

  

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

1.7.4.1. Die Verwaltung von Fragmenten und Registerkarten

Der Code von [MainActivity] bezüglich der 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 {

  // der Fragment-Manager
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // der Fragment-Container 
  private ViewPager mViewPager;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      // übergeordnetes Element
    super.onCreate(savedInstanceState);
    // Ansicht
    setContentView(R.layout.activity_main);
    // Symbolleiste
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    // der Fragment-Manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // Der Fragment-Container ist mit dem Fragment-Manager verknüpft
    // d. h., das Fragment Nr. i des Fragmentcontainers ist das vom Fragmentmanager ausgegebene Fragment Nr. i
    mViewPager = (ViewPager) findViewById(R.id.container);
    mViewPager.setAdapter(mSectionsPagerAdapter);
    // Die Registerleiste ist ebenfalls mit dem Fragmentcontainer verknüpft
    // d. h., die Registerkarte Nr. i zeigt das Fragment Nr. i des Containers an
    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
   }


  // ein Fragment
  public static class PlaceholderFragment extends Fragment {
 ...
  }

  // Der Fragment-Manager
  // wird mit den Fragmenten beauftragt, die in der Hauptansicht angezeigt werden sollen
  // muss die Methoden [getItem] und [getCount] definieren – die übrigen sind optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {
...
  }
}
  • Zeile 28: Android stellt einen View-Container vom Typ [android.support.v4.view.ViewPager] (Zeile 12) bereit. Diesem Container muss ein View- oder Fragment-Manager zugewiesen werden. Dieser wird vom Entwickler bereitgestellt;
  • Zeile 25: Der in diesem Beispiel verwendete Fragment-Handler. 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: Die Referenz der Komponente [toolbar] der Ansicht wird über ihre ID abgerufen;
  • Zeile 38: Diese Symbolleiste wird zur Aktionsleiste (ein Android-Konzept) der Aktivität;
  • Zeile 40: Der Fragment-Manager wird instanziiert. Der Konstruktorparameter ist die Android-Klasse [android.support.v4.app.FragmentManager] (Zeile 10);
  • Zeile 44: In der Ansicht [activity_main.xml] wird die Referenz auf den Fragment-Container über dessen ID abgerufen;
  • Zeile 45: Der Fragment-Manager wird mit dem Fragment-Container verknüpft. Das bedeutet, dass, wenn der Fragment-Container aufgefordert wird, das Fragment Nr. i anzuzeigen, dieses vom Fragment-Manager angefordert wird;
  • Zeile 48: Über die Kennung wird eine Referenz auf die Registerkartenleiste abgerufen;
  • Zeile 49: Der Registerkarten-Manager ist mit dem Fragment-Container verknüpft. Das bedeutet, dass der Container das Fragment Nr. i anzeigt, wenn auf die Registerkarte Nr. i geklickt wird. Durch die Verknüpfung zwischen dem Registerkarten-Manager und dem Fragment-Container entfällt jegliche Verwaltung der Registerkarten. Somit müssen wir keinen Ereignis-Handler für den Klick auf eine Registerkarte definieren. Die Verknüpfung mit dem Fragment-Container stellt dies standardmäßig bereit. Wir werden ein Beispiel betrachten, bei dem es mehr Fragmente als Registerkarten gibt. In diesem Fall wird diese Verknüpfung nicht hergestellt.

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


// der Fragment-Manager
  // Er wird dazu aufgerufen, die in der Hauptansicht anzuzeigenden Fragmente bereitzustellen
  // muss die Methoden [getItem] und [getCount] definieren – die anderen sind optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // Fragment-Nr. Position
    @Override
    public Fragment getItem(int position) {
      // ein Fragment [PlaceHolder] wird instanziiert und zurückgegeben
      return PlaceholderFragment.newInstance(position + 1);
    }

    // gibt die Anzahl der verwalteten Fragmente zurück
    @Override
    public int getCount() {
      return 3;
    }

    // optional – vergibt einen Titel für die verwalteten Fragmente
    @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 Anwendung angezeigten Fragmente hängen von dieser ab. Der Fragment-Handler wird vom Entwickler definiert;
  • Zeile 5: Der Fragment-Manager erweitert die Android-Klasse [android.support.v4.app.FragmentPagerAdapter]. Der Konstruktor ist vorgegeben. Wir müssen mindestens die folgenden beiden 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 des Fragments Nr. i zurückgibt, ist optional. Da der Registerkarten-Manager mit dem Fragment-Manager verknüpft wurde, entspricht der Titel der Registerkarte Nr. i dem Titel des Fragments Nr. i. Somit werden die Titel in den Zeilen 27–33 zu den Titeln der Registerkarten;

  • Zeilen 18–21: getCount gibt die Anzahl der verwalteten Fragmente an, hier drei;
  • Zeilen 11–15: getItem(i) gibt das Fragment Nr. i zurück. Hier sind alle Fragmente vom Typ [PlaceholderFragment] identisch;
  • Zeilen 24–35: getPageTitle(int i) gibt den Titel des Fragments Nr. i zurück;

1.7.4.2. Die angezeigten Fragmente

  

Die Fragmente der Aktivität haben hier alle denselben Typ und sind alle der folgenden Ansicht zugeordnet: XML [fragment_main]:


<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 layout vom Typ [RelativeLayout];
  • Zeilen 11–14: die einzige Komponente der Ansicht (Fragment): ein [TextView], identifiziert durch [section_label];

In [MainActivity] werden Fragmente vom folgenden Typ [PlaceholderFragment] verwaltet:


// ein Fragment
  public static class PlaceholderFragment extends Fragment {
      // einen im Fragment angezeigten Text
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
    }

    // gibt ein Fragment mit einer Information zurück: die als Parameter übergebene Fragmentnummer
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // Fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // integrierte Information
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // Ergebnis
      return fragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Die Ansicht [fragment_main] wird instanziiert
      View rootView = inflater.inflate(R.layout.fragment_main, container, false);
      // [TextView] wurde gefunden
      TextView textView = (TextView) rootView.findViewById(R.id.section_label);
      // ihr Inhalt wird geändert
      textView.setText(getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER)));
      // Die Ansicht wird zurückgegeben
      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. Ihre Methode [newInstance] (Zeile 10) ermöglicht es, Instanzen vom Typ [PlaceholderFragment] zu erhalten;
  • 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 die Methode [onCreateView] aus Zeile 22 definieren. Diese Methode muss die dem Fragment zugeordnete Ansicht zurückgeben.

  • Zeile 25: Die Ansicht [fragment_main.xml] ist dem Fragment zugeordnet;
  • Zeile 27: Diese Ansicht enthält eine Komponente [TextView], deren Referenz über ihre Kennung abgerufen wird;
  • Zeile 29: In der Komponente [TextView] wird ein Text angezeigt;
    • [getString] ist eine Methode der übergeordneten Klasse [AppCompatActivity];
    • Das erste Argument ist eine Komponentennummer. [R.string.section_format] bezeichnet die Nummer 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 das Argument Nr. 1 (%1) als Ganzzahl ($d) formatiert werden muss;
    • das zweite Argument von [getString] ist der Wert, der dem Argument $1 der Zeile (d) oben zugewiesen werden soll;
    • [getArguments] gibt die Referenz des Bundles der Fragmentargumente an. Dabei ist zu beachten, dass jedes Argument mit dem folgenden Bundle erstellt wurde (Zeilen f–h):

    // gibt ein Fragment mit einer Information zurück: die als Parameter übergebene Fragmentnummer
    public static PlaceholderFragment newInstance(int sectionNumber) {
        // Fragment
      PlaceholderFragment fragment = new PlaceholderFragment();
      // integrierte Information
      Bundle args = new Bundle();
      args.putInt(ARG_SECTION_NUMBER, sectionNumber);
      fragment.setArguments(args);
      // Ergebnis
      return fragment;
}
  • (Fortsetzung)
    • getArguments().getInt(ARG_SECTION_NUMBER) gibt daher den Wert [sectionNumber] aus den Zeilen (g) und (b) oben zurück;
  • Zeile 31: Die so erstellte Ansicht wird ausgegeben;

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üelement, das durch [action_settings] (Zeile 5) identifiziert wird;
  • Zeile 6: die Bezeichnung der 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 vorstehende Code entspricht der folgenden Darstellung (das Menü befindet sich oben rechts im Android-Ausführungsfenster):

 

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


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    // Das Menü aufblasen; dadurch werden Elemente zur Aktionsleiste hinzugefügt, sofern diese vorhanden ist.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    // Klicks auf Aktionsleistenelemente werden hier verarbeitet. Die Aktionsleiste wird
    // Klicks auf die Schaltfläche „Home/Nach oben“ automatisch verarbeiten, solange
    // Sie in AndroidManifest.xml eine übergeordnete Aktivität angeben.
    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 verarbeitet. 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 es sich bei der angeklickten Option um die Option mit der ID [action_settings] handelt, wird nichts unternommen und es wird angegeben, dass das Ereignis verarbeitet wurde (Zeile 17);
  • Zeile 20: Das Ereignis wird an die übergeordnete Klasse weitergeleitet;

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


  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    Log.d("menu", "création menu en cours");
    // Das Menü aufrufen; dadurch werden Elemente zur Aktionsleiste hinzugefügt, sofern diese vorhanden ist.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
  }

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Behandeln Sie hier Klicks auf Elemente der Aktionsleiste. Die Aktionsleiste wird
    // Klicks auf die Schaltfläche „Home/Nach oben“ automatisch verarbeiten, solange
    // Sie in AndroidManifest.xml eine übergeordnete Aktivität angeben.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
      Log.d("menu", "action_settings selected");
      return true;
    }
    // übergeordnete
    return super.onOptionsItemSelected(item);
}

1.7.4.4. Die schwebende Schaltfläche

Die generierte Ansicht enthält 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-Support bereitgestelltes Bild, nämlich das eines Briefumschlags.

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


    // schwebende Schaltfläche
    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: Die Referenz der schwebenden Schaltfläche wird aus der der Aktivität zugeordneten Ansicht (activity_main) abgerufen;
  • Zeilen 3–9: Man ordnet ihr einen Handler zu, um den Klick darauf zu verarbeiten;
  • Zeile 6: Die Klasse [Snackbar] ermöglicht es, mit ihrer Methode [Snackbar.make] kurzlebige Meldungen in der Ansicht anzuzeigen. Das erste Argument ist eine Ansicht, aus der [Snackbar] eine übergeordnete 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: Man kann auf die angezeigte Meldung klicken und so eine Aktion auslösen. Hier ist dem Klick 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ührung des Projekts

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

Image

Wenn man auf die Registerkarte Nr. i klickt, wird das Fragment Nr. i im Ansichtsbehälter angezeigt. Dies ist an dem in [4] angezeigten Text zu erkennen. Man kann außerdem feststellen, dass man von einer Registerkarte zur nächsten wechseln kann, indem man die Ansicht mit der Maus nach rechts oder links zieht (Swipe). Wir werden sehen, dass man dieses Verhalten steuern kann.

Wenn man auf die Menüoption in [6] klickt, erhält man folgende Protokolle:

 

1.7.6. Lebenszyklus der Fragmente

  • In [1] sieht man, dass die Methode [onCreateView] und die nachfolgenden Methoden bei der ersten Anzeige des Fragments und jedes Mal, wenn die Aktivität es erneut anzeigen muss, ausgeführt werden;

Um den Lebenszyklus der Aktivität und der Fragmente zu verfolgen, fügen wir die folgenden Logs in den Code von [MainActivity] ein:


// Konstruktor
  public MainActivity(){
    Log.d("MainActivity","constructor");
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    Log.d("MainActivity","onCreate");
      // übergeordnet
    super.onCreate(savedInstanceState);
...
  }

  // ein Fragment
  public static class PlaceholderFragment extends Fragment {
    // ein im Fragment angezeigter Text
    private static final String ARG_SECTION_NUMBER = "section_number";

    public PlaceholderFragment() {
      Log.d("PlaceholderFragment", "constructor");
    }

    // gibt ein Fragment mit einer Information zurück: die als Parameter übergebene Fragmentnummer
    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 Protokolleinträge 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: Erstellung der Aktivität;
  • Zeile 2: Ausführung ihrer 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;

An dieser Stelle sei noch einmal an den Code erinnert, der für die Erstellung der Fragmente zuständig ist:


  // der Fragment-Manager
  // Er wird damit beauftragt, die in der Hauptansicht anzuzeigenden Fragmente bereitzustellen
  // muss die Methoden [getItem] und [getCount] definieren – die anderen sind optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    public SectionsPagerAdapter(FragmentManager fm) {
      super(fm);
    }

    // Fragment-Nr. Position
    @Override
    public Fragment getItem(int position) {
      // Ein Fragment [PlaceHolder] wird instanziiert und gerendert
      return PlaceholderFragment.newInstance(position + 1);
    }
...
  • Zeilen 11–15: Ein Fragment wird durch [newInstance] instanziiert, sobald der Fragment-Container eines anfordert;

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

Klicken wir nun auf die 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: Das Fragment Nr. 3 wird instanziiert und initialisiert. Zur Erinnerung: Es wird das Fragment Nr. 2 angezeigt;

Klicken wir nun auf Registerkarte Nr. 3. Dort gibt es keine Log-Einträge. Wahrscheinlich, weil das anzuzeigende Fragment Nr. 3 bereits instanziiert worden war. Kehren wir nun zu Registerkarte Nr. 1 zurück. Die Log-Einträge lauten dann wie folgt:

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

Das Fragment Nr. 1 wird nicht erneut instanziiert, aber seine Methode [onCreateView] wird erneut ausgeführt. Dieses Verhalten wiederholt sich bei den beiden anderen Fragmenten.

Aus diesen Protokollen lässt sich Folgendes ableiten:

  • die Aktivität wurde einmal instanziiert und anschließend initialisiert;
  • jedes Fragment wurde einmal instanziiert;
  • dass die Methode [onCreateView] jedes Fragments mehrmals ausgeführt wurde;

Wichtig zu wissen und durch die Protokolle bestätigt ist, dass standardmäßig, wenn ein Fragment Nr. 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 das Fragment Nr. 1 angezeigt werden soll, die Fragmente 1 und 2 instanziiert und initialisiert wurden. Die Protokolle zeigen außerdem, dass die Methode [getItem(i)] nur einmal aufgerufen wird, auch wenn das Fragment Nr. i selbst mehrmals angezeigt wird. Es scheint also, dass der Fragment-Container [ViewPager], der das Fragment Nr. i anzeigen soll, dieses einmal beim Fragment-Manager [SectionsPagerAdapter] anfordert. Danach fordert er es nicht erneut an und verwendet weiterhin das bereits erhaltene Fragment.

Schließlich liefern die Protokolle Hinweise auf die Fragmentmethode [onCreateView]:

  • Beim Start wurden die Fragmente 1 und 2 instanziiert und ihre Methode [onCreateView] ausgeführt;
  • beim Wechsel von Fragment 1 zu Fragment 2 wird die Methode [onCreateView] von Fragment 2 nicht erneut ausgeführt. Man kann sie daher nicht zur Aktualisierung von Fragment 2 verwenden. Der Benutzer könnte jedoch mit Fragment 1 eine Operation durchgeführt haben, deren Ergebnis von Fragment 2 angezeigt werden sollte. Es zeigt sich, dass die Methode [onCreateView] nicht zur Aktualisierung von Fragment 2 verwendet werden kann. Es muss eine andere Lösung gefunden werden;

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

1.8.1. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-06] in [Exemple-07], um in dieses die Android-Annotationen einzufügen. Befolgen Sie dazu die Vorgehensweise aus Abschnitt 1.4. Wir erhalten folgendes Ergebnis:

1.8.2. Gradle-Konfiguration

 

Wir passen die Datei [build.gradle] wie folgt an:


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Seit Android-Gradle-Plugin 0.11 muss android-apt >= 1.3 verwendet werden
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    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 Bibliothek [Android Annotations] erforderliche Konfiguration hinzugefügt (siehe Abschnitt 1.4).

1.8.3. Hinzufügen der ersten Anmerkungen AA

Wir werden Anmerkungen AA in [MainActivity] erstellen:

  

Die Klasse [MainActivity] entwickelt sich wie folgt:


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // der Fragment-Manager
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // der Fragment-Container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
  // der Tab-Manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // die schwebende Schaltfläche
  @ViewById(R.id.fab)
  protected FloatingActionButton fab;


  // Konstruktor
  public MainActivity() {
    Log.d("MainActivity", "constructor");
  }

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // Symbolleiste
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // der Fragment-Manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // Der Fragment-Container ist mit dem Fragment-Manager verknüpft
    // d. h., das Fragment Nr. i des Fragmentcontainers ist das vom Fragmentmanager ausgegebene Fragment Nr. i
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // Die Registerleiste ist ebenfalls mit dem Fragmentcontainer verknüpft
    // d. h., die Registerkarte Nr. i zeigt das Fragment Nr. i des Containers an
    tabLayout.setupWithViewPager(mViewPager);

    // schwebende Schaltfläche
    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 Anmerkung [@EActivity] macht aus [MainActivity] eine Klasse, die von AA verwaltet wird. Ihr Parameter [R.layout.activity_main] ist die Kennung 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] eingefügt. Dabei handelt es sich um den Registerkarten-Manager;
  • Zeilen 14–15: Die durch [R.id.fab] identifizierte Komponente wird in das Feld [fab] eingefügt. Dabei handelt es sich um die schwebende Schaltfläche;
  • Zeilen 23–50: Der Code, der zuvor in der Methode „[onCreate]“ enthalten war, wird in eine Methode mit beliebigem Namen verschoben, die jedoch mit „[@AfterViews]“ annotiert ist (Zeile 23). In der so gekennzeichneten Methode ist sichergestellt, dass alle mit [@ViewById] gekennzeichneten Komponenten der visuellen Benutzeroberfläche initialisiert wurden;
  • außerdem wurden Logs eingefügt, um den Lebenszyklus der Aktivität zu verfolgen;

Man beachte, dass die Annotation [@EActivity] eine Klasse [MainActivity_] generiert, die die eigentliche Aktivität des Projekts darstellt. Daher muss die Datei [AndroidManifest.xml] wie folgt geändert werden:


<?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 nun das Projekt erneut aus und überprüfen Sie, ob weiterhin die Benutzeroberfläche mit Registerkarten angezeigt wird.

1.8.4. Neuschreiben der Fragmente

Wir werden die Verwaltung der Fragmente im Projekt noch einmal überarbeiten. Derzeit ist die Klasse [PlaceholderFragment] eine statische interne Klasse der Aktivität [MainActivity]. Wir kehren nun zu einem üblicheren Anwendungsfall zurück, bei dem die Fragmente in externen Klassen definiert sind. Außerdem führen wir die Annotationen AA für die Fragmente ein.

Das Projekt [Exemple-07] entwickelt sich wie folgt:

  

Oben ist die Klasse [PlaceholderFragment] zu sehen, die aus der Klasse [MainActivity] ausgelagert wurde. Sie wird 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;

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

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

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

  // Konstruktor
  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)));
    // übergeordnetes Element
    super.onResume();
    // Anzeige
    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 wird mit der Annotation [@EFragment] versehen, deren Parameter die ID der dem Fragment zugeordneten Ansicht XML ist, in diesem Fall die Ansicht [fragment_main.xml];
  • Zeilen 19–20: fügen in das Feld [textViewInfo] die Referenz der Komponente von [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 vor der Anzeige der dem Fragment zugeordneten Ansicht ausgeführt. Sie kann verwendet werden, um die anzuzeigende Benutzeroberfläche zu aktualisieren;
  • Zeile 47: Die gleichnamige Methode der übergeordneten Klasse muss aufgerufen werden;
  • Zeile 49: Es ist unklar, ob die Methode [onResume] vor der Initialisierung des Feldes in Zeile 20 ausgeführt werden kann oder nicht. Die Logs, die zur Verfolgung des Lebenszyklus des Fragments gesetzt wurden, werden dies zeigen. Vorerst und vorsichtshalber führen wir einen Null-Test durch;
  • Zeile 51: Die Informationen des Feldes [textViewInfo] werden mit dem ganzzahligen Argument aktualisiert, das dem Fragment bei seiner Erstellung übergeben wurde;

Die Klasse [MainActivity] verliert ihre interne Klasse [PlaceholderFragment] und ihr Fragment-Manager entwickelt sich wie folgt weiter:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private Fragment[] fragments;
    // Anzahl der Fragmente
    private static final int FRAGMENTS_COUNT = 3;
    // Fragmentnummer
    private static final String ARG_SECTION_NUMBER = "section_number";

    // Hersteller
    public SectionsPagerAdapter(FragmentManager fm) {
      // übergeordnetes Element
      super(fm);
      // Initialisierung des Fragment-Arrays
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length; i++) {
        // Ein Fragment wird erstellt
        fragments[i] = new PlaceholderFragment_();
        // Dem Fragment können Argumente übergeben werden
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
    }

    // Fragment Nr. Position
    @Override
    public Fragment getItem(int position) {
        Log.d("MainActivity", String.format("getItem[%s]", position));      
      return fragments[position];
    }

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

    // optional – vergibt einen Titel für die verwalteten Fragmente
    @Override
    public CharSequence getPageTitle(int position) {
      return String.format("Onglet n° %s", (position + 1));
    }
  }
  • Zeile 4: Die Fragmente werden in ein Array gespeichert;
  • Zeilen 16–23: Die Initialisierung des Fragment-Arrays erfolgt im Konstruktor. Sie sind vom Typ [PlaceholderFragment_] (Zeile 18) und nicht vom Typ [PlaceholderFragment]. Die Klasse [PlaceholderFragment] wurde nämlich mit einer Annotation AA versehen und wird eine von [PlaceholderFragment] abgeleitete Klasse [PlaceholderFragment_] erzeugen, 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: Die Titel der Fragmente wurden geändert. Da diese gleichzeitig die Titel der Registerkarten sind, sollte eine Änderung in der Registerkartenleiste sichtbar sein;

Kompilieren wir [Make] und [1] dieses Projekts:

 
  • zu [2], man sieht, dass die von der Bibliothek AA generierten Klassen im Ordner [app / build / generated / source / apt / debug] liegen (man muss sich in der Perspektive [Project] befinden, um [2] zu sehen);

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

1.8.5. Überprüfung der Protokolle

Beim Starten der Anwendung werden folgende Protokolle ausgegeben:

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: Erstellung der einzigen Aktivität;
  • Zeile 2: Methode [afterViews] der Aktivität: Die von [@ViewById] annotierten Felder werden initialisiert;
  • Zeilen 3–5: Erstellung der drei Fragmente;
  • Zeilen 6–7: Der Fragmentcontainer [ViewPager] fordert die ersten beiden Fragmente an;
  • Zeilen 8–9: Methoden von Fragment 2;
  • Zeilen 10–11: Methoden von Fragment 1;
  • Zeilen 12–13: Methode [onResume] von Fragment 1;
  • Zeilen 14–15: Methode [onResume] des Fragments 2;
  • Zeile 16: Erstellung des Menüs für die Aktivität;

Es ist zu beachten, dass hier die Antwort auf eine zuvor gestellte Frage vorliegt: Die Methode [onResume] aus Fragment 1 (Zeile 12) wird beispielsweise nach der Methode [afterViews] aus demselben Fragment (Zeile 11) ausgeführt. Wenn also die Methode [onResume] ausgeführt wird, kann sie die von [@ViewById] annotierten Felder verwenden. Wir können die Methode [onResume] nun wie folgt schreiben:


  @Override
  public void onResume() {
    Log.d("PlaceholderFragment", String.format("onResume %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // übergeordnetes Element
    super.onResume();
    // Anzeige
    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 Fragmentcontainer [ViewPager] fordert das Fragment Nr. 3 an;
  • Zeilen 2–3: Methoden des Fragments Nr. 3. Zur Erinnerung: Dieses Fragment wurde bereits beim Start der Anwendung instanziiert;
  • Zeilen 4–5: Die Methode [onResume] des Fragments Nr. 3 wird ausgeführt. Zur Erinnerung: Es wird das Fragment Nr. 2 angezeigt;

Wechseln wir nun von Registerkarte 2 zu Registerkarte 3. Es gibt keinen Log-Eintrag. Daher wird keine der Methoden [onCreateView, afterViews, onResume] des Fragments Nr. 3 ausgeführt. Der Text [Hello World from section:3] wird nur deshalb korrekt angezeigt, weil dieser Text bereits im vorherigen Schritt bei der Anzeige von Fragment Nr. 2 erstellt worden war. Wir erinnern uns nämlich daran, dass in diesem Schritt die Methode [onResume] aus Fragment Nr. 3 ausgeführt worden war. Hier wird deutlich, dass ebenso wenig wie die Methode [onCreateView] auch die Methode [onResume] nicht zur Aktualisierung von Fragment 3 verwendet werden kann. Hätte man 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

Man sieht, dass alle Methoden von Fragment 1 ausgeführt wurden. Man sieht, 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 folgende Protokolle:

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

Erstaunlich, nicht wahr? Alle Methoden des Fragments Nr. 3 werden erneut ausgeführt.

Um diese Phänomene zu verstehen, muss man sich vor Augen halten, dass der Fragment-Container standardmäßig, wenn er das Fragment i anzeigt, die Fragmente i-1, i und i+1 initialisiert. Schauen wir uns die Logs noch einmal vor dem Hintergrund dieser Information an.

Zunächst die Protokolle beim Start der Anwendung:

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 anzeigen wird, werden die Fragmente 1 und 2 initialisiert (Zeilen 8–15).

Nun wechseln wir 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 anzeigen wird, müssen die Fragmente 1, 2 und 3 initialisiert werden. Die Fragmente 1 und 2 sind bereits aus dem vorherigen Schritt initialisiert. Fragment 3 wird in den Zeilen 2–5 initialisiert.

Wir wechseln von Registerkarte 2 zu Registerkarte 3. Es gibt keine Protokolle. Da der Fragmentcontainer Fragment 3 anzeigen wird, müssen die Fragmente 2 und 3 initialisiert werden. Da sie jedoch bereits seit dem vorherigen Schritt initialisiert sind, ist dies hier nicht zu sehen. Was hier nicht zu sehen ist: Das Fragment 1, das nicht an Fragment 3 angrenzt, verliert seinen Zustand, da dieser 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 anzeigen wird, muss auch Fragment 2 initialisiert werden. Dies ist bereits seit dem vorherigen Schritt der Fall. Im selben Schritt ging der Zustand von Fragment 1 verloren. Er wird daher in den Zeilen 1–4 zurückgesetzt. Was hier nicht zu sehen ist: Fragment 3, das nicht an Fragment 1 angrenzt, verliert seinen Zustand, der somit nicht im Speicher erhalten bleibt.

Beim Wechsel von Registerkarte 1 zur benachbarten Registerkarte 2 ergeben sich folgende 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 anzeigen wird, müssen die Fragmente 1, 2 und 3 initialisiert werden. Die Fragmente 1 und 2 sind bereits aus dem vorherigen Schritt initialisiert. Fragment 3 wird auf die Zeilen 1–4 initialisiert.

Was haben wir gelernt?

  • Dass die Standardverwaltung von Fragmenten sehr speziell ist und man sie kennen muss, wenn man sich nicht die Haare raufen will. Man kann diesen Verwaltungsmodus ändern, und das werden wir etwas später tun;
  • dass bei dieser Standardverwaltung keine der Methoden [onCreateView, onResume] verwendet werden kann, um das anzuzeigende Fragment zu aktualisieren, da nicht sicher ist, ob sie ausgeführt werden;

1.8.6. onDestroyView

Die Methode [onDestroyView] ist Teil des Lebenszyklus von Fragmenten (siehe Abschnitt 1.7.6):

Es ist ersichtlich, dass im Lebenszyklus eines Fragments:

  • die Methode [onCreateView] mehrmals ausgeführt werden kann;
  • bevor später zur Methode [onCreateView] zurückgekehrt wird, muss zwangsläufig die Methode [onDestroyView] [2] durchlaufen werden;

Wir werden diese Methoden in die Fragmente einfügen, um ihren Lebenszyklus besser verfolgen zu können. Der Code des Fragments sieht nun 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;

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

...

  @Override
  public void onDestroyView() {
    // Protokoll
    Log.d("PlaceholderFragment", String.format("onDestroyView %s", getArguments().getInt(ARG_SECTION_NUMBER)));
    // übergeordnet
    super.onDestroyView();
  }

}

Führen wir die Anwendung aus. 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: Erstellung der einzigen Aktivität;
  • Zeile 2: Methode [afterViews] der Aktivität: Ihre mit [@ViewById] annotierten Felder werden initialisiert;
  • Zeilen 3–5: Erstellung der drei Fragmente;
  • Zeilen 6–7: Der Fragmentcontainer [ViewPager] fordert die ersten beiden Fragmente an;
  • Zeilen 8–9: Die Ansicht von Fragment 2 wird erstellt (nicht unbedingt sichtbar gemacht);
  • Zeilen 10–11: Die Ansicht von Fragment 1 wird erstellt (muss nicht unbedingt sichtbar sein);
  • Zeilen 12–13: Methode [onResume] des Fragments 1;
  • Zeilen 14–15: Methode [onResume] des Fragments 2;
  • Zeile 16: Erstellung des Aktivitätsmenüs;

Wechseln wir 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 von Fragment 3 wird erstellt (nicht unbedingt angezeigt);
  • Zeilen 4–5: Die Methode [onResume] des Fragments 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;

Man kehrt von Registerkarte 3 zu Registerkarte 1 zurück:


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 zuvor ein [onDestroyView] durchlaufen hatte;
  • Zeile 5: Nun wird die Methode [onDestroyView] des Fragments 3 ausgeführt. Auch hier gilt: Wenn der Benutzer zu Fragment 3 oder zu einem benachbarten Fragment zurückkehrt, wird der Lebenszyklus dieses Fragments erneut ausgeführt;

1.8.7. setUserVisibleHint

Die Lebenszyklusmethode [onCreateView] instanziiert die dem Fragment zugeordnete 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;

....

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

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

  ...

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // Protokoll
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s isVisibleToUser=%s", getArguments().getInt(ARG_SECTION_NUMBER), isVisibleToUser));
  }
}

Beim Start lauten die Protokolleinträge wie folgt:


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 in den Zeilen 7, 9–10 zeigen, dass nur Fragment 1 sichtbar wird. Man sieht auch, dass es sichtbar wird, bevor seine Methode [onCreateView] 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 ist 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 ist ausgeblendet (Zeile 2), Fragment 1 wird angezeigt (Zeile 3);

Was haben wir gelernt?

  • Die Methode [setUserVisibleHint] wird einmal ausgeführt, wobei die Eigenschaft von [isVisibleToUser] auf true gesetzt wird, und zwar für das Fragment, das angezeigt werden soll;
  • Es lässt sich nicht feststellen, wann diese Methode im 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 bei den Fragmenten 2 und 3 das Gegenteil der Fall war;

1.8.8. setOffscreenPageLimit

Die vorherigen Protokolle zeigen, dass der Fragment-Container [ViewPager], wenn er das Fragment Nr. i anzeigen will, den Lebenszyklus der benachbarten Fragmente i-1 und i+1 durchläuft, sofern dies nicht bereits geschehen ist. Dieses Verhalten kann über die Methode [ViewPager].setOffscreenPageLimit gesteuert werden:

// Fragment-Offset
    [ViewPager].setOffscreenPageLimit(n);

Mit der obigen Anweisung

  1. wenn der Fragmentcontainer [ViewPager] im Begriff ist, das Fragment Nr. i anzuzeigen, führt er, sofern dies noch nicht geschehen ist, den Lebenszyklus der benachbarten Fragmente des Intervalls [i-n, i+n] aus;
  2. wird anschließend das Fragment j angezeigt:
    • tritt dasselbe Phänomen für die benachbarten Fragmente des Intervalls [j-n, j+n] auf;
    • Die in Schritt 1 initialisierten Fragmente, die sich nicht mehr in der Nachbarschaft [j-n, j+n] des neuen Fragments befinden, können dann einer Operation [onDestroyView] unterzogen werden. Allerdings habe ich bei anderen Anwendungen, insbesondere der aus Kapitel 3, beobachtet, dass dies nicht systematisch der Fall war;

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


  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    // Symbolleiste
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    // der Fragment-Manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // Der Fragmentcontainer ist mit dem Fragmentmanager verknüpft
    // d. h., das Fragment Nr. i des Fragmentcontainers ist das vom Fragmentmanager ausgegebene Fragment Nr. i
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // Das Wischen zwischen Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);

    // Offset der Fragmente
    mViewPager.setOffscreenPageLimit(mSectionsPagerAdapter.getCount() - 1);

    // Die Registerleiste ist ebenfalls mit dem Fragmentcontainer verknüpft
    // d. h., die Registerkarte Nr. i zeigt das Fragment Nr. i des Containers an
    tabLayout.setupWithViewPager(mViewPager);

    // Schwebende Schaltfläche
    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 das Fragment Nr. 1 anzeigt, initialisiert er somit gleichzeitig die Fragmente 2, 3, …, n mit n = 1 + mSectionsPagerAdapter.getCount() – 1 = mSectionsPagerAdapter.getCount(). Es werden also alle Fragmente initialisiert. Wenn sich das Anzeigefenster auf ein anderes Fragment verschiebt, stellt der Fragmentcontainer fest:
    • feststellen, dass alle an das neue Fragment angrenzenden Fragmente bereits initialisiert sind, und wird daher keine Initialisierung vornehmen;
    • da die Nachbarschaft des neuen Fragments ebenfalls alle Fragmente abdeckt, wird keines davon vom Fragment-Container „deinitialisiert“;

Insgesamt sollten alle Fragmente beim Start der Anwendung instanziiert und initialisiert werden und danach nie wieder. Das überprüfen wir nun anhand der Protokolle.

Beim Start haben wir folgende 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 forderte er zwei an;
  • Zeilen 14–25: Der Lebenszyklus der drei Fragmente wird ausgeführt;

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 die Methoden ihres Lebenszyklus nicht mehr ausgeführt. Das ist ein sehr vorhersehbares Verhalten der Fragmente, das ihre Verwendung enorm erleichtert.

Was wir herausfinden wollen, ist eine Möglichkeit, ein Fragment zu aktualisieren, das angezeigt werden soll – und zwar unabhängig davon, welche Anordnung der Fragmente der Entwickler gewählt hat. Die Protokolle haben uns zwei Dinge gezeigt:

  • Die Methode [setUserVisibleHint, true] wird immer für das Fragment ausgeführt, das 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-Anordnung ab. Das 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, lauteten wie folgt:


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
  • Man sieht, dass, wenn Fragment 1 sichtbar wird, seine Ansicht noch nicht erstellt wurde. Man kann also noch nicht darauf zugreifen. Dies kann dann im Laufe des Lebenszyklus des Fragments erfolgen, beispielsweise in den Methoden [onCreateView] (Zeile 11) oder [onResume] (Zeilen 13–14). Da wir die Annotationen AA verwenden, müssen wir die Methode [onCreateView] normalerweise nicht schreiben. Daher scheint die Methode [onResume] hier am besten geeignet zu sein, um Fragment 1 zu aktualisieren;

Als wir von Registerkarte 1 zu Registerkarte 2 gewechselt sind, lauteten die Protokolle wie folgt:


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 gibt es nur die Methode [setUserVisibleHint, true] in Zeile 4, um Fragment 2 zu aktualisieren;

Als wir von Registerkarte 2 zu Registerkarte 3 gewechselt sind, lauteten die Protokolleinträge wie folgt:


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 gibt es nur die Methode [setUserVisibleHint, true] in Zeile 2, um Fragment 3 zu aktualisieren;

Als wir von Registerkarte 3 zu Registerkarte 1 gewechselt sind, lauteten die Protokolleinträge wie folgt:


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 muss die Methode [onResume] aus Fragment 1 (Zeilen 6–7) verwendet werden, um Fragment 1 zu aktualisieren.

An diesem Beispiel sehen wir also, dass zur Aktualisierung eines Fragments, das angezeigt werden soll, zwei Methoden zur Verfügung stehen: [setUserVisibleHint] und [onResume].

Wir werden diese Lösung in einem neuen Projekt umsetzen, in dem jedes Fragment die Anzahl seiner Aufrufe anzeigen soll – was wir als „Besuch“ bezeichnen. Daher muss seine Anzeige jedes Mal aktualisiert werden, wenn es angezeigt wird. Genau das ist das Problem, das wir lösen wollen.

Zuvor betrachten wir jedoch den letzten Schritt im Lebenszyklus einer Aktivität oder eines Fragments, nämlich den, bei dem es zerstört wird. Das System kann von sich aus eine Aktivität löschen, wenn andere Aktivitäten mit höherer Priorität nicht verfügbare Ressourcen beanspruchen. Um diese freizugeben, wird das System bestimmte Aktivitäten löschen. 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 in 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>

Wir kopieren einfach die erste Menüoption und fügen sie ein und passen das Ergebnis an (Zeilen 9 und 10). Die Bezeichnung dieser neuen Option wird in die Datei [strings.xml] [2] eingefügt:


<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 wird in der Klasse [MainActivity] der Klick auf die Option [Terminate] verarbeitet:


  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    Log.d("menu", "onOptionsItemSelected");
    // Klicks auf Elemente der Aktionsleiste werden hier verarbeitet. Die Aktionsleiste wird
    // Klicks auf die Schaltfläche „Home/Nach oben“ automatisch verarbeiten, solange
    // Sie in AndroidManifest.xml eine übergeordnete Aktivität angeben.
    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");
      //Nach Beendigung der übergeordneten Aktivität
      finish();
      return true;
    }
    // übergeordneten Aktivität
    return super.onOptionsItemSelected(item);
}
  • Zeilen 14–19: Man kopiert die Zeilen 10–13 und fügt sie ein und passt 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 klicken wir, sobald die erste Ansicht angezeigt wird, auf die Menüoption [Terminate]. Die Protokolle lauten dann wie folgt:

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: Klick auf die Option „[Terminate]“;
  • Zeile 4: Die Methode „[onDestroy]“ der Aktivität wird aufgerufen;
  • Zeilen 4–5: Die Methode [onDestroyView] des Fragments 1 wird aufgerufen, anschließend deren Methode [onDestroy];
  • Zeilen 6–9: Dieser Vorgang wiederholt sich für die beiden anderen Fragmente;

Es sei also daran erinnert, dass die Methode [onDestroy] der Aktivität und der Fragmente aufgerufen wird, wenn die Aktivität vom System, vom Entwickler oder vom Benutzer gelöscht wird. Diese Methode kann genutzt werden, um Informationen zu speichern, beispielsweise lokal auf dem Tablet, damit sie wieder abgerufen werden können, wenn der Benutzer die Anwendung erneut startet.

1.9. Beispiel-08: Aktualisierung eines Fragments mit variabler Fragmentanordnung

1.9.1. Erstellung des Projekts

Das Projekt [Exemple-07] wird in [Exemple-08] dupliziert. Dazu wird die in Abschnitt 1.4 beschriebene Vorgehensweise zum Duplizieren von [Exemple-02] in [Exemple-03] befolgt.

1.9.2. Neuschreiben des Fragments [PlaceholderFragment]

Der neue Code für das Fragment [PlaceholderFragment] lautet wie folgt. Er funktioniert unabhängig von der den Fragmenten zugewiesenen Adjazenz (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;

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

  // Komponente der Benutzeroberfläche
  @ViewById(R.id.section_label)
  protected TextView textViewInfo;
  // Daten
  private boolean afterViewsDone = false;
  private boolean initDone = false;
  private String text;
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private int numVisit = 0;

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

  // Konstruktor
  public PlaceholderFragment() {
    Log.d("PlaceholderFragment", "constructor");
  }


  @AfterViews
  protected void afterViews() {
    // Speicher
    afterViewsDone = true;
    // Protokoll
    Log.d("PlaceholderFragment", String.format("afterViews %s %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    if (!initDone) {
      // Ausgangstext
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // Initialisierung abgeschlossen
      initDone = true;
    }
    // Anzeige des aktuellen Textes
    textViewInfo.setText(text);
  }


  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
...
  }

  @Override
  public void onDestroyView() {
...
  }

  @Override
  public void onResume() {
...
  }

  // Fragment aktualisieren
  public void update() {
    // Die auszuführende Arbeit hängt von der Besuchsnummer ab
    if (numVisit > 1) {
      // Protokoll
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // Text geändert
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // Lokale Informationen für Protokolle
  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 möglicherweise mehrmals ausgeführt werden. Sie wurde verwendet, um den Text des Fragments zu initialisieren (Zeile 42). Dies geschieht weiterhin, doch um den Vorgang auf ein einziges Mal zu beschränken, verwalten wir einen booleschen Wert [initDone] (Zeile 44), um anzuzeigen, dass die Initialisierung bereits erfolgt ist und nicht erneut durchgeführt werden muss;
  • Zeilen 56–59: Wir führen die Methode [onDestroyView] ein, um zu vermerken, dass beim nächsten Mal, wenn das Fragment erneut angezeigt wird, sein Lebenszyklus erneut durchlaufen wird;
  • Die Protokolle haben gezeigt, 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] hingegen wird nicht immer nach der Methode [@AfterViews] ausgeführt. Die Protokolle haben gezeigt, dass mindestens eine der beiden Methoden nach der Methode [@AfterViews] ausgeführt wird. Die Protokolle haben nie gezeigt, dass beide Methoden gemeinsam nach der Methode [@AfterViews] ausgeführt werden könnten. Es ist entweder die eine oder die andere. Vorsichtshalber wird ein boolescher Wert [updateDone] gesetzt, sobald eine Aktualisierung vorgenommen wurde;

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


  // Daten
  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) {
    // übergeordnet
    super.setUserVisibleHint(isVisibleToUser);
    // Speicher
    this.isVisibleToUser = isVisibleToUser;
    // Protokoll
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // Anzahl der Besuche
    if (isVisibleToUser) {
      // Inkrement
      numVisit++;
      // Fragment aktualisieren
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // Das Fragment wird ausgeblendet
      updateDone = false;
    }
  }

  @Override
  public void onResume() {
    // übergeordnet
    super.onResume();
    // Protokoll
    Log.d("PlaceholderFragment", String.format("onResume %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // Aktualisierung
    if (isVisibleToUser && !updateDone) {
      update();
      updateDone = true;
    }
}
  • Zeile 14: Der Status „sichtbar“ oder „nicht sichtbar“ 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 der boolesche Wert [updateDone] an true übergeben;
  • Zeilen 26–28: Wenn das Fragment ausgeblendet werden soll, wird der boolesche Wert [updateDone] auf false zurückgesetzt. Wir benötigen nämlich ein Ereignis, um den booleschen Wert [updateDone], der auf true gesetzt wurde, sobald die Methode [update] aufgerufen wird, wieder auf false zurückzusetzen, damit neue Aktualisierungen vorgenommen werden können. Dazu nutzen wir die Tatsache, dass das Fragment nicht mehr sichtbar ist. Sobald es wieder sichtbar wird, muss die Aktualisierung des Fragments erneut erfolgen;
  • Zeilen 32–42: Die Protokolle zeigen, dass je nach der für die Fragmente gewählten Adjazenz die Methode [onResume] ausgeführt werden kann, obwohl das Fragment nicht sichtbar ist. Ist es nicht sichtbar, wird die Aktualisierung nicht durchgeführt (Zeile 39), und wie bei [setMenuVisibility] verwalten wir den booleschen Wert [updateDone].

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


  @Override
  public void onDestroyView() {
    // übergeordnetes Element
    super.onDestroyView();
    // Indikator-Aktualisierung
    afterViewsDone = false;
    // Protokoll
    Log.d("PlaceholderFragment", String.format("onDestroyView %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
}

Die Methode [onDestroyView] wird ausgeführt, wenn ein Lebenszyklus des Fragments endet. Ein weiterer Zyklus kann später wieder aufgenommen werden.

  • Zeile 6: Die Methode [onDestroyView] entfernt jegliche Verknüpfung mit der dem Fragment zugeordneten Ansicht. Diese wird im nächsten Lebenszyklus des Fragments neu erstellt. Vorerst müssen wir den booleschen Wert von [afterViews] auf false setzen, um anzugeben, dass die Verknüpfung mit der Ansicht nicht mehr besteht;

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


    // Anzahl der Fragmente
  private final int FRAGMENTS_COUNT = 5;
  // Nachbarschaft der Fragmente
  private final int OFF_SCREEN_PAGE_LIMIT=2;


  // Fragmentmanager
  private SectionsPagerAdapter mSectionsPagerAdapter;

   @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");

    ....

    // Offset der Fragmente
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

...
}

Die Protokolle beim Start 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 Fragmente an, die an Fragment 1 angrenzen;
  • Zeilen 9, 11, 13: Die Methode [setUserVisibleHint] dieser Fragmente wird mit [visibleToUser] bis false ausgeführt;
  • Zeile 14: Die Methode [setUserVisibleHint] des Fragments 1 wird mit [visibleToUser] bis true ausgeführt;
  • Zeilen 15–17: Die Methode [afterViews] der drei benachbarten Segmente wird aufgerufen. Hier handelt es sich also um einen Fall, in dem diese Methode aufgerufen wird, nachdem ein Fragment sichtbar geworden ist (Fragment 1, Zeile 14);
  • Zeilen 18–20: Die Methode [onResume] für die drei benachbarten Segmente wird aufgerufen;

Man wechselt 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 die Anordnung der Fragmente um eine Position nach rechts verschoben wird, wird Fragment 4 vom Fragmentcontainer angefordert;
  • Zeile 2: Die Methode [setUserVisibleHint] des Fragments 4 wird mit [visibleToUser] bis false aufgerufen;
  • Zeile 3: Die Methode [setUserVisibleHint] des Fragments 1 wird mit [visibleToUser] bis false aufgerufen. Tatsächlich ist Fragment 1 nun ausgeblendet;
  • Zeile 4: Die Methode [setUserVisibleHint] von Fragment 2 wird mit [visibleToUser] bis true aufgerufen. Fragment 2 ist nun sichtbar;
  • Zeilen 5–6: Der Lebenszyklus von Fragment 4 wird fortgesetzt;

Man wechselt 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 die Anordnung der Fragmente um eine Position nach rechts verschoben wird, wird Fragment 5 vom Fragmentcontainer angefordert;
  • Zeile 2: Die Methode [setUserVisibleHint] des Fragments 5 wird mit [visibleToUser] bis false aufgerufen;
  • Zeile 3: Die Methode [setUserVisibleHint] des Fragments 2 wird von [visibleToUser] bis false aufgerufen. Tatsächlich ist das Fragment 2 nun ausgeblendet;
  • Zeile 4: Die Methode [setUserVisibleHint] des Fragments 3 wird mit [visibleToUser] bis true aufgerufen. Das Fragment 3 ist nun sichtbar;
  • Zeilen 5–6: Der Lebenszyklus von Fragment 5 wird fortgesetzt;

Man wechselt 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. Es ist zu beachten, dass der Lebenszyklus von Fragment 4 nicht ausgeführt wird. Dies geschah bereits zwei Schritte zuvor;
  • Zeile 3: Fragment 1 verlässt die Nachbarschaft des angezeigten Fragments 4. Seine Methode [onDestroyView] wird ausgeführt. Beim nächsten Mal, wenn es angezeigt wird, wird sein Ansichtszyklus [onCreateView, afterViews, onResume] erneut ausgeführt;

Man wechselt 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. Es ist zu beachten, dass der Lebenszyklus von Fragment 5 nicht ausgeführt wird. Dies geschah bereits zwei Schritte zuvor;
  • Zeile 3: Fragment 2 verlässt die Nachbarschaft des angezeigten Fragments 5. Seine Methode [onDestroyView] wird ausgeführt;

Man wechselt 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. Tatsächlich hatte es die Verbindung zu seiner Ansicht verloren;
  • 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 verlassen die Nachbarschaft des angezeigten Fragments;
  • Zeile 7: Fragment 1 wird aktualisiert;
 

Die Protokolle haben nie gezeigt, dass die Methoden [setUserVisibleHint] und [onResume] beide versucht haben, das Fragment zu aktualisieren. Es ist entweder die eine oder die andere. Der Leser wird gebeten, weitere Tests durchzuführen und die Protokolle zu verfolgen, um das Konzept der Adjazenz und des Lebenszyklus von Fragmenten besser zu verstehen.

Nehmen wir nun eine vollständige Adjazenz an und führen wir dieselben Tests durch.

In [MainActivity]:


  // Anzahl der Fragmente
  private final int FRAGMENTS_COUNT = 5;
  // Nachbarschaft der Fragmente
private final int OFF_SCREEN_PAGE_LIMIT = FRAGMENTS_COUNT - 1;

Die Protokolle beim Start 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;

Es erfolgt ein 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 wird ausgeblendet;
  • Zeile 2: Fragment 2 wird angezeigt;

Man wechselt 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;

Man wechselt 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;

Man wechselt 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;

Man wechselt 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;

Wir wechseln 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;

Man stellt fest, dass das Verhalten der Fragmente bei vollständiger Adjazenz wesentlich vorhersehbarer ist.

Legen wir nun eine Null-Adjazenz fest und schauen wir, was passiert. Die Klasse [MainActivity] entwickelt sich wie folgt:


  // Anzahl der Fragmente
  private final int FRAGMENTS_COUNT = 5;
  // Nachbarschaft der Fragmente
private final int OFF_SCREEN_PAGE_LIMIT = 0;

Die Protokolle beim Start 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 ist zu sehen, dass der Fragmentcontainer zwei Fragmente angefordert hat, nämlich die Nr. 1 und Nr. 2. Es verhält sich also so, als gäbe es eine Adjazenz von 1. Die Adjazenz von 0 wurde somit ignoriert.

1.9.3. Kommunikation zwischen Fragmenten

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

  • Wenn der Benutzer mit dem Fragment i interagiert, wechselt die Anwendung von einem Zustand E1 zu einem Zustand E2;
  • eine Aktion des Benutzers auf Fragment i bewirkt, dass Fragment j angezeigt wird;
  • Wie lässt sich das Fragment j mit dem aktuellen Zustand E2 der Anwendung aktualisieren?

Aus den vorangegangenen Beispielen wissen wir, wie das Fragment j aktualisiert wird. Aber wo findet man den Zustand E2 der Anwendung, um es zu aktualisieren?

Es gibt verschiedene Lösungen für dieses Problem. Eine davon haben wir bereits gesehen: Das Fragment i kann den Anwendungsstatus E2 über Argumente an das Fragment j übergeben. Diese Methode haben wir in der Klasse [MainActivity] bei der Erstellung der Fragmente kennengelernt:


      for (int i = 0; i < fragments.length; i++) {
        // Ein Fragment wird erstellt
        fragments[i] = new PlaceholderFragment_();
        // dem Fragment können Argumente übergeben werden
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
}

Diese Lösung ist hier nicht sofort einsetzbar. Denn wenn der Benutzer auf die Registerkarte j klickt, wodurch das Fragment j angezeigt wird, wird unser Code nicht aufgerufen. Es wird ausschließlich Systemcode ausgeführt. In einem späteren Projekt werden wir sehen, wie man den Klick auf eine Registerkarte abfangen kann, aber vorerst werden wir einen anderen Weg einschlagen.

Wir haben über den Zustand der Anwendung gesprochen: die Gesamtheit der Daten, die von der Anwendung im Laufe der Zeit verwaltet werden. Hier besteht die Anwendung aus einer Aktivität und n Fragmenten, die alle beim Start der Anwendung einmalig instanziiert werden und deren Lebensdauer der Lebensdauer der Anwendung entspricht. Daher kommen jedes dieser Elemente oder mehrere davon gemeinsam als Kandidaten für die Speicherung des Anwendungszustands in Frage. 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 Anwendungsstatus in dieser 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 einige Log-Einträge in die Klasse [PlaceholderFragment] einfügen:


  // Fragment aktualisieren
  public void update() {
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // Die auszuführende Aufgabe hängt von der Besuchsnummer ab
    if (numVisit > 1) {
      // Protokoll
      Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
      // Text geändert
      textViewInfo.setText(String.format("%s update(%s)", text, (numVisit - 1)));
    }
  }

  // Lokale Informationen für Protokolle
  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 [getInfos] zeigt einen Teil des Anwendungsstatus an;

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


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: Man sieht, dass in den Methoden [setUserVisibleHint] die Methode [getActivity()==null] aufgerufen wird, wenn das Fragment noch nicht sichtbar ist (isVisibleToUser == false);
  • Zeile 19: Man sieht, dass, wenn der Ausführungsfluss die Methode [update] von Fragment 1 erreicht, die Methode [getActivity] die Aktivität tatsächlich zurückgibt;

Wenn man die Fragment-Adjazenz auf 4 (vollständige Adjazenz) setzt, lauten die Protokolle 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

Es ergeben sich dieselben Ergebnisse. Daraus lässt sich ableiten, dass die Methode [getActivity] die Aktivität des Fragments zurückgibt, sobald das Fragment sichtbar ist. Es fällt außerdem auf, dass die Methode [getActivity] einen Wert zurückgibt, sobald die Ausführung die Methode [update] des Fragments erreicht, das 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. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-07] in [Exemple-08]. Dazu folgen wir der Vorgehensweise, die in Abschnitt 1.4 zum Duplizieren von [Exemple-02] in [Exemple-03] beschrieben wurde.

1.10.2. Die Sitzung

In diesem neuen Projekt möchten wir, dass die Fragmente die Gesamtzahl der vom Benutzer angezeigten Fragmente anzeigen. Dazu muss ein Zähler gepflegt werden, auf den alle Fragmente zugreifen können. Wir werden das Objekt, das die von den Fragmenten gemeinsam genutzten Daten kapselt, als „Sitzung“ bezeichnen. Diese Terminologie stammt aus der Webentwicklung, wo man die Daten, die von verschiedenen, vom selben Benutzer angeforderten Ansichten gemeinsam genutzt werden sollen, in einer Sitzung speichert. Die Kapselung der von den verschiedenen Fragmenten gemeinsam genutzten Informationen in einem einzigen Objekt sorgt für mehr Lesbarkeit.

Die Klasse [Session] sieht wie folgt aus:

  

package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // Anzahl der besuchten Fragmente
  private int numVisit;

  // Getter und Setter

  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 Annotation AA. Das Attribut [scope] bezeichnet den Geltungsbereich (oder die Lebensdauer) der so annotierten Klasse. Hier bewirkt das Attribut [scope = EBean.Scope.Singleton], dass die Klasse [Session] ein Singleton ist: Sie wird einmal und nur einmal beim Start der Anwendung instanziiert. Die Referenz einer mit [EBean] annotierten Klasse kann anschließend in eine andere Klasse injiziert werden. Dies ist das Konzept der Abhängigkeitsinjektion;

1.10.3. Die Aktivität [MainActivity]

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


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  ...

  // Session-Injektion
  @Bean(Session.class)
  protected Session session;

  // Anzahl der Fragmente
  private final int FRAGMENTS_COUNT = 5;
  // Nachbarschaft der Fragmente
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

    @AfterInject
  protected void afterInject(){
    Log.d("MainActivity", "afterInject");

    // Initialisierung der Sitzung
    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 so annotierte Feld darf nicht den Geltungsbereich [private] haben;
  • Zeile 15: Die Annotation [@AfterInject] dient dazu, eine Methode zu benennen, die aufgerufen werden soll, sobald alle Injektionen der Klasse abgeschlossen sind. Wenn also die Methode [afterInject] in Zeile 16 aufgerufen wird, ist die Referenz aus Zeile 8 bereits initialisiert;
  • Zeile 20: Der Besucherzähler wird auf Null gesetzt;

1.10.4. Das Fragment [PlaceholderFragment]

Das Fragment [PlaceholderFragment] verläuft wie folgt:


@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends Fragment {

....

  // Sitzung
  protected Session session;

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // übergeordnetes Element
    super.setUserVisibleHint(isVisibleToUser);
    // Speicher
    this.isVisibleToUser = isVisibleToUser;
    // Protokoll
    Log.d("PlaceholderFragment", String.format("setUserVisibleHint %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // Anzahl der Besuche
    if (isVisibleToUser) {
      // Fragment aktualisieren
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // Das Fragment wird ausgeblendet
      updateDone = false;
    }
  }

  // Fragment aktualisieren
  public void update() {
    // Protokoll
    Log.d("PlaceholderFragment", String.format("update %s : %s", getArguments().getInt(ARG_SECTION_NUMBER), getInfos()));
    // Sitzung
    if (session == null) {
      session = ((MainActivity) getActivity()).getSession();
    }
    // Besuchsnummer erhöhen
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // Text geändert
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }
  • Zeile 7: die Sitzung;
  • Zeilen 35–37: Wir wissen, dass bei Aufruf der Methode [update] die Methode [getActivity] die Aktivität korrekt zurückgibt. Wir nutzen dies, um die Sitzung abzurufen und lokal zu speichern (Zeile 36);
  • Zeilen 39–41: Um die Besuchsnummer zu erhöhen, holen wir diese aus der Sitzung. Wir hätten diesen Code auch in die Methode [setUserVisibleHint] ab Zeile 19 einfügen können, da wir wissen, dass die Methode [getActivity] die Aktivität dann zurückgibt. Wir entscheiden uns hier, dieser Methode keine besondere Rolle zuzuweisen und den fragment-spezifischen Code in die dafür vorgesehene Methode [update] zu verlagern;
  • Zeile 43: Zeigt die Besuchsnummer an;

Wenn man diese Anwendung mit 5 Fragmenten und einer Nachbarschaft von 2 Fragmenten ausführt, 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: Es ist ersichtlich, dass die Methode [afterInject] der Aktivität vor ihrer Methode [afterViews] ausgeführt wird;

Der Leser ist eingeladen, diese neue Anwendung zu testen.

1.10.5. Wischgeste deaktivieren

In der vorherigen Anwendung wechselt die aktuelle Ansicht, wenn man mit der Maus im Android-Emulator nach links oder rechts wischt, je nach Fall zur rechten oder linken Ansicht. Dieses Standardverhalten ist nicht immer erwünscht. Wir werden lernen, wie man das Wischen zwischen den Ansichten (Swipe) deaktiviert.

Kehren wir zur Hauptansicht XML zurück:

  

Im Code XML der Ansicht finden wir den Code des Fragment-Containers:


  <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 bezeichnet die Klasse, die die Seiten der Aktivität verwaltet. Diese Klasse findet sich in der Aktivität [MainActivity]:


import android.support.v4.view.ViewPager;
...

@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // der Fragment-Manager
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // der Fragment-Container
  @ViewById(R.id.container)
  protected ViewPager mViewPager;
...

In Zeile 12 ist der Fragmentcontainer vom Typ [android.support.v4.view.ViewPager] (Zeile 1). Um das Scannen zu deaktivieren, muss diese Klasse wie folgt abgeleitet werden:

  

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 {

  // Steuert das Wischen
  private boolean isSwipeEnabled;

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

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

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

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

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

}
  • Zeile 8: Die Klasse [MyPager] erweitert die Android-Klasse [ViewPager] (Zeile 4);
  • Bei einem Wischen mit der Hand 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 unterbinden;
  • Zeile 11: Der boolesche Wert, der angibt, ob die Handbewegung akzeptiert wird oder nicht.

Anschließend muss nun unser neuer Seitenhandler verwendet werden. Dies geschieht in der Ansicht XML [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"/>

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


package exemples.android;

...
@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

  // Der Fragment-Manager
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // der Fragmentcontainer
  @ViewById(R.id.container)
  protected MyPager mViewPager;

  @AfterViews
  protected void afterViews() {
    Log.d("MainActivity", "afterViews");
...
    // Der Fragmentcontainer ist dem Fragmentmanager zugeordnet
    // d. h., das Fragment Nr. i des Fragmentcontainers ist das vom Fragmentmanager ausgegebene Fragment Nr. i
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // Das Wischen zwischen Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);
    // Die Registerleiste ist ebenfalls mit dem Fragmentcontainer verknüpft
...
  • Zeile 12: Der Seitenmanager hat nun den Typ [MyPager];
  • Zeile 23: Das Wischen mit der Hand wird entweder deaktiviert oder nicht.

Testen Sie diese neue Version. Deaktivieren Sie das Scrollen oder lassen Sie es aktiviert 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 Scrollen deaktiviert sein. Wir werden nicht erneut darauf hinweisen.

1.10.6. Scrollen zwischen Fragmenten deaktivieren

Fahren wir mit einer Verbesserung des Tab-Managers fort. Wenn man von Tab 1 zu Tab 4 wechselt, sieht man, wie die beiden dazwischenliegenden Tabs 2 und 3 durchlaufen. Dies wird im Android-Jargon als smoothScrolling bezeichnet. Dieses Verhalten kann bei einer großen Anzahl von Registerkarten störend sein. Es lässt sich unterbinden, indem man den folgenden Code in den Fragment-Manager [MyPager] einfügt:


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

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

  // Setter
...

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

Da der Tab-Manager mit dem Fragment-Manager [MyPager] verknüpft wurde, wird beim Klicken auf den Tab Nr. i das Fragment Nr. i vom Fragment-Container über die oben genannte 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 bei [false] sorgt für einen sofortigen Übergang zwischen dem alten und dem neuen Fragment (kein Scrollen), bei [true] für einen Übergang über scrolling. Hier ist das zweite Argument der Wert des Feldes in Zeile 4, ein Feld, das der Entwickler mit der Methode in den Zeilen 16–18 festlegen kann;

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


...
    // Offset der Fragmente
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // Wischen zwischen Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);

    // kein Scrollen
    mViewPager.setScrollingEnabled(false);
...

Führen Sie das Projekt erneut aus und überprüfen Sie, ob beispielsweise zwischen den Registerkarten 1 und 4 kein scrolling mehr vorhanden ist. Im weiteren Verlauf werden wir das Scrollen immer deaktivieren. Wir werden nicht noch einmal darauf 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.

Zunächst kopieren wir die Ansicht [vue1.xml] aus dem Projekt [Exemple-04] in das Projekt [Exemple-09] [1]:

 
  • in [1] die Ansicht [vue1.xml];
  • in [3] weist die Ansicht Fehler auf, die auf fehlende Texte in der Datei [res/values/strings.xml] zurückzuführen sind;

In [2] werden die fehlenden Texte hinzugefügt, indem sie aus der Datei [res/values/strings.xml] des Projekts [Exemple-04] übernommen werden


<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>
  <!-- Ansicht 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 wurden 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 {

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

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

  • Zeile 10: Die Anmerkung [@EFragment] bewirkt, dass das von der Aktivität verwendete Fragment tatsächlich die Klasse [Vue1Fragment_] ist. Das muss man sich merken. Das Fragment ist der Ansicht [vue1.xml] zugeordnet;
  • Zeilen 14–15: Die durch [R.id.editTextNom] identifizierte Komponente wird in das Feld [editTextNom] in Zeile 15 eingefügt;
  • Zeilen 18–20: Die Methode [doValider] verarbeitet das „Click“-Ereignis auf der Schaltfläche mit der Kennung [R.id.buttonValider];
  • Zeile 21: Der erste Parameter von [Toast.makeText] ist vom Typ [Activity]. Die Methode [Fragment.getActivity()] ermöglicht es, die Aktivität zu ermitteln, in der sich das Fragment befindet. Dabei handelt es sich um [MainActivity], da wir in dieser Architektur nur eine Aktivität haben, die verschiedene Ansichten oder Fragmente anzeigt;

In der Klasse [MainActivity] entwickelt sich der Fragment-Manager wie folgt:


public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private Fragment[] fragments;
    // Fragmentnummer
    private static final String ARG_SECTION_NUMBER = "section_number";

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // übergeordnetes Element
      super(fm);
      // Initialisierung des Fragment-Arrays
      fragments = new Fragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        // Ein Fragment wird erstellt
        fragments[i] = new PlaceholderFragment_();
        // Dem Fragment können Argumente übergeben werden
        Bundle args = new Bundle();
        args.putInt(ARG_SECTION_NUMBER, i + 1);
        fragments[i].setArguments(args);
      }
      // ein Fragment von +
      fragments[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 (auf den Unterstrich achten);

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

1.10.8. Alle Fragmente von derselben abstrakten Klasse ableiten

Auch das neue Fragment [Vue1Fragment] muss aktualisiert werden, wenn es angezeigt wird. Dazu müssen wir einen Code erstellen, der dem für das Fragment [PlaceholderFragment] erstellten ähnelt. Um Wiederholungen zu vermeiden, werden wir alles, was sich faktorisieren lässt, in eine abstrakte Klasse auslagern, von der alle Fragmente der Anwendung erben werden.

Dazu erstellen wir ein neues Projekt.

1.11. Beispiel 10: Alle Fragmente von einer abstrakten Klasse ableiten

1.11.1. Erstellung des Projekts

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

1.11.2. Verwaltung des Debug-Modus

Wir erweitern das Projekt um die Möglichkeit, die Protokolle des Debug-Modus anzuzeigen oder zu unterdrücken. Dazu fügen wir der Klasse [MainActivity] eine statische Konstante hinzu:


  // Debug-Modus
public static final boolean IS_DEBUG_ENABLED = false;

1.11.3. Die abstrakte übergeordnete Klasse 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 Daten
  private boolean isVisibleToUser = false;
  private boolean updateDone = false;
  private String className;

  // Daten,  auf die untergeordnete Klassen zugreifen können
  protected boolean afterViewsDone = false;
  protected boolean isDebugEnabled = true;

  // Aktivität
  protected MainActivity activity;

  // Sitzung
  protected Session session;

  // Konstruktor
  public AbstractFragment() {
    // Init
    isDebugEnabled = MainActivity.IS_DEBUG_ENABLED;
    className = getClass().getSimpleName();
    // Protokoll
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // übergeordnet
    super.setUserVisibleHint(isVisibleToUser);
    ...
  }

  @Override
  public void onDestroyView() {
    // Elternteil
    super.onDestroyView();
    ...
  }

  @Override
  public void onResume() {
    // Elternteil
    super.onResume();
    ...
  }

  // lokale Informationen
  protected String getParentInfos() {
    return String.format("className=%s, isVisibleToUser=%s, updateDone=%s, afterViewsDone=%s", className, isVisibleToUser, updateDone, afterViewsDone);
  }

  // Fragment aktualisieren
  protected void update() {
    ...
    // Die untergeordnete Klasse wird aufgefordert, sich zu aktualisieren
    updateFragment();
  }

  protected abstract void updateFragment();
}
  • Zeile 7: Die Klasse [AbstractFragment] erweitert die Android-Klasse [Fragment];
  • Jedes Fragment muss aktualisiert werden können. Aus diesem Grund schreibt die übergeordnete Klasse [AbstractFragment] ihren untergeordneten Klassen vor, dass sie über eine Methode [updateFragment] (Zeile 68) verfügen müssen, die sie aufruft (Zeile 65);
  • Zeile 19: Die Klasse speichert eine Referenz auf die Aktivität der Anwendung;
  • Zeile 22: Die Klasse speichert eine Referenz 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 von Zeile 16;
  • Zeile 28: Der Name der instanziierten Klasse, also der Name einer Unterklasse, wird gespeichert;
  • Zeilen 15–22: Diese Felder verfügen über das Attribut [protected], damit die untergeordneten Klassen darauf zugreifen können. Es ist zu beachten, dass die untergeordneten Klassen die Existenz der Booleschen Werte [isVisibleToUser] und [updateDone] (Zeilen 10–11) ignorieren;
  • Zeile 57: Die Methode [getParentInfos] verfügt über das Attribut [protected], damit die untergeordneten Klassen sie aufrufen können;

Die Methoden [setUserVisibleHint, onDestroyView, onResume] entsprechen weiterhin denen in der Klasse [PlaceholderFragment] des vorherigen Projekts:


@Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // übergeordneter
    super.setUserVisibleHint(isVisibleToUser);
    // Speicher
    this.isVisibleToUser = isVisibleToUser;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("setUserVisibleHint : %s", getParentInfos()));
    }
    // Fall, in dem das Fragment sichtbar wird
    if (isVisibleToUser) {
      // Fragment aktualisieren
      if (afterViewsDone && !updateDone) {
        update();
        updateDone = true;
      }
    } else {
      // das Fragment wird verlassen
      updateDone = false;
    }
  }

  @Override
  public void onDestroyView() {
    // übergeordnetes Element
    super.onDestroyView();
    // Flag aktualisieren
    afterViewsDone = false;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onDestroyView : %s", getParentInfos()));
    }
  }

  @Override
  public void onResume() {
    // übergeordnet
    super.onResume();
    // Protokoll
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("onResume : %s", getParentInfos()));
    }
    if (isVisibleToUser) {
      // Aktualisierung
      if (!updateDone) {
        update();
        updateDone = true;
      }
    }
  }

Die Methode [update] lautet wie folgt:


  // Fragment-Aktualisierung
  protected void update() {
    // Aktivität und Sitzung werden abgerufen
    if (activity == null) {
      Activity activity = getActivity();
      if (activity != null) {
        this.activity = (MainActivity) activity;
        this.session = this.activity.getSession();
      }
    }
    // die untergeordnete Klasse wird aufgefordert, sich zu aktualisieren
    updateFragment();
}

Dem obigen Code zufolge ist ein Fragment sichtbar, wenn die Methode [update] dieses Fragments ausgeführt wird. Dies ist wichtig, da es bedeutet, dass die Methode [Fragment.getActivity] dann eine Referenz auf die Aktivität der Anwendung zurückgibt (siehe Abschnitt 1.10.8), was anschließend Zugriff auf die Sitzung ermöglicht.

  • Zeilen 4–10: Die Aktivität und die Sitzung werden initialisiert, sofern dies nicht bereits geschehen ist;
  • Zeile 12: Die Methode [updateFragment] der untergeordneten Klasse wird aufgerufen. Wenn diese 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] entwickelt sich wie folgt:


package exemples.android;

import android.support.v4.app.Fragment;
import android.util.Log;
import android.widget.TextView;
import org.androidannotations.annotations.*;

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.fragment_main)
public class PlaceholderFragment extends AbstractFragment {

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

  // Daten
  private boolean initDone;

  // Daten
  private String text;
  private int numVisit;

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

  // Konstruktor
  public PlaceholderFragment() {
    super();
    // Protokoll
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", "constructor");
    }
  }


  @AfterViews
  protected void afterViews() {
    // Speicher
    afterViewsDone = true;
 ...
  }

  // Fragment-Update
  public void updateFragment() {
  ...
  }

}
  • Zeile 10: Die Klasse [PlaceholderFragment] erweitert die Klasse [AbstractFragment]. Bei dieser Architektur umfasst das Schreiben eines Fragments Folgendes:
    • die Methode [@AfterViews] zu schreiben, die dazu dient, das Fragment zu Beginn seines Lebenszyklus zu initialisieren oder es zurückzusetzen, falls zuvor ein [onDestroyView] stattgefunden hat. Zeile 39 ist zwingend erforderlich, um den Lebenszyklus des Fragments korrekt zu verwalten;
    • die Methode [updateFragment] zu schreiben, die das Fragment unmittelbar vor seiner Anzeige aktualisiert. Diese Methode kann die Sitzung ihrer übergeordneten Klasse nutzen;
    • die Ereignisbehandler des Fragments zu schreiben. 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() {
    // Speicher
    afterViewsDone = true;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("afterViews %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    if (!initDone) {
      // Ausgangstext
      text = getString(R.string.section_format, getArguments().getInt(ARG_SECTION_NUMBER));
      // Initialisierung abgeschlossen
      initDone = true;
    }
    // aktueller Text anzeigen
    textViewInfo.setText(text);
  }

  // Fragment aktualisieren
  public void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), getParentInfos(), getLocalInfos()));
    }
    // Besuchsnummer erhöhen
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // Text geändert
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
  }

  // Lokale Informationen für Protokolle
  protected String getLocalInfos() {
    return String.format("numVisit=%s, initDone=%s, getActivity()==null:%s",
      numVisit, initDone, getActivity() == null);
  }
  • Zeile 7 und 23: In den Protokollen werden die Informationen der übergeordneten Klasse mit der geerbten Methode [getParentInfos] angezeigt;

1.11.5. Die Klasse [Vue1Fragment]

  

Die Klasse [Vue1Fragment] weist dieselbe Struktur auf 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 {

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

  // Daten
  private int numVisit;

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

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

  // Lokale Informationen für Protokolle
  protected String getLocalInfos() {
    return String.format("numVisit=%s", numVisit);
  }

  // Fragment aktualisieren
  @Override
  protected void updateFragment() {
    // Besuchsnummer erhöhen
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // Die Besuchsnummer wird angezeigt
    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 den Booleschen Wert [afterViewsDone] auf true zu setzen, da diese Information von der übergeordneten Klasse verwendet wird;
  • Zeilen 42–49: Die Methode [updateFragment] dient dazu, eine kurze Meldung mit der Besuchsnummer anzuzeigen (Zeile 48) und diese Nummer in der Sitzung zu erhöhen (Zeilen 44–46);

Der Leser ist eingeladen, dieses neue Projekt zu testen.

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

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

1.11.6. Zuordnung von Registerkarten zu Fragmenten

In der Klasse [MainActivity], die die Registerkarten verwaltet, steht:


// Die Registerleiste ist ebenfalls mit dem Fragmentcontainer verknüpft
// d. h., die Registerkarte Nr. i zeigt das Fragment Nr. i des Containers an
tabLayout.setupWithViewPager(mViewPager);

Zeile 3 verknüpft den Registerkarten-Manager mit dem Fragment-Container. Eine Folge dieser Verknüpfung haben wir bereits gesehen: Wenn der Benutzer auf die Registerkarte Nr. i klickt, lässt der Fragment-Container das Fragment Nr. i anzeigen. Das Gegenteil haben wir noch nicht gesehen: Wenn der Fragment-Container angewiesen wird, das Fragment Nr. i anzuzeigen, wird automatisch die Registerkarte 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, wird der Fragment-Container aufgefordert, das Fragment Nr. i anzuzeigen. Wir werden dann sehen, ob die Registerkarte Nr. i ausgewählt wurde oder nicht.

Dieser Schritt beginnt mit der Änderung 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] definiert [2]:

<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>
  <!-- Ansicht 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>
  <!-- Menü -->
  <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 Verarbeitung von Klicks auf diese Menüoptionen erfolgt in der Klasse [MainActivity]:


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

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // Das angezeigte Fragment wird geändert
      mViewPager.setCurrentItem(i);
    }
  }
  • Zeile 2: Die Methode [onOptionsItemSelected] wird aufgerufen, wenn ein Klick auf eine der Menüoptionen erfolgt;
  • Zeile 8: Die ID der angeklickten Option wird abgerufen;
  • Zeilen 9–36: Die verschiedenen Fälle werden von der Methode switch verarbeitet;
  • Zeilen 16–36: Der Klick auf die Option [Fragment i] führt zur Methode [showFragment(i-1)] in den Zeilen 41–45;
  • Zeile 43: Der Fragment-Container wird aufgefordert, das angeforderte Fragment anzuzeigen;
  • Zeile 42: Zuvor wird überprüft, ob dies möglich ist (Bedingung 1) und ob es erforderlich ist (Bedingung 2);

Der Leser ist eingeladen, diese neue Version zu testen. Man stellt fest, dass bei der Anforderung der Anzeige des Fragments Nr. i dieses tatsächlich angezeigt wird und die Registerkarte Nr. i selbst ausgewählt ist.

Nachdem wir nun gesehen haben, wie die Verknüpfung zwischen Registerkarten und Fragmenten funktioniert, wollen wir uns einem anderen Fall zuwenden: dem Fall, in dem die Verwaltung der Registerkarten von der der Fragmente getrennt 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. Erstellung des Projekts

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

1.12.2. Ziele

Die neue Anwendung wird zwei Registerkarten haben:

  • Die erste Registerkarte zeigt immer das Fragment [Vue1] an;
  • die zweite Registerkarte zeigt ein im Menü ausgewähltes Fragment an;

Image

  • in [1] das Fragment [Vue1];
  • in [2] das vom Benutzer ausgewählte Fragment vom Typ [PlaceholderFragment];
  • in [3] wird die Zählung der Besuche fortgesetzt;

1.12.3. Die Sitzung

  

Die neue Sitzung lautet wie folgt:


package exemples.android;

import org.androidannotations.annotations.EBean;

@EBean(scope = EBean.Scope.Singleton)
public class Session {
  // Anzahl der aufgerufenen Fragmente
  private int numVisit;
  // Nummer des Fragments vom Typ [PlaceholderFragment], das im zweiten Reiter angezeigt wird
  private int numFragment;

  // Getter und Setter
...
}
  • Zeile 10: Wir werden das Klicken auf die Registerkarten selbst verwalten. Wenn auf eine Registerkarte geklickt wird, muss das Fragment wiederhergestellt werden, das bei der letzten Auswahl angezeigt wurde. Das Feld [numFragment] speichert die Nummer des Fragments für Registerkarte Nr. 2, eine Zahl in [0, Fragments_COUNT-2]. Wenn auf Registerkarte Nr. 2 geklickt wird, wird in der Sitzung die Nummer des anzuzeigenden Fragments abgerufen;

1.12.4. Das Menü

  

Das Menü [res / menu / menu_main.xml] entwickelt 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>

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

1.12.5. Die Klasse [MainActivity]

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


  // der Registerkarten-Manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
...
@AfterViews
  protected void afterViews() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }
    ...

    // kein Scrolling
    mViewPager.setScrollingEnabled(false);

    // Anzeige „Ansicht1“
    mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

    // Zu Beginn gibt es nur eine Registerkarte
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);

    // Ereignismanager
    tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        // Eine Registerkarte wurde ausgewählt – das vom Fragment-Container angezeigte Fragment wird geändert
        ...
      }

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

      }

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

      }
    });

...

}
  • Zeile 17: Das erste vom Fragmentcontainer angezeigte Fragment ist das Fragment [Vue1Fragment]. Konstruktionsbedingt ist dies das letzte Fragment des Containers;
  • Zeilen 20–22: Da keine Zuordnung zwischen Registerkarten und dem Fragmentcontainer vorgenommen wurde, 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 vergeben einen Titel. In den vorherigen Beispielen war der Titel der Registerkarten der Titel der Fragmente. Das ist nun vorbei. Daher entfernen wir die Methode [getPageTitle] aus dem Fragment-Manager. Wir brauchen sie nicht mehr:

    // optional – Vergabe eines Titels für die verwalteten Fragmente
    @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 wird auf dieser Registerkarte angezeigt? Man muss verstehen, dass Registerkarten und Fragmente zwei voneinander unabhängige Konzepte sind. Das angezeigte Fragment ist immer dasjenige, das vom Fragment-Container ausgewählt wurde. Wechselt man die Registerkarte und fordert den Container nicht auf, 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 das in Zeile 17 ausgewählte: das Fragment [Vue1Fragment];
  • Zeilen 26–30: Die Methode, die geschrieben werden muss, um den Wechsel der Registerkarte durch den Benutzer zu verarbeiten;

Die Methode [onTabSelected] in den Zeilen 26–30 wird ausgelöst, sobald ein Registerwechsel erfolgt (wenn der Benutzer auf ein bereits ausgewähltes Register klickt, passiert nichts). Ihr Code lautet wie folgt:


      @Override
      public void onTabSelected(TabLayout.Tab tab) {
        if (IS_DEBUG_ENABLED) {
          Log.d("onglets", "onTabSelected");
        }
        // Eine Registerkarte wurde ausgewählt – das vom Fragmentcontainer angezeigte Fragment wird geändert
        // Position der Registerkarte
        int position = tab.getPosition();
        // Nummer des anzuzeigenden Fragments
        int numFragment;
        switch (position) {
          case 0:
            // Fragmentnummer [Vue1Fragment]
            numFragment = FRAGMENTS_COUNT - 1;
            break;
          default:
            // Fragmentnummer [PlaceholderFragment]
            numFragment = session.getNumFragment();
        }
        // Fragment anzeigen
        mViewPager.setCurrentItem(numFragment);
}
  • Zeile 8: Die Position der angeklickten Registerkarte wird abgerufen. Hier wird entweder die Zahl 0 oder 1 zurückgegeben;
  • Zeilen 12–15: Wenn die erste Registerkarte angeklickt wurde, werden Vorbereitungen getroffen, um das Fragment [Vue1Fragment] anzuzeigen;
  • Zeilen 16–18: In allen 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 war. Die Nummer dieses Fragments wurde damals in der Anwendungssitzung gespeichert;
  • Zeile 21: Der Fragment-Container wird aufgefordert, 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) {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("menu", "onOptionsItemSelected");
    }
    // Bearbeitung der Menüoptionen
    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;
      }
    }
    // Bearbeiteter Eintrag
    return true;
}
  • Zeilen 16–31: Verwaltung der 4 Menüoptionen. Jeder Handler ruft die Methode [showFragment] mit der Nummer des anzuzeigenden Fragments auf;

Die Methode [showFragment] lautet wie folgt:


  // Registerkarte Nr. 2
  private TabLayout.Tab tab2 = null;

  private void showFragment(int i) {
    if (i < FRAGMENTS_COUNT && mViewPager.getCurrentItem() != i) {
      // Wenn die zweite Registerkarte noch nicht existiert, wird sie angelegt
      if (tab2 == null) {
        tab2 = tabLayout.newTab();
        tabLayout.addTab(tab2);
      }
      // Der Titel der zweiten Registerkarte wird festgelegt
      tab2.setText(String.format("Fragment n° %s", (i + 1)));
      // Das angezeigte Fragment wird geändert
      mViewPager.setCurrentItem(i);
      // Die Nummer des angezeigten Ausschnitts wird in der Sitzung gespeichert
      session.setNumFragment(i);
      // Die Registerkarte 2 wird ausgewählt – es geschieht nichts, wenn diese bereits ausgewählt ist
      tab2.select();
    }
}
  • Man erinnere sich daran, dass zu Beginn der Anwendung nur eine Registerkarte vorhanden ist;
  • Zeile 2: Zu Beginn ein Verweis auf Registerkarte Nr. 2, null;
  • Zeile 5: Die Anzeigebedingungen haben sich gegenüber der vorherigen Version nicht geändert;
  • Zeilen 7–10: Wenn die Registerkarte Nr. 2 noch nicht existiert, wird sie angelegt (Zeile 8) und zur Registerkartenleiste hinzugefügt (Zeile 9);
  • Zeile 12: In den Titel der zweiten Registerkarte wird die Nummer des anzuzeigenden Fragments eingetragen, wobei die Nummerierung bei 1 beginnt;
  • Zeile 14: Das gewünschte Fragment wird angezeigt;
  • Zeile 16: Seine Nummer wird in die Sitzung geschrieben;
  • Zeile 18: Die 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 fordert dann den Fragment-Container auf, das bereits in Zeile 14 angezeigte Fragment anzuzeigen. Ein einfacher Test in der Methode [onTabSelected] verhindert diesen Fall:

        // Anzeige des Fragments nur, wenn erforderlich
        if (numFragment != mViewPager.getCurrentItem()) {
          mViewPager.setCurrentItem(numFragment);
}

Der Leser ist eingeladen, diese neue Version zu testen.

1.12.6. Verbesserungen

Wir verfügen nun über ein gutes Verständnis der Fragmente, ihres Lebenszyklus, des Konzepts der Fragmentbenachbarung und ihrer Beziehung zur Registerleiste. Außerdem verfügen wir über eine robuste Architektur, die gerade den Test aus Beispiel 11 bestanden hat:

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

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

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

In diesem Beispiel wollen wir die minimalen Beziehungen zwischen Aktivität und Fragmenten definieren. Dazu verwenden wir:

  • eine Schnittstelle [IMainActivity], die festlegt, was die 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. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-11] in [Exemple-12] gemäß der Vorgehensweise in Abschnitt 1.4. Dabei erhalten wir folgendes Ergebnis:

1.13.2. Die Schnittstelle [IMainActivity]

Aus den vorangegangenen Beispielen geht hervor, dass die Fragmente Zugriff auf die von der Aktivität instanziierte Sitzung benötigen. Außerdem ist in diesen Beispielen zwar nicht sichtbar, aber vorhersehbar: Die Ereignisbehandler der Fragmente enden manchmal mit einem Wechsel der Ansicht. Die Aktivität wird aufgefordert, diesen Wechsel durchzuführen. Die Schnittstelle [IMainActivity] könnte dann wie folgt aussehen:

  

package exemples.android;

public interface IMainActivity {

  // Zugriff auf die Sitzung
  Session getSession();

  // Wechsel der Ansicht
  void navigateToView(int position);

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;
}

In Zeile 12 ist eine Konstante zu beachten, 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 kaum:


  // Daten  für untergeordnete Klassen zugänglich
  protected boolean afterViewsDone = false;
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;

  // Aktivität
  protected IMainActivity mainActivity;
  protected Activity activity;

...
  // Fragment aktualisieren
  protected void update() {
    // Aktivität und Sitzung werden abgerufen
    if (mainActivity == null) {
      this.activity = getActivity();
      if (this.activity != null) {
        this.mainActivity = (IMainActivity) activity;
        this.session = this.mainActivity.getSession();
      }
    }
    // die untergeordnete Klasse wird aufgefordert, sich zu aktualisieren
    updateFragment();
}
  • Zeilen 6 und 7: Es werden zwei Arten von Verweisen auf die Aktivität gepflegt:
    • 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 jede Aktivität;

Diese beiden Referenzen verweisen natürlich auf dasselbe Objekt. Dieses wird jedoch als zwei verschiedene Typen angesehen. Dadurch vermeiden wir Typumwandlungen zur Laufzeit;

  • Zeile 14: Wir rufen mithilfe der Methode [getActivity] eine Referenz auf die Aktivität 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] sowie die Sitzung;

1.13.4. Änderung des Fragment-Managers

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


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private AbstractFragment[] fragments;
    // Fragmentnummer
    private static final String ARG_SECTION_NUMBER = "section_number";

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // Elternteil
      super(fm);
      // Initialisierung des Fragment-Arrays
      fragments = new AbstractFragment[FRAGMENTS_COUNT];
      for (int i = 0; i < fragments.length - 1; i++) {
        ...
      }
      // ein Fragment von +
      fragments[fragments.length - 1] = new Vue1Fragment_();
    }

    // Fragment-Nr. Position
    @Override
    public AbstractFragment getItem(int position) {
      ...
    }

    // gibt die Anzahl der verwalteten Fragmente zurück
    @Override
    public int getCount() {
      ...
    }
}

1.13.5. Änderung der Klasse [MainActivity]

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


@EActivity(R.layout.activity_main)
public class MainActivity extends AppCompatActivity implements IMainActivity{

...
  // Sitzung einfügen
  @Bean(Session.class)
  protected Session session;
...
  // Sitzung abrufen
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // die Ansicht „position“ wird angezeigt
    if(mViewPager.getCurrentItem()!=position){
      // Anzeige des Fragments
      mViewPager.setCurrentItem(position);
    }
  }

  • Zeilen 10–12: Die Methode [getSession] existierte bereits;
  • Zeilen 15–22: Die Methode [navigateToView] lässt das Fragment Nr. [position] anzeigen;
  • Zeile 17: Es wird geprüft, ob etwas zu tun ist;
  • Zeile 19: Das Fragment Nr. [position] wird angezeigt;

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

1.13.6. Änderung der Anzeige der Fragmente in [MainActivity]

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


    // Anzeige von Ansicht1
mViewPager.setCurrentItem(FRAGMENTS_COUNT - 1);

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

navigateToView(...);

Führen Sie anschließend die Anwendung aus. Sie sollte weiterhin funktionieren.

1.13.7. Fazit

Von nun an werden wir immer die vorherige Architektur verwenden:

  • eine Aktivität, die die Schnittstelle [IMainActivity] implementiert;
  • Fragmente, die die Klasse [AbstractFragment] erweitern, wodurch sie verpflichtet sind, die Methode [updateFragment] zu implementieren. Diese müssen außerdem über eine Methode [@AfterViews] verfügen, in der sie den booleschen Wert [afterViewsDone] auf true setzen;
  • eine Sitzung, die die zwischen Fragmenten und Aktivitäten auszutauschenden Daten kapselt;

1.14. Beispiel-13: Beispiel-05 mit Fragmenten

Im Projekt [Exemple-05] haben wir die Navigation zwischen Ansichten eingeführt. Dabei handelte es sich um eine 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. Erstellung des Projekts

Wir duplizieren das vorherige Projekt [Exemple-12] in [Exemple-13] gemäß der Vorgehensweise in Abschnitt 1.4. Wir erhalten folgendes Ergebnis:

1.14.2. Strukturierung des Projekts

Wir werden nun damit beginnen, Pakete zur Organisation des Codes zu verwenden. Derzeit lassen sich zwei unterschiedliche Bereiche unterscheiden:

  • die Verwaltung der Aktivitäten;
  • die Verwaltung der Fragmente;

Dafür erstellen wir zwei Pakete: [exemples.android.activity] und [exemples.android.fragments]:

 

Auf die gleiche Weise erstellen wir das Paket „[exemples.android.fragments]“:

In [8] erstellen wir ein drittes Paket namens [architecture], in das wir die Entitäten [IMainActivity, AbstractFragment, Session, MyPager] einfügen, die die Grundbausteine der Architektur unserer Anwendung bilden. Dies soll uns daran erinnern, dass wir uns für eine bestimmte Architektur entschieden haben. Verschieben Sie anschließend die vorhandenen Elemente des Projekts wie in [9] angegeben. Jede Verschiebung muss durch einen Klick auf die Schaltfläche [Refactor] bestätigt werden.

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

 

Beim Verschieben der Klassen in die Pakete hat Android Studio die erforderlichen Änderungen am Anwendungscode vorgenommen (z. B. Zeilen 18–21). Die von den Zeilen 15 und 17 betroffenen Klassen wurden nicht verschoben. Sie werden von der Android-Annotations-Bibliothek generiert. Bei diesen Klassen müssen die „imports“ manuell geändert werden. Diese Zeilen lauten somit:

 

Nachdem dies erledigt ist, treten keine Kompilierungsfehler mehr auf. Führen Sie die Anwendung aus. Es erscheint nun folgender Fehler:

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

Dieser Fehler stammt aus dem Manifest der Anwendung:

  

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

Die Zeilen 3 und 12 legen fest, dass die angegebene Aktivität [exemples.android.MainActivity_] ist. Da die Aktivität jedoch in das Paket [activity] migriert wurde, muss Zeile 12 nun wie folgt lauten:


      android:name=".activity.MainActivity_"

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

1.14.3. Bereinigung des Projekts

Im neuen Projekt:

  • gibt es keine Registerkarten, schwebenden Schaltflächen oder Menüs mehr;
  • die Fragmente „[PlaceholderFragment]“ sind verschwunden. Die App verwaltet nun zwei Fragmente: „[Vue1Fragment]“, das bereits vorhanden ist, und „[Vue2Fragment]“, das noch erstellt werden muss;
  • 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 Ansicht [res / layout / fragment_main.xml], die mit diesem Fragment [2] verknüpft ist.

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 {
  // Anzahl der besuchten Fragmente
  private int numVisit;
  // Nr. des Fragments vom Typ [PlaceholderFragment], das im zweiten Reiter angezeigt wird
  private int numFragment;

  // Getter und Setter

  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 behalten nichts aus dieser Sitzung bei.

Kompilieren Sie das Projekt. Die fehlerhaften Zeilen sind diejenigen, die den Inhalt der Sitzung verwendet haben. Löschen Sie diese. In der Klasse [Vue1Fragment] wird außerdem die Variable [numVisit] aus dem Code entfernt, der nun wie folgt lautet:


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 {

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

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

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


  // Fragment aktualisieren
  @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 Position in der Ansicht definiert;
  • im Code der Aktivität [MainActivity];

Das Menü wird ebenfalls an zwei Stellen entfernt:

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

Der Code der 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>
  • Die Zeilen [28-31, 41-47] werden entfernt;
  • die Symbolleiste in den Zeilen 18–24 wird ebenfalls entfernt;

Der Code des Menüs [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>
  • Die Zeilen 9–24 werden entfernt. Damit bleibt eine Option erhalten, die nicht verwendet wird. Dies dient lediglich als Beispiel für die Deklaration einer Menüoption, die per Kopieren/Einfügen reproduziert werden kann;

In der Klasse [MainActivity] entfernen wir alles, was sich auf die Registerkarten, die schwebende Schaltfläche, die Symbolleiste und das Menü bezieht. Um diese Verweise zu finden, ist es am einfachsten, ihre Deklaration zu löschen:


  // der Registerkarten-Manager
  @ViewById(R.id.tabs)
  protected TabLayout tabLayout;
  // die schwebende Schaltfläche
  @ViewById(R.id.fab)
protected FloatingActionButton fab;

und die Anwendung neu zu kompilieren. Die fehlerhaften Zeilen sind diejenigen, die auf die entfernten Elemente verweisen. Löschen Sie dann alle diese Zeilen. Passen Sie außerdem den Fragment-Manager so an, dass er nicht mehr auf das Fragment [PlaceholderFragment] verweist, das wir entfernt haben:


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private AbstractFragment[] fragments;

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // übergeordnetes Element
      super(fm);
    }

    // Fragment-Nr. Position
    @Override
    public AbstractFragment getItem(int position) {
      // Protokoll
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

    // gibt die Anzahl der verwalteten Fragmente zurück
    @Override
    public int getCount() {
      return fragments.length;
    }
}
  • Zeilen 7–10: Die gesamte Fragmentgenerierung wurde entfernt;

Zu diesem Zeitpunkt sollten keine Kompilierungsfehler mehr auftreten. In der Klasse [MainActivity] haben wir 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 {

  // der Fragment-Container
  @ViewById(R.id.container)
  protected MyPager mViewPager;
// die Symbolleiste
@ViewById(R.id.toolbar)
protected Toolbar toolbar;

  // Sitzungseinbindung
  @Bean(Session.class)
  protected Session session;

  // Anzahl der Fragmente
  private final int FRAGMENTS_COUNT = 5;
  // Nachbarschaft der Fragmente
  private final int OFF_SCREEN_PAGE_LIMIT = 2;

  // Debug-Modus
  public static final boolean IS_DEBUG_ENABLED = true;

  // der Fragment-Manager
  private SectionsPagerAdapter mSectionsPagerAdapter;

  // Konstruktor
  public MainActivity() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "constructor");
    }
  }

  @AfterViews
  protected void afterViews() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterViews");
    }

    // Symbolleiste – hier wird der Name der Anwendung angezeigt
    setSupportActionBar(toolbar);

    // der Fragment-Manager
    mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());

    // Der Fragment-Container ist mit dem Fragment-Manager verknüpft
    // d. h., das Fragment Nr. i des Fragmentcontainers ist das vom Fragmentmanager ausgegebene Fragment Nr. i
    mViewPager.setAdapter(mSectionsPagerAdapter);

    // Offset der Fragmente
    mViewPager.setOffscreenPageLimit(OFF_SCREEN_PAGE_LIMIT);

    // Das Wischen zwischen Fragmenten wird unterbunden
    mViewPager.setSwipeEnabled(false);

    // kein Scrollen
    mViewPager.setScrollingEnabled(false);

    // Ansicht1
    navigateToView(FRAGMENTS_COUNT - 1);

  }

  @AfterInject
  protected void afterInject() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
  }

  // Sitzung abrufen
  public Session getSession() {
    return session;
  }

  @Override
  public void navigateToView(int position) {
    // Die Ansicht „position“ wird angezeigt
    if (mViewPager.getCurrentItem() != position) {
      // Fragment anzeigen
      mViewPager.setCurrentItem(position);
    }
  }

  // der Fragment-Manager
  // Er wird dazu aufgerufen, die in der Hauptansicht anzuzeigenden Fragmente bereitzustellen
  // muss die Methoden [getItem] und [getCount] definieren – die anderen sind optional
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private AbstractFragment[] fragments;

    // Konstruktor
    public SectionsPagerAdapter(FragmentManager fm) {
      // übergeordnetes Element
      super(fm);
    }

    // Fragment-Nr. Position
    @Override
    public AbstractFragment getItem(int position) {
      // Protokoll
      if (IS_DEBUG_ENABLED) {
        Log.d("SectionsPagerAdapter", String.format("getItem[%s]", position));
      }
      return fragments[position];
    }

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

Es sind noch einige Änderungen vorzunehmen:

  • Löschen Sie Zeile 31, da sie nicht mehr benötigt wird;
  • Zeile 33: Setzen Sie die Fragment-Adjazenz auf 1;
  • Zeile 76: Wechseln Sie zur Ansicht 0. Diese wird als Erstes angezeigt;
  • Zeile 108: Initialisieren Sie das Array mit dem Fragment [Vue1Fragment_]:

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

Es gibt also nur ein einziges Fragment. Führen Sie die Anwendung aus. Sie sollten folgendes Ergebnis erhalten:

Image

Die Schaltfläche „[Valider]“ sollte funktionieren.

1.14.4. Erstellung der Fragmente und der zugehörigen Ansichten

Die Anwendung wird zwei Ansichten haben, nämlich die des Projekts [Exemple-05]. Die Ansicht [vue1.xml] ist im aktuellen Projekt bereits vorhanden. Wir duplizieren nun [vue2.xml] von [Exemple-05] nach [Exemple-12] (öffnen Sie beide Projekte und führen Sie Kopieren/Einfügen zwischen ihnen durch).

 
  • in [1], die neue Ansicht. Beim Versuch, diese zu bearbeiten, treten Fehler in [2] auf. Wir müssen die Datei [strings.xml] [3] ändern, um die von dieser neuen Ansicht referenzierten Zeichenfolgen 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>
  <!-- Ansicht 1 -->
  <string name="titre_vue1">Vue n° 1</string>
  <string name="txt_nom">Quel est votre nom ?</string>
  <string name="btn_valider">Valider</string>
  <!-- Ansicht 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 [Vue1Fragment] in [Vue2Fragment]:

  

und ändern 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() {
    // Speicher
    afterViewsDone = true;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // Fragment-Aktualisierung
  @Override
  protected void updateFragment() {
  }
}
  • Zeile 9: Das Fragment wird der Ansicht [res / layout / vue2.xml] zugeordnet;
  • Zeile 10: Die Klasse erweitert die abstrakte Klasse [AbstractFragment];
  • Zeilen 12–20: die obligatorische Methode [@AfterViews];
  • Zeilen 23–25: Die Methode [updateFragment] ist obligatorisch;

1.14.5. Einrichtung der Fragmente und der Navigation zwischen ihnen

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


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};

    ...
}

Die Schnittstelle [IMainActivity] gewährleistet die Navigation zwischen den Ansichten mit ihrer Methode [navigateToView]. Wir werden den Klick auf die Schaltfläche [Vue n° 2] des Fragments [Vue1Fragment] verarbeiten:


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 {

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

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

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

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    mainActivity.navigateToView(1);
  }

  // Fragment aktualisieren
  @Override
  protected void updateFragment() {
  }
}
  • Zeilen 37–40: Die Methode [showVue2] verarbeitet das Ereignis „Klick“ auf die Schaltfläche [Vue n° 2];
  • Zeile 39: Die Navigation erfolgt mit der Methode [navigateToView] der Aktivität. An dieser Stelle sei daran erinnert, dass die Aktivität in der übergeordneten Klasse in folgender Form gespeichert wurde:

  // Aktivität
protected IMainActivity mainActivity;

und dass diese Aktivität bereits initialisiert wurde, sobald man in einen beliebigen Ereignis-Handler gelangt.

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

protected Activity activity;

Ein ähnlicher Code findet sich 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() {
    // Speicher
    afterViewsDone = true;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("Vue2Fragment", String.format("afterViews %s", getParentInfos()));
    }
  }

  // Ereignisbehandler ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // Fragment-Aktualisierung
  @Override
  protected void updateFragment() {
  }
}
  • Zeilen 24–27: Die Methode [showVue1] verarbeitet das „Klick“-Ereignis auf der Schaltfläche [Vue n° 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:

  • Eingabe eines Namens in Ansicht Nr. 1;
  • Anzeige dieses Namens in Ansicht Nr. 2;

Damit Ansicht Nr. 1 den eingegebenen Namen an Ansicht Nr. 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;

  // Getter und Setter
...
}
  • Zeile 8: der eingegebene Name;

Die Klasse [MainActivity] initialisiert die Sitzung wie folgt:


  // Session-Injektion
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // Sitzung initialisieren
    session.setNom("");
}

1.14.7. Abschließendes Schreiben der Fragmente

Im Fragment [Vue1Fragment] ändern wir den Code des Klick-Handlers für die Schaltfläche [Valider]:


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 {

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

...
  // Ereignisbehandler ----------------------------------

  @Click(R.id.buttonValider)
  protected void doValider() {
    // Der eingegebene Name wird gespeichert
    String nom = editTextNom.getText().toString();
    // Anzeige
    Toast.makeText(activity, nom, Toast.LENGTH_LONG).show();
  }

  @Click(R.id.buttonVue2)
  protected void showVue2() {
    // Der eingegebene Name wird in die Sitzung geschrieben
    session.setNom(editTextNom.getText().toString());
    // Wechsel zur Ansicht Nr. 2
    mainActivity.navigateToView(1);
  }

  // Fragment aktualisieren
  @Override
  protected void updateFragment() {

  }
}
  • Zeilen: 31–37: Wir verarbeiten den Klick auf die Schaltfläche [Vue n° 2];
  • Zeile 34: Bevor zur Ansicht Nr. 2 gewechselt wird, wird der eingegebene Name in die Sitzung geschrieben, damit die neue Ansicht darauf zugreifen kann;

Die Ansicht [Vue2Fragment] 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 {

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

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

  // Ereignisbehandler ----------------------------------------------
  @Click(R.id.buttonVue1)
  protected void showVue1() {
    mainActivity.navigateToView(0);
  }

  // Fragment-Aktualisierung
  @Override
  protected void updateFragment() {
    // Der in der Sitzung eingegebene Name wird abgerufen
    String nom = session.getNom();
    // Anzeige
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
  }
}

Wenn die Ansicht Nr. 2 angezeigt wird, muss der in Ansicht Nr. 1 eingegebene Name angezeigt werden. Es ist bekannt, dass unmittelbar nach ihrer Anzeige die Methode [updateFragment] ausgeführt wird. Daher kann der Code zur Anzeige des Namens in dieser Methode (Zeilen 36–42) eingefügt werden.

  • Zeilen 16–17: Deklaration der einzigen visuellen Komponente der Ansicht;
  • Zeile 39: Der in Ansicht Nr. 1 eingegebene Name wird aus der Sitzung abgerufen;
  • Zeile 41: Die Bezeichnung „[textViewBonjour]“ wird geändert;

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

1.14.8. Verwaltung des Lebenszyklus von Fragmenten

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


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

Diese Methode ist unvollständig. Tatsächlich muss immer der Fall berücksichtigt werden, dass das Fragment nach einer Operation [onDestroyView] wiederverwendet wird. In diesem Fall wird die Ansicht von Fragment 1 neu generiert, und der zuvor möglicherweise eingegebene Name verschwindet aus der Ansicht. Das ist nicht erwünscht. Derzeit bleibt der eingegebene Name sichtbar, da die Nachbarschaft der Fragmente von 1 dazu führt, dass der Lebenszyklus des Fragments [Vue1Fragment] nur einmal ausgeführt wird. Es ist jedoch ratsam, den Fall der Wiederverwendung des Fragments zu berücksichtigen.

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

  • Man kann die Tatsache nutzen, dass die Methode [update] bei jeder Anzeige des Fragments systematisch ausgeführt wird, um den eingegebenen Namen zu aktualisieren;
  • man kann diese Aktualisierung nur dann vornehmen, wenn die Methode [@AfterViews] erneut ausgeführt wird. Wir entscheiden uns für den letztgenannten Ansatz;

Wir ändern den Code von [Vue1Fragment] wie folgt:


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

    // Daten
    private String nom;

    @AfterViews
    protected void afterViews() {
        // Speicher
        afterViewsDone = true;
        // Protokoll
        if (isDebugEnabled) {
            Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
        }
        // Der angezeigte Text wird (neu) initialisiert
        editTextNom.setText(nom);
    }

    // Ereignisbehandler ----------------------------------

...

    @Click(R.id.buttonVue2)
    protected void showVue2() {
        // Der eingegebene Name wird notiert, um ihn wieder abrufen zu können, falls das Fragment wiederverwendet wird
        nom = editTextNom.getText().toString();
        // Der eingegebene Name wird in die Sitzung geschrieben
        session.setNom(nom);
        // es wird zur Ansicht Nr. 2 gewechselt
        activity.navigateToView(1);
}
  • Zeile 27: Bevor von Ansicht 1 zu Ansicht 2 gewechselt wird, wird der eingegebene Name gespeichert;
  • Zeile 17: Bei jeder neuen Ausführung des Lebenszyklus des Fragments wird der zuletzt eingegebene Name erneut angezeigt;

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


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

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

  // Fragmentaktualisierung
  @Override
  protected void updateFragment() {
    // Der in der Sitzung eingegebene Name wird abgerufen
    String nom = session.getNom();
    // Anzeige des Namens
    textViewBonjour.setText(String.format("Bonjour %s !", nom));
}
  • Die einzige visuelle Komponente der Ansicht (Zeile 3) wird bei jeder Anzeige der Ansicht aktualisiert (Zeile 21). Die Methode [@AfterViews] muss daher nichts hinzufügen;

1.14.9. Fazit

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

  • eine Aktivität, die die Schnittstelle [IMainActivity] implementiert;
  • Fragmente, die die Klasse [AbstractFragment] erweitern, wodurch sie verpflichtet sind, die Methode [updateFragment] zu implementieren. Diese müssen zudem über eine Methode [@AfterViews] verfügen, in der sie den booleschen Wert [afterViewsDone] auf true setzen;
  • eine Sitzung, die die zwischen Fragmenten und Aktivitäten auszutauschenden Daten kapselt;

1.15. Beispiel 14: Eine zweischichtige Architektur

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

1.15.1. Erstellung des Projekts

Wir duplizieren das vorherige Projekt [Exemple-12] in [Exemple-13] gemäß der Vorgehensweise in Abschnitt 1.4. Wir erhalten folgendes Ergebnis:

1.15.2. Die Ansicht [vue1]

Die Anwendung wird nur die Ansicht [vue1.xml] enthalten. Daher löschen wir die andere Ansicht [vue2.xml] sowie das zugehörige Fragment:

 

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

 

Korrigieren Sie die folgende Zeile 4 im Fragment-Manager [SectionsPagerAdapter]


  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private AbstractFragment[] fragments = new AbstractFragment[]{new Vue1Fragment_(), new Vue2Fragment_()};
...

Die obige Zeile 4 lautet nun:


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

Entfernen Sie die nun überflüssigen Importe [Ctrl-Shift-O]. Es sollten nun keine Kompilierungsfehler mehr auftreten. Führen Sie das Projekt aus: Ansicht Nr. 1 sollte erscheinen. Diese werden wir nun ändern.

Wir erstellen die Ansicht [vue1.xml], mit der Zufallszahlen generiert werden können:

 

Sie besteht aus folgenden Komponenten:

Id
Type
Rôle
1
edtNbAleas
EditText
nombre de nombres aléatoires à générer dans l'intervalle entier [a,b]
2
edtA
EditText
valeur de a
2
edtB
EditText
valeur de b
4
btnExécuter
Button
lance la génération des nombres
5
ListView
lstReponses
liste des nombres générés dans l'ordre inverse de leur génération. On voit d'abord le dernier généré ;

Sein Code 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: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 Bezeichnungen, 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>
  <!-- Ansicht 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>
  <!-- Farben anwenden -->
  <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, ist keine Kommunikation zwischen den Fragmenten vorgesehen. 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. In den Zeilen, die Elemente der nun leeren Sitzung verwendet haben, treten Fehler auf. Löschen Sie diese Zeilen und überprüfen Sie, ob die Kompilierung nun fehlerfrei verläuft.

1.15.4. Das Fragment [Vue1Fragment]

  

Wir ändern das vorhandene Fragment [Vue1Fragment] 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 {

  // Elemente der visuellen Benutzeroberfläche
  @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;

  // Liste der Antworten auf einen Befehl
  private List<String> reponses = new ArrayList<>();
  // Listview-Adapter
  private ArrayAdapter<String> adapterReponses;

  // die Eingaben
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
    // Speicher
    afterViewsDone = true;
    // Protokoll
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("afterViews %s", getParentInfos()));
    }
    // Fehlermeldungen werden ausgeblendet
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
    // Eventuelle vorherige Fehlermeldungen werden ausgeblendet
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // Die Gültigkeit der Eingaben wird geprüft
    if (!isPageValid()) {
      return;
    }
  }

  // Die Gültigkeit der eingegebenen Daten wird überprüft
  private boolean isPageValid() {
...
  }

  @Override
  protected void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
  }
}
  • Hier gibt es nur ein Fragment, dessen Lebenszyklus nur einmal ausgeführt wird, und zwar beim Start der Anwendung. Aus diesem Grund werden die Methoden [@AfterViews] (Zeilen 46–57) und [udateFragment] (Zeilen 75–81) nur einmal beim Start der Anwendung ausgeführt;
  • Zeilen 55–56: Die beiden Fehlermeldungen der Ansicht (siehe unten) [1-2] werden ausgeblendet;
 
  • Zeilen 59–60: Die Methode, die beim Klicken auf die Schaltfläche „[Exécuter]“ ausgeführt wird;
  • Zeilen 71–73: Die Gültigkeit der Eingaben wird überprüft;

Die Methode [isPageValid] lautet wie folgt:


  // die Eingaben
  private int nbAleas;
  private int a;
  private int b;

...

// Die Gültigkeit der eingegebenen Daten wird überprüft
  private boolean isPageValid() {
    // Eingabe der Anzahl der Zufallszahlen
    nbAleas = 0;
    Boolean erreur;
    int nbErreurs = 0;
    try {
      nbAleas = Integer.parseInt(edtNbAleas.getText().toString());
      erreur = (nbAleas < 1);
    } catch (Exception ex) {
      erreur = true;
    }
    // Fehler?
    if (erreur) {
      nbErreurs++;
      txtErrorAleas.setVisibility(View.VISIBLE);
    }
    // Eingabe von a
    a = 0;
    erreur = false;
    try {
      a = Integer.parseInt(edtA.getText().toString());
    } catch (Exception ex) {
      erreur = true;
    }
    // Fehler?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // Eingabe von b
    b = 0;
    erreur = false;
    try {
      b = Integer.parseInt(edtB.getText().toString());
      erreur = b < a;
    } catch (Exception ex) {
      erreur = true;
    }
    // Fehler?
    if (erreur) {
      nbErreurs++;
      txtErrorIntervalle.setVisibility(View.VISIBLE);
    }
    // Zurück
    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 Eingaben gültig sind, andernfalls false. Sind Eingaben ungültig, werden die entsprechenden Fehlermeldungen angezeigt;

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

1.15.5. Die Schicht [métier]

  

Die Schicht [métier] verfügt über die folgende Schnittstelle [IMetier]:


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 Intervall [a,b] zurück. Es ist außerdem vorgesehen, dass sie in jedem dritten Fall eine Ausnahme auslöst, die ebenfalls in die von der Methode zurückgegebenen Antworten aufgenommen wird. Letztendlich gibt diese Methode eine Liste von Objekten vom Typ [Exception] oder [Integer] zurück.

Die Implementierung [Metier] 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) {
        // die Liste der Objekte
        List<Object> réponses = new ArrayList<Object>();
        // einige Überprüfungen
        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"));
        }
        // Fehler?
        if (réponses.size() != 0) {
            return réponses;
        }
        // Zufallszahlen werden generiert
        Random random = new Random();
        for (int i = 0; i < n; i++) {
            // Es wird 1 von 3 Mal eine zufällige Ausnahme ausgelöst
            int nombre = random.nextInt(3);
            if (nombre == 0) {
                réponses.add(new AleaException("Exception aléatoire"));
            } else {
                // ansonsten wird eine Zufallszahl zwischen zwei Grenzwerten zurückgegeben [a,b]
                réponses.add(Integer.valueOf(a + random.nextInt(b - a + 1)));
            }
        }
        // Ergebnis
        return réponses;
    }
}
  • Zeile 9: Die Annotation AA [@EBean] wird auf die Klasse [Metier] angewendet, um Referenzen dieser Klasse in die Schicht [Présentation] einfügen zu können. Das Attribut (scope = EBean.Scope.Singleton) sorgt dafür, dass von der Klasse [Metier] nur ein einziges Exemplar instanziiert wird. Es wird also immer dieselbe Referenz injiziert, wenn man sie mehrmals in die Schicht [Présentation] injiziert;
  • der Rest des Codes ist klassisch;

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], was sie zu einer unkontrollierten Ausnahme macht: Man muss sie weder in einem try/catch-Block behandeln noch in die Signatur der Methoden aufnehmen;

1.15.6. Die Aktivität [MainActivity] neu betrachtet

  

Ebene

[metier]

Aktivität

Ansicht

Benutzer

Die Aktivität implementiert die Schnittstelle [IMetier] der Schicht [métier]. Somit hat das Fragment/die Ansicht ausschließlich die Aktivität als Ansprechpartner.

Die Aktivität [MainActivity] implementiert bereits die Schnittstelle [IMainActivity]. Damit sie auch die Schnittstelle [IMetier] implementiert, kann man:

  • die Schnittstelle [IMetier] zu den von der Aktivität implementierten Schnittstellen hinzufügen;
  • sicherzustellen, dass die Schnittstelle [IMainActivity] ihrerseits die Schnittstelle [IMetier] erweitert. Diesen Weg wählen wir;

Die Schnittstelle [IMainActivity] sieht dann wie folgt aus:

  

package exemples.android.architecture;

import exemples.android.metier.IMetier;

public interface IMainActivity extends IMetier {

  // Zugriff auf die Sitzung
  Session getSession();

  // Wechsel der Ansicht
  void navigateToView(int position);

  // Debug-Modus
  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 {

  ...

  // Sitzungseinbindung
  @Bean(Session.class)
  protected Session session;

  // Geschäftsprozess-Injektion
  @Bean(Metier.class)
  protected IMetier metier;

...
  // Implementierung IMetier --------------------------------------------------------------------
  @Override
  public List<Object> getAleas(int a, int b, int n) {
    return metier.getAleas(a, b, n);
}
  • Zeilen 11–12: Die Schicht [métier] wird in die Aktivität eingefügt. Dazu wird die Annotation AA [@Bean] verwendet, deren Parameter die Klasse mit der Annotation AA [@EBean] ist;
  • Zeile 2: Die Aktivität implementiert die Schnittstelle [IMainActivity] und damit auch die Schnittstelle [IMetier] der Schicht [métier];
  • Zeilen 16–19: Implementierung der einzigen Methode der Schnittstelle [IMetier]. Der Aufruf wird lediglich an die Schicht [métier] delegiert;

1.15.7. Das Fragment [Vue1Fragment] noch einmal betrachtet

  

Der Code der Klasse [Vue1Fragment] entwickelt sich wie folgt weiter:


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 {

  // Elemente der visuellen Benutzeroberfläche
  @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;

  // Liste der Antworten auf einen Befehl
  private List<String> reponses = new ArrayList<>();
  // Listview-Adapter
  private ArrayAdapter<String> adapterReponses;

  // die Eingaben
  private int nbAleas;
  private int a;
  private int b;

  @AfterViews
  protected void afterViews() {
   ...
  }

  @Click(R.id.btn_Executer)
  void doExecuter() {
  ...
  }

  // Die Gültigkeit der eingegebenen Daten wird überprüft
  private boolean isPageValid() {
   ...
  }

  @Override
  protected void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d("Vue1Fragment", String.format("updateFragment %s", getParentInfos()));
    }
    // wird nur einmal beim Start der Anwendung ausgeführt
    // Der Adapter für ListView wird erstellt – dazu muss die Variable [activity] initialisiert worden sein
    adapterReponses=new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
  }
}
  • Zeilen 69–70: Der Adapter der Komponente vom Typ [ListView] wird festgelegt;

Die Komponente [ListView] dient dazu, eine Liste von Elementen anzuzeigen. Dies geschieht mithilfe eines Adapters vom Typ [ListAdapter], der wiederum mit der Datenquelle verbunden ist, die die Komponente [ListView] versorgen soll. Um den Adapter für ein [ListView] zu definieren, steht die folgende Methode [ListView.setAdapter] zur Verfügung:


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 [ListView] anzeigt;
  • [resource] ist die Ganzzahl, die die Ansicht identifiziert, die zur Anzeige eines Elements von [ListView] verwendet wird. Diese Ansicht kann beliebig komplex sein. Sie wird vom Entwickler entsprechend seinen Anforderungen erstellt;
  • [textViewResourceId] ist die Ganzzahl, die eine Komponente [TextView] in der Ansicht [resource] identifiziert. Die angezeigte Zeichenfolge wird von dieser Komponente dargestellt;
  • [objects]: die Liste der Objekte, die von [ListView] angezeigt werden. Die Methode [toString] der Objekte wird verwendet, um das durch [textViewResourceId] identifizierte Objekt in der durch [TextView] identifizierten Ansicht, die durch [resource] identifiziert wird, anzuzeigen.

Die Aufgabe des Entwicklers besteht darin, die Ansicht [resource] zu erstellen, die jedes Element des [ListView] anzeigt. Für den einfachen Fall, in dem wie hier nur eine einfache Zeichenkette angezeigt werden soll, stellt Android die Ansicht mit der Kennung [android.R.layout.simple_list_item_1] bereit. Diese enthält eine Komponente mit der Kennung [TextView], die durch [android.R.id.text1] identifiziert wird. Dies ist die in Zeile 69 verwendete Methode zum Erstellen des Adapters für [ListView]. 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 Konstruktors [ArrayAdapter] ist die Aktivität, die in einem Fragment durch [getActivity] ermittelt und hier in der Variablen [activity] der übergeordneten Klasse gespeichert wurde. Dieses Feld ist nicht immer mit einem Wert belegt. So zeigen die Protokolle, dass es beim Aufruf der Methode [@AfterViews] noch nicht initialisiert wurde und daher die Zeilen 69–70 nicht in diese Methode eingefügt werden können. In der Methode [updateFragment] ist dies möglich, da bekannt ist, dass bei Ausführung dieser Methode zwangsläufig [activity!=null] vorhanden ist. Der Adapter ist hier der in Zeile 37 definierten Datenquelle [reponses] zugeordnet;

Die Methode [doExecuter] verarbeitet den Klick auf die Schaltfläche [Exécuter]. Ihr Code lautet wie folgt:


@Click(R.id.btn_Executer)
  void doExecuter() {
    // Eventuelle vorherige Fehlermeldungen werden ausgeblendet
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    // Die vorherigen Antworten werden gelöscht
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    // Die Gültigkeit der Eingaben wird geprüft
    if (!isPageValid()) {
      return;
    }
    // Die Zufallszahlen werden von der Aktivität angefordert
    List<Object> data = mainActivity.getAleas(a, b, nbAleas);
    // Aus diesen Daten wird eine Liste von Strings erstellt
    for (Object o : data) {
      if (o instanceof Exception) {
        reponses.add(((Exception) o).getMessage());
      } else {
        reponses.add(o.toString());
      }
    }
    // Listview aktualisieren
    adapterReponses.notifyDataSetChanged();
  }
  • Zeilen 7–8: ListView soll geleert werden. Dazu wird die Datenquelle [reponses] geleert und der mit ListView verknüpfte Adapter aufgefordert, sich zu aktualisieren;
  • Zeilen 10–12: Bevor die angeforderte Aktion ausgeführt wird, wird überprüft, ob die eingegebenen Werte korrekt sind;
  • Zeile 14: Die Liste der Zufallszahlen wird von der Aktivität angefordert. Man erhält eine Liste von Objekten, wobei jedes Objekt vom Typ [Integer] oder [AleaException] ist;
  • Zeilen 16–22: Anhand der erhaltenen Objektliste wird die Datenquelle [reponses] aktualisiert, die von ListView angezeigt wird;
  • Zeile 24: Der Adapter von ListView wird aufgefordert, sich zu aktualisieren;

1.15.8. Ausführung

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

1.16. Beispiel 15: Client-Server-Architektur

Wir befassen uns mit einer gängigen Architektur für eine Android-Anwendung, bei der die Android-Anwendung mit entfernten Webdiensten kommuniziert. Wir haben nun folgende Architektur:

Der Android-App wurde eine Schicht [DAO] 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 zweischichtige Architektur:

Die Kunden fragen bestimmte URL aus der Ebene [web / jSON] ab und erhalten eine Textantwort im Format jSON (JavaScript Object Notation). Hier verarbeitet unser Webdienst eine einzige URL vom Typ [/a/b], die eine Zufallszahl im Intervall [a,b] zurückgibt. Wir beschreiben die Anwendung in der folgenden Reihenfolge:

Der Server

  • seine Schicht [métier];
  • sein mit Spring implementierter Dienst [web / jSON];

Der Client

  • seine Schicht [DAO]. Es wird keine Schicht [métier] geben;

1.16.1. Der Server [web / jSON]

Wir möchten die folgende Architektur aufbauen:

1.16.1.1. Projekterstellung

Wir werden den Webdienst mit dem Spring-Ökosystem [http://spring.io/] erstellen. Wir rufen die Website [http://start.spring.io/] (Juni 2016) auf, über die wir ein Gradle-Projekt mit den für unser Projekt erforderlichen Abhängigkeiten generieren können. Da es sich nicht um ein Android-Projekt handelt, bietet Android Studio hierfür keine Unterstützung:

  • in [1]: Wählen Sie ein Gradle-Projekt aus;
  • in [2-3]: die Eigenschaften der vom Projekt generierten Abhängigkeit jar (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. Es wird dann eine ZIP-Datei eines Gradle-Skelettprojekts erstellt und zum Download bereitgestellt;

Was muss in [2-3] eingegeben werden? Wir haben bereits Gradle-Abhängigkeiten verwendet. Die des vorherigen Projekts lautete beispielsweise wie folgt:

 

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Seit Android Gradle Plugin 0.11 muss android-apt >= 1.3 verwendet werden
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  ...
}
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 hat die Form [groupId:artifactId:version]. Was im Formular auf der Website [http://start.spring.io/] abgefragt wird:
    • in [2] ist [groupId];
    • in [3] ist [artifactId];

Entpacken Sie die ZIP-Datei obenu in den Ordner „Andere Projekte“:

Öffnen Sie mit Android Studio das Gradle-Projekt „[server-01] [1-2]“. Das geöffnete Projekt ist „[3]“ (Projektperspektive).

1.16.1.2. Gradle-Konfiguration

  

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


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 beziehen sich auf IDE Eclipse. 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 des Spring-Ökosystems [http://projects.spring.io/spring-boot/]. Dieses Plugin legt die Versionen der am häufigsten mit Spring verwendeten Abhängigkeiten fest. Dadurch müssen deren Versionen nicht angegeben werden (Zeilen 30 und 31). Die Version entspricht dann der durch die verwendete Spring-Boot-Version festgelegten Version (Zeile 3);
  • Zeilen 22–23: die zu verwendende Java-Version, hier Version 1.8;
  • Zeilen 25–27: die Binär-Repositorys, die zum Herunterladen der Abhängigkeiten verwendet werden sollen;
  • Zeile 26: Bezieht sich auf das zentrale Maven-Repository. 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 enthält alle Binärdateien, die zum Erstellen eines Spring-Webdienstes erforderlich sind;
  • Zeile 31: Diese Abhängigkeit enthält alle Binärdateien, die für die Tests benötigt werden, insbesondere für die Tests JUnit;
  • Eine Abhängigkeit [compile] bedeutet, dass die Abhängigkeit für die Kompilierung des Projekts benötigt wird. Eine Abhängigkeit [testCompile] bedeutet, dass die Abhängigkeit ausschließlich für die Ausführung der Tests benötigt wird. Sie ist daher nicht in der Binärdatei des Projekts enthalten;

Wir führen eine erste Bereinigung der Gradle-Datei durch:


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

// Projekt-Binärdatei
jar {
  baseName = 'server-01'
  version = '0.0.1-SNAPSHOT'
}

// Java-Versionen
sourceCompatibility = 1.8
targetCompatibility = 1.8

// Maven-Repositorys
repositories {
  mavenLocal()
  mavenCentral()
}

// Abhängigkeiten
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 des Entwicklungsrechners hinzugefügt. Dieses wird bei der Installation von Maven angelegt (siehe Abschnitt 6.10). Befindet sich die angeforderte Abhängigkeit bereits im lokalen Maven-Repository, wird sie nicht aus dem zentralen Maven-Repository angefordert;
  • Zeilen 19–22: Eine Gradle-Aufgabe zum Erzeugen der Projektbinärdatei. Wir werden sie verwenden, um zu sehen, was dabei geschieht;
  • Führen Sie in [1-4] die Aufgabe [jar] aus, die in der Datei [build.gradle] definiert ist ([1] befindet sich oben rechts und neben der Datei IDE);

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

  

Der Name des Archivs stammt direkt aus den Angaben zur Aufgabe [jar] in der Datei [build.gradle] (Zeilen 19–22).

Alle Abhängigkeiten des Projekts lassen sich wie folgt darstellen:

 

In [1] ist zu sehen, 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 hat die Abhängigkeiten mitgeliefert, die eine Spring-Webanwendung MVC wahrscheinlich benötigen wird. Das bedeutet, dass einige davon möglicherweise unnötig sind. Spring Boot eignet sich ideal für ein Tutorial:

  • Es bringt die Abhängigkeiten mit, die wir wahrscheinlich benötigen werden;
  • 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 sich zahlreiche Beispiele für die Verwendung von Spring Boot.

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


// Spring Boot
...
// Abhängigkeiten
dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}

// Plugin zum Erstellen einer Maven-konformen Binärdatei im lokalen Maven-Repository
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 {
      // Ändern Sie den Verweis auf Ihr Repository: e.g. http://my.org/repo
      url 'file://D:\\maven'
    }
  }
}
  • Zeile 10: Wir importieren ein Gradle-Plugin namens [maven-publish], mit dem die Binärdatei des Projekts unter Einhaltung der Maven-Standards in einem Maven-Repository veröffentlicht werden kann;
  • 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 Binärdatei veröffentlicht wird, in diesem Fall ein lokales Maven-Repository;

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

Wenn man in [2] die Aufgabe [publish] ausführt, wird die Binärdatei des Projekts erstellt und in dem Ordner installiert, der in Zeile 23 der Datei [build.gradle] angegeben ist:

 

Mit der Aufgabe [jar] lässt sich die Binärdatei des Projekts generieren. Diese Binärdatei enthält keine Abhängigkeiten und ist daher nicht ausführbar. Es ist möglich, eine Binärdatei mit allen Abhängigkeiten zu generieren, die ausführbar ist. Dazu fügen wir der Datei [build.gradle] den folgenden Code hinzu:


// Erstellen einer Binärdatei mit allen Abhängigkeiten
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: Hier muss der vollständige Name der ausführbaren Klasse des Projekts angegeben werden:
  

Der Code dieser 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]“ generiert und kann unter dem Namen „[1-7]“ ausgeführt werden:

1.16.1.3. Projektkonfiguration

Die Gradle-Konfiguration reicht nicht aus. Wir müssen außerdem das Projekt konfigurieren. Da es sich nicht um ein von 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 Option aus dem Menü [1-2]:

Im weiteren Verlauf wird der Leser aufgefordert, das folgende Projekt anzulegen. Wir kommentieren den endgültigen Code des Projekts [3].

1.16.1.4. Die Ebene [métier]

  

Die Ebene [métier] folgt dem Aufbau der Ebene [métier] aus dem vorherigen Beispiel. Sie wird die folgende Schnittstelle [IMetier] aufweisen:


package exemples.android.server.metier;

public interface IMetier {
  // Zufallszahl in [a,b]
    int getAlea(int a, int b);
}
  • Zeile 5: Die Methode, die in [a,b] eine Zufallszahl generiert

Der Code der 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) {
    // einige Überprüfungen
    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);
    }
    // Ergebnisgenerierung
    Random random=new Random();
    random.setSeed(new Date().getTime());
    return a + random.nextInt(b - a + 1);
  }
}

Wir gehen nicht näher auf die Klasse ein: Sie entspricht der im vorherigen Beispiel vorgestellten, mit dem Unterschied, dass sie nicht zufällig Ausnahmen auslöst. Zu beachten ist lediglich die Spring-Annotation in Zeile 8 ([@Service]), die dafür sorgt, dass Spring die Klasse als Singleton instanziiert und ihre Referenz für andere Spring-Komponenten verfügbar macht. Hier hätten auch andere Spring-Annotationen mit demselben Effekt verwendet werden können. Spring-Komponenten haben Standardnamen, die als Attribut der verwendeten Annotation angegeben werden können. Ohne dieses Attribut, wie hier, trägt die Spring-Komponente den Namen der Klasse, wobei das erste Zeichen klein geschrieben wird. Somit trägt die Spring-Komponente hier standardmäßig den Namen [metier];

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


package exemples.android.server.metier;

public class AleaException extends RuntimeException {

  // Fehlercode
  private int code;

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

  // Getter und Setter
....
}
  • Zeile 3: [AleaException] erweitert die Klasse [RuntimeException]. Es handelt sich also um eine unkontrollierte Ausnahme (es besteht keine Verpflichtung, sie mit einem try/catch-Block abzufangen);
  • 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 sogenannte MVC-Architekturmodell (Model-View-Controller) wie folgt:

Die Bearbeitung einer Client-Anfrage läuft wie folgt ab:

  1. Anfrage – die angeforderten URL haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Die [Dispatcher Servlet] ist die Spring-Klasse, die eingehende URL verarbeitet. Sie „leitet“ die URL an die Aktion weiter, die sie verarbeiten soll. Diese Aktionen sind Methoden bestimmter Klassen, die als [Contrôleurs] bezeichnet werden. Das „C“ in MVC ist hier die Zeichenfolge [Dispatcher Servlet, Contrôleur, Action]. Wenn keine Aktion zur Verarbeitung des eingehenden URL konfiguriert wurde, antwortet das Servlet [Dispatcher Servlet], dass das angeforderte URL nicht gefunden wurde (Fehler 404 NOT FOUND);
  1. Verarbeitung
  • Die ausgewählte Aktion kann die Parameter parami nutzen, die ihr das Servlet [Dispatcher Servlet] übermittelt hat. Diese können aus verschiedenen Quellen stammen:
    • aus dem Pfad [/param1/param2/...] des URL,
    • aus den Parametern [p1=v1&p2=v2] des URL,
    • aus Parametern, die der Browser zusammen mit seiner Anfrage übermittelt hat;
  • Bei der Bearbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die Schicht [metier] [2b]. Sobald die Client-Anfrage bearbeitet wurde, kann diese verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
  • die Aktion fordert die Anzeige einer bestimmten Ansicht an: [3]. Diese Ansicht zeigt Daten an, die als Modell der Ansicht bezeichnet werden. Das ist das M in MVC. Die Aktion erstellt dieses Modell M [2c] und fordert eine Ansicht V auf, sich anzuzeigen: [3];
  1. Antwort – die ausgewählte Ansicht V verwendet die von der Aktion erstellte Vorlage M, um die dynamischen Teile der Antwort HTML zu initialisieren, die sie an den Client senden muss, und sendet diese Antwort anschließend.

Bei einem Webservice / jSON wird die vorstehende Architektur leicht modifiziert:

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

Ein Beispiel für die Serialisierung eines Java-Objekts in die Zeichenkette „jSON“ und für die Deserialisierung einer Zeichenkette „jSON“ in ein Java-Objekt ist in den Anhängen zu Abschnitt 6.14 dargestellt.

Kehren wir zur Schicht [web] unserer Anwendung zurück:

In unserer Anwendung gibt es nur einen Controller:

  

Der Webdienst / jSON sendet seinen Clients eine Antwort vom Typ [Response] mit folgendem Inhalt:


package exemples.android.server.web;

import java.util.List;

public class Response<T> {

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

    // Konstruktoren
    public Response() {

    }

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

    // Getter und Setter
...
}
  • Zeile 13: Das Feld [T body] ist die vom Client erwartete Antwort. Wir haben uns hier für eine generische Antwort vom Typ T entschieden, anstatt des Typs Integer mit der erwarteten Zufallszahl. Wir möchten diese Klasse in anderen Situationen wiederverwenden können. Bei der Bearbeitung der Kundenanfrage 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: bei 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] lautet wie folgt:


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 {

  // Geschäftslogik
  @Autowired
  private IMetier metier;
  // Mapper JSON
  @Autowired
  private ObjectMapper mapper;

  // Zufallszahlen
  @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 {

    // die Antwort
    Response<Integer> response = new Response<>();
    // Wir verwenden die Geschäftsschicht
    try {
      response.setBody(metier.getAlea(a, b));
      response.setStatus(0);
    } catch (AleaException e) {
      response.setStatus(e.getCode());
      response.setMessages(getMessagesFromException(e));
    }
    // die Antwort wird zurückgegeben
    return mapper.writeValueAsString(response);
  }

  private List<String> getMessagesFromException(Throwable e) {
    // Liste der Nachrichten
    List<String> messages = new ArrayList<String>();
    // man durchläuft den Ausnahmestapel
    Throwable th = e;
    while (th != null) {
      messages.add(e.getMessage());
      th = th.getCause();
    }
    // das Ergebnis wird zurückgegeben
    return messages;
  }

}
  • Zeile 17: Die Annotation [@Controller] gibt an, dass es sich bei der Klasse um einen Controller MVC handelt, dessen Methoden Anfragen für bestimmte URL der Webanwendung verarbeiten;
  • Zeilen 21–22: Die Annotation [@Autowired] weist Spring an, eine Komponente vom Typ [IMetier] in das Feld zu injizieren. Dies ist die zuvor erwähnte Klasse [Metier]. Da wir dieser die Annotation [@Service] zugewiesen haben, wird sie als Spring-Komponente behandelt;
  • Zeilen 24–25: Wir verfahren ebenso mit einem Mapper vom Typ jSON, den wir später definieren werden. Unser Webdienst wird seine Antwort in Form einer Zeichenkette jSON 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 wurden ihre Parameter von Spring initialisiert: MVC. Wir werden sehen, wie das funktioniert. Übrigens wird sie nur ausgeführt, weil der Webserver eine Anfrage HTTP GET für das URL aus 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 Methode HTTP. Es gibt hauptsächlich zwei davon, GET und POST. Die Methode [POST] wird verwendet, wenn der Client seiner Anfrage HTTP ein Dokument anhängen möchte;
    • [produces]: Legt einen der Header der Antwort HTTP fest, die an den Kunden gesendet wird. Hier wird in den Kopfzeilen von HTTP, die mit der Antwort an den Kunden gesendet werden, eine enthalten sein, die ihm mitteilt, dass die Antwort in Form einer Zeichenkette jSON gesendet wird. Dieser Header ist nicht obligatorisch. Er wird dem Kunden zu Informationszwecken mitgeteilt, falls dieser Antworten erwartet, die verschiedene Formen annehmen können;
    • [consumes]: ist hier nicht vorhanden. Er dient dazu, die Header HTTP anzugeben, die der Anfrage HTTP des Kunden beigefügt sein müssen, damit diese akzeptiert wird;
  • Zeile 29: Die Anmerkung [@ResponseBody] gibt an, dass das von der Methode erzeugte Ergebnis an den Kunden gesendet werden muss. Ohne diese Anmerkung wird die Antwort der Methode als Schlüssel betrachtet, mit dem die Seite HTML ausgewählt wird, die an den Kunden gesendet werden soll. In einem Webdienst /jSON gibt es keine Seiten HTML;
  • 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"). Es ist zu beachten, dass {a} und {b} Komponenten eines URL sind und somit vom Typ String sind. Die Konvertierung von String in den Typ der Parameter kann fehlschlagen. Spring MVC löst dann eine Ausnahme aus. Fassen wir zusammen: Wenn ich mit einem Browser den URL /100/200 anfordere, wird die Methode getAlea in Zeile 30 mit den ganzzahligen Parametern a=100, b=200 ausgeführt;
  • Zeile 36: Die Schicht [métier] wird um eine Zufallszahl im Intervall [a,b] gebeten. Wir erinnern uns, dass die Methode [metier].getAlea eine Ausnahme auslösen kann;
  • Zeile 37: kein Fehler;
  • Zeile 39: Fehlercode;
  • Zeile 40: Die Liste der Meldungen in der Antwort entspricht der des Ausnahmestapels (Zeilen 46–57). Hier wissen wir, dass der Stapel nur eine Ausnahme enthält, wollten aber eine allgemeinere Methode zeigen;
  • Zeile 43: Die Antwort vom Typ [Response<Integer>] wird in Form einer Zeichenkette jSON zurückgegeben;

1.16.1.6. Konfiguration des Spring-Projekts

  

Es gibt verschiedene Möglichkeiten, Spring zu konfigurieren:

  • mit den Dateien XML;
  • mit Java-Code;
  • mit einer Mischung aus beidem;

Wir entscheiden uns dafür, unsere Webanwendung mit Java-Code zu konfigurieren. Die folgende Klasse „[Config]“ sorgt für 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 {
  // Webkonfiguration ------------------------------------
  @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 Komponente [Metier] mit der Annotation [@Service] im Paket [exemples.android.server.metier];
    • die Komponente [WebController], die mit [@Controller] annotiert ist, im Paket [exemples.android.server.web];
  • Zeile 13: Die Annotation [@EnableWebMvc] ermöglicht es Spring Boot, eine Reihe von Standardkonfigurationen für eine Spring-Anwendung MVC selbst vorzunehmen. Dies entlastet den Entwickler entsprechend;
  • Zeilen 16, 22, 27 und 33: Die Annotation [@Bean] definiert ebenfalls Spring-Komponenten (Beans), genau wie die beiden zuvor erwähnten Annotationen (@Service, @Controller). Hier annotiert die Annotation [@Bean] eine Methode und keine Klasse, und das Ergebnis der Methode ist die Spring-Komponente. Da in der Annotation [@Bean] kein Namensattribut vorhanden ist, trägt die erstellte Spring-Komponente den Namen der annotierten Methode;
  • Zeilen 16–20: Definieren die Bean [dispatcherServlet]. Dabei handelt es sich um einen vordefinierten Spring-Namen (MVC), der den Front-Controller der Anwendung MVC definiert, ein Objekt, über das alle Client-Anfragen laufen und das diese (daher der Name) an die verschiedenen [@Controller] der Spring-Anwendung MVC weiterleitet;
  • Zeile 18: Der Bean [dispatcherServlet] ist eine Instanz der Klasse [DispatcherServlet], die von Spring MVC bereitgestellt wird;
  • Zeilen 22–25: Der Bean [servletRegistrationBean] dient dazu, festzulegen, welche URL von der Anwendung akzeptiert werden. In Zeile 24 werden alle URL akzeptiert;
  • Zeilen 27–30: Die Bean [embeddedServletContainerFactory] dient dazu, den in den Projektabhängigkeiten enthaltenen eingebetteten Server zu definieren, der die Webanwendung hosten soll. Zeile 29 gibt an, dass es sich um einen Tomcat-Server handelt, der auf Port 8080 läuft. Standardmäßig werden die Binärdateien dieses Webservers über die Abhängigkeit [org.springframework.boot:spring-boot-starter-web] in der Gradle-Datei bereitgestellt;

1.16.1.7. Ausführung des Webdienstes / jSON

  

Das Projekt wird über die folgende ausführbare Klasse [Boot] 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) {
    // Anwendung wird ausgeführt
    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 handelt es sich um die Klasse [Config], die wir gerade beschrieben haben. Der zweite Parameter ist das Argument-Array, das an die Methode [main] übergeben wird (Zeile 7);

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

 

In der Konsole erscheinen daraufhin eine Reihe von Protokollen:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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 [/**] an den Handler vom Typ [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/**] Handler vom Typ [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-Servlet [DispatcherServlet] wird geladen und konfiguriert;
  • Zeile 20: Das Webserver-URL [/{a}/{b}] wird erkannt;

Öffnen wir nun einen Browser und testen wir den Webdienst /URL:

Wir erhalten jedes Mal die Darstellung jSON eines Objekts vom Typ [Response<Integer>].

Anstelle eines Standard-Browsers verwenden wir nun die Erweiterung [Advanced Rest Client] „ “ des Chrome-Browsers (siehe Anhang, Abschnitt 6.13):

Image

  • in [1], das angeforderte URL;
  • in [2], mittels eines GET;
  • in [3], die Anfrage wird gesendet;

Image

  • in [4] die Header HTTP der Serverantwort. Dabei ist zu beachten, dass diese angibt, dass es sich bei dem gesendeten Dokument um eine Zeichenkette jSON handelt;
  • in [5] die empfangene Zeichenkette jSON;

1.16.1.8. Erstellung 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 der Anwendung mit all ihren Abhängigkeiten zu generieren. Angepasst an die vorliegende Anwendung sieht diese Konfiguration wie folgt aus:


// Erstellen einer Binärdatei mit allen Abhängigkeiten
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 generieren, kann man wie folgt vorgehen: [1-5]:

Um die ausführbare Datei auszuführen, wird der Webdienst, falls er gestartet ist, angehalten ([1]), anschließend wird das Archiv ausgeführt ([2-4]):

 

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

1.16.1.9. Protokollverwaltung

Wenn man das ausführbare Archiv ausführt, stellt man fest, dass man nicht dieselben Protokolle erhält wie bei der Ausführung des Projekts über die Datei IDE. Es werden Protokolle im Modus [DEBUG] ausgegeben:


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

Die Protokollierungsstufe lässt sich verwalten, indem man eine Datei [logback.xml] im Ordner [resources] des Projekts hinzufügt:

  

Diese Datei könnte folgenden Inhalt haben:


<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- Encodern wird standardmäßig der Typ zugewiesen
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- Überprüfung der Protokollstufe -->
  <root level="info"> <!-- Info, Debug, Warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

Die Protokollstufe wird in Zeile 12 gesteuert. Wenn man nun das ausführbare Archiv neu generiert und ausführt, erhält man 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 des Webservers / jSON

Der Android-Client wird folgende Architektur aufweisen:

Der Client wird aus zwei Komponenten bestehen:

  1. eine [Présentation]-Schicht (Ansicht + Aktivität), analog zu der, die wir im Beispiel [Exemple-14] untersucht haben;
  2. die Schicht [DAO], die den zuvor behandelten Dienst [web / jSON] anspricht.

1.16.2.1. Erstellung des Projekts

Wir duplizieren das vorherige Projekt [Exemple-14] in [Exemple-15] gemäß der Vorgehensweise in Abschnitt 1.4. Wir erhalten folgendes Ergebnis:

Im weiteren Verlauf wird der Leser aufgefordert, das folgende Projekt anzulegen.

1.16.2.2. Gradle-Konfiguration

 

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


buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    // Seit Android-Gradle-Plugin 0.11 muss android-apt >= 1.3 verwendet werden
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
  compileSdkVersion 23
  buildToolsVersion "23.0.3"
  defaultConfig {
    applicationId "exemples.android"
    minSdkVersion 15
    targetSdkVersion 23
    versionCode 1
    versionName "1.0"
  }

  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
  }

  // Erforderliche Paketierungsoptionen, um das APK erstellen zu können
  packagingOptions {
    exclude 'META-INF/ASL2.0'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/notice.txt'
    exclude 'META-INF/license.txt'
  }
}

def AAVersion = '4.0.0'
dependencies {
  apt "org.androidannotations:androidannotations:$AAVersion"
  compile "org.androidannotations:androidannotations-api:$AAVersion"
  apt "org.androidannotations:rest-spring:$AAVersion"
  compile "org.androidannotations:rest-spring-api:$AAVersion"
  compile 'com.android.support:appcompat-v7:23.4.0'
  compile 'com.android.support:design:23.4.0'
  compile 'org.springframework.android:spring-android-rest-template:2.0.0.M3'
  compile 'com.fasterxml.jackson.core:jackson-databind:2.7.4'
  compile fileTree(include: ['*.jar'], dir: 'libs')
  testCompile 'junit:junit:4.12'
}

repositories {
  maven {
    url 'https://repo.spring.io/libs-milestone'
  }
}

Wir gehen nur auf die Punkte ein, die noch nicht behandelt wurden:

  • Zeilen 46–47: Einfügen eines Plugins AA. Das Plugin [rest-spring-api] ermöglicht es, den Client-Server-Datenaustausch an die Bibliothek AA zu delegieren;
  • Zeile 50: Die Bibliothek [spring-android-rest-template] wird von AA verwendet, um den Client-Server-Datenaustausch sicherzustellen. Die Version [2.0.0.M3] ist eine sogenannte „Milestone“-Version, die nicht in den üblichen Maven-Repositories zu finden ist. Daher muss in den Zeilen 56–59 das zu verwendende Repository (Zeile 58) angegeben werden, um die Bibliothek zu finden;
  • Zeile 51: eine Bibliothek jSON;
  • Zeilen 33–39: Ohne diese Eigenschaft treten beim Erstellen der Binärdatei APK des Projekts Fehler auf;

1.16.2.3. Das Manifest der Android-App

  

Die Datei [AndroidManifest.xml] muss angepasst werden. Standardmäßig sind Internetzugriffe nämlich deaktiviert. Sie müssen über eine spezielle 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: Internetzugriffe sind erlaubt;

1.16.2.4. Die Ebene [DAO]

  

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

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


package exemples.android.dao;

public interface IDao {

  // Zufallszahl
  int getAlea(int a, int b);

  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

  // maximale Wartezeit (ms) für die Antwort des Servers
  void setTimeout(int timeout);

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

}
  • Zeile 6: die Methode des Webdienstes / jSON zum Abrufen einer Zufallszahl im Intervall [a,b] dieses Webdienstes;
  • Zeile 9: die URL des Webdienstes / jSON zur Erzeugung von Zufallszahlen;
  • Zeile 12: Es wird eine maximale Wartezeit festgelegt, um auf die Antwort des Servers zu warten;
  • Zeile 15: Es soll eine Wartezeit vor der Ausführung der Anfrage an den Server festgelegt werden, um dem Benutzer Zeit zu geben, seine Anfrage abzubrechen;

1.16.2.4.2. Die Schnittstelle [WebClient]
  

Die Schnittstelle [WebClient] ist für die Kommunikation mit dem Webdienst zuständig. Ihr 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 Zufallszahl im Intervall [a,b]
  @Get("/{a}/{b}")
  Response<Integer> getAlea(@Path("a") int a, @Path("b") int b);
}
  • Zeile 12: [WebClient] ist eine Schnittstelle, die die Bibliothek AA anhand der dort hinzugefügten Annotationen selbst implementieren wird. Diese Schnittstelle muss die Aufrufe an URL implementieren, die vom Webdienst / jSON bereitgestellt werden:

  // Zufallszahl
  @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 Annotation AA. Der Wert des Attributs [converters] ist ein Array von Konvertern. Hier sorgt der Konverter [MappingJackson2HttpMessageConverter.class] dafür, dass eine vom Server gesendete Zeichenkette jSON automatisch deserialisiert wird. So sieht man in Zeile (d), dass URL [/{a}/{b}] einen Typ String zurückgibt, bei dem es sich tatsächlich um eine Zeichenkette jSON handelt (Zeile b). Anhand dieser Informationen und der Angaben zum erwarteten Typ in Zeile 16 wird die Instanz [WebClient] des Clients die empfangene Zeichenkette in einen Typ [Response<Integer>] deserialisieren;
  • Zeile 15: Eine Annotation AA, die angibt, dass URL mit einer Methode HTTP GET aufgerufen werden muss. Der Parameter der Anmerkung [@Get] ist die vom Webdienst erwartete Form von URL. Es reicht aus, den Parameter [value] aus der Annotation [@RequestMapping] (Zeile b) in die aufgerufene Methode im Controller [WebController] des Servers zu übernehmen. Die geschweiften Klammern {} umschließen die Parameter von URL, die in die Parameter der Methode in Zeile 16 übernommen werden müssen. Die Syntax von [@Path("a") int a] bewirkt, dass der Parameter [a] der Methode dem Wert {a} von URL zugewiesen wird. Wenn der Parameter von URL und der der Methode denselben Namen tragen, wie hier, kann man einfacher [@Path int a] schreiben;

Im Fall einer Abfrage HTTP POST hätte die aufgerufene Methode folgende Signatur:


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

Die Annotation [@Body] bezeichnet den gesendeten Wert. Dieser wird automatisch in jSON serialisiert. Auf der Serverseite ergibt sich folgende Signatur:


  // Zufallszahlen
  @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: Es wird angegeben, dass eine Anfrage mit dem Format HTTP POST erwartet wird und dass der Hauptteil dieser Anfrage (das gesendete Objekt) in Form einer Zeichenkette jSON (Attribut consumes) übermittelt werden;
  • Zeile 4: Der übermittelte Wert wird im Parameter [@RequestBody T body] der Methode abgerufen;

Kehren wir zum Code der Klasse [WebClient] zurück:


@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {
  • Wir müssen den URL des anzurufenden Webdienstes angeben können. Dies wird durch die Erweiterung der von AA bereitgestellten Schnittstelle [RestClientRootUrl] erreicht. Diese Schnittstelle stellt eine Methode [setRootUrl(urlServiceWeb] bereit, mit der der URL des anzurufenden Webdienstes festgelegt werden kann;
  • außerdem möchten wir den Aufruf des Webdienstes steuern, da wir die Wartezeit auf die Antwort begrenzen wollen. Zu diesem Zweck erweitern wir die Schnittstelle [RestClientSupport], die die Methode [setRestTemplate] bereitstellt, mit der wir:
    • das Objekt [RestTemplate], das zur Verwaltung des Datenaustauschs zwischen Client und Server dient, selbst anlegen;
    • dieses Objekt so konfigurieren, dass die maximale Wartezeit 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> {

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

    // Konstruktoren
    public Response() {

    }

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

    // Getter und Setter
...
}

Es handelt sich um die Klasse [Response], die bereits serverseitig verwendet wird (Abschnitt 1.16.1.5). Aus programmiertechnischer Sicht verhält es sich so, als würde die Client-Schicht [DAO] direkt mit dem Controller [WebController] des Webdienstes kommunizieren:

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

1.16.2.4.4. Implementierung der Schicht [DAO]
  

Die Schnittstelle [IDao] wird mit der folgenden Klasse [Dao] 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-Client REST
  @RestService
  protected WebClient webClient;

  // Mapper jSON
  private ObjectMapper mapper = new ObjectMapper();
  // Wartezeit vor der Ausführung einer Anfrage
  private int delay;

// Schnittstelle 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 daraus eine Bean AA zu machen, die wir an anderer Stelle injizieren können;
  • Zeilen 19–20: Wir injizieren die spätere Implementierung der von uns beschriebenen Schnittstelle [WebClient]. Diese Injektion wird durch die Annotation [@RestService] gewährleistet;
  • die anderen Methoden implementieren die Schnittstelle [IDao] (Zeilen 27–46);

Methode [setTimeout]

Die Methode [setTimeout] lautet wie folgt:


  @Override
  public void setTimeout(int timeout) {
    // Das Timeout für Client-Anfragen wird festgelegt: REST
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
    // restTemplate wird erstellt
    RestTemplate restTemplate = new RestTemplate(factory);
    // Der Konverter wird festgelegt: jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // Der restTemplate des Web-Clients wird festgelegt
    webClient.setRestTemplate(restTemplate);
}
  • Die Schnittstelle [WebClient] wird von einer Klasse AA implementiert, die die Gradle-Abhängigkeit [org.springframework.android:spring-android-rest-template] verwendet. [spring-android-rest-template] implementiert den Dialog des Clients mit dem Webserver / jSON mithilfe einer Klasse vom Typ [RestTemplate];
  • Zeile 4: Die Klasse [SimpleClientHttpRequestFactory] wird durch die Abhängigkeit [spring-android-rest-template] bereitgestellt. Mit ihr können wir die maximale Wartezeit für die Antwort des Servers festlegen (Zeilen 5–6);
  • Zeile 8: Wir erstellen das Objekt vom Typ [RestTemplate], das als Grundlage für die Kommunikation mit dem Webdienst dient. Als Parameter übergeben wir ihm das soeben erstellte Objekt [factory];
  • Zeile 10: Der Dialog zwischen Client und Server kann verschiedene Formen annehmen. Der Austausch erfolgt über Textzeilen, und wir müssen dem Objekt vom Typ [RestTemplate] mitteilen, was es mit dieser Textzeile tun soll. Dazu stellen wir ihm Konverter zur Verfügung, also Klassen, die Textzeilen verarbeiten können. Die Auswahl des Konverters erfolgt in der Regel über die Header HTTP, die die Textzeile begleiten. In diesem Fall wissen wir, dass wir ausschließlich Textzeilen im Format jSON empfangen. Außerdem haben wir in Abschnitt 1.16.1.7 gesehen, dass der Server den Header HTTP sendet:

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

In Zeile 10 ist der einzige Konverter für [RestTemplate] ein jSON-Konverter, der mit der Bibliothek [Jackson] implementiert ist. Es gibt eine Besonderheit bei diesen Konvertern: AA erfordert, dass wir ihn auch in der Anmerkung des Web-Clients [WebClient] angeben:


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

In Zeile 1 muss ein Konverter angegeben werden, obwohl wir diesen bereits programmgesteuert festlegen.

  • Zeile 12: Das so erstellte Objekt [RestTemplate] wird in die Implementierung der Schnittstelle [WebClient] eingefügt, und dieses Objekt wird den Client-Server-Dialog durchführen;

Methode [getAlea]

Die Methode [getAlea] lautet wie folgt:


  @Override
  public int getAlea(int a, int b) {
    // Dienstausführung
    Response<Integer> info;
    DaoException ex;
    try {
      // Warten
      waitSomeTime(delay);
      // Dienstausführung
      info = webClient.getAlea(a, b);
      int status = info.getStatus();
      if (status == 0) {
        // Das Ergebnis wird zurückgegeben
        return info.getBody();
      } else {
        // Ausnahme wird protokolliert
        ex = new DaoException(mapper.writeValueAsString(info.getMessages()), status);
      }
    } catch (JsonProcessingException | RuntimeException e) {
      // Ausnahme protokollieren
      ex = new DaoException(e, 100);
    }
    // Ausnahme auslösen
    throw ex;
  }
...
  // private Methoden -------------------
  private void waitSomeTime(int delay) {
    try {
      Thread.sleep(delay);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
}
  • Zeile 8: Es wird [delay] Millisekunden gewartet;
  • Zeile 10: Es wird lediglich die Methode mit derselben Signatur in der Klasse aufgerufen, die die Schnittstelle [WebClient] implementiert;
  • Zeile 11: Die vom Server erhaltene Antwort wird anhand ihres [status] analysiert;
  • Zeilen 12–14: Wenn auf der Serverseite kein Fehler aufgetreten ist (status = 0), wird das Ergebnis der Methode zurückgegeben;
  • Zeile 17: Wenn ein Fehler auf der Serverseite aufgetreten ist (status!=0), wird eine Ausnahme vorbereitet, ohne sie auszulösen. Der Server hat eine Liste mit Fehlermeldungen übermittelt. Wir erstellen eine Ausnahme mit der Zeichenkette jSON aus der Liste der Servermeldungen als einzige Meldung;
  • Zeilen 19–22: Weitere Ausnahmefälle;
  • Zeile 24: Wenn wir hier angelangt sind, ist zwangsläufig eine Ausnahme aufgetreten. Diese wird nun ausgelöst;

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


package exemples.android.dao;

import java.util.ArrayList;
import java.util.List;

public class DaoException extends RuntimeException {

  // Fehlercode
  private int code;

  // Konstruktoren
  public DaoException() {
  }

  public DaoException(String detailMessage, int code) {
    super(detailMessage);
    this.code = code;
  }

  public DaoException(Throwable throwable, int code) {
    super(throwable);
    this.code = code;
  }

  // Getter und Setter
...
}
  • Zeile 6: Die Ausnahme [DaoException] ist eine unkontrollierte Ausnahme;

Methode [setUrlServiceWebJson]

Die Methode [setUrlServiceWebJson] lautet wie folgt:


  @Override
  public void setUrlServiceWebJson(String urlServiceWebJson) {
    // Der URL des Dienstes REST wird festgelegt
    webClient.setRootUrl(urlServiceWebJson);
}
  • Zeile 4: Der URL des Webdienstes wird über die Methode [setRootUrl] der Schnittstelle [WebClient] festgelegt. Diese Methode existiert, weil diese Schnittstelle die Schnittstelle [RestClientRootUrl] erweitert;

1.16.2.5. Das Paket [architecture]

Das Paket [architecture] umfasst die Elemente, die die Anwendung strukturieren:

1.16.2.5.1. Die Schnittstelle [IMainActivity]

Die Schnittstelle [IMainActivity] listet die Methoden auf, die von der Aktivität der Anwendung implementiert werden müssen:


package exemples.android.architecture;

import exemples.android.dao.IDao;

public interface IMainActivity extends IDao {

  // Zugriff auf die Sitzung
  Session getSession();

  // Wechsel der Ansicht
  void navigateToView(int position);

  // Warten
  void beginWaiting();

  void cancelWaiting();

  // Debug-Modus
  boolean IS_DEBUG_ENABLED = true;
  // Wartezeit auf die Antwort
  int TIMEOUT = 1000;
  // Nachbarschaft der Fragmente
  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 Ladebildes der Anwendung hinzugefügt (Zeilen 14, 16);
  • Zeile 21: Die maximale Wartezeit auf die Antwort des Servers wird auf 1 Sekunde festgelegt;

1.16.2.5.2. Die Klasse [Utils]

In der Klasse [Utils] wurden statische Hilfsmethoden zusammengefasst, die von verschiedenen Stellen der Anwendungsarchitektur aus aufgerufen werden können:


package exemples.android.architecture;

import java.util.ArrayList;
import java.util.List;

public class Utils {

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

  // Liste der Meldungen einer Ausnahme – Version 2
  static public String getMessagesForAlert(Throwable th) {
    // Der anzuzeigende Text wird zusammengestellt
    StringBuilder texte = new StringBuilder();
    List<String> messages = getMessagesFromException(th);
    int n = messages.size();
    for (String message : messages) {
      texte.append(String.format("%s : %s\n", n, message));
      n--;
    }
    // Ergebnis
    return texte.toString();
  }

}
  • Zeilen 9–18: Erstellt die Liste der in einer Throwable enthaltenen Fehlermeldungen;
  • Zeilen 21–32: Basierend auf der vorherigen Methode wird aus der erhaltenen Liste der Meldungen der Text erstellt, der in einer Android-Warnmeldung angezeigt werden soll;
  • Zeilen 27–28: Die Meldungen werden nummeriert. Die kleinste Nummer (1) entspricht der ersten Ausnahme und die höchste Nummer der jüngsten Ausnahme im Ausnahmestapel;

1.16.2.5.3. Die abstrakte Klasse [AbstractFragment]

Die Klasse [AbstractFragment] hat zwei Aufgaben:

  1. sicherzustellen, dass die Methode [updateFragments] der untergeordneten Klassen bei der Anzeige des Fragments immer und nur einmal aufgerufen wird;
  2. den Zustand und die Methoden der untergeordneten Klassen, bei denen dies möglich ist, auszulagern;

Aufgrund der zweiten Aufgabe haben wir die Operationen zur Verwaltung des Ladebildes in diese Klasse aufgenommen: Alle Fragmente einer asynchronen Android-Anwendung müssen diese Art von Problem bewältigen:


  // Wartezeitverwaltung
  protected void beginWaiting() {
    // Die Sanduhr wird eingeblendet
    mainActivity.beginWaiting();
  }

  protected void cancelWaiting() {
    // Die Sanduhr wird entfernt
    mainActivity.cancelWaiting();
}

1.16.2.6. Die Ansicht

1.16.2.6.1. Die Ansicht [vue1.xml]
  

Im Vergleich zum vorherigen Beispiel entwickelt sich die Ansicht [vue1.xml] wie folgt:

 
 
  • Bei [1] muss der Benutzer vor jedem Aufruf des Webdienstes die URL des Webdienstes sowie die Wartezeit [2] angeben;
  • In [3] werden die Antworten gezählt;
  • In [4] kann der Benutzer seine Anfrage abbrechen;
  • in [5] wird ein Warteindikator angezeigt, wenn die Zahlen angefordert werden. Er verschwindet, sobald alle Zahlen empfangen wurden oder der Vorgang abgebrochen wurde;

Image

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

Der Leser wird aufgefordert, die Datei [vue1.xml] aus den Beispielen zu laden. Im Folgenden geben wir die Identifikatoren der neuen Komponenten an:

Image

Type
Id
1
EditText
edt_nbaleas
2
TextView
txt_errorNbAleas
3
EditText
edt_a
4
EditText
edt_b
5
TextView
txt_errorIntervalle
6
EditText
editTextUrlServiceWeb
7
TextView
textViewErreurUrl
8
EditText
editTextDelay
9
TextView
textViewErreurDelay
10
Button
btn_Executer
11
Button
btn_Annuler
12
TextView
txt_Reponses
13
ListView
lst_reponses

Die Schaltflächen [10-11] befinden sich physisch übereinander. Zu einem bestimmten Zeitpunkt wird nur eine der beiden sichtbar sein.

1.16.2.6.2. Das Fragment [Vue1Fragment]
  

Das Grundgerüst des Fragments [Vue1Fragment] 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 {

  // Elemente der Benutzeroberfläche
  @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;
...
  // Lokale Daten
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;

  @AfterViews
  void afterViews() {
    // Speicher
    afterViewsDone=true;
    // zunächst keine Fehlermeldungen
    txtErrorAleas.setVisibility(View.INVISIBLE);
    txtErrorIntervalle.setVisibility(View.INVISIBLE);
    txtMsgErreurUrlServiceWeb.setVisibility(View.INVISIBLE);
    textViewErreurDelay.setVisibility(View.INVISIBLE);
    // Schaltfläche [Annuler] ausgeblendet
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnExecuter.setVisibility(View.VISIBLE);
    // Antwortliste
    reponses = new ArrayList<>();
  }
...
  • Zeilen 24–49: die Referenzen auf die Komponenten der Ansicht [vue1.xml] (Zeile 20);
  • Zeilen 55–69: die Methode [@AfterViews], die ausgeführt wird, wenn die Referenzen aus den Zeilen 24–49 initialisiert wurden;
  • Zeile 58: Nicht vergessen – erforderlich für den Lebenszyklus des Fragments;
  • Zeilen 60–63: Die Fehlermeldungen werden ausgeblendet;
  • Zeilen 65–66: Die Schaltfläche [Annuler] (Zeile 65) wird ausgeblendet und die Schaltfläche [Exécuter] (Zeile 66) wird angezeigt. Zur Erinnerung: Sie liegen physisch übereinander;
  • Zeile 68: Das Feld aus Zeile 52 enthält die Liste der Zeichenfolgen, die von der Methode ListView aus den Antworten angezeigt werden sollen;

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


  @Override
  protected void updateFragment() {
    // Der Adapter für die Antwortliste wird erstellt
    adapterReponses = new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, reponses);
    listReponses.setAdapter(adapterReponses);
}
  • Zeilen 4–5: Der Adapter „ListView“ für die Antworten wird erstellt. Er wird in einer Instanzvariablen gespeichert, damit er den anderen Methoden der Klasse zur Verfügung steht;

Ein „Klick“ auf die Schaltfläche [Exécuter] löst die Ausführung der folgenden Methode aus:


// die Eingaben
  private int nbAleas;
  private int a;
  private int b;
  private String urlServiceWebJson;
  private int delay;

  // Lokale Daten
  private int nbInfos;
  private List<String> reponses;
  private ArrayAdapter<String> adapterReponses;
  private boolean hasBeenCanceled;

  @Click(R.id.btn_Executer)
  protected void doExecuter() {
    // Die vorherigen Antworten werden gelöscht
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // Der Antwortzähler wird auf 0 zurückgesetzt
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // Gültigkeit der Eingaben prüfen
    if (!isPageValid()) {
      return;
    }
    // Initialisierung der Aktivität
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // Zufallszahlen werden angefordert
    for (int i = 0; i < nbAleas; i++) {
      getAlea(a, b);
    }
    // Wartephase wird gestartet
    beginWaiting();
  }

  @Background(id = "alea")
  void getAlea(int a, int b) {
    // Hier sollte so wenig wie möglich geschehen
    // Auf jeden Fall keine Anzeige – diese muss im UiThead erfolgen
    try {
      // Das Ergebnis wird im UiThread angezeigt
      showInfo(mainActivity.getAlea(a, b));
    } catch (RuntimeException e) {
      // Die Ausnahme wird im UiThread angezeigt
      showAlert(e);
    }
  }
  • Zeilen 17–18: Die vorherige Antwortliste wird vom Server gelöscht. Dazu wird in Zeile 17 die Datenquelle [reponses], die dem Adapter ListView zugeordnet ist, geleert;
  • Zeile 19: Ein boolescher Wert, der uns anzeigt, ob der Benutzer seine Anfrage abgebrochen hat oder nicht;
  • Zeilen 21–22: Es wird ein Zähler mit dem Wert Null für die Anzahl der Antworten angezeigt;
  • Zeilen 24–26: Die Eingaben aus den Zeilen [2-6] werden abgerufen und auf ihre Gültigkeit überprüft. Ist eine davon ungültig, wird die Methode abgebrochen (Zeile 25) und der Benutzer kehrt zur Benutzeroberfläche zurück;
  • Zeilen 28–29: Sind alle eingegebenen Daten gültig, werden die URL des Webdienstes (Zeile 28) sowie die Wartezeit vor jedem Aufruf des Dienstes (Zeile 29) an die Aktivität übermittelt. Diese Informationen werden von der Schicht [DAO] benötigt, und es sei daran erinnert, dass die Aktivität mit dieser kommuniziert;
  • Zeilen 31–33: Die Zufallszahlen werden nacheinander von der Methode [getAlea] in Zeile 39 angefordert;
  • Zeile 38: Die Methode [getAlea] ist mit der Annotation AA [@Background] versehen, wodurch sie in einem anderen Thread (Ausführungsfluss, Prozess) als der, in dem die visuelle Benutzeroberfläche ausgeführt wird. Es ist nämlich zwingend erforderlich, jeden Internetaufruf in einem anderen Thread als dem der visuellen Benutzeroberfläche auszuführen. Somit können zu einem bestimmten Zeitpunkt mehrere Threads vorhanden sein:
    • denjenigen, der die visuelle Benutzeroberfläche UI (User Interface) anzeigt und deren Ereignisse verarbeitet,
    • die [nbAleas]-Threads, die jeweils eine Zufallszahl vom Webdienst anfordern. Diese Threads werden asynchron gestartet: Der Thread „UI“ startet einen Thread „[getAlea]“ (Zeile 32), der eine Zufallszahl vom Webdienst anfordert und nicht auf dessen Beendigung wartet. Die Beendigung wird ihm durch ein Ereignis signalisiert. Somit werden die [nbAleas]-Threads parallel gestartet. Es ist möglich, die Anwendung so zu konfigurieren, dass sie jeweils nur einen Thread startet. In diesem Fall gibt es eine Warteschlange der auszuführenden Threads;

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

  • Zeile 44: Die Methode [getAlea] der Aktivität wird aufgerufen. Dies geschieht also in einem separaten Thread, der nicht mit dem von UI zusammenfällt. Dieser führt den Aufruf des Webdienstes durch und wartet nicht auf die Antwort. Er wird später durch ein Ereignis darüber informiert, dass die Antwort verfügbar ist. Zu diesem Zeitpunkt wird in Zeile 44 die Methode [showInfo] mit der empfangenen Antwort als Parameter aufgerufen;
  • Zeilen 45–47: Die Ausführung der Webanfrage kann eine Ausnahme auslösen. In diesem Fall werden die Fehlermeldungen der Ausnahme in einer Warnmeldung angezeigt;
  • Zeile 35: Es wird auf die Ergebnisse gewartet:
    • Es wird ein Warnhinweis angezeigt;
    • die Schaltfläche [Annuler] ersetzt die Schaltfläche [Exécuter]. Da die gestarteten Threads asynchron sind, wartet der Thread von UI nicht auf sie, und Zeile 35 wird vor deren Beendigung ausgeführt. Sobald die Methode [beginWaiting] beendet ist, kann UI wieder auf Benutzeraktionen reagieren, wie beispielsweise den Klick auf die Schaltfläche [Annuler]. Wären die gestarteten Threads synchron gewesen, würde man erst dann zu Zeile 35 gelangen, wenn alle Threads beendet wären. Ein Abbruch dieser Threads wäre dann nicht mehr sinnvoll;

Die Methode [showInfo] lautet wie folgt:


  @UiThread
  protected void showInfo(int alea) {
    if (!hasBeenCanceled) {
      // Eine weitere Information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // Sind wir fertig?
      if (nbInfos == nbAleas) {
        // Das Warten wird beendet
        cancelWaiting();
      }
      // Die Information wird zur Liste der Antworten hinzugefügt
      reponses.add(0, String.valueOf(alea));
      // Die Antworten werden angezeigt
      adapterReponses.notifyDataSetChanged();
    }
}
  • Die Methode [showInfo] wird innerhalb des Threads [getAlea] aufgerufen, der durch [@Background] markiert ist. Diese Methode aktualisiert die visuelle Schnittstelle UI. Dies ist nur möglich, wenn sie innerhalb des Threads von UI 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 [Exécuter] ersetzt die Schaltfläche [Annuler]);
  • Zeilen 12–15: Die empfangene Zufallszahl wird der von der Komponente [ListView listReponses] angezeigten Antwortliste hinzugefügt und diese wird aktualisiert;

Die Methode [showAlert] lautet wie folgt:


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

Die Logik entspricht der der Methode [showInfo]:

  • Zeile 1: Die Anmerkung [@UiThread] ist obligatorisch;
  • 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 [Annuler] 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 [1] fest;
    • [setMessage]: Legt die Meldung fest, die im Warnfenster [2] angezeigt wird;
    • [setNeutral]: Legt die Schaltfläche fest, mit der das Warnfenster geschlossen wird [3];
    • [show]: Fordert die Anzeige des Warnfensters an;
 

Der „Klick“ auf die Schaltfläche [Annuler] wird mit der folgenden Methode verarbeitet:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // Speicher
    hasBeenCanceled=true;
    // Die asynchrone Aufgabe wird abgebrochen
    BackgroundExecutor.cancelAll("alea", true);
    // Wartezeit beendet
    cancelWaiting();
}
  • Zeile 4: Es wird vermerkt, dass der Benutzer seine Anfrage abgebrochen hat;
  • Zeile 6: Alle durch die Zeichenfolge [alea] identifizierten Aufgaben werden abgebrochen. Der zweite Parameter [true] bedeutet, dass diese Aufgaben auch dann abgebrochen werden müssen, wenn sie bereits gestartet wurden. Die Kennung [alea] wird verwendet, um die Methode [getAlea] des Fragments (Zeile 1 unten) zu qualifizieren:

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

Hinweis: Es hat sich herausgestellt, dass Zeile 6 des Codes der Methode [doAnnuler] fehlerhaft funktionierte. Aus diesem Grund wurde der boolesche Wert [hasBeenCanceled] hinzugefügt. Tatsächlich wurde im Falle einer Ausnahme (Server nicht erreichbar) das Warnfenster n-mal angezeigt, wenn n Zufallszahlen angefordert worden waren.

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 in der Ansicht, die der Aktivität [MainActivity] zugeordnet 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">
      <!-- Wartebild -->
      <ProgressBar
        android:id="@+id/loadingPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"/>

    </android.support.v7.widget.Toolbar>
    <!-- Wartebild -->
  </android.support.design.widget.AppBarLayout>
...
  • Zeilen 17–21: das Ladebild;

1.16.2.7.2. Die Aktivität [MainActivity]

Die Aktivität [MainActivity] unterscheidet sich kaum von ihrer Vorgängerin [Exemple-14]. Zunächst wird ihr die Ebene [DAO] zugeführt:


  // Einspritzventil
  @Bean(Dao.class)
  protected IDao dao;
...
  @AfterInject
  protected void afterInject() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // Konfiguration der Ebene [DAO]
    setTimeout(TIMEOUT);
}
  • Zeilen 2–3: Einfügen der Ebene [DAO] durch eine Anmerkung AA;
  • Zeilen 5–13: Code, der nach dieser Einfügung ausgeführt wird;
  • Zeile 12: Festlegung des timeout der Schicht [DAO]

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


  // Implementierung IMainActivity --------------------------------------------------------------------
  @Override
  public void navigateToView(int position) {
    // die Positionsansicht wird angezeigt
    if (mViewPager.getCurrentItem() != position) {
      // Anzeige des Fragments
      mViewPager.setCurrentItem(position);
    }
  }

  // Verwaltung des Wartebildes
  public void cancelWaiting() {
    loadingPanel.setVisibility(View.INVISIBLE);
  }

  public void beginWaiting() {
    loadingPanel.setVisibility(View.VISIBLE);
  }

  // Implementierung IDao --------------------------------------------------------------------

  @Override
  public int getAlea(int a, int b) {
    // Ausführung
    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ührung des Projekts

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

Image

Um zu erfahren, was in [1] eingegeben werden muss, gehen Sie wie folgt vor. Öffnen Sie ein Befehlsfenster 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 virtuelle Maschine VirtualBox Ihrem Rechner die Adressen IP hinzugefügt (Zeilen 10 und 18). Diese Adressen sind besonders praktisch, da sie nicht von der Windows-Firewall blockiert werden. Zeile 30 gibt die Adresse IP 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 hier die Firewall, falls Sie eine haben.

Testen Sie die Anwendung in den folgenden Fällen:

  • 100 Zufallszahlen im Intervall [1000, 2000] ohne Wartezeit;
  • 2000 Zufallszahlen im Intervall [10000, 20000] ohne Wartezeit und brechen Sie die Wartezeit vor Ende der Generierung ab;
  • 5 Zufallszahlen im Intervall [100, 200] mit einer Wartezeit von 5000 ms und brechen Sie die Wartezeit vor Ende der Generierung ab;

1.16.2.9. Abbruchbehandlung

Um nachverfolgen zu können, was geschieht, wenn der Benutzer den Abbruch anfordert oder dieser aufgrund einer Ausnahme ausgelöst wird, fügen wir der Schnittstelle [IDao] (siehe Abschnitt 1.16.2.4.1) die folgende Methode hinzu:


package exemples.android.dao;

public interface IDao {

  ...

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

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


  // Debug-Modus
  private boolean isDebugEnabled;
  // Klassenname
  private String className;
..
  // Konstruktor
  public Dao() {
    // Klassenname
    className = getClass().getSimpleName();
  }
...
  // Schnittstelle IDao -------------------------------------------------------------------
  @Override
  public int getAlea(int a, int b) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // Dienstausführung
    Response<Integer> info;
...
  @Override
  public void setDebugMode(boolean isDebugEnabled) {
    this.isDebugEnabled = isDebugEnabled;
}
  • Zeile 9: Wir notieren den Namen der Klasse;
  • Zeilen 16–18: Wir schreiben jedes Mal einen Log-Eintrag, wenn die Methode [getAlea] aufgerufen wird;

Außerdem fügen wir im Fragment [Vue1Fragment] die folgenden Log-Einträge hinzu:


  @UiThread
  protected void showInfo(int alea) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    ....
  }

  @UiThread
  protected void showAlert(Throwable th) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    ...
    }
}

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
   ...
}

Jedes Mal, wenn das Fragment [Vue1Fragment] eine Information von der Ebene [DAO] erhält, wird ein Log ausgegeben. Außerdem wird das Ereignis protokolliert, wenn die Methode [doAnnuler] aufgerufen wird.

Test 1

Es werden 5 Zahlen angefordert, obwohl der Server nicht gestartet wurde. Es liegen folgende Protokolleinträge vor:

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. Zur Erinnerung: Es handelt sich um asynchrone Aufrufe, die vom Fragment [VueFragment] getätigt werden, und dieses wartet nicht auf das Ergebnis seines Aufrufs;
  • Zeile 7: Die erste Anfrage HTTP wurde gestellt, und das Fragment [VueFragment] hat seine erste Ausnahme erhalten;
  • Zeile 8: Es fordert daraufhin die Abbruch aller Anfragen an;
  • Zeilen 9–12: Es ist jedoch zu sehen, dass er die folgenden vier Ausnahmen erhält. Somit 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 und klicken wir auf [Annuler], bevor diese Verzögerung abgelaufen ist. 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 Stornierung der Anfragen angefordert;
  • Zeile 8: Es ist zu sehen, dass [Vue1_Fragment] fünf Werte erhält. Auch hier wurden alle ausstehenden asynchronen Anfragen ausgeführt;

Aus diesem Grund mussten wir einen booleschen Wert [hasBeenCanceled] verwalten, um zu vermeiden, dass etwas angezeigt wird, obwohl ein Abbruch angefordert wurde. Im Code für den Abbruch:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // Speicher
    hasBeenCanceled = true;
    // Die asynchrone Aufgabe wird abgebrochen
    BackgroundExecutor.cancelAll("alea",true);
    // Wartezeit beendet
    cancelWaiting();
}

führt der Code in Zeile 10 nicht das aus, was erwartet wird. Möglicherweise liegt dies daran, dass sich die asynchronen Aufgaben dieselbe, mit [@Background] annotierte Methode teilen:


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

1.17. Beispiel 16: Umgang mit Asynchronität mit RxAndroid

Wir schlagen nun vor, die für Android-Anwendungen erforderliche Asynchronität mit einer Bibliothek namens RxJava, [http://reactivex.io/] und deren für die Android-Umgebung abgeleitete Version [RxAndroid] zu handhaben. Dazu werden wir den Kurs [Introduction à RxJava. Application aux environnements Swing et Android] nutzen.

1.17.1. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-1] in [Exemple-16]:

1.17.2. Gradle-Konfiguration

  

In [build.gradle] fügen wir die Abhängigkeit zur Bibliothek [RxAndroid] hinzu:


dependencies {
  ...
  compile 'io.reactivex:rxandroid:1.2.0'
}

1.17.3. Die Schicht [DAO]

  

1.17.4. Die Schnittstelle [IDao]

Die Schnittstelle [IDao] sieht nun wie folgt aus:


package exemples.android.dao;

import rx.Observable;

public interface IDao {

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

  // URL des Webdienstes
  void setUrlServiceWebJson(String url);

  // Maximale Wartezeit (ms) für die Antwort des Servers
  void setTimeout(int timeout);

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

  // Debug-Modus
  void setDebugMode(boolean isDebugEnabled);
}
  • Zeile 8: Die Methode [getAlea] gibt nun einen Typ [Observable] aus der Bibliothek RxJava zurück (Zeile 3). Das Prinzip lautet wie folgt:

Ein Strom von Elementen vom Typ Observable&lt;T&gt; wird von einem oder mehreren Abonnenten (Subscriber, Beobachtern, Konsumenten) vom Typ Subscriber&lt;T&gt; beobachtet. Die Bibliothek RxJava ermöglicht es, dass der Observable<T>-Strom in einem Thread vom Typ T1 und sein Beobachter vom Typ Subscriber<T> in einem Thread vom Typ T2 ausgeführt werden, ohne dass sich der Entwicklersich um die Verwaltung des Lebenszyklus dieser Threads und um naturgemäß schwierige Probleme wie den Datenaustausch zwischen Threads und deren 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();

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

  // generische Abfrage
  protected <T> Observable<T> getResponse(final IRequest<T> request) {
    // Dienstausführung
    return rx.Observable.create(new rx.Observable.OnSubscribe<T>() {
      @Override
      public void call(Subscriber<? super T> subscriber) {
        DaoException ex = null;
        // Dienstausführung
        try {
          // Die Anfrage wird synchron gestellt und die Antwort an den Abonnenten weitergeleitet
          Response<T> response = request.getResponse();
          // Fehler?
          int status = response.getStatus();
          if (status != 0) {
            // Die Ausnahme wird protokolliert
            ex = new DaoException(mapper.writeValueAsString(response.getMessages()), status);
          } else {
            // Die Antwort wird gesendet
            subscriber.onNext(response.getBody());
            // Das Ende des Observables wird gemeldet
            subscriber.onCompleted();
          }
        } catch (JsonProcessingException | RuntimeException e) {
          // Die Ausnahme wird protokolliert
          ex = new DaoException(e, 100);
        }
        // Ausnahme?
        if (ex != null) {
          // Die Ausnahme wird ausgelöst
          subscriber.onError(ex);
        }
      }
    });
  }

}
  • Die Klasse [AbstractDao] enthält als Hauptelement eine generische Methode [getResponse], die dazu dient, vom Server einen Typ [Response<T>] abzurufen, wobei T der vom Client gewünschte Ergebnistyp HTTP (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 verfügt nur über eine Methode [getResponse], und genau diese Methode liefert die gewünschte Antwort [Response<T>];
  • dank der beiden vorangegangenen Elemente kann die Klasse [AbstractDao] als übergeordnete Klasse für jede Client-Schicht [Dao] 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 Client HTTP 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 des Typs [rx.Observable.OnSubscribe<T>], einer 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, einem Beobachter das Ende der Emissionen mitzuteilen;

Ein Typ [Observable<T>] unterliegt bestimmten Einschränkungen:

  • Er sendet seine Elemente mit der Methode [onNext(T element)];
  • die Methode [onCompleted] muss genau einmal aufgerufen werden, sobald keine Elemente mehr an den Beobachter gesendet werden müssen;
  • die Methode [onCompleted] wird nicht aufgerufen, wenn die Methode [onError(Throwable th)] bereits aufgerufen wurde;

In unserem Beispiel:

  • ist der Beobachter das Fragment [Vue1Fragment]. Es ist dieses Fragment, das die von [Observable<T>] gesendeten Elemente (Element oder Ausnahme) verarbeitet;
  • Der erstellte Typ [Observable<T>] gibt nur ein einziges Element aus (Zeile 37);
  • Zeile 29: Sendet eine synchrone Anfrage HTTP an den Server und erhält den Typ [Response<T>]. Diese Anfrage HTTP wird durch den Typ [IRequest] gewährleistet, der als Parameter an die generische Methode [getResponse] übergeben wird;
  • Zeile 31: Der Typ status wird aus der Antwort abgerufen;
  • Zeilen 32–34: Wenn es sich bei diesem status um einen Fehler handelt, wird eine Ausnahme vorbereitet;
  • Zeilen 36–39: Wenn dieser status nicht von einem Fehler stammt, wird die vom Kunden tatsächlich erwartete Antwort ausgegeben (Zeile 37) und dem Beobachter mitgeteilt, dass keine weiteren Ausgaben mehr erfolgen werden (Zeile 39);
  • Zeilen 41–44: Wenn die Anfrage HTTP mit einer Ausnahme endet, wird diese protokolliert;
  • Zeilen 46–49: Wenn sich die Ausnahme [ex] von null unterscheidet, wird sie an den Beobachter gesendet. Es ist hier nicht erforderlich, die Methode [onCompleted] aufzurufen, um dem Beobachter mitzuteilen, dass keine weiteren Elemente mehr ausgegeben werden. Dies ist implizit;

Aus diesen Erläuterungen lässt sich Folgendes ableiten:

  • die generische Methode [<T> Observable<T> getResponse(final IRequest<T> request)] einen Typ [Observable<T>] zurückgibt, der entweder ein Element vom Typ T oder eine Ausnahme ausgibt;
  • dass diese Methode als einzigen Parameter einen Typ [IRequest<T>] akzeptiert, dessen einzige Methode [getResponse()] den Zugriff HTTP ausführt, der 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 {

  // Client des Dienstes REST
  @RestService
  protected WebClient webClient;

  // Wartezeit vor der Ausführung der Anfrage
  private int delay;
  // Debug-Modus
  private boolean isDebugEnabled;
  // Name der Klasse
  private String className;

  // Konstruktor
  public Dao() {
    // Name der Klasse
    className = getClass().getSimpleName();
  }


  // Schnittstelle IDao -------------------------------------------------------------------
  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // Web-Client-Ausführung
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // Warten
        waitSomeTime(delay);
        // synchroner Aufruf von HTTP
        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 Abfrage HTTP wird über die Schnittstelle AA [webClient] durchgeführt, wie zuvor bereits geschehen. Wir wissen, dass wir einen Typ [Response<Integer>] erhalten werden, der genau der Typ ist, den die Methode [IRequest<Integer>.getReponse()] zurückgeben muss;
  • Zeile 36: Hier wird eine Eigenschaft namens closure verwendet: die Fähigkeit, bei ihrer Erstellung Werte, die außerhalb der Instanz liegen, in diese zu kapseln, in diesem Fall die Werte von [a, b] aus Zeile 24. Dadurch kann die Methode [IRequest<Integer>.getReponse()] ohne Parameter auskommen. Diese wurden in den Methodenkörper geschrieben. Und wo man normalerweise die Parameter der Methode ändern würde (a, b) -> (x, y), erstellt man hier eine neue Instanz von [IRequest<Integer>], die die Werte von x und y einkapselt;

1.17.7. Die Klasse [MainActivity]

Die Klasse [MainActivity], die die Schnittstelle [IDao] implementiert, entwickelt sich wie folgt:


  // Implementierung von IDao --------------------------------------------------------------------

  @Override
  public Observable<Integer> getAlea(int a, int b) {
    // Ausführung
    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() {
    // die vorherigen Antworten werden gelöscht
    reponses.clear();
    adapterReponses.notifyDataSetChanged();
    hasBeenCanceled = false;
    // der Antwortzähler wird auf 0 zurückgesetzt
    nbInfos = 0;
    infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
    // Gültigkeit der Eingaben prüfen
    if (!isPageValid()) {
      return;
    }
    // Initialisierung der Aktivität
    mainActivity.setUrlServiceWebJson(urlServiceWebJson);
    mainActivity.setDelay(delay);
    // Zufallszahlen werden angefordert
    getAleasInBackground(a, b);
    // Wartephase wird gestartet
    beginWaiting();
}
  • Zeile 18: Die Zufallszahlen werden von der Methode [getAleasInBackground] angefordert, die so benannt ist, weil die Zahlen in einem anderen Thread als dem der Benutzeroberfläche angefordert werden;

  private int nbReponses = 0;
  // Abonnements für Observables
  private List<Subscription> abonnements;

// Anmerkung: [Background] ist überflüssig
  void getAleasInBackground(int a, int b) {
    // Zunächst keine Antworten und keine Abonnements
    nbReponses = 0;
    abonnements.clear();
    // Das Observable wird vorbereitet
    Observable<Integer> response = Observable.empty();
    // die Ergebnisse der verschiedenen Aufrufe werden zusammengeführt HTTP
    // sie werden auf einem E/A-Thread ausgeführt
    for (int i = 0; i < nbAleas; i++) {
      response = response.mergeWith(mainActivity.getAlea(a, b).subscribeOn(Schedulers.io()));
    }
    // Das kumulierte Observable wird auf dem Thread des UI beobachtet
    response = response.observeOn(AndroidSchedulers.mainThread());
    try {
      // Das Observable wird ausgeführt
      abonnements.add(response.subscribe(new Action1<Integer>() {
        @Override
        public void call(Integer alea) {
          // Die Informationen werden der Liste der Antworten hinzugefügt
          showInfo(alea);
        }
      }, new Action1<Throwable>() {
        @Override
        public void call(Throwable th) {
          // Fehlermeldung
          showAlert(th);
          // Wartezeit beendet
          doAnnuler();
        }
      }, new Action0() {
        @Override
        public void call() {
          // Wartezeit beendet
          cancelWaiting();
        }
      }));
    } catch (RuntimeException e) {
      // Die Ausnahme wird im UiThread angezeigt
      showAlert(e);
    }
}
  • Zeile 3: Ein Observable hat Abonnenten. Die Verbindung zwischen einem Abonnenten und dem von ihm beobachteten Prozess wird als Abonnement (Subscription) bezeichnet. Hier gibt es nur einen beobachteten Prozess und einen Abonnenten. Es gibt also nur ein Abonnement. Grundsätzlich gehen wir so vor, als könnten mehrere Prozesse von verschiedenen Beobachtern beobachtet werden, was zu mehreren Abonnements führen würde;
  • Zeilen 11–18: Hier wird der beobachtete Prozess (Observable) konfiguriert. Man muss sich bewusst sein, dass es sich hierbei lediglich um eine Konfiguration handelt: Der Prozess wird nicht ausgeführt;
  • Zeile 11: Man geht von einem leeren Observable aus, einem Observable, das nichts ausgibt;
  • Zeilen 14–16: Zu diesem leeren Observable werden die Observables [nbAleas] hinzugefügt, bei denen es sich um die Abfragen [nbAleas] handelt, die Zufallszahlen [nbAleas] zurückgeben;
  • Zeile 15: Wie zuvor wird die Zufallszahl Nr. i von der Klasse [MainActivity] angefordert. Man muss unbedingt verstehen, dass hier noch keine Abfrage HTTP ausgeführt wird. Die Methode [mainActivity.getAlea(a, b)] wird ausgeführt und gibt einen Typ [Observable<Integer>] zurück. Dies ist ein Prozess, der beobachtet wird, sobald er gestartet wird;
  • Zeile 15: Die Methode [subscribeOn(Schedulers.io())] fordert, dass der Prozess (sobald er ausgeführt wird) auf einem E/A-Thread ausgeführt wird. Die Bibliothek RxJava bietet verschiedene Thread-Typen an. Der E/A-Thread ist für Aufrufe von HTTP geeignet;
  • Zeile 15: Das Observable Nr. i wird mit dem ursprünglichen Observable aus Zeile 11 zusammengeführt: Aus den Observables [nbAleas], die jeweils ein Element ausgeben, wird ein Observable erstellt, das [nbAleas] Elemente ausgibt. Dieses wird beobachtet. Dieses Observable sendet die Benachrichtigung [onCompleted], sobald alle Observables, aus denen es besteht, ihre eigene Benachrichtigung [onCompleted] gesendet haben. Dadurch müssen wir nicht mehr die Antworten zählen, wie wir es in der vorherigen Version getan haben, um festzustellen, ob wir alle erwarteten Zahlen erhalten haben;
  • Zeile 18: An dieser Stelle haben wir ein Observable konfiguriert, das sich aus [nbAleas] Observables zusammensetzt, die jeweils auf einem E/A-Thread ausgeführt werden;
  • Zeile 18: Die Methode [observeOn(AndroidSchedulers.mainThread())] dient dazu, festzulegen, auf welchem Thread die Beobachtung der vom Observable ausgegebenen Werte erfolgen soll. Hier gehört der Thread „[AndroidSchedulers.mainThread())]“ zur Bibliothek „RxAndroid“ und nicht zu „RxJava“. Er bezeichnet den UI-Thread, der auch als Event-Loop bezeichnet wird. Dieser Punkt ist wichtig: In einer Android-Anwendung darf die Änderung einer UI-Komponente nur im UI-Thread erfolgen, andernfalls kommt es zu einer Ausnahme;
  • Zeilen 19–45: Nachdem der zu beobachtende Prozess nun konfiguriert wurde, wird er ausgeführt;
  • Zeile 21: Es ist die Operation [Observable.subscribe], die die Ausführung des beobachteten Prozesses auslöst. Diese Operation startet die zuvor konfigurierten asynchronen Prozesse [nbAleas]. Die Ergebnisse dieser Prozesse werden dem Beobachter automatisch im UI-Thread zur Verfügung gestellt;
  • Wir erinnern uns, dass das Observable drei Arten von Ereignissen ausgibt:
    • [onNext]: wenn es ein Element ausgibt;
    • [onError]: wenn eine Ausnahme aufgetreten ist;
    • [onCompleted]: wenn es signalisiert, dass es keine Elemente mehr ausgibt;

Die Methode [Observable.subscribe] hat als Parameter drei Objekte vom Typ [Action1<Integer>, Action1<Throwable>, Action0], deren Methoden [call] dazu dienen, jedes dieser drei Ereignisse zu verarbeiten;

  • Zeilen 21–27: Der erste Parameter vom Typ [Action1<Integer>] dient zur Verarbeitung des Ereignisses [onNext]. Seine Methode [call] empfängt das Element, das vom Observable gesendet wurde (Zeile 23);
  • Zeile 25: Die Methode [showInfo] aus dem vorherigen Beispiel wird wiederverwendet;
  • Zeilen 27–35: Der zweite Parameter vom Typ [Action1<Throwable>] dient zur Verarbeitung des Ereignisses [onError]. Seine Methode [call] empfängt die Ausnahme, die vom Observable ausgelöst wurde (Zeile 29);
  • Zeile 31: Die Methode [showAlert] aus dem vorherigen Beispiel wird wiederverwendet;
  • Zeile 33: Es wird der Vorgang zum Abbrechen der Benutzeranfrage gestartet. Dabei werden alle derzeit ausgeführten Observables abgebrochen;
  • Zeilen 35–41: Der dritte Parameter vom Typ [Action0] dient zur Verarbeitung des Ereignisses [onCompleted]. Seine Methode [call] erhält keine Parameter;
  • Zeile 39: Die Wartezeit wird abgebrochen;

Die Methode [showInfo] entwickelt sich wie folgt:


  // Anmerkung: [UiThread] ist überflüssig
  protected void showInfo(int alea) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("showInfo(%s)", alea));
    }
    if (!hasBeenCanceled) {
      // eine weitere Information
      nbInfos++;
      infoReponses.setText(String.format("Liste des réponses (%s)", nbInfos));
      // Die Information wird zur Liste der Antworten hinzugefügt
      reponses.add(0, String.valueOf(alea));
      // die Antworten werden angezeigt
      adapterReponses.notifyDataSetChanged();
    }
}

Die Methode weist zwei Änderungen auf:

  • Zeile 1: Die Anmerkung AA [@UiThread] wurde entfernt;
  • Die Antworten werden nicht mehr gezählt, um festzustellen, ob die Wartezeit beendet werden soll oder nicht. Diese Information wird nun durch das Ereignis [onCompleted] des Observables bereitgestellt;

Die Methode [showAlert] entwickelt sich wie folgt:


  // Anmerkung: [UiThread] unnötig
  protected void showAlert(Throwable th) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Exception reçue");
    }
    if (!hasBeenCanceled) {
      // alles wird abgebrochen
      doAnnuler();
      // steht auf dem Plakat
      new AlertDialog.Builder(activity).setTitle("Des erreurs se sont produites").setMessage(Utils.getMessagesForAlert(th)).setNeutralButton("Fermer", null).show();
    }
}
  • Die einzige Änderung betrifft Zeile 1: Die Anmerkung „AA [@UiThread]“ wurde entfernt;

Schließlich ändert sich die Methode [doAnnuler] wie folgt:


  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), "Annulation demandée");
    }
    // Speicher
    hasBeenCanceled = true;
    // asynchrone Aufgaben werden abgebrochen
    if (abonnements != null) {
      for (Subscription abonnement : abonnements) {
        abonnement.unsubscribe();
      }
    }
    // Wartezeit beendet
    cancelWaiting();
}
  • Zeile 12: Kündigt ein Abonnement und damit die Beobachtung des zugehörigen Prozesses;

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 mit dem vorherigen Beispiel durchgeführt haben (Abschnitt 1.16.2.8).

1.17.10. Verwaltung der Kündigung

Wir führen dieselben Tests wie im vorherigen Beispiel durch (Abschnitt 1.16.2.9).

Test 1

Es werden 5 Zahlen angefordert, obwohl der Server nicht gestartet wurde. Es liegen folgende Protokolle vor:

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 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 und klicken wir auf [Annuler], bevor diese Verzögerung abgelaufen ist. Die Protokolle 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 Protokolleinträge mehr, was zeigt, dass der Beobachter (Vue1Fragment) keine Benachrichtigungen mehr vom beobachteten Prozess erhält.

Dies ist das erwartete Verhalten bei einer Stornierung. Wir können daher im Code von [Vue1Fragment] den booleschen Wert [hasBeenCanceled] entfernen, den wir im vorherigen Beispiel eingeführt hatten, da die Stornierung nicht wie erwartet funktionierte.

Die Tatsache, dass der Beobachter nach dem Abbrechen des Beobachtbaren keine Benachrichtigungen mehr erhält, bedeutet nicht, dass die Abfragen HTTP selbst abgebrochen werden. Um dies zu verdeutlichen, ändern wir die Klasse [Dao] wie folgt:


  @Override
  public Observable<Integer> getAlea(final int a, final int b) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(String.format("%s", className), String.format("getAlea [%s, %s] en cours", a, b));
    }
    // Webclient-Ausführung
    return getResponse(new IRequest<Integer>() {
      @Override
      public Response<Integer> getResponse() {
        // Warten
        waitSomeTime(delay);
        // Synchroner Aufruf von HTTP
        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 Abfrage HTTP aus Zeile 14;

Die Protokolle für Test Nr. 2 lauten dann 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: Die Antworten auf die fünf Abfragen HTTP werden ordnungsgemäß empfangen. Aufgrund des Abbruchs des Beobachtbaren werden diese Elemente jedoch nicht an den Beobachter übermittelt;

1.17.11. Fazit

Im weiteren Verlauf dieses Dokuments werden Client-Server-Anwendungen aus folgenden Gründen mit der Bibliothek RxAndroid anstelle der Bibliothek AA realisiert:

  1. RxAndroid kann in einer Android-Anwendung verwendet werden, die AA nicht nutzt;
  2. RxAndroid erleichtert nicht nur asynchrone Operationen, sondern bietet auch zahlreiche Methoden, um aus einem Observable ein neues zu erstellen. Diese Methoden haben in AA keine Entsprechung;
  3. Sobald man eine mit AA annotierte Klasse ableiten möchte, wie beispielsweise ein Fragment, stößt man auf erhebliche Probleme. 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 Bibliothek RxAndroid befassen möchten, können das Dokument [Introduction à RxJava. Application aux environnements Swing et Android] lesen. Darin wird RxAndroid ohne die Bibliothek AA verwendet.

1.18. Beispiel 17: Komponenten zur Dateneingabe

Wir werden ein neues Projekt erstellen, um einige gängige Komponenten in Dateneingabeformularen vorzustellen.

1.18.1. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-13] in [Exemple-17]:

Das neue Projekt wird nur eine Ansicht namens [vue1.xml] enthalten. Daher löschen wir die Ansicht [vue2.xml] und die zugehörigen Fragmente [Vue2Fragment] sowie [2]. Wir berücksichtigen diese Änderung im Fragment-Manager von [Mainactivity]:


  // Unser Fragment-Manager muss für jede Anwendung neu definiert werden
  // muss die folgenden Methoden definieren: getItem, getCount, getPageTitle
  public class SectionsPagerAdapter extends FragmentPagerAdapter {

    // die Fragmente
    private final Fragment[] fragments = {new Vue1Fragment_()};
....
}

Führen Sie das Projekt erneut aus. Es sollte nun wie zuvor die Ansicht Nr. 1 anzeigen. Wir werden auf der Grundlage dieses Projekts weiterarbeiten.

1.18.2. Die Ansicht XML des Formulars

  

Die von der Datei [vue1.xml] erzeugte Ansicht sieht wie folgt aus:

Image

Der Text XML 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 wichtigsten Komponenten des Formulars sind folgende:

  • Zeile 2: ein vertikales Layout [ScrollView]. Damit lässt sich
  • ein Formular darzustellen, das größer ist als der Bildschirm des
  • Tablet darzustellen. Das gesamte Formular wird durch
  • Scrollen;
 
  • Zeilen 125–132: ein Kontrollkästchen
  • Zeilen 134–159: eine Gruppe aus drei Optionsfeldern
  • Zeilen 161–166: eine Suchleiste
  • Zeilen 16–176: ein Eingabefeld
  • Zeilen 178–186: ein Ja/Nein-Schalter
  • Zeilen 188–195: ein Eingabefeld für die Uhrzeit
  • Zeilen 197–207: ein mehrzeiliges Eingabefeld
  • Zeilen 209–215: eine Dropdown-Liste
  • Zeilen 217–225: ein Datumsfeld
  • Alle anderen Komponenten sind [TextView], die Texte 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>
  <!-- Ansicht 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. Der Formularausschnitt

  

Die Klasse [Vue1Fragment] lautet wie folgt:


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;

// Ein Fragment ist eine Ansicht, die von einem Fragment-Container angezeigt wird
@EFragment(R.layout.vue1)
public class Vue1Fragment extends AbstractFragment {

  // die Felder der vom Fragment angezeigten Ansicht
  @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;

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

  @AfterViews
  void afterViews() {
    // Die erste Schaltfläche wird aktiviert
    radioButton1.setChecked(true);
    // der Kalender
    datePicker1.setCalendarViewShown(false);
    // das seekBar
    seekBar.setMax(100);
    seekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {

      public void onStopTrackingTouch(SeekBar seekBar) {
      }

      public void onStartTrackingTouch(SeekBar seekBar) {
      }

      public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        seekBarValue.setText(String.valueOf(progress));
      }
    });
    // die Dropdown-Liste
    list = new ArrayList<>();
    list.add("list 1");
    list.add("list 2");
    list.add("list 3");
  }


  @SuppressLint("DefaultLocale")
  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    ...
  }
 
@Override
  protected void updateFragment() {
    // Initialisierung des Adapters für die Dropdown-Liste
    dataAdapter = new ArrayAdapter<>(activity, android.R.layout.simple_spinner_item, list);
    dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    dropDownList.setAdapter(dataAdapter);
  }
}
  • Zeilen 22–49: Hier werden die Referenzen aller Komponenten des Formulars abgerufen: XML, [vue1] (Zeile 18);
  • Zeile 58: Mit der Methode [setChecked] kann ein Optionsfeld oder ein Kontrollkästchen aktiviert werden;
  • Zeile 60: Standardmäßig zeigt die Komponente [DatePicker] sowohl ein Datums-Eingabefeld als auch einen Kalender an. In Zeile 60 wird der Kalender entfernt;
  • Zeile 62: Mit [SeekBar].setMax() lässt sich der Maximalwert des Schiebereglers festlegen. Der Minimalwert ist 0;
  • Zeilen 63–74: Hier werden die Ereignisse des Schiebereglers verwaltet. Bei jeder vom Benutzer vorgenommenen Änderung soll der Wert des Schiebereglers in [TextView] aus Zeile 49 angezeigt werden;
  • Zeile 71: Der Parameter [progress] stellt den Wert des Schiebereglers dar;
  • Zeilen 76–79: Eine Liste von [String], die wir der Dropdown-Liste zuordnen werden;
  • Zeile 90: die Methode [updateFragment] des Fragments. Bei ihrer Ausführung wurde die Variable [activity] der übergeordneten Klasse initialisiert;
  • Zeile 92: Die Datenquelle [list] wird dem Adapter der Dropdown-Liste zugeordnet;
  • Zeilen 93–94: Der Adapter [dataAdapter] wird der Dropdown-Liste [dropDownList] zugeordnet;
  • Zeile 84: Die Methode [doValider] wird dem Klick auf die Schaltfläche [Valider] zugeordnet;

Die Methode [doValider] dient dazu, die vom Benutzer eingegebenen Werte anzuzeigen. Ihr Code lautet wie folgt:


  @Click(R.id.formulaireButtonValider)
  protected void doValider() {
    // Liste der anzuzeigenden Nachrichten
    List<String> messages = new ArrayList<>();
    // Kontrollkästchen
    boolean isChecked = checkBox1.isChecked();
    messages.add(String.format("CheckBox1 [checked=%s]", isChecked));
    // Optionsfelder
    int id = radioGroup.getCheckedRadioButtonId();
    String radioGroupText = id == -1 ? "" : ((RadioButton) activity.findViewById(id)).getText().toString();
    messages.add(String.format("RadioGroup [checked=%s]", radioGroupText));
    // das SeekBar
    int progress = seekBar.getProgress();
    messages.add(String.format("SeekBar [value=%d]", progress));
    // das Eingabefeld
    String texte = String.valueOf(saisie.getText());
    messages.add(String.format("Saisie simple [value=%s]", texte));
    // der Schalter
    boolean état = switch1.isChecked();
    messages.add(String.format("Switch [value=%s]", état));
    // das Datum
    int an = datePicker1.getYear();
    int mois = datePicker1.getMonth() + 1;
    int jour = datePicker1.getDayOfMonth();
    messages.add(String.format("Date [%d, %d, %d]", jour, mois, an));
    // der mehrzeilige Text
    String lignes = String.valueOf(multiLignes.getText());
    messages.add(String.format("Saisie multi-lignes [value=%s]", lignes));
    // die Uhrzeit
    int heure = timePicker1.getHour();
    int minutes = timePicker1.getMinute();
    messages.add(String.format("Heure [%d, %d]", heure, minutes));
    // die Dropdown-Liste
    int position = dropDownList.getSelectedItemPosition();
    String selectedItem = String.valueOf(dropDownList.getSelectedItem());
    messages.add(String.format("DropDownList [position=%d, item=%s]", position, selectedItem));
    // Anzeige
    doAfficher(messages);
}
  • Zeile 4: Die eingegebenen Werte werden in einer Meldungsliste gesammelt;
  • Zeile 6: Die Methode [CheckBox].isCkecked() ermöglicht es, festzustellen, ob ein Kontrollkästchen aktiviert ist oder nicht;
  • Zeile 9: Mit der Methode [RadioGroup].getCheckedButtonId() lässt sich die ID des ausgewählten Optionsfelds abrufen oder -1, wenn keines ausgewählt wurde;
  • Zeile 10: Mit dem Code [activity.findViewById(id)] lässt sich das markierte Optionsfeld ermitteln und somit dessen Beschriftung abrufen;
  • Zeile 13: Mit der Methode [SeekBar].getProgress() kann der Wert eines Schiebereglers abgerufen werden;
  • Zeile 19: Mit der Methode [Switch].isChecked() lässt sich feststellen, ob ein Schalter den Wert On (true) oder Off (false) hat;
  • Zeile 22: Mit der Methode [DatePicker].getYear() lässt sich das ausgewählte Jahr mithilfe eines [DatePicker]-Objekts abrufen;
  • Zeile 23: Mit der Methode [DatePicker].getMonth() lässt sich der ausgewählte Monat mit einem Objekt [DatePicker] im Intervall [0,11] abrufen;
  • Zeile 24: Mit der Methode [DatePicker].getDayOfMonh() lässt sich der ausgewählte Tag des Monats mit einem Objekt [DatePicker] im Intervall [1,31] abrufen;
  • Zeile 30: Mit der Methode [TimePicker].getHour() lässt sich die ausgewählte Uhrzeit mit einem Objekt [TimePicker] abrufen;
  • Zeile 31: Mit der Methode [TimePicker].getMinute() lassen sich die ausgewählten Minuten mit einem Objekt [TimePicker] abrufen;
  • Zeile 34: Mit der Methode [Spinner].getSelectedItemPosition() lässt sich die Position des ausgewählten Elements in einer Dropdown-Liste abrufen;
  • Zeile 35: Mit der Methode [Spinner].getSelectedItem() kann das in einer Dropdown-Liste ausgewählte Objekt abgerufen werden;

Die Methode [doAfficher], die die Liste der eingegebenen Werte anzeigt, lautet wie folgt:


    private void doAfficher(List<String> messages) {
        // Der anzuzeigende Text wird erstellt
        StringBuilder texte = new StringBuilder();
        for (String message : messages) {
            texte.append(String.format("%s\n", message));
        }
        // Anzeige
        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 Objekt vom Typ [StringBuilder] erstellt. Zum Verketten 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ührung des Projekts

Führen Sie das Projekt aus und testen Sie die verschiedenen Eingabekomponenten.

1.19. Beispiel 18: Verwendung einer Ansichtsvorlage

1.19.1. Erstellung des Projekts

Wir erstellen ein neues Projekt [Exemple-18] durch Kopieren des Projekts [Exemple-13].

1.19.2. Die View-Vorlage

Wir möchten die beiden Ansichten des Projekts übernehmen und in eine Vorlage einbinden:

  

Image

Jede der beiden Ansichten wird auf die gleiche Weise strukturiert:

  • in [1] eine Kopfzeile;
  • in [2] eine linke Spalte, die Links enthalten könnte;
  • in [3] eine Fußzeile;
  • in [4] ein Inhaltsbereich.

Dies wird durch Ändern der Basisansicht [activity_main.xml] der Aktivität erreicht;

Der Code XML der Ansicht [main] 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>
  • Der Header [1] ergibt sich aus den Zeilen 38–54;
  • der linke Bereich [2] ergibt sich aus den Zeilen 56–84;
  • die Fußzeile [3] ergibt sich aus den Zeilen 86–101;
  • Der Inhalt [4] wird aus den Zeilen 78–84 gebildet;

Die Ansicht XML [main] verwendet Informationen aus den Dateien [res / values / colors.xml] und [res / values / strings.xml]:

  

Die Datei [colors.xml] lautet wie folgt:


<?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 Datei [strings.xml] lautet wie folgt:


<?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 eine Ausführungsumgebung für dieses Projekt und führen Sie es aus.

1.20. Beispiel 19: Die Komponente [ListView]

Die Komponente [ListView] ermöglicht es, eine bestimmte Ansicht für jedes Element einer Liste zu wiederholen. Die wiederholte Ansicht kann beliebig komplex sein, von einer einfachen Zeichenfolge bis hin zu einer Ansicht, in der Informationen für jedes Element der Liste eingegeben werden können. Wir erstellen nun die folgende Komponente [ListView]:

Image

Jede Ansicht der Liste besteht aus drei Komponenten:

  • ein [TextView] mit Informationen;
  • ein [CheckBox];
  • ein anklickbares [TextView];

1.20.1. Erstellung des Projekts

Wir erstellen ein neues Projekt „[Exemple-19]“, indem wir das Projekt „[Exemple-18]“ kopieren.

  

Wir werden das Projekt wie in [3] beschrieben weiterentwickeln.

1.20.2. Die Sitzung

  

Die Sitzung speichert die Daten, die zwischen Aktivität und 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 {
  // eine Liste von Daten
  private List<Data> liste=new ArrayList<>();

  // Getter und Setter
...
}
  • Zeile 11: die von beiden Sichten genutzte Datenliste;

Die Klasse [Data] sieht wie folgt aus:


package exemples.android.architecture;

public class Data {

    // Daten
    private String texte;
    private boolean isChecked;

    // Konstruktor
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }

    // Getter und Setter
    ...
}
  • Zeile 6: Der Text, der in das erste [TextView] jedes Elements der Liste eingefügt wird;
  • Zeile 7: Der Boolesche Wert, der dazu dient, das [checkBox] jedes Elements der Liste zu markieren oder nicht;

1.20.3. Die Aktivität [MainActivity]

Der Code der Methode [@AfterInject] lautet nun wie folgt:


  // Session-Injektion
  @Bean(Session.class)
  protected Session session;
...
  @AfterInject
  protected void afterInject() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // Erstellung einer Datenliste
    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 ursprüngliche Ansicht [Vue1]

Die Ansicht XML [vue1.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 Komponente [TextView] [2];
  • Zeilen 27–35: die Komponente [ListView] [4];
  • Zeilen 18–25: die Komponente [Button] [3];

1.20.5. Die von [ListView] wiederholte Ansicht

Die durch [ListView] wiederholte Ansicht ist die folgende Ansicht [list_data]:


<?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 Komponente [TextView] [1];
  • Zeilen 16–23: die Komponente [CheckBox] [2];
  • Zeilen 25–35: die Komponente [TextView] [3];

1.20.6. Das Fragment [Vue1Fragment]

  

Das Fragment [Vue1Fragment] verwaltet die Ansicht XML [vue1]. 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 {

  // Die Felder der vom Fragment angezeigten Ansicht
  @ViewById(R.id.listView1)
  protected ListView listView;
  // Der Listenadapter
  private ListAdapter adapter;
  // Initialisierung abgeschlossen
  private boolean initDone = false;

  @AfterViews
  void afterViews() {
    // Speicher
    afterViewsDone = true;
  }

  @Click(R.id.button_vue2)
  void navigateToView2() {
    // Navigation zur Ansicht 2
    mainActivity.navigateToView(1);
  }

  public void doRetirer(int position) {
   ...
  }

  @Override
  protected void updateFragment() {
    if (!initDone) {
      // Daten werden dem [ListView] zugeordnet
      adapter = new ListAdapter(activity, R.layout.list_data, session.getListe(), this);
      initDone = true;
    }
    // Fall, in dem das Fragment (neu) generiert wurde – in diesem Fall muss das ListView erneut mit seinem Adapter verknüpft werden
    listView.setAdapter(adapter);
    // Fall, in dem andere Fragmente die Datenquelle geändert haben – in diesem Fall muss das ListView aktualisiert werden
    adapter.notifyDataSetChanged();
  }
}
  • Zeile 15: Die Ansicht XML [vue1] ist dem Fragment zugeordnet;
  • Zeilen 26–30: Die Methode [@AfterViews] führt keine Aktion aus. Sie ist jedoch erforderlich, um die Variable [afterViewsDone] auf true zu setzen, da diese 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 Adjazenz des angezeigten Fragments verlassen und somit seinen Lebenszyklus zurücksetzen könnte. Dies ist hier nicht der Fall, wäre es aber, wenn die Anwendung drei Fragmente mit einer Adjazenz von 1 hätte;
  • Zeile 44: Der Adapter von [ListView] muss nur einmal initialisiert werden;
  • Zeile 46: Diesem [ListView] wird ein Adapter vom Typ [ListAdapter] zugeordnet. Wir werden diese Klasse erstellen. Sie leitet sich von der Klasse [ArrayAdapter] ab, die wir bereits verwendet haben, um Daten einem [ListView] zuzuordnen. Wir übergeben verschiedene Informationen an den Konstruktor von [ListAdapter]:
    • eine Referenz auf die aktuelle Aktivität,
    • die ID der Ansicht, die für jedes Element der Liste instanziiert wird,
    • eine Datenquelle zur Befüllung der Liste,
    • eine Referenz auf das Fragment. Diese wird verwendet, um den Klick auf einen Link [Retirer] des [ListView] mithilfe der Methode [doRetirer] in Zeile 38 zu verarbeiten;
  • Zeile 50: Der Adapter wird dem [ListView] zugeordnet. Gleichzeitig wird die Datenquelle [listes] dem [ListView] zugeordnet. Dieser Vorgang wird hier jedes Mal ausgeführt, wenn die Ansicht Nr. 1 angezeigt wird. Tatsächlich müsste er jedoch nur dann erfolgen, wenn die Methode [@AfterViews] ausgeführt wurde. Hier wird die Anweisung zu oft ausgeführt. Es besteht Bedarf an einem booleschen Wert, der anzeigt, dass die Methode [@AfterViews] gerade ausgeführt wurde und dass daher [ListView] wieder mit seinem Adapter verknüpft werden muss;
  • Zeile 52: [ListView] wird aktualisiert. In diesem Beispiel ist dies sinnlos, da nur die Ansicht Nr. 1 die Datenquelle von [ListView] ändern kann. Betrachten wir nun einen allgemeineren Fall, in dem auch Ansicht Nr. 2 die Datenquelle des [ListView] ändern könnte. Auf solche Beispiele wird später in diesem Dokument noch eingegangen. In diesem Fall muss beim Wechsel von Ansicht Nr. 2 zu Ansicht Nr. 1 das [ListView] der Ansicht Nr. 1 aktualisiert werden;

1.20.7. Der Adapter [ListAdapter] des [ListView]

Die Klasse [ListAdapter]

  • konfiguriert die Datenquelle des [ListView];
  • verwaltet die Anzeige der verschiedenen Elemente von [ListView];
  • verwaltet die Ereignisse dieser Elemente;

Der Code lautet wie folgt:


package exemples.android.fragments;

import java.util.List;
...
public class ListAdapter extends ArrayAdapter<Data> {

    // den Ausführungskontext
    private Context context;
    // die ID des Anzeigelayouts einer Zeile in der Liste
    private int layoutResourceId;
    // die Daten der Liste
    private List<Data> data;
    // das Fragment, das das [ListView] anzeigt
    private Vue1Fragment fragment;
    // der Adapter
    final ListAdapter adapter = this;

    // Hersteller
    public ListAdapter(Context context, int layoutResourceId, List<Data> data, Vue1Fragment fragment) {
        super(context, layoutResourceId, data);
        // die Informationen werden gespeichert
        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: Vergessen Sie nicht, den Konstruktor der übergeordneten Klasse [ArrayAdapter] mit den ersten drei Parametern aufzurufen;
  • Zeilen 22–25: Die Informationen des Konstruktors werden gespeichert;
  • Zeile 29: Die Methode [getView] wird wiederholt von [ListView] aufgerufen, um die Ansicht des Elements Nr. [position] zu generieren. Das zurückgegebene Ergebnis [View] ist eine Referenz auf die erstellte Ansicht.

Der Code der Methode [getView] lautet wie folgt:


@Override
    public View getView(final int position, View convertView, ViewGroup parent) {
        // die aktuelle Zeile von ListView wird erstellt
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // der Text
        TextView textView = (TextView) row.findViewById(R.id.txt_Libellé);
        textView.setText(data.get(position).getTexte());
        // das Kontrollkästchen
        CheckBox checkBox = (CheckBox) row.findViewById(R.id.checkBox1);
        checkBox.setChecked(data.get(position).isChecked());
        // der Link [Retirer]
        TextView txtRetirer = (TextView) row.findViewById(R.id.textViewRetirer);
        txtRetirer.setOnClickListener(new OnClickListener() {

            public void onClick(View v) {
                fragment.doRetirer(position);
            }
        });
        // Verarbeitung des Klicks auf das Kontrollkästchen
        checkBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                data.get(position).setChecked(isChecked);
            }
        });
        // die Zeile wird angezeigt
        return row;
}
  • Zeile 2: Die Methode erhält drei Parameter. Wir werden nur den ersten verwenden;
  • Zeile 4: Wir erstellen die Ansicht des Elements Nr. [position]. Dabei handelt es sich um die Ansicht [list_data], deren ID als zweiter Parameter an den Konstruktor übergeben wurde. Anschließend rufen wir die Referenzen der Komponenten der soeben instanziierten Ansicht ab;
  • Zeile 6: Wir rufen die Referenz von [TextView] Nr. 1 ab;
  • Zeile 7: Man weist ihm einen Text aus der Datenquelle zu, die als dritter Parameter an den Konstruktor übergeben wurde;
  • Zeile 9: Die Referenz von [CheckBox] Nr. 2 wird abgerufen;
  • Zeile 10: Es wird mit einem Wert aus der Datenquelle von [ListView] markiert oder nicht markiert;
  • Zeile 12: Die Referenz von [TextView] Nr. 3 wird abgerufen;
  • Zeilen 13–18: Der Klick auf den Link [Retirer] wird verarbeitet;
  • Zeile 16: Dieser Klick wird von der Methode [Vue1Fragment].doRetirer verarbeitet. Es erscheint in der Tat logischer, dieses Ereignis von dem Fragment verarbeiten zu lassen, das das [ListView] anzeigt. Dieses verfügt über einen Überblick, den die Klasse [ListAdapter] nicht hat. Die Referenz des Fragments [Vue1Fragment] wurde als vierter Parameter an den Konstruktor der Klasse übergeben;
  • Zeilen 20–25: Hier wird der Klick auf das Kontrollkästchen verarbeitet. Die darauf ausgeführte Aktion wirkt sich auf die angezeigten Daten aus. Dies hat folgenden Grund: [ListView] ist eine Liste, die nur einen Teil ihrer Elemente anzeigt. Daher ist ein Element der Liste mal ausgeblendet, mal angezeigt. Wenn das Element Nr. i angezeigt werden soll, wird die Methode [getView] aus Zeile 2 oben für die Position Nr. i aufgerufen. In Zeile 10 wird der Status des Kontrollkästchens anhand der Daten, mit denen es verknüpft ist, neu berechnet. Daher muss diese den Status des Kontrollkästchens im Laufe der Zeit speichern;

1.20.8. Ein Element aus der Liste entfernen

Der Klick auf den Link [Retirer] wird im Fragment [Vue1Fragment] durch die folgende Methode [doRetirer] verarbeitet:


  public void doRetirer(int position) {
    // das Element mit der Nummer [position] wird aus der Liste entfernt
    List<Data> liste = mainActivity.getListe();
    liste.remove(position);
    // Die Scrollposition wird gespeichert, um dorthin zurückzukehren
    // lesen
    // [http://stackoverflow.com/questions/3014089/maintain-save-restore-scroll-position-when-returning-to-a-listview]
    // Position des ersten Elements: vollständig sichtbar oder nicht
    int firstPosition = listView.getFirstVisiblePosition();
    // Y-Versatz dieses Elements relativ zum oberen Rand von ListView
    // misst die Höhe des möglicherweise verdeckten Teils
    View v = listView.getChildAt(0);
    int top = (v == null) ? 0 : v.getTop();
    // Das [ListView] wird aktualisiert
    adapter.notifyDataSetChanged();
    // man positioniert sich an der richtigen Stelle des ListView
    listView.setSelectionFromTop(firstPosition, top);
}
  • Zeile 1: Man erhält die Position des angeklickten Links [Retirer] im Fragment [ListView];
  • Zeile 3: Die Datenliste wird abgerufen;
  • Zeile 4: Das Element mit der Nummer [position] wird entfernt;
  • Zeile 15: Man aktualisiert das [ListView]. Ohne diese Aktion ändert sich optisch nichts.
  • Zeilen 5–13, 17: ein recht komplexer Vorgang. Ohne diesen passiert Folgendes:
    • Das Element [ListView] zeigt die Zeilen 15–18 der Datenliste an,
    • Zeile 16 wird gelöscht,
    • die Zeile 15 darüber setzt ihn vollständig zurück und der [ListView] zeigt dann die Zeilen 0–3 der Datenliste an;

Mit den oben genannten Zeilen erfolgt das Löschen, und der Cursor von [ListView] bleibt auf der Zeile stehen, die auf die gelöschte Zeile folgt.

1.20.9. Die Ansicht XML [Vue2]

Der Code XML der 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: die Komponente [TextView] Nr. 1;
  • Zeilen 26–33: die Komponente [TextView] Nr. 2;
  • Zeilen 17–24: die Komponente [Button] Nr. 3;

1.20.10. Das Fragment [Vue2Fragment]

123

Das Fragment [Vue2Fragment] verwaltet die Ansicht XML [vue2]. 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 {

    // die Felder der Ansicht
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

    @AfterViews
    void initFragment(){
        // Speicher
        afterViewsDone=true;
    }

  @Click(R.id.button_vue1)
    void navigateToView1() {
        // man navigiert zur Ansicht 1
        mainActivity.navigateToView(0);
    }

    @Override
    protected void updateFragment() {
        // Es werden die Elemente der Liste angezeigt, die in Ansicht 1 ausgewählt wurden
        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: Der im [TextView] Nr. 2 anzuzeigende Text wird berechnet;
  • Zeilen 35–39: Die von der Aktivität [ListView] angezeigte Datenliste wird durchlaufen. Sie ist in der Aktivität gespeichert;
  • Zeile 36: Wenn der Datensatz Nr. i markiert wurde, wird die zugehörige Bezeichnung in einen Typ [StringBuilder] eingefügt;
  • Zeile 41: Der Typ [TextView] zeigt den berechneten Text an;

1.20.11. Ausführung

Erstellen Sie eine Ausführungskonfiguration für dieses Projekt und führen Sie sie aus.

1.20.12. Verbesserung

Im vorangegangenen Beispiel haben wir eine Datenquelle vom Typ List&lt;Data&gt; verwendet, wobei die Klasse [Data] wie folgt aussah:


package exemples.android.fragments;

public class Data {

    // Daten
    private String texte;
    private boolean isChecked;

    // Hersteller
    public Data(String texte, boolean isCkecked) {
        this.texte = texte;
        this.isChecked = isCkecked;
    }
...

}

In Zeile 7 wurde ein boolescher Wert verwendet, um das Kontrollkästchen der Elemente von [ListView] zu steuern. Häufig muss die [ListView] Daten anzeigen, die durch Aktivieren eines Kontrollkästchens ausgewählt werden können, ohne dass das Element der Datenquelle über ein diesem Kontrollkästchen entsprechendes boolesches Feld verfügt. In diesem Fall kann wie folgt vorgegangen werden:

Die Klasse [Data] sieht dann wie folgt aus:


package exemples.android.fragments;

public class Data {

    // Daten
    private String texte;

    // Konstruktor
    public Data(String texte) {
        this.texte = texte;
    }

    // Getter und Setter
...
}

Man erstellt eine von der vorherigen abgeleitete Klasse [CheckedData]:


package exemples.android.fragments;

public class CheckedData extends Data {

    // Aktivierungsfeld
    private boolean isChecked;

    // Konstruktor
    public CheckedData(String text, boolean isChecked) {
        // übergeordnetes Element
        super(text);
        // lokal
        this.isChecked = isChecked;
    }

    // Getter und Setter
...
}

Anschließend muss lediglich überall im Code (MainActivity, ListAdapter, Vue1Fragment, Vue2Fragment) den Typ „[Data]“ durch den Typ „[CheckedData]“ zu ersetzen. Zum Beispiel in „[MainActivity]“:


  @AfterInject
  protected void afterInject() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d("MainActivity", "afterInject");
    }
    // Es wird eine Liste mit Daten erstellt
    List<CheckedData> liste = session.getListe();
    for (int i = 0; i < 20; i++) {
      liste.add(new CheckedData("Texte n° " + i, false));
    }
}

Das Projekt dieser Version wird Ihnen unter dem Namen [Exemple-19B] bereitgestellt.

1.21. Beispiel 20: Verwendung eines Menüs

1.21.1. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-19B] in das Projekt [Exemple-20]:

3

Wir werden die Schaltflächen in den Ansichten 1 und 2 entfernen und durch Menüoptionen [1-2] ersetzen.

1.21.2. Die Definition XML der Menüs

  

Die Datei [res / menu / menu_vue1] definiert das Menü der 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>

Die Menüelemente werden durch folgende Informationen definiert:

  • android:id: die Kennung des Elements;
  • android:title: die Bezeichnung des Elements;
  • app:showsAsAction: Gibt an, ob das Menüelement in der Aktionsleiste der Aktivität platziert werden kann. [ifRoom] bedeutet, dass das Element in der Aktionsleiste platziert werden soll, sofern dort Platz dafür ist;
  • Ein Menüpunkt kann selbst ein Untermenü sein (Tag <menu>, Zeilen 25, 29);

Die Datei [res / menu / menu_vue2] definiert das Menü der 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. Die 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 {

  // Daten, auf die die untergeordneten Klassen zugreifen können
  final protected boolean isDebugEnabled = IMainActivity.IS_DEBUG_ENABLED;
  protected String className;

  // Aktivität
  protected IMainActivity mainActivity;
  protected Activity activity;

  // Sitzung
  protected Session session;

  // Menü
  private Menu menu;
  private int[] menuOptions;
  private boolean initDone;

  // Konstruktor
  public AbstractFragment() {
    // Initialisierung
    className = getClass().getSimpleName();
    // Protokoll
    if (isDebugEnabled) {
      Log.d("AbstractFragment", String.format("constructor %s", className));
    }
  }

@Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // Speicher
    this.menu = menu;
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    // Die # Menüoptionen werden abgerufen, falls dies noch nicht geschehen ist
    if (!initDone) {
      // Die # Optionen des Menüs werden abgerufen
      List<Integer> menuOptionsIds = new ArrayList<>();
      getMenuOptions(menu, menuOptionsIds);
      // Die Liste der Optionen wird in ein Array übertragen
      menuOptions = new int[menuOptionsIds.size()];
      for (int i = 0; i < menuOptions.length; i++) {
        menuOptions[i] = menuOptionsIds.get(i);
      }
      // Aktivität
      this.activity = getActivity();
      this.mainActivity = (IMainActivity) activity;
      this.session = this.mainActivity.getSession();
      // Speicher
      initDone = true;
    }

    // Das untergeordnete Fragment wird aufgefordert, sich einzurichten
    updateFragment();
  }


  private void getMenuOptions(Menu menu, List<Integer> menuOptionsIds) {
   ...
  }

  // Anzeige der Menüoptionen -----------------------------------
  protected void setAllMenuOptions(boolean isVisible) {
    ....
  }

  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    ...
  }

  // Tochterklasse aktualisieren
  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, insbesondere nachdem die Methode [updateFragment] aufgerufen wurde. Dies lässt vermuten, 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 das Menü anhand seiner ursprünglichen Beschreibung erstellt werden kann. Diese Möglichkeit werden wir hier nicht nutzen, da wir eine Annotation AA verwenden, 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 Kennungen aller Menüelemente 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 verbundene 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 Initialisierung der Klasse bereits erfolgt ist, sodass sie nicht erneut durchgeführt werden muss (Zeile 50);
  • Zeile 63: Wir fordern das untergeordnete Fragment auf, sich 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], mit der man die Identifikatoren der Elemente eines Menüs abrufen kann, lautet wie folgt:


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

Die Methode [setAllMenuOptions] ermöglicht es, alle Menüoptionen ein- bzw. auszublenden;


  protected void setAllMenuOptions(boolean isVisible) {
    // alle Menüoptionen werden aktualisiert
    for (int menuItemId : menuOptions) {
      menu.findItem(menuItemId).setVisible(isVisible);
    }
}

Mit der Methode [setMenuOptions] können bestimmte Menüoptionen ein- oder ausgeblendet werden;


  protected void setMenuOptions(MenuItemState[] menuItemStates) {
    // bestimmte Menüoptionen werden aktualisiert
    for (MenuItemState menuItemState : menuItemStates) {
      menu.findItem(menuItemState.getMenuItemId()).setVisible(menuItemState.isVisible());
    }
}

Die Klasse [MenuItemState] lautet wie folgt:

  

package exemples.android.architecture;

public class MenuItemState {

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

  // Konstruktoren
  public MenuItemState() {

  }

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

  // Getter und Setter
...
}

1.21.4. Die Menüverwaltung im Fragment [Vue1Fragment]

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() {
    // man wechselt zur Ansicht 2
    mainActivity.navigateToView(1);
  }

  @OptionsItem(R.id.actionValider)
  void valider() {
    // eine Meldung wird angezeigt
    Toast.makeText(activity, "Valider", Toast.LENGTH_SHORT).show();
  }

  private boolean actionCacherMontrerTout = true;
  @OptionsItem(R.id.actionCacherMontrerTout)
  void cacherMontrerTout() {
    // Zustandswechsel
    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() {
    // Zustand wird geändert
    actionCacherMontrerActions = !actionCacherMontrerActions;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, actionCacherMontrerActions)});
  }

  private boolean actionCacherMontrerActionsValider = true;
  @OptionsItem(R.id.actionCacherMontrerActionsValider)
  void actionCacherMontrerActionsValider() {
    // Zustand wird geändert
    actionCacherMontrerActionsValider = !actionCacherMontrerActionsValider;
    setMenuOptions(new MenuItemState[]{new MenuItemState(R.id.menuActions, true), new MenuItemState(R.id.actionValider, actionCacherMontrerActionsValider)});
  }
...

  @Override
  protected void updateFragment() {
    ....
    // Das Menü wird aktualisiert
    //setMenuOptions(...)
  }
}
  • Zeile 2: Das Menü [res / menu / menu_vue1.xml] wird dem Fragment zugeordnet;
  • 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 Anmerkung [@OptionsItem(R.id.navigationVue2)] kennzeichnet die Methode, die bei einem Klick auf die Menüoption [Navigation / Vue 2] ausgeführt werden soll;
  • Zeilen 19–25: Um einen Menüzweig auszublenden, reicht es aus, dessen Stammoption auszublenden;
  • Zeile 24: Die Stammoptionen [menuNavigation, menuActions] werden ein- bzw. ausgeblendet;
  • Zeile 40: Um eine Option eines Menüzweigs einzublenden, muss nicht nur diese selbst, sondern auch alle Optionen eingeblendet werden, auf die man stößt, wenn man von der Blattoption zur Menüwurzel zurückgeht;

1.21.5. Die Menüverwaltung im Fragment [Vue2Fragment]

Ähnlicher Code findet sich im Fragment der Ansicht Nr. 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 {

  // Die Felder der Ansicht
  @ViewById(R.id.textViewResultats)
  TextView txtResultats;

  @OptionsItem(R.id.navigationVue1)
  void navigateToView1() {
    // Navigieren zur Ansicht 1
    mainActivity.navigateToView(0);
  }

  @Override
  protected void updateFragment() {
    // Die in Ansicht 1 ausgewählten Listenelemente werden angezeigt
    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);
    // Das Menü wird aktualisiert
    // setMenuOptions(...)
  }
}
  • Zeile 35: Die Option [Navigation / Vue 1] wird angezeigt;
  • Zeilen 17–20: Beim Klicken auf die Option [Navigation / Vue1] wird die Methode [navigateToView1] aufgerufen;

1.21.6. Ausführung

Erstellen Sie einen Ausführungskontext für dieses Projekt und führen Sie es aus.

1.22. Beispiel 21: Refactoring der abstrakten Klasse [AbstractFragment]

Das vorherige Beispiel hat gezeigt, dass, wenn das Fragment über ein Menü verfügt, seine Methode „[onCreateOptionsMenu]“ ein geeigneter Ort ist, um das Fragment aufzufordern, sich zu aktualisieren:

  • Sie wird genau einmal aufgerufen, wenn das Fragment angezeigt wird;
  • 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 sich dadurch auszeichnet, dass es 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. Erstellung des Projekts

Wir duplizieren das Projekt [Exemple-12] im Projekt [Exemple-21]:

1.22.2. Das Menü der Fragmente

  

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>

Man muss hier verstehen, dass die Aktivität bereits über ein eigenes Menü verfügt: [menu_main]:


<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ü dem der Aktivität hinzugefügt: Man hat also die Optionen beider Menüs. In diesem Fall ist das Menü der Fragmente leer. Daher wird nur das Menü der Aktivität angezeigt.

1.22.3. Die Fragmente

  

Wir greifen auf die abstrakte Klasse [AbstractFragment] aus dem vorherigen Beispiel zurück (siehe Abschnitt 1.21.3). Wir ordnen den beiden Fragmenten das Menü [menu_fragment] zu:


@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 den beiden Fragmenten [PlaceholderFragment] und [Vue1Fragment] entfernen wir alle Verweise auf die alte abstrakte Klasse [AbstractFragment].

1.22.4. Ausführung

Führen Sie die Anwendung aus und überprüfen Sie, ob sie funktioniert. Verfolgen Sie die Protokolle, um zu sehen, wann die Methode [onCreateOptionsMenu] der Klasse [AbstractFragment] ausgeführt wird. Diese ruft nun die Methode [updateFragment] der untergeordneten Fragmente auf.

1.23. Beispiel 22: Speichern/Wiederherstellen des Zustands der Aktivität und der Fragmente

1.23.1. Das Problem

Wir befassen uns hier mit dem Problem der Drehung des Android-Geräts (Hochformat <--> Querformat). Zur Veranschaulichung greifen wir das vorherige Beispiel 21 wieder auf:

Image

Wenn wir das Gerät [1] drehen, erhalten wir die folgende neue Ansicht:

Image

Man sieht, dass:

  • bei [1] die Registerkarte [Fragment n° 3] verschwunden ist;
  • in [2] wird zwar der Text von Fragment Nr. 3 angezeigt, der Besucherzähler ist jedoch falsch;

Bei dieser Rotation lauten die Logs 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: Man sieht, dass die Aktivität vollständig neu aufgebaut wurde;
  • Zeilen 3–7: Das Gleiche gilt für die fünf von der Aktivität verwalteten Fragmente;
  • Zeile 21: Fragment Nr. 3 wird angezeigt. Man sieht, dass die Besuchsnummer vor der Inkrementierung 0 ist;

Das nach der Rotation erzielte Ergebnis lässt sich dann wie folgt erklären:

  • Die Klasse [MainActivity] erstellt zunächst eine Registerleiste mit einem einzigen Register, das mit „[Vue 1]“ beschriftet ist. Dies ist das Register, das angezeigt wird;
  • Nach der Drehung des Geräts zeigt der Seitenmanager [mViewPager] dasselbe Fragment erneut an, in diesem Fall also Fragment Nr. 3. Dabei ist zu beachten, dass Registerkarten und Fragmente unterschiedliche Konzepte sind und einen unterschiedlichen Lebenszyklus haben. Die Methode [updateFragment] des Fragments Nr. 3 wird ausgeführt:

  public void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // Besuchsnummer erhöhen
    numVisit = session.getNumVisit();
    numVisit++;
    session.setNumVisit(numVisit);
    // Text geändert
    textViewInfo.setText(String.format("%s, visite %s", text, numVisit));
}
  • Zeile 7: Die letzte Besuchsnummer wird aus der Sitzung gelesen. Diese wurde jedoch, wie alles andere auch, neu aufgebaut, und die Besuchsnummer wurde auf Null zurückgesetzt. Dies erklärt das in Fragment Nr. 3 angezeigte Ergebnis;

1.23.2. Methoden zum Speichern und Wiederherstellen der Aktivität und der Fragmente

1.23.2.1. Lösung 1: Manuelles Speichern

Beim Drehen des Geräts werden zwei Methoden der Aktivität aufgerufen:


// Verwaltung der Sicherung/Wiederherstellung der Aktivität ------------------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // übergeordnet
    super.onSaveInstanceState(outState);
    // Aktionsstatus sichern
    // ....
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // übergeordnet
    super.onCreate(savedInstanceState);
     // Wiederherstellung der Aktivität
    // ...
  }
  • Zeilen 2–8: Die Methode [onSaveInstanceState] wird vom System beim Drehen aufgerufen. Hier kann die Sicherung der Aktivität erfolgen. Wenn nichts unternommen wird, wird nichts gesichert. Die Sicherung des Aktivitätsstatus muss im Parameter [Bundle outState] erfolgen, der an die Methode übergeben wird. Die Klasse [Bundle] ähnelt einem Dictionary. Sie verfügt über die Methode [putString, putInt, putLong, putBoolean, putChar, ...] mit zwei Parametern: void putT(String key, T value);
  • Zeilen 10–16: Die Methode [onCreate] wird beim Anlegen der Aktivität aufgerufen. Wenn der Status der Aktivität gespeichert wurde, wird diese Speicherung im Parameter [Bundle savedInstanceState] an die Methode übergeben. Um die gespeicherten Werte abzurufen, stehen Methoden wie [getString, getInt, getLong, geBoolean, getChar, ...] mit einem Parameter zur Verfügung: T getT(String key);

Die Fragmente verfügen über dieselben beiden Methoden, um ihren Zustand zu sichern.

Wir werden diese Informationen nutzen, um den Zustand von Beispiel 21 zu sichern und wiederherzustellen. Dazu duplizieren wir das Projekt [Exemple-21] in [Exemple-22].

1.23.2.2. Lösung 2: Automatische Speicherung

In der Android-Dokumentation wird darauf hingewiesen, dass beim Drehen des Geräts die Zerstörung eines Fragments durch die Verwendung der Anweisung [Fragment].setRetainInstance(true) verhindert werden kann. In mehreren Artikeln von [StackOverflow] wird empfohlen, diese Anweisung nur für Fragmente ohne visuelle Benutzeroberfläche [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] zu verwenden. Ich habe diese Anweisung anhand von zwei Beispielen getestet: Beispiel-17 (Absatz 1.18 – eine Anwendung mit einem Fragment, das ein Formular anzeigt) und Beispiel-21 (Absatz 1.22), eine Anwendung mit fünf Fragmenten. In beiden Fällen erwies sich diese einzige Anweisung, die auf alle Fragmente der Anwendung angewendet wurde, als unzureichend, um die bei der Drehung des Geräts angezeigte Ansicht korrekt wiederherzustellen. Anstatt zwei Vorlagen zu erstellen – eine basierend auf [setRetainInstance(true)] und eine andere basierend auf [setRetainInstance(false)], dem Standardwert – habe ich mich entschieden, den Empfehlungen von [StackOverflow] zu folgen und den Standardwert false der Methode [setRetainInstance(boolean )] beizubehalten. Die Anweisung: [Fragment].setRetainInstance(true) wurde im weiteren Verlauf dieses Dokuments nie verwendet.

1.23.3. Die Methode zum Sichern/Wiederherstellen des Projekts [Exemple-22]

Das Projekt [Exemple-22] entwickelt sich wie folgt:

  

Hier tauchen zwei neue Klassen auf:

  • [PlaceHolderFragmentState], die den Zustand eines Fragments vom Typ [PlaceHolderFragment] speichert;
  • [Vue1FragmentState], die den Status des Fragments vom Typ [Vue1Fragment] speichert;

Es handelt sich um folgende Klassen:


package exemples.android;

public class Vue1FragmentState {
  // Status Vue1Fragment
  private boolean hasBeenVisited=false;
  // Getter und Setter
...
}
  • Zeile 5: Der Boolesche Wert [hasBeenVisited] ist wahr, wenn das Fragment [Vue1Fragment] mindestens einmal aufgerufen (angezeigt) wurde. Dieses Feld wurde für das Beispiel angelegt, da das Fragment [Vue1Fragment] nichts zu speichern hat;

Die Klasse [PlaceHolderFragmentState] lautet wie folgt:


package exemples.android;

public class PlaceHolderFragmentState {
  // Status „besucht“ oder „nicht besucht“
  private boolean hasBeenVisited;
  // angezeigter Text
  private String text;

  // Getter und Setter
...
}
  • Zeile 5: Hier findet sich der Boolesche Wert [hasBeenVisited];
  • Zeile 7: Der Text, der vom Fragment zum Zeitpunkt der Speicherung angezeigt wird. Wir haben gesehen, dass dieser Text bei der Drehung verloren gegangen ist;

Der Status der Fragmente wird in der Sitzung gespeichert, und die Aktivität ist dafür zuständig, diese Sitzung zu sichern bzw. wiederherzustellen. 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 {
  // Anzahl der besuchten Fragmente
  private int numVisit;
  // Nummer des Fragments vom Typ [PlaceholderFragment], das auf der zweiten Registerkarte angezeigt wird
  private int numFragment = -1;
  // Nummer der ausgewählten Registerkarte
  private int selectedTab = 0;
  // Nr. der aktuellen Ansicht
  private int currentView;

  // Fragmente gespeichert ---------------
  private Vue1FragmentState vue1FragmentState;
  private PlaceHolderFragmentState[] placeHolderFragmentStates = new PlaceHolderFragmentState[IMainActivity.FRAGMENTS_COUNT - 1];

  // Konstruktor
  public Session() {
    for (int i = 0; i < placeHolderFragmentStates.length; i++) {
      placeHolderFragmentStates[i] = new PlaceHolderFragmentState();
    }
    vue1FragmentState = new Vue1FragmentState();
  }
  // Getter und Setter
...
}
  • Zeile 18: Der Status des Fragments [Vue1Fragment];
  • Zeile 19: der Status der Fragmente vom Typ [PlaceHolderFragment];
  • Zeilen 22–27: Im Sitzungsgenerator werden die Felder der 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:


  // Verwaltung des Speicherns und Wiederherstellens der Aktivität ----------------------------
  @Override
  protected void onSaveInstanceState(Bundle outState) {
    // übergeordnetes Element
    super.onSaveInstanceState(outState);
    // Sitzungsspeicherung
    try {
      outState.putString("session", jsonMapper.writeValueAsString(session));
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      try {
        Log.d(className, String.format("onSaveInstanceState session=%s", jsonMapper.writeValueAsString(session)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    // übergeordnet
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
      // Sitzungswiederherstellung
      try {
        session = jsonMapper.readValue(savedInstanceState.getString("session"), new TypeReference<Session>() {
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
      // Protokoll
      if (IS_DEBUG_ENABLED) {
        try {
          Log.d(className, String.format("onCreate session=%s", jsonMapper.writeValueAsString(session)));
        } catch (JsonProcessingException e) {
          e.printStackTrace();
        }
      }
    }
}
  • Zeile 8: Die Sitzung wird in Form ihrer Zeichenkette jSON gespeichert;
  • Zeile 29: Die Sitzung wird anhand ihrer Zeichenkette jSON wiederhergestellt;

Um das Speichern und Wiederherstellen der Fragmente zu verwalten, wird die abstrakte Klasse [AbstractFragment] wie folgt erweitert:


// Verwaltung von Sicherung und Wiederherstellung -----------------------------------------------
  @Override
  public void setUserVisibleHint(boolean isVisibleToUser) {
    // übergeordnet
    super.setUserVisibleHint(isVisibleToUser);
    // Sicherung?
    if (this.isVisibleToUser && !isVisibleToUser && !saveFragmentDone) {
      // Das Fragment wird ausgeblendet – es wird gesichert
      saveFragment();
      saveFragmentDone = true;
    }
    // Speicher
    this.isVisibleToUser = isVisibleToUser;
  }

  @Override
  public void onActivityCreated(Bundle savedInstanceState) {
    // übergeordnet
    super.onActivityCreated(savedInstanceState);
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "onActivityCreated");
    }
    // Das Fragment muss wiederhergestellt werden
    fragmentHasToBeInitialized = true;
  }


  @Override
  public void onSaveInstanceState(final Bundle outState) {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, "onSaveInstanceState");
    }
    // übergeordnetes Element
    super.onSaveInstanceState(outState);
    // Das Fragment wird nur gespeichert, wenn es sichtbar ist
    if (isVisibleToUser && !saveFragmentDone) {
      saveFragment();
      saveFragmentDone = true;
    }
  }

  // Unterklassen
  protected abstract void updateFragment();

  protected abstract void saveFragment();
  • Es wird beschlossen, den Status der Fragmente in der Sitzung zu zwei Zeitpunkten zu sichern:
    • Zeilen 2–14: wenn das Fragment von „sichtbar“ auf „ausgeblendet“ wechselt;
    • Zeilen 29–42: wenn das System anzeigt, dass eine Sicherung des Fragments durchgeführt werden muss und dieses sichtbar ist (Zeile 38);

Dieser Mechanismus verhindert, dass Speichervorgänge häufiger als nötig durchgeführt werden. Da der Zustand des Fragments i bereits gespeichert wurde, als es von sichtbar auf ausgeblendet wechselte, ist es nämlich unnötig, das Fragment i erneut zu speichern, wenn das Fragment j angezeigt wird und eine Drehung durchgeführt wird. Wenn es seit seiner letzten Speicherung nicht erneut angezeigt wurde, hat sich sein Zustand nicht geändert. Nur der Zustand des Fragments j muss gespeichert werden. Dieser Mechanismus hat noch einen weiteren Vorteil: Nicht nur bei einer Drehung des Geräts muss der Zustand eines Fragments gespeichert werden. Es gibt auch den Fall der reinen Navigation zwischen Fragmenten, beispielsweise in einem System mit Registerkarten. Man möchte dann ein Fragment in dem Zustand wiederfinden, in dem man es zuletzt verlassen hat, als es angezeigt wurde. Dieser Zustand kann teilweise verloren gegangen sein, wenn dieses Fragment zu einem bestimmten Zeitpunkt aus der Nachbarschaft der angezeigten Fragmente verschwunden ist. Das Fragment wird dann nicht vollständig rekonstruiert, wohl aber die zugehörige Ansicht. Die Sicherung, die vorgenommen wurde, als das Fragment ausgeblendet wurde, dient dazu, den letzten Zustand dieser Ansicht wiederherzustellen;

  • Zeilen 10, 40: Um zwei aufeinanderfolgende Speichervorgänge zu vermeiden, wird der Boolesche Wert [saveFragmentDone] verwendet, um anzuzeigen, dass bereits ein Speichervorgang stattgefunden hat;
  • Zeilen 9, 39: Das untergeordnete Fragment wird aufgefordert, seinen Zustand zu speichern. Die Methode [saveFragment] ist abstrakt (Zeile 47). Es obliegt daher den untergeordneten Klassen, sie zu implementieren;
  • Zeilen 16–26: Die Methode [onActivityCreated] wird verwendet, um den Booleschen Wert [fragmentHasToBeInitialized] auf „wahr“ zu setzen. Das Tochterfragment muss nämlich wissen, dass es den Zustand des Fragments vollständig anhand eines Zustands zurücksetzen muss, den es in der Sitzung vorfindet;

Ebenfalls in der Klasse [AbstractFragment] wird die Methode [onCreateOptionsMenu] wie folgt angepasst:


// Aktualisierung des Fragments
  @Override
  public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    // Speicher
    this.menu = menu;
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("création menu en cours"));
    }
    ...
    // Das untergeordnete Fragment wird aufgefordert, sich zu aktualisieren
    updateFragment();
    // Sicherung erforderlich
    saveFragmentDone = false;
  }
  • Zeile 14: Wir haben gesehen, dass der boolesche Wert [saveFragmentDone] auf vrai gesetzt wurde, als eine Speicherung erfolgte. Zu einem bestimmten Zeitpunkt muss er wieder auf faux zurückgesetzt werden. Wenn die Methode [updateFragment] (Zeile 12) des untergeordneten Fragments ausgeführt wird, wird dieses sichtbar. Ein Fragment muss jedoch gespeichert werden, wenn es sichtbar ist, und zwar genau in dem Moment, in dem es vom sichtbaren in den ausgeblendeten Zustand wechselt. Daher wird der Boolesche Wert [saveFragmentDone] auf false gesetzt, damit die Speicherung erfolgen kann;

1.23.4. Speichern des Fragments [Vue1Fragment]

Die Speicherung der Fragmente erfolgt in der Methode [saveFragment], die von der übergeordneten Klasse [AbstractFragment] aufgerufen wird:


// Fragmentstatus sichern
  @Override
  public void saveFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("saveFragment 1 %s - %s", className, getLocalInfos()));
    }
    // Speicherung des Fragmentstatus in der Sitzung
    Vue1FragmentState state = new Vue1FragmentState();
    state.setHasBeenVisited(true);
    session.setVue1FragmentState(state);
    // Protokoll
    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 Fragmentstatus in der Sitzung. Wenn die Methode [saveFragment] aufgerufen wird, ist das Fragment sichtbar. Daher muss der boolesche Wert [hasBeenVisited] auf vrai gesetzt werden (Zeile 10);

1.23.5. Speichern des Fragments [PlaceHolderFragment]

Das Speichern der Fragmente erfolgt in der Methode [saveFragment], die von der übergeordneten Klasse [AbstractFragment] aufgerufen wird:


  @Override
  public void saveFragment() {
    // Der Fragmentstatus wird in der Sitzung gespeichert
    PlaceHolderFragmentState state = new PlaceHolderFragmentState();
    state.setText(textViewInfo.getText().toString());
    state.setHasBeenVisited(true);
    session.getPlaceHolderFragmentStates()[getArguments().getInt(ARG_SECTION_NUMBER) - 1] = state;
    // Protokoll
    if (isDebugEnabled) {
      try {
        Log.d(className, String.format("saveFragment state=%s", jsonMapper.writeValueAsString(state)));
      } catch (JsonProcessingException e) {
        e.printStackTrace();
      }
    }
}
  • Zeilen 4–7: Speicherung 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 vrai gesetzt;
  • Zeile 7: Der Status des Fragments wird im Array [placeHolderFragmentStates] gespeichert. Die Nummer des zu initialisierenden Elements entspricht der Abschnittsnummer des Fragments minus eins;

1.23.6. Wiederherstellung des Fragments [Vue1Fragment]

Die Wiederherstellung der Fragmente erfolgt in der Methode [updateFragment]:


@Override
  protected void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 1 %s - %s", className, getLocalInfos()));
    }
    // Wiederherstellung?
    if (fragmentHasToBeInitialized) {
      // Zustandswiederherstellung
      hasBeenVisited = session.getVue1FragmentState().isHasBeenVisited();
      fragmentHasToBeInitialized = false;
    }
    // Protokoll
    if (isDebugEnabled) {
      Log.d(className, String.format("updateFragment 2 %s - %s", className, getLocalInfos()));
    }
    // Navigation?
    boolean navigation = session.getCurrentView() != IMainActivity.FRAGMENTS_COUNT - 1;
    if (navigation) {
      // Besuchsnummer erhöhen
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // Besuchsnummer anzeigen
      Toast.makeText(activity, String.format("Visite n° %s", numVisit), Toast.LENGTH_SHORT).show();
    }
    // aktuelle Ansichtsnummer ändern
    session.setCurrentView(IMainActivity.FRAGMENTS_COUNT - 1);
  }
  • Zeilen 8–12: Wiederherstellung des Fragmentzustands. Der Boolesche Wert [fragmentHasToBeInitialized] wurde von der übergeordneten Klasse [AbstractFragment] initialisiert. Wenn er den Wert vrai annimmt, wurde das Fragment gerade neu aufgebaut und muss zurückgesetzt werden. Genau hier geschieht dies. In diesem konkreten Beispiel gibt es nichts zu tun. Wir haben lediglich gezeigt, dass man den Wert des Booleschen Werts [hasBeenVisited] im gespeicherten Zustand des Fragments (Zeile 10) abrufen kann;
  • Zeile 11: Man darf nicht vergessen, den Wert von [fragmentHasToBeInitialized] wieder auf faux zurückzusetzen, damit bei einer späteren Rückkehr zu diesem Fragment – sofern das Gerät nicht gedreht wurde – keine unnötige Neuinitialisierung des Fragments erfolgt;
  • Zeilen 18–26: Inkrementierung des Aufrufzählers. Hier gibt es eine Schwierigkeit: Bei der Wiederherstellung des Fragments soll dieser Zähler nicht inkrementiert werden. Wir müssen hier unterscheiden zwischen:
    • einer einfachen Navigation, die den Benutzer zurück zur Registerkarte „[Vue 1]“ führt;
    • einer Wiederherstellung, wenn der Benutzer sein Gerät dreht, während die Registerkarte „[Vue 1]“ angezeigt wird;

Diese beiden Fälle werden anhand der in der Sitzung gespeicherten Ansichtsnummer unterschieden. Diese Nummer entspricht der zuletzt angezeigten Ansicht (Zeile 28).

  • Zeile 18: Es erfolgt eine Navigation und keine Wiederherstellung, wenn die Nummer der zuletzt angezeigten Ansicht sich von der der aktuellen Ansicht unterscheidet;
  • Zeilen 21–25: Inkrementierung des Besuchszählers und dessen Anzeige;

1.23.7. Wiederherstellung des Fragments [PlaceHolderFragment]

Das Wiederherstellen der Fragmente erfolgt in der Methode [updateFragment]:


  // Daten
  private String text;
  private int numVisit;
  private String newText;
  private boolean hasBeenVisited = false;
  private ObjectMapper jsonMapper = new ObjectMapper();
...

public void updateFragment() {
    // Protokoll
    if (isDebugEnabled) {
      Log.d("PlaceholderFragment", String.format("update %s - %s - %s", getArguments().getInt(ARG_SECTION_NUMBER), className, getLocalInfos()));
    }
    // Um welches Fragment handelt es sich?
    int numSection = getArguments().getInt(ARG_SECTION_NUMBER);
    int numView = numSection - 1;
    // Muss das Fragment initialisiert werden?
    if (fragmentHasToBeInitialized) {
      // Ausgangstext
      text = getString(R.string.section_format, numSection);
      fragmentHasToBeInitialized = false;
    }
    // Navigation?
    boolean navigation = session.getCurrentView() != numView;
    if (navigation) {
      // Besuchsnummer erhöhen
      numVisit = session.getNumVisit();
      numVisit++;
      session.setNumVisit(numVisit);
      // geänderter Text
      newText = String.format("%s, visite %s", text, numVisit);
    } else {
      // Es handelt sich um eine Wiederherstellung
      PlaceHolderFragmentState state = session.getPlaceHolderFragmentStates()[numView];
      newText = state.getText();
    }
    // Textanzeige
    textViewInfo.setText(newText);
    // Aktuelle Ansicht
    session.setCurrentView(numView);
}
  • Zeilen 15–16: Hier wird die Nummer der Ansicht ermittelt, die gerade aktualisiert wird;
  • Zeilen 18–22: Fall, in dem sich das Fragment nach einer Änderung der Geräteausrichtung in einem Speicher-/Wiederherstellungszyklus befindet. Hier muss es wiederhergestellt werden. In der Regel geht es darum, bestimmte Felder des Fragments wiederherzustellen;
  • Zeile 20: Das Feld [text] in Zeile 2 muss den ursprünglich vom Fragment angezeigten Text enthalten: [Hello world from section i]. Dieser muss hier neu generiert werden;
  • Zeile 21: Es ist zu erkennen, dass die Initialisierung des Fragments erfolgt ist;
  • Zeilen 24–36: Wie zuvor beim Fragment [Vue1Fragment] darf die Inkrementierung des Besuchszählers bei einer Wiederherstellung nicht erfolgen. Wie zuvor müssen wir zwischen Navigation und Wiederherstellung unterscheiden;
  • Zeilen 32–36: Fall der Wiederherstellung;
  • Zeile 34: Der Zustand des Fragments vor der Drehung des Geräts wird aus der Sitzung abgerufen;
  • Zeile 35: Dort wird der Text abgerufen, der damals angezeigt wurde;
  • Zeile 38: Dieser Text wird erneut angezeigt;
  • Zeile 40: Die Nummer der neu angezeigten Ansicht wird in der Sitzung vermerkt;

1.23.8. Verwaltung der Registerkarten

In den vorangegangenen Abschnitten wurde die Verwaltung von Registerkarten nicht behandelt. In Beispiel 21 ist jedoch bei der Drehung des Geräts ein Problem aufgetreten: Nur die erste Registerkarte „[Vue 1]“ blieb erhalten. Die zweite Registerkarte ging verloren.

Wir lösen dieses Problem in der Klasse [MainActivity] wie folgt:


@AfterViews
  protected void afterViews() {
    // Protokoll
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "afterViews");
    }
    // Symbolleiste
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

 ...

    // 1. Registerkarte
    TabLayout.Tab tab = tabLayout.newTab();
    tab.setText("Vue 1");
    tabLayout.addTab(tab);
    // 2. Registerkarte?
    int numFragment = session.getNumFragment();
    if (numFragment != -1) {
      TabLayout.Tab tab2 = tabLayout.newTab();
      tab2.setText(String.format("Fragment n° %s", (numFragment + 1)));
      tabLayout.addTab(tab2);
    }

    // Welche Registerkarte soll ausgewählt werden?
    tabLayout.getTabAt(session.getSelectedTab()).select();

...

  }
  • Zeilen 14–16: Erstellung der ersten Registerkarte;
  • Zeilen 18–23: Erstellung der zweiten Registerkarte. Um festzustellen, ob diese erstellt werden muss, wird in der Sitzung die Nummer des in Registerkarte 2 angezeigten Fragments überprüft. Ist diese Nummer ungleich -1, ihrem Ausgangswert, wird die zweite Registerkarte erstellt. Zu diesem Zeitpunkt gibt es zwei Registerkarten, von denen standardmäßig die erste ausgewählt ist;
  • Zeile 26: Wir suchen in der Sitzung nach der Nummer des Registerblatts, das vor dem Speichern/Wiederherstellen ausgewählt war, und wählen dieses erneut aus. Wenn das Feld [selectedTab] noch nicht durch den Code initialisiert wurde, wird sein Anfangswert 0 verwendet;