Rheinwerk Computing < openbook >

 
Inhaltsverzeichnis
Vorwort
Teil I Grundlagen
1 Android – eine offene, mobile Plattform
2 Hallo Android!
3 Von der Idee zur Veröffentlichung
Teil II Elementare Anwendungsbausteine
4 Wichtige Grundbausteine von Apps
5 Benutzeroberflächen
6 Multitasking
Teil III Gerätefunktionen nutzen
7 Telefonieren und surfen
8 Sensoren, GPS und Bluetooth
Teil IV Dateien und Datenbanken
9 Dateien lesen, schreiben und drucken
10 Datenbanken
Teil V Multimedia und Produktivität
11 Multimedia
12 Kontakte und Organizer
A Einführung in Kotlin
B Jetpack Compose
C Häufig benötigte Codebausteine
D Literaturverzeichnis
E Die Begleitmaterialien
Stichwortverzeichnis

Ihre Meinung?
Spacer
<< zurück
Android 11 von Thomas Künneth
Das Praxisbuch für App-Entwickler
Buch: Android 11

Android 11
Pfeil 4 Wichtige Grundbausteine von Apps
Pfeil 4.1 Was sind Activities?
Pfeil 4.1.1 Struktur von Apps
Pfeil 4.1.2 Lebenszyklus von Activities
Pfeil 4.2 Kommunikation zwischen Anwendungsbausteinen
Pfeil 4.2.1 Intents
Pfeil 4.2.2 Kommunikation zwischen Activities
Pfeil 4.2.3 Broadcast Receiver
Pfeil 4.3 Fragmente
Pfeil 4.3.1 Grundlagen
Pfeil 4.3.2 Ein Fragment in eine Activity einbetten
Pfeil 4.3.3 Mehrspaltenlayouts
Pfeil 4.4 Berechtigungen
Pfeil 4.4.1 Normale und gefährliche Berechtigungen
Pfeil 4.4.2 Tipps und Tricks zu Berechtigungen
Pfeil 4.5 Navigation
Pfeil 4.5.1 Jetpack Navigation
Pfeil 4.5.2 Die Klasse »BottomNavigationView«
Pfeil 4.6 Zusammenfassung
 
Zum Seitenanfang

4    Wichtige Grundbausteine von Apps Zur vorigen ÜberschriftZur nächsten Überschrift

In diesem Kapitel lernen Sie drei wichtige Bausteine von Android-Apps genauer kennen: Activities, Broadcast Receiver und Fragmente. Fragmente und Activities strukturieren die App für den Anwender sichtbar, Broadcast Receiver arbeiten hinter den Kulissen.

Die Bildschirme von Smartphones sind im Vergleich zu den Anzeigen von Notebooks oder Desktop-Systemen klein. Beim Bau von Apps müssen Sie sich deshalb überlegen, welche Informationen Sie dem Benutzer zu welchem Zeitpunkt präsentieren. Tablets wiederum bieten genügend Platz für die gleichzeitige Darstellung von Dokumentbereichen, Symbol- und Werkzeugleisten sowie Menüs. Die Herausforderung für Sie als Entwickler besteht darin, jeden Formfaktor so gut wie möglich zu unterstützen. Das Bewegen innerhalb des Programms, die Navigation, sollte für Ihre Anwender dabei logisch und in sich schlüssig sein, damit diese Ihr Werk nicht frustriert zur Seite legen. Android fördert die saubere Strukturierung einer App durch Activities und Fragmente.

 
Zum Seitenanfang

4.1    Was sind Activities? Zur vorigen ÜberschriftZur nächsten Überschrift

In den ersten drei Kapiteln haben Sie schon kurz Bekanntschaft mit Activities gemacht. Diese in sich geschlossenen Komponenten beinhalten in der Regel eine Benutzeroberfläche und repräsentieren Aktionen wie Anruf tätigen, SMS senden, Termindetails bearbeiten oder Monatskalender anzeigen. Jeder Activity steht ein Bereich für ihre Bedienelemente zur Verfügung, der normalerweise den Bildschirm ausfüllt. Er kann aber auch kleiner sein und über anderen schweben.

 
Zum Seitenanfang

4.1.1    Struktur von Apps Zur vorigen ÜberschriftZur nächsten Überschrift

Eine App besteht häufig aus mehreren Activities. In welcher Reihenfolge diese aufgerufen werden, kann von Aktionen des Benutzers abhängen – wenn er beispielsweise einen Befehl in der Action Bar antippt – oder durch die Programmlogik vorgegeben sein. Wird in einer Activity eine Liste von Kontakten angezeigt, so führt das Antippen eines Listenelements zur Detailansicht. Hier navigiert der Anwender nicht bewusst zu einer anderen »Seite«, sondern das Programm tut dies für ihn. Jede App sollte eine Hauptaktivität haben, die beim ersten Programmstart aufgerufen wird. Oftmals handelt es sich hierbei um eine Art Menü- oder Auswahlseite, die zu den eigentlichen Modulen oder Unterseiten verzweigt.

Der Fluss innerhalb einer App und über Anwendungsgrenzen hinweg entsteht also unter anderem durch das Starten von Folgeaktivitäten. Die zu diesem Zeitpunkt ausgeführte Activity wird vom System angehalten und auf einen Stapel, den sogenannten Back Stack, gelegt. Drückt der Benutzer die Zurück-Schaltfläche (mit der Einführung von Android 3 wurde sie »virtualisiert« und erscheint als Element der System Bar), führt die entsprechende Wischgeste aus oder wird die aktive Activity auf eine andere Weise beendet, entfernt das System sie von diesem Stapel und reaktiviert die »darunterliegende«. Ausführliche Informationen dazu finden Sie in Abschnitt 4.1.2, »Lebenszyklus von Activities«.

Manifestdatei

Zu jeder Android-App gehört mindestens eine Beschreibungsdatei mit dem Namen AndroidManifest.xml, die eine Liste der Komponenten enthält, aus denen das Programm besteht. Außerdem werden in ihr die benötigten Berechtigungen, etwaige Anforderungen an die Hardware sowie bestimmte zusätzlich verwendete Bibliotheken vermerkt. Früher mussten Sie in dieser Datei auch Angaben zur mindestens nötigen oder gewünschten Android-Version machen. Seit dem Wechsel auf Android Studio und Gradle wird diese Information stattdessen in der Datei build.gradle eingetragen (siehe Abschnitt 2.1.2, »Projektstruktur«). Während des Build-Vorgangs entsteht automatisch das fertige Manifest.

Projekte können aus einem oder mehreren Modulen bestehen. Zu jedem Modul gehört eine build.gradle-Datei. Auch das Manifest ist in jedem Modul vorhanden. Hierzu ein Beispiel: Beim Anlegen von Hallo Android in Kapitel 2 wurde vom Projektassistenten im Projektverzeichnis Hallo_Android das Modul app erzeugt. Die modulspezifische build.gradle-Datei befindet sich unter …\Hallo_Android\app. Die Manifestdatei AndroidManifest.xml finden Sie im Verzeichnis …\Hallo_Android\app\src\main. Sie ist in Listing 4.1 zu sehen.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.thomaskuenneth.androidbuch.halloandroid">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
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>

Listing 4.1    Die Datei »AndroidManifest.xml«

Das Attribut package erhält seinen Wert aus dem Feld Package name des Assistenten zum Anlegen von Projekten. Die zum Tag <application /> gehörenden Attribute android:icon, android:roundIcon sowie android:label legen Anwendungssymbol und Titel fest. android:theme beeinflusst das Aussehen der App. Der Wert referenziert einen Eintrag in der Datei styles.xml im Unterverzeichnis res/values. Dort wird festgelegt, dass die App ein von Theme.AppCompat.Light.DarkActionBar abgeleitetes, eigenes Theme nutzt.

Für jede Activity enthält das Manifest ein Element <activity />, dessen zwingend vorhandenes Attribut android:name den Klassennamen beinhaltet. Haben Sie den führenden Punkt vor MainActivity bemerkt? Er ist ein Hinweis auf einen fehlenden Paketteil. In so einem Fall wird der Inhalt des Attributs package eingefügt. Auch Activities können einen Titel haben, der üblicherweise in der Action Bar im oberen Bereich des Bildschirms erscheint. Sie setzen ihn mit android:label, er darf aber, wie in meinem Beispiel, auch fehlen. Gleiches gilt für das Icon: Sind Icon oder Label nicht vorhanden, greift Android auf die gleichnamigen Attribute des <application />-Tags zurück.

Mehr über die Bedeutung von android:allowBackup erfahren Sie in Kapitel 9, »Dateien lesen, schreiben und drucken«. Auch die ausführliche Erläuterung des Elements <intent-filter /> muss noch etwas warten. Fürs Erste soll der Hinweis genügen, dass Sie auf diese Weise eine Activity zur Hauptaktivität machen, die sich über den Programmstarter oder eine Verknüpfung auf dem Home Screen aufrufen lässt.

Trennung von Programmlogik und Ressourcen

Das Element <application /> meiner Beispiel-Manifestdatei enthält das Attribut android:label. Dessen Wert ist die Zeichenkette @string/app_name. Die Datei strings.xml im Verzeichnis res/values enthält Schlüssel-Wert-Paare, die einem Bezeichner eine Zeichenkette zuordnen. Der Wert des Attributs android:label ergibt sich also aus dem Schlüssel app_name, der in strings.xml eingetragen wurde. Beim Anlegen eines Projekts wird der im Projektassistenten eingetragene Name der App übernommen.

Die Speicherung von Texten an einem zentralen Ort hat mehrere Vorteile, beispielsweise werden identische Textteile leichter entdeckt, als wenn diese in den Quelltexten der Klassen verborgen sind. Damit lässt sich – wenn auch in eher bescheidenem Umfang – Speicherplatz sparen. Außerdem macht die Trennung von Daten und Programmlogik die Internationalisierung, also die Übersetzung einer App in verschiedene Sprachen, viel einfacher.

Hierzu wird für jede zu unterstützende Sprache im Ordner res ein Verzeichnis angelegt, dessen Name mit values- beginnt und mit dem ISO-Sprachschlüssel endet. Für Deutsch ist dieser Schlüssel de, das Verzeichnis muss also values-de heißen. Jeder dieser Ordner erhält eine eigene Version von strings.xml, deren Bezeichner stets gleich sind. Die Texte hingegen liegen in den jeweiligen Sprachen vor. Texte in der Standardsprache verbleiben in values; für Hallo Android wurde Deutsch als Standardsprache verwendet. Zur Erinnerung zeigt Listing 4.2 nochmals einen kurzen Auszug:

<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<!-- Willkommensmeldung -->
<string name="welcome">
Guten Tag. Schön, dass Sie mich gestartet haben.
Bitte verraten Sie mir Ihren Namen.
</string>
...

Listing 4.2    Auszug aus der Datei »strings.xml« des Projekts »Hallo_Android«

Um eine App ins Englische zu übersetzen, müssen Sie im Verzeichnis res den Ordner values-en anlegen. Anschließend erzeugen Sie in diesem eine lokalisierte Version der Datei strings.xml. Hierbei unterstützt Android Studio Sie: Klicken Sie im Werkzeugfenster Project die Datei strings.xml mit der rechten Maustaste an, und wählen Sie dann Open Translations Editor.

Der Translations Editor ist in Abbildung 4.1 zu sehen. Sein Globussymbol öffnet ein Pop-up, in dem Sie die gewünschte Sprache auswählen können: Klicken Sie auf English (en). Einträge, für die Sie noch keine Übersetzung hinterlegt haben, erscheinen in roter Schrift. Falls eine Zeichenkette nicht übersetzt werden kann, setzen Sie in der korrespondierenden Zeile ein Häkchen bei Untranslatable. Die entsprechende Tabellenspalte ist unter Umständen so schmal, dass Sie nur Unt... lesen können. Ziehen Sie sich die Spalten einfach in die gewünschte Breite.

Bei Projekten, die Sie nicht weitergeben müssen, lohnt sich der Aufwand, Texte zu übersetzen, sehr wahrscheinlich nicht. In diesem Fall können Sie mit genau einer Version von strings.xml arbeiten und diese im Verzeichnis values belassen. Planen Sie hingegen eine Veröffentlichung in Google Play, rate ich Ihnen, als Standardsprache auf Englisch zu setzen, denn auf diese Weise maximieren Sie die Zahl potenzieller Nutzer. Tragen Sie hierzu in die Datei values/strings.xml stets englische Texte ein, und fügen Sie in values-de/strings.xml entsprechende deutsche Übersetzungen hinzu.

Der Translations Editor

Abbildung 4.1    Der Translations Editor

Grundsätzlich gilt: Wenn für die aktuell eingestellte Systemsprache eine Datei strings.xml existiert und diese den benötigten Bezeichner enthält, wird der ihm zugeordnete Text verwendet. Andernfalls greift Android auf strings.xml in values zu. Das bedeutet: Alle referenzierten Bezeichner müssen sich für die Standardsprache auflösen lassen. Gelingt dies nicht, wirft die App zur Laufzeit einen Fehler. Lokalisierungen, also Übersetzungen in andere Sprachen, müssen hingegen nicht vollständig vorliegen.

Übrigens können Sie bequem testen, ob Ihr Programm die erwarteten Texte ausgibt. Gehen Sie im Emulator oder einem echten Gerät in den Einstellungen auf die Seite System • Sprachen und Eingabe • Sprachen. Fügen Sie dann eine neue Sprache hinzu, und schieben Sie diese ganz nach oben (Abbildung 4.2).

Die Werte der Manifestattribute android:icon und android:roundIcon werden auf ganz ähnliche Weise aufgelöst. Wie Sie bereits wissen, enthält der Ordner res mehrere Unterverzeichnisse, die mit drawable oder mipmap beginnen. .png-, .jpg- und .gif-Bitmaps sowie Vektorgrafiken, die dorthin kopiert werden, sind über @mipmap/<xyzz> (Launcher-Icon) und @drawable/<xyz> (alle übrigen) erreichbar. <xyz> ist der Dateiname ohne Erweiterung.

Die bevorzugte Sprache auswählen

Abbildung 4.2    Die bevorzugte Sprache auswählen

Auf Ressourcen zugreifen

Android stellt zahlreiche Methoden zur Verfügung, um auf Ressourcen zuzugreifen. Die folgende Activity zeigt einige Beispiele. Sie gehört zu dem Projekt ZugriffAufRessourcen.

package com.thomaskuenneth.androidbuch.zugriffaufressourcen

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import java.util.*

class ZugriffAufRessourcenActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val view = findViewById<TextView>(R.id.textview)
view.append("getString(R.string.app_name): "
+ getString(R.string.app_name))
val cal: Calendar = Calendar.getInstance()
view.append("\n\n" + getString(R.string.datum,
"Heute ist der",
cal.get(Calendar.DAY_OF_YEAR),
cal.get(Calendar.YEAR)))
val b1 = resources.getBoolean(R.bool.bool1)
val b2 = resources.getBoolean(R.bool.bool2)
view.append("\n\nb1=$b1, b2=$b2")
view.append("\n\nadams=${resources.getInteger(R.integer.adams)}")
view.setTextColor(resources.getColor(R.color.eine_farbe, theme))
}
}

Listing 4.3    Die Klasse »ZugriffAufRessourcenActivity«

Die Activity gibt vier Zeilen aus, nämlich den Namen der App, den aktuellen Tag des Jahres (nebst Jahr), zwei Boolean-Werte und eine Zahl vom Format Int. Damit Ihre App funktioniert, müssen Sie die Ressourcen Ihres Programms erweitern. Fügen Sie in strings.xml diese Zeile ein:

<string name="datum">%1$s %2$d. Tag des Jahres %3$d</string>

Zur Laufzeit der App wird daraus zum Beispiel der Text »Heute ist der 117. Tag des Jahres 2020«. Ich habe einen Teil des auszugebenden Strings im Quelltext belassen, um Ihnen die Nachvollziehbarkeit zu erleichtern. In Ihrer App würden Sie auch den Text »Heute ist der« auslagern. Dem Bezeichner datum in strings.xml wird ein Text mit drei Platzhaltern zugewiesen. Die Ziffern 1, 2 und 3 hinter Prozentzeichen geben die Reihenfolge an, in der sie beim Aufruf der Methode getString(R.string.datum, ...) mit Inhalten gefüllt werden. Die Buchstaben s und d hinter Dollarzeichen kennzeichnen den Typ; s steht für String und d für Dezimalzahl.

Auf diese Weise können Sie Texte mit Werten anreichern, die erst zur Laufzeit bekannt sind. Übrigens müssen Sie bei der Übersetzung in andere Sprachen nicht auf die Reihenfolge der Platzhalter achten, weil Sie durch die Angabe der Ziffer ja festlegen, auf welchen Parameter in getString() Sie sich beziehen. Das ist nicht nur bei Monats- und Jahresangaben sehr praktisch.

Der dritte view.append()-Aufruf fügt dem Textfeld zwei Boolean-Werte hinzu, die mit dem Ausdruck resources.getBoolean() ermittelt werden. Aber woher kommen die beiden Bezeichner bool1 und bool2? Falls Sie das Projekt nicht aus den Begleitmaterialien übernommen haben, erzeugen Sie im Ordner res/values die Datei diverses.xml, und fügen Sie die folgenden Zeilen ein:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="bool1">true</bool>
<bool name="bool2">false</bool>
</resources>

Listing 4.4    »diverses.xml«

Sogenannte einfache Ressourcen werden über ihr name-Attribut referenziert. Neben Boolean-Werten gibt es unter anderem Farben und Integer. Die folgende Zeile weist dem Bezeichner adams die Zahl 42 zu:

<integer name="adams">42</integer>

Der Zugriff in Kotlin ähnelt dem Auslesen von boolean-Werten:

view.append("\n\nadams=${resources.getInteger(R.integer.adams)}")

Um eine Farbe zu definieren, verwenden Sie das XML-Tag <color />. Das folgende Beispiel weist dem Bezeichner eine_farbe den Hexadezimalwert ff123456 zu. Das erste Byte (ff) legt den Alpha-Wert, also die Deckkraft, fest. ff (= 255) bedeutet »vollständig undurchsichtig«. Die nachfolgenden Werte 12, 34 und 56 geben jeweils den Rot-, Grün- und Blauanteil an.

<color name="eine_farbe">#ff123456</color>

Sie können adams und eine_farbe in einer XML-Datei mit frei wählbarem Namen ablegen; ich habe der Einfachheit halber diverses.xml verwendet. Übrigens zeigt Android Studio im Texteditor am linken Rand eine Vorschau der Farbe, was in Abbildung 4.3 zu sehen ist.

Farbdefinition mit Vorschau

Abbildung 4.3    Farbdefinition mit Vorschau

Und so setzen Sie die Schriftfarbe eines Textfeldes auf den gerade eben definierten Wert:

view.setTextColor(resources.getColor(R.color.eine_farbe, theme))

Activities spiegeln für den Anwender die Funktionen Ihrer App wider. Ob ein Programm einfach zu bedienen ist, hängt gerade auf Smartphones nicht nur von der Ausgestaltung der Bedienoberfläche ab, sondern auch von seiner Navigierbarkeit: Wie schnell und intuitiv gelingt dem Benutzer das, was er mit der App erledigen wollte? Activity und davon abgeleitete Klassen versuchen, Sie als Entwickler beim Bau von einfach zu bedienenden Apps zu unterstützen. Im folgenden Abschnitt stelle ich Ihnen die Struktur und den Lebenszyklus von Activities ausführlicher vor.

 
Zum Seitenanfang

4.1.2    Lebenszyklus von Activities Zur vorigen ÜberschriftZur nächsten Überschrift

Activities werden sowohl vom System als auch von anderen Activities aufgerufen. Damit dies funktioniert, müssen Sie die Activities Ihrer App in der Manifestdatei eintragen. Jede Aktivität erhält ein eigenes <activity />-Element, dessen Attribut android:name auf den in der Regel voll qualifizierten Klassennamen verweist. Dieses Attribut muss zwingend vorhanden sein. Fehlt der Paketteil des Klassennamens (beginnt der Eintrag also mit einem Punkt), wird das im Attribut package eingetragene Paket substituiert.

Jede App sollte eine Hauptaktivität definieren, die beispielsweise beim Antippen des Programm-Icons im Anwendungsstarter aufgerufen wird. In der Manifestdatei fügen Sie ihr deshalb ein <intent-filter />-Element hinzu, dessen Kindelement <action /> die Activity als Haupteinstiegspunkt in die Anwendung kennzeichnet. <category /> sorgt dafür, dass die Activity im Programmstarter angezeigt wird. Der Assistent zum Anlegen von Projekten tut dies automatisch, außer Sie selektieren No Activity. In diesem Fall wird dem Projekt gar keine Activity hinzugefügt.

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

Die generierten Klassen sind recht einfach gehalten, demonstrieren aber die grundlegenden Vorgehensweisen beim Bau des ausgewählten Activity-Typs. Die Minimalvariante Empty Activity zeigt eine Oberfläche an, die in der Layoutdatei activity_main.xml definiert wird. Die Kotlin-Klasse sieht folgendermaßen aus:

package com.thomaskuenneth.androidbuch.test

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}

Listing 4.5    Vom Projektassistenten angelegte Klasse »MainActivity«

Activities leiten von android.app.Activity oder deren Kindern ab. Eines dieser Kinder ist die Klasse androidx.appcompat.app.AppCompatActivity. Sie ist nicht Bestandteil der Android-Klassenbibliothek, sondern gehört zu der Jetpack-Komponente Appcompat. Google stellt mit Jetpack eine Sammlung von Bibliotheken zur Verfügung, die eine Art Schale um die Klassenbibliothek bilden. Anstatt direkt auf Standardklassen zuzugreifen, können Apps Varianten dieser Klassen verwenden. Diese prüfen, ob eine bestimmte Funktion von der Plattform des Geräts zur Laufzeit angeboten wird. Wenn ja, wird diese genutzt (abgesehen von wenigen Ausnahmen, zum Beispiel Fragmente). Falls nicht, stellt die Bibliothek einen »Nachbau« zur Verfügung. Der Vorteil: Apps werden wesentlich unabhängiger von der Plattformversion und können trotzdem neue Funktionen nutzen. Der Nachteil ist, dass die Bibliothek mit jeder App ausgeliefert wird.

Lassen Sie uns wieder den Activities zuwenden. Die Methode onCreate() wird von praktisch jeder selbst geschriebenen Activity überschrieben. Android ruft sie während der Initialisierungsphase einer Aktivität auf, und sie erledigt normalerweise folgende Aufgaben:

  1. Aufrufen der gleichnamigen Elternmethode

  2. Initialisieren von Instanzvariablen

  3. Setzen der Benutzeroberfläche

  4. Wiederherstellen eines gespeicherten Zustands

Der Aufruf von super.onCreate() ist obligatorisch; unterbleibt er, wird zur Laufzeit eine SuperNotCalledException ausgelöst. Das Setzen der Benutzeroberfläche erfolgt typischerweise durch die Anweisung setContentView(). Der übergebene Parameter, zum Beispiel R.layout.activity_main, referenziert ein sogenanntes Layout. Es wird in einer gleichnamigen XML-Datei (beispielsweise activity_main.xml) gespeichert. Ausführliche Hinweise zum Bau der Benutzeroberfläche finden Sie in Kapitel 5, »Benutzeroberflächen«.

Einige Kindklassen von Activity setzen selbst ein Layout. Dies ist zum Beispiel bei ListActivity der Fall. Dann dürfen Sie setContentView() natürlich nicht aufrufen. Wie Sie solche Activities mit Daten füllen, entnehmen Sie bitte Googles Entwicklerdokumentation.

Zustand speichern und wiederherstellen

Das Wiederherstellen eines gespeicherten Zustands ist praktisch, um »Wiederanlaufzeiten« einer Activity zu optimieren. Wie Sie im folgenden Abschnitt noch ausführlicher sehen werden, startet und stoppt Android Activities unter bestimmten Umständen automatisch. Ein Grund ist der Orientierungswechsel, also das Drehen des Geräts vom Hochkant- in das Querformat (oder umgekehrt).

In so einem Fall beendet Android die laufende Activity und startet sie im Anschluss daran wieder. Was sich im ersten Moment vielleicht absurd anhört, ist durchaus praktisch. Denn oft möchte man die Benutzeroberfläche in Abhängigkeit von der Haltung des Geräts anordnen.

Um den Neustart zu beschleunigen, kann man beim Beenden einer Activity gewisse Daten, den sogenannten Instance State, in einem Zwischenspeicher ablegen, der bei einem erneuten Start übergeben wird. Wie das funktioniert, zeigt das Projekt InstanceStateDemo. Der an onCreate() übergebene Parameter savedInstanceState verweist auf diesen Zwischenspeicher. Er hat den Typ android.os.Bundle und sammelt Schlüssel-Wert-Paare. Als Schlüssel werden Strings verwendet. Werte können unter anderem primitive Datentypen sowie deren Felder sein, aber auch Datentypen, die das Interface android.os.Parcelable implementieren.

package com.thomaskuenneth.androidbuch.instancestatedemo

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

private val TAG = InstanceStateDemoActivity::class.simpleName
class InstanceStateDemoActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
Log.d(TAG, "savedInstanceState war null")
} else {
val time = System.currentTimeMillis() - savedInstanceState
.getLong(TAG)
Log.d(TAG, "wurde vor $time Millisekunden beendet")
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putLong(TAG, System.currentTimeMillis())
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
Log.d(TAG, "onRestoreInstanceState()")
}
}

Listing 4.6    Die Klasse »InstanceStateDemoActivity«

Wenn Sie die App starten, wird zunächst nur der Text »savedInstanceState war null« in Logcat ausgegeben. Ändern Sie nun die Orientierung des Emulators, indem Sie die Symbole inline image oder inline image der Emulator-Steuerleiste anklicken. Android beendet daraufhin die Activity und startet sie neu. Da dieses Mal die Bundle-Referenz nicht mehr null ist, ändert sich die Konsolenausgabe: Die App gibt in Millisekunden die Zeit aus, die seit dem letzten Beenden vergangen ist.

Um Daten beim Beenden Ihrer Activity in einem Bundle abzulegen, überschreiben Sie die Methode onSaveInstanceState() und nutzen die put...()-Methoden der übergebenen Bundle-Referenz. Innerhalb von onCreate() verwenden Sie korrespondierende get...()-Aufrufe, um Daten wieder auszulesen. Beachten Sie hierbei aber, dass die Referenz null sein kann. Um NullPointerExceptions zu vermeiden, müssen Sie Zugriffe auf das Bundle in jedem Fall mit einer entsprechenden if-Abfrage versehen. Welche Daten sollten Sie für einen erneuten Wiederanlauf sichern? Inhalte oder Status von Bedienelementen, beispielsweise Eingaben in Textfelder, werden automatisch durch das System wiederhergestellt. Sie müssen sie also nicht speichern. Damit das klappt, rufen Sie in Ihrer Implementierung von onSaveInstanceState() als Erstes die Elternmethode auf.

Sehr praktisch ist der hier vorgestellte Mechanismus für Werte, deren Berechnung oder Ermittlung zeitaufwendig ist, was unter anderem bei Webservice-Aufrufen der Fall sein kann. Allerdings – und diese Einschränkung ist sehr wichtig – dürfen Sie damit nur transiente Daten ablegen. Android garantiert nämlich nicht, dass onSaveInstanceState() immer aufgerufen wird. Der richtige Ort für das Persistieren von Nutzdaten ist hingegen die Methode onPause() (die Sie gleich kennenlernen werden). Als Faustregel gilt: Alles, was Sie in Dateien oder Datenbanken ablegen, sind wichtige Nutzdaten.

Sie können das Wiederherstellen eines früheren Zustands übrigens aus onCreate() auslagern, indem Sie die Methode onRestoreInstanceState() überschreiben. Diese wird nach der Abarbeitung von onStart() aufgerufen. Allerdings nur, wenn auch onCreate() aufgerufen wurde. Ein solches Refactoring dient also nur der Strukturierung Ihres Codes. Sofern Sie nicht sehr viele Daten wiederherstellen müssen oder die Wiederherstellungslogik an Kindklassen delegieren möchten, lohnt der Aufwand vermutlich nicht. Wenn Sie sie überschreiben, denken Sie bitte daran, mit super.onRestoreInstanceState() die Elternimplementierung aufzurufen.

Wichtige Callback-Methoden

Mit onCreate() und onSaveInstanceState() kennen Sie bereits zwei sehr wichtige Activity-Methoden. Die Klasse ActivityLifecycleDemoActivity des Projekts ActivityLifecycleDemo stellt Ihnen einige weitere vor. Die App (Abbildung 4.4) hat eine einfache Benutzeroberfläche, zwei Schaltflächen und ein Textfeld, das nach dem ersten Start die Meldung »Ich habe die laufende Nummer 1« anzeigt. Zu diesem Zeitpunkt wurden der Reihe nach die Methoden onCreate(), onStart() und onResume() abgearbeitet. Entsprechende Meldungen sehen Sie im Logcat. Denken Sie daran, dass Sie dessen Textflut sehr schön mit Filtern bändigen können. Legen Sie am besten einen mit dem Wert »ActivityLifecycleDemoActivity« im Feld Log Tag an.

package com.thomaskuenneth.androidbuch.activitylifecycledemo

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

private val TAG = ActivityLifecycleDemoActivity::class.simpleName
private var zaehler = 1
class ActivityLifecycleDemoActivity : AppCompatActivity() {

private var lokalerZaehler = zaehler++

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
log("onCreate")
setContentView(R.layout.activity_main)
val tv: TextView = findViewById(R.id.textview)
tv.text = getString(R.string.msg, lokalerZaehler)
val buttonNew: Button = findViewById(R.id.id_new)
buttonNew.setOnClickListener {
val i = Intent(this,
ActivityLifecycleDemoActivity::class.java)
startActivity(i)
}
val buttonFinish: Button = findViewById(R.id.id_finish)
buttonFinish.setOnClickListener { finish() }
}

override fun onStart() {
super.onStart()
log("onStart")
}

override fun onRestart() {
super.onRestart()
log("onRestart")
}

override fun onResume() {
super.onResume()
log("onResume")
}

override fun onPause() {
super.onPause()
log("onPause")
}

override fun onDestroy() {
super.onDestroy()
log("onDestroy")
}

private fun log(methodName: String) {
Log.d(TAG, "$methodName() #$lokalerZaehler")
}
}

Listing 4.7    Die Klasse »ActivityLifecycleDemoActivity«

Klicken Sie nun auf Neue Activity. Nach einer kurzen Animation erscheint ActivityLifecycleDemoActivity erneut, und dieses Mal wird im Textfeld die Meldung »Ich habe die laufende Nummer 2« ausgegeben. Ein Blick in Logcat zeigt, dass die Methode onPause() vor den drei Methoden onCreate(), onStart() und onResume() der »neuen« Aktivität durchlaufen wurde. Drücken Sie nun die Zurück-Schaltfläche des Emulators bzw. Geräts. Wie erwartet ist wieder die erste Activity zu sehen. Neben deren Methoden onRestart(), onStart() und onResume() hat Android zusätzlich die Methoden onPause() und onDestroy() der beendeten Aktivität durchlaufen.

Die Beispielanwendung »ActivityLifecycleDemo«

Abbildung 4.4    Die Beispielanwendung »ActivityLifecycleDemo«

Android verwaltet Activities auf einem Stapel, dem sogenannten Back Stack. Die obenauf liegende Aktivität ist aktiv, wird also gerade vom Benutzer bedient. Durch Drücken der (virtuellen) Zurück-Schaltfläche bzw. durch Auslösen der Wischgeste wird sie vom Stapel entfernt, und die Activity, die zuletzt vor ihr auf den Back Stack gelegt wurde, läuft wieder an. Der Stapel arbeitet also nach dem klassischen Prinzip »last in, first out«.

Sie können eine Activity beenden, indem Sie deren Methode finish() aufrufen. Die Entwicklerdokumentation rät dazu, dies nach Möglichkeit nicht zu tun, da der Anwender erwartet, Zurück drücken oder anklicken zu müssen, um zur vorhergehenden Aktivität zurückzukehren. Es gibt allerdings gute Gründe dafür, die Methode in bestimmten Situationen zu nutzen. Denken Sie beispielsweise an Activities, in denen der Benutzer neue Daten eingibt oder bestehende Daten verändert: In diesem Fall müssen Sie dem Anwender sogar die Wahl lassen, Änderungen zu übernehmen oder zu verwerfen. Dies wird üblicherweise mit den Schaltflächen Fertig und Abbrechen realisiert.

Im Grunde kennen Activities drei Zustände:

  • Die Activity ist im Vordergrund und wird durch den Anwender aktiv genutzt. Google nennt diesen Status resumed oder running.

  • Die Activity ist noch sichtbar, befindet sich aber nicht mehr im Vordergrund oder hat nicht den Fokus. Das ist beispielsweise der Fall, wenn sie (teilweise) von einem Dialog oder einer anderen Activity verdeckt wird. Dieser Zustand wird paused genannt.

  • Die Activity befindet sich im Hintergrund. Sie wird vollständig von einer anderen bedeckt und gilt deshalb als stopped.

Pausierende und gestoppte Aktivitäten verbleiben vollständig im Speicher, behalten also den Zustand ihrer Klassen- und Instanzvariablen. Eine Activity im Zustand paused ist weiterhin an den Fenstermanager angebunden und ist zum Teil sichtbar. Deshalb wird sie vom System nur in Notsituationen zerstört, beispielsweise wenn der Arbeitsspeicher sehr knapp wird. Activities mit dem Status stopped hingegen wurden bereits vom Fenstermanager abgekoppelt. Ihr Speicher wird deshalb freigegeben, wenn er anderweitig benötigt wird.

Der vollständige Lebenszyklus einer Activity ist in Abbildung 4.5 zu sehen. Er beginnt mit dem ersten Aufruf von onCreate() und endet mit einem einmaligen Aufruf von onDestroy(). Grundsätzlich sichtbar ist die Activity zwischen den Aufrufen von onStart() und onStop(). Die Activity muss sich aber nicht zwangsläufig im Vordergrund befinden. Ressourcen, die für die Interaktion mit dem Anwender nötig sind, werden hier zugeteilt bzw. freigegeben.

Zwischen den Aufrufen von onResume() und onPause() befindet sich die Aktivität im Vordergrund und hat die Aufmerksamkeit des Benutzers. Deshalb eignen sich beide Methoden eigentlich gut dafür, Animationen oder Sound-Untermalung zu starten bzw. zu beenden. Dennoch sollten Sie davon Abstand nehmen, ungefragt mit der Wiedergabe von Musik, Tönen oder Videos zu beginnen. Bereiten Sie alles vor, aber lassen Sie dem Anwender die Chance, den Zeitpunkt selbst zu wählen. Was Sie hingegen auch ungefragt tun können (und sollten): Die Ausführung der Methode onPause() ist genau der richtige Zeitpunkt, um wichtige Daten Ihrer App zu sichern. In Abschnitt 6.4, »Mehrere Apps gleichzeitig nutzen«, komme ich noch einmal auf den Lebenszyklus von Activities zurück

Lebenszyklus einer Activity

Abbildung 4.5    Lebenszyklus einer Activity

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
Zur Rheinwerk-Konferenz für Kotlin
 Buchempfehlungen
Zum Rheinwerk-Shop: Kotlin

Kotlin


Zum Rheinwerk-Shop: Praxisbuch Usability und UX

Praxisbuch Usability und UX


Zum Rheinwerk-Shop: Flutter und Dart

Flutter und Dart


Zum Rheinwerk-Shop: App-Design

App-Design


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz
InfoInfo

 
 


Copyright © Rheinwerk Verlag GmbH 2023
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 
[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern