Grundlagen von Android und Kotlin 04.2: Komplexe Lebenszyklussituationen

Dieses Codelab ist Teil des Kurses „Grundlagen von Android und Kotlin“. Sie können diesen Kurs am besten nutzen, wenn Sie die Codelabs der Reihe nach durcharbeiten. Alle Codelabs des Kurses sind auf der Landingpage für Codelabs zu den Grundlagen von Android und Kotlin aufgeführt.

Einführung

Im letzten Codelab haben Sie die Lebenszyklen von Activity und Fragment kennengelernt und die Methoden untersucht, die aufgerufen werden, wenn sich der Lebenszyklusstatus in Aktivitäten und Fragmenten ändert. In diesem Codelab sehen Sie sich den Aktivitätslebenszyklus genauer an. Außerdem erfahren Sie mehr über die Android Jetpack-Lifecycle-Bibliothek, mit der Sie Lifecycle-Ereignisse mit besser organisiertem und leichter zu wartendem Code verwalten können.

Was Sie bereits wissen sollten

  • Was eine Aktivität ist und wie Sie eine in Ihrer App erstellen.
  • Die Grundlagen der Lebenszyklen von Activity und Fragment sowie die Callbacks, die aufgerufen werden, wenn eine Aktivität zwischen den Status wechselt.
  • Wie Sie die onCreate()- und onStop()-Lebenszyklus-Callback-Methoden überschreiben, um Vorgänge zu verschiedenen Zeiten im Lebenszyklus der Aktivität oder des Fragments auszuführen.

Lerninhalte

  • Wie Sie Teile Ihrer App in den Lifecycle-Callbacks einrichten, starten und beenden.
  • Wie Sie die Android Lifecycle-Bibliothek verwenden, um einen Lifecycle-Observer zu erstellen und den Lebenszyklus von Aktivitäten und Fragmenten einfacher zu verwalten.
  • Wie sich das Beenden von Android-Prozessen auf die Daten in Ihrer App auswirkt und wie Sie diese Daten automatisch speichern und wiederherstellen können, wenn Android Ihre App schließt.
  • Wie sich die Geräteausrichtung und andere Konfigurationsänderungen auf die Lebenszyklusstatus auswirken und den Status Ihrer App beeinflussen.

Aufgabe

  • Ändern Sie die DessertClicker-App so, dass sie eine Timerfunktion enthält, und starten und stoppen Sie den Timer zu verschiedenen Zeiten im Aktivitätslebenszyklus.
  • Ändern Sie die App so, dass die Android-Lifecycle-Bibliothek verwendet wird, und wandeln Sie die DessertTimer-Klasse in einen Lifecycle-Observer um.
  • Richten Sie die Android Debug Bridge (adb) ein und verwenden Sie sie, um das Herunterfahren des App-Prozesses und die dann auftretenden Lebenszyklus-Callbacks zu simulieren.
  • Implementieren Sie die Methode onSaveInstanceState(), um App-Daten beizubehalten, die verloren gehen können, wenn die App unerwartet geschlossen wird. Fügen Sie Code hinzu, um diese Daten wiederherzustellen, wenn die App neu gestartet wird.

In diesem Codelab erweitern Sie die DessertClicker-App aus dem vorherigen Codelab. Sie fügen einen Hintergrund-Timer hinzu und konvertieren die App dann für die Verwendung der Android Lifecycle Library.

Im vorherigen Codelab haben Sie gelernt, wie Sie den Aktivitäts- und Fragmentlebenszyklus beobachten können, indem Sie verschiedene Lebenszyklus-Callbacks überschreiben und protokollieren, wann das System diese Callbacks aufruft. In dieser Aufgabe sehen Sie sich ein komplexeres Beispiel für die Verwaltung von Lebenszyklusaufgaben in der DessertClicker-App an. Sie verwenden einen Timer, der jede Sekunde eine Log-Anweisung mit der Anzahl der Sekunden ausgibt, die er bereits läuft.

Schritt 1: DessertTimer einrichten

  1. Öffnen Sie die DessertClicker App aus dem letzten Codelab. Wenn Sie die App nicht haben, können Sie DessertClickerLogs hier herunterladen.
  2. Maximieren Sie in der Ansicht Project (Projekt) java > com.example.android.dessertclicker und öffnen Sie DessertTimer.kt. Derzeit ist der gesamte Code auskommentiert, sodass er nicht als Teil der App ausgeführt wird.
  3. Wählen Sie den gesamten Code im Editorfenster aus. Wählen Sie Code > Comment with Line Comment (Code > Mit Zeilenkommentar kommentieren) aus oder drücken Sie Control+/ (Command+/ auf einem Mac). Mit diesem Befehl werden alle Codezeilen in der Datei auskommentiert. (In Android Studio werden möglicherweise Fehler zu nicht aufgelösten Referenzen angezeigt, bis Sie die App neu erstellen.)
  4. Die Klasse DessertTimer enthält startTimer() und stopTimer(), mit denen der Timer gestartet und gestoppt wird. Wenn startTimer() ausgeführt wird, gibt der Timer jede Sekunde eine Log-Nachricht mit der Gesamtzahl der Sekunden aus, die seit dem Start vergangen sind. Die Methode stopTimer() stoppt wiederum den Timer und die Log-Anweisungen.
  1. Öffnen Sie MainActivity.kt. Fügen Sie oben in der Klasse, direkt unter der Variablen dessertsSold, eine Variable für den Timer hinzu:
private lateinit var dessertTimer : DessertTimer;
  1. Scrollen Sie nach unten zu onCreate() und erstellen Sie direkt nach dem Aufruf von setOnClickListener() ein neues DessertTimer-Objekt:
dessertTimer = DessertTimer()


Nachdem Sie nun ein Dessert-Timer-Objekt haben, sollten Sie überlegen, wann der Timer gestartet und gestoppt werden soll, damit er nur läuft, wenn die Aktivität auf dem Bildschirm angezeigt wird. In den nächsten Schritten sehen Sie sich einige Optionen an.

Schritt 2: Timer starten und stoppen

Die Methode onStart() wird kurz vor dem Sichtbarwerden der Aktivität aufgerufen. Die onStop()-Methode wird aufgerufen, nachdem die Aktivität nicht mehr sichtbar ist. Diese Rückrufe eignen sich gut, um den Timer zu starten und zu stoppen.

  1. Starte in der Klasse MainActivity den Timer im onStart()-Callback:
override fun onStart() {
   super.onStart()
   dessertTimer.startTimer()

   Timber.i("onStart called")
}
  1. Stoppe den Timer in onStop():
override fun onStop() {
   super.onStop()
   dessertTimer.stopTimer()

   Timber.i("onStop Called")
}
  1. Kompilieren Sie die App und führen Sie sie aus. Klicken Sie in Android Studio auf den Bereich Logcat. Geben Sie im Logcat-Suchfeld dessertclicker ein, um nach den Klassen MainActivity und DessertTimer zu filtern. Sobald die App gestartet wird, beginnt auch der Timer sofort zu laufen.
  2. Klicken Sie auf die Schaltfläche Zurück. Der Timer wird wieder angehalten. Der Timer wird beendet, weil sowohl die Aktivität als auch der Timer, den sie steuert, beendet wurden.
  3. Kehren Sie über den Bildschirm mit den zuletzt verwendeten Apps zur App zurück. In Logcat sehen Sie, dass der Timer bei 0 neu startet.
  4. Klicken Sie auf die Schaltfläche Freigeben. Beachten Sie, dass der Timer in Logcat weiterhin läuft.

  5. Klicken Sie auf die Schaltfläche Startseite. Beachten Sie, dass der Timer in Logcat nicht mehr läuft.
  6. Kehren Sie über den Bildschirm „Zuletzt verwendet“ zur App zurück. In Logcat sehen Sie, dass der Timer wieder an der Stelle fortgesetzt wird, an der er unterbrochen wurde.
  7. Kommentieren Sie in MainActivity in der Methode onStop() den Aufruf von stopTimer() aus. Durch Auskommentieren von stopTimer() wird der Fall veranschaulicht, in dem Sie einen Vorgang in onStart() starten, aber vergessen, ihn in onStop() wieder zu beenden.
  8. Kompilieren und führen Sie die App aus und klicken Sie nach dem Start des Timers auf die Startbildschirmtaste. Auch wenn die App im Hintergrund ausgeführt wird, läuft der Timer und es werden kontinuierlich Systemressourcen verwendet. Wenn der Timer weiterläuft, führt das zu einem Speicherleck in Ihrer App und ist wahrscheinlich nicht das gewünschte Verhalten.

    Im Allgemeinen gilt: Wenn Sie etwas in einem Callback einrichten oder starten, beenden oder entfernen Sie es im entsprechenden Callback. So vermeiden Sie, dass etwas ausgeführt wird, wenn es nicht mehr benötigt wird.
  1. Entfernen Sie das Kommentarzeichen in der Zeile in onStop(), in der Sie den Timer stoppen.
  2. Schneiden Sie den startTimer()-Aufruf aus onStart() aus und fügen Sie ihn in onCreate() ein. Diese Änderung zeigt den Fall, in dem Sie eine Ressource sowohl in onCreate() initialisieren als auch starten, anstatt sie mit onCreate() zu initialisieren und mit onStart() zu starten.
  3. Kompilieren Sie die App und führen Sie sie aus. Der Timer sollte wie erwartet starten.
  4. Klicken Sie auf „Zuhause“, um die App zu beenden. Der Timer wird wie erwartet angehalten.
  5. Kehren Sie über den Bildschirm mit den zuletzt verwendeten Apps zur App zurück. Beachten Sie, dass der Timer in diesem Fall nicht neu gestartet wird, da onCreate() nur beim Start der App aufgerufen wird, nicht wenn eine App in den Vordergrund zurückkehrt.

Wichtige Hinweise:

  • Wenn Sie eine Ressource in einem Lebenszyklus-Callback einrichten, müssen Sie sie auch wieder abbauen.
  • Richten Sie die Testumgebung in entsprechenden Methoden ein und bauen Sie sie wieder ab.
  • Wenn Sie etwas in onStart() einrichten, müssen Sie es in onStop() wieder beenden oder entfernen.

In der DessertClicker App ist es relativ einfach zu erkennen, dass der Timer, der in onStart() gestartet wurde, in onStop() gestoppt werden muss. Es gibt nur einen Timer, daher ist es nicht schwer, sich zu merken, wie man ihn stoppt.

In einer komplexeren Android-App richten Sie möglicherweise viele Dinge in onStart() oder onCreate() ein und bauen sie dann alle in onStop() oder onDestroy() wieder ab. Beispielsweise müssen Sie möglicherweise Animationen, Musik, Sensoren oder Timer einrichten und wieder entfernen sowie starten und stoppen. Wenn Sie eine vergessen, führt das zu Fehlern und Problemen.

Die Lifecycle-Bibliothek, die Teil von Android Jetpack ist, vereinfacht diese Aufgabe. Die Bibliothek ist besonders nützlich, wenn Sie viele sich bewegende Teile verfolgen müssen, von denen sich einige in unterschiedlichen Lebenszyklusphasen befinden. Die Bibliothek kehrt die Funktionsweise von Lebenszyklen um: Normalerweise teilt die Aktivität oder das Fragment einer Komponente (z. B. DessertTimer) mit, was bei einem Lebenszyklus-Callback zu tun ist. Wenn Sie jedoch die Lifecycle-Bibliothek verwenden, überwacht die Komponente selbst die Änderungen des Lebenszyklus und führt dann die erforderlichen Aktionen aus.

Die Lifecycle-Bibliothek besteht aus drei Hauptteilen:

  • Lebenszyklus-Inhaber, also die Komponenten, die einen Lebenszyklus haben (und ihn daher „besitzen“). Activity und Fragment sind die Eigentümer des Lebenszyklus. Lebenszyklusinhaber implementieren die LifecycleOwner-Schnittstelle.
  • Die Klasse Lifecycle, die den tatsächlichen Status eines Lifecycle-Inhabers enthält und Ereignisse auslöst, wenn sich der Lifecycle ändert.
  • Lifecycle-Beobachter, die den Lebenszyklusstatus beobachten und Aufgaben ausführen, wenn sich der Lebenszyklus ändert. Lifecycle-Beobachter implementieren die LifecycleObserver-Schnittstelle.

In dieser Aufgabe konvertieren Sie die DessertClicker-App, sodass sie die Android-Lebenszyklusbibliothek verwendet. Außerdem erfahren Sie, wie die Bibliothek die Arbeit mit den Lebenszyklen von Android-Aktivitäten und ‑Fragmenten erleichtert.

Schritt 1: DessertTimer in einen LifecycleObserver umwandeln

Ein wichtiger Teil der Lifecycle-Bibliothek ist das Konzept der Lifecycle-Beobachtung. Durch die Beobachtung können Klassen (z. B. DessertTimer) den Aktivitäts- oder Fragmentlebenszyklus kennen und sich als Reaktion auf Änderungen an diesen Lebenszyklusstatus selbst starten und beenden. Mit einem Lifecycle-Observer können Sie die Verantwortung für das Starten und Stoppen von Objekten aus den Methoden für Aktivitäten und Fragmente entfernen.

  1. Öffnen Sie den Kurs DesertTimer.kt.
  2. Ändern Sie die Klassensignatur der Klasse DessertTimer so:
class DessertTimer(lifecycle: Lifecycle) : LifecycleObserver {

Diese neue Klassendefinition hat zwei Auswirkungen:

  • Der Konstruktor verwendet ein Lifecycle-Objekt, das den Lebenszyklus darstellt, den der Timer beobachtet.
  • Die Klassendefinition implementiert die LifecycleObserver-Schnittstelle.
  1. Fügen Sie unter der Variablen runnable einen init-Block zur Klassendefinition hinzu. Verwenden Sie im init-Block die Methode addObserver(), um das vom Inhaber (der Aktivität) übergebene Lifecycle-Objekt mit dieser Klasse (dem Observer) zu verbinden.
 init {
   lifecycle.addObserver(this)
}
  1. Annotieren Sie startTimer() mit @OnLifecycleEvent annotation und verwenden Sie das Lebenszyklusereignis ON_START. Alle Lifecycle-Ereignisse, die Ihr Lifecycle-Observer beobachten kann, befinden sich in der Klasse Lifecycle.Event.
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun startTimer() {
  1. Wiederholen Sie den Vorgang für stopTimer() und verwenden Sie das Ereignis ON_STOP:
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stopTimer()

Schritt 2: MainActivity ändern

Ihre MainActivity-Klasse ist bereits ein Lifecycle-Inhaber durch Vererbung, da die FragmentActivity-Superklasse LifecycleOwner implementiert. Daher müssen Sie nichts unternehmen, um den Lebenszyklus Ihrer Aktivität zu berücksichtigen. Sie müssen lediglich das Lebenszyklusobjekt der Aktivität an den DessertTimer-Konstruktor übergeben.

  1. Öffnen Sie MainActivity. Ändern Sie in der Methode onCreate() die Initialisierung von DessertTimer, um this.lifecycle einzuschließen:
dessertTimer = DessertTimer(this.lifecycle)

Die lifecycle-Property der Aktivität enthält das Lifecycle-Objekt, das zu dieser Aktivität gehört.

  1. Entfernen Sie den Aufruf von startTimer() in onCreate() und den Aufruf von stopTimer() in onStop(). Sie müssen DessertTimer nicht mehr mitteilen, was in der Aktivität zu tun ist, da DessertTimer den Lebenszyklus jetzt selbst beobachtet und automatisch benachrichtigt wird, wenn sich der Lebenszyklusstatus ändert. In diesen Rückrufen wird jetzt nur noch eine Nachricht protokolliert.
  2. Kompilieren Sie die App, führen Sie sie aus und öffnen Sie Logcat. Der Timer hat wie erwartet mit dem Countdown begonnen.
  3. Klicke auf die Startbildschirmtaste, um die App in den Hintergrund zu verschieben. Der Timer wurde wie erwartet gestoppt.

Was passiert mit meiner App und ihren Daten, wenn Android die App im Hintergrund schließt? Dieser schwierige Grenzfall ist wichtig.

Wenn Ihre App in den Hintergrund verschoben wird, wird sie nicht beendet, sondern nur angehalten und wartet darauf, dass der Nutzer zu ihr zurückkehrt. Eines der Hauptanliegen des Android-Betriebssystems ist es jedoch, die im Vordergrund ausgeführte Aktivität reibungslos laufen zu lassen. Wenn ein Nutzer beispielsweise eine GPS-App verwendet, um einen Bus zu erreichen, ist es wichtig, dass die GPS-App schnell gerendert wird und die Wegbeschreibung weiterhin angezeigt wird. Es ist weniger wichtig, dass die DessertClicker-App, die der Nutzer möglicherweise seit einigen Tagen nicht mehr verwendet hat, reibungslos im Hintergrund ausgeführt wird.

Android reguliert Hintergrund-Apps, damit die Vordergrund-App problemlos ausgeführt werden kann. Android begrenzt beispielsweise die Menge an Verarbeitung, die von Apps im Hintergrund ausgeführt werden kann.

Manchmal beendet Android sogar einen gesamten App-Prozess, einschließlich aller mit der App verknüpften Aktivitäten. Android führt diese Art von Beendigung durch, wenn das System überlastet ist und die Gefahr besteht, dass es zu visuellen Verzögerungen kommt. Zu diesem Zeitpunkt werden keine zusätzlichen Callbacks oder kein zusätzlicher Code ausgeführt. Der Prozess Ihrer App wird einfach im Hintergrund beendet. Für den Nutzer sieht es aber nicht so aus, als ob die App geschlossen wurde. Wenn der Nutzer zu einer App zurückkehrt, die vom Android-Betriebssystem geschlossen wurde, wird sie von Android neu gestartet.

In dieser Aufgabe simulieren Sie das Herunterfahren eines Android-Prozesses und sehen sich an, was mit Ihrer App passiert, wenn sie neu gestartet wird.

Schritt 1: Prozessbeendigung mit adb simulieren

Die Android Debug Bridge (adb) ist ein Befehlszeilentool, mit dem Sie Anweisungen an Emulatoren und Geräte senden können, die an Ihren Computer angeschlossen sind. In diesem Schritt verwenden Sie adb, um den Prozess Ihrer App zu schließen und zu sehen, was passiert, wenn Android Ihre App beendet.

  1. Kompilieren Sie Ihre App und führen Sie sie aus. Klicken Sie einige Male auf das Cupcake.
  2. Drücke die Startbildschirmtaste, um deine App in den Hintergrund zu verschieben. Ihre App wird jetzt beendet und kann geschlossen werden, wenn Android die von der App verwendeten Ressourcen benötigt.
  3. Klicken Sie in Android Studio auf den Tab Terminal, um das Befehlszeilenterminal zu öffnen.
  4. Geben Sie adb ein und drücken Sie die Eingabetaste.

    Wenn Sie eine lange Ausgabe sehen, die mit Android Debug Bridge version X.XX.X beginnt und mit tags to be used by logcat (see logcat —help) endet, ist alles in Ordnung. Wenn stattdessen adb: command not found angezeigt wird, prüfen Sie, ob der Befehl adb in Ihrem Ausführungspfad verfügbar ist. Eine Anleitung finden Sie im Kapitel Dienstprogramme unter „adb zum Ausführungspfad hinzufügen“.
  5. Kopieren Sie diesen Kommentar und fügen Sie ihn in die Befehlszeile ein. Drücken Sie dann die Eingabetaste:
adb shell am kill com.example.android.dessertclicker

Mit diesem Befehl werden alle verbundenen Geräte oder Emulatoren angewiesen, den Prozess mit dem Paketnamen dessertclicker zu beenden, aber nur, wenn die App im Hintergrund ausgeführt wird. Da sich Ihre App im Hintergrund befand, wird auf dem Geräte- oder Emulatorbildschirm nichts angezeigt, was darauf hinweist, dass Ihr Prozess beendet wurde. Klicken Sie in Android Studio auf den Tab Run, um die Meldung „Application terminated“ (Anwendung beendet) zu sehen. Klicken Sie auf den Tab Logcat, um zu sehen, dass der onDestroy()-Callback nie ausgeführt wurde. Ihre Aktivität wurde einfach beendet.

  1. Über den Bildschirm „Zuletzt verwendet“ können Sie zur App zurückkehren. Ihre App wird dort angezeigt, unabhängig davon, ob sie in den Hintergrund verschoben oder vollständig beendet wurde. Wenn Sie über den Bildschirm „Letzte Apps“ zur App zurückkehren, wird die Aktivität neu gestartet. Die Aktivität durchläuft den gesamten Satz von Start-up-Lifecycle-Callbacks, einschließlich onCreate().
  2. Beachten Sie, dass beim Neustart der App Ihr „Score“ (sowohl die Anzahl der verkauften Desserts als auch der Gesamtumsatz) auf die Standardwerte (0) zurückgesetzt wird. Wenn Android meine App geschlossen hat, warum wurde der Status nicht gespeichert?

    Wenn das Betriebssystem Ihre App neu startet, versucht Android, den Status wiederherzustellen, den die App zuvor hatte. Android speichert den Status einiger Ihrer Ansichten in einem Bundle, wenn Sie die Aktivität verlassen. Einige Beispiele für Daten, die automatisch gespeichert werden, sind der Text in einem EditText-Feld (sofern im Layout eine ID festgelegt ist) und der Backstack Ihrer Aktivität.

    Manchmal kennt das Android-Betriebssystem jedoch nicht alle Ihre Daten. Wenn Sie beispielsweise eine benutzerdefinierte Variable wie revenue in der DessertClicker-App haben, kennt das Android-Betriebssystem diese Daten oder ihre Bedeutung für Ihre Aktivität nicht. Sie müssen diese Daten selbst in das Bundle einfügen.

Schritt 2: Bundle-Daten mit onSaveInstanceState() speichern

Die Methode onSaveInstanceState() ist der Callback, mit dem Sie alle Daten speichern, die Sie möglicherweise benötigen, wenn das Android-Betriebssystem Ihre App beendet. Im Diagramm für den Lebenszyklus-Callback wird onSaveInstanceState() aufgerufen, nachdem die Aktivität beendet wurde. Wird jedes Mal aufgerufen, wenn Ihre App in den Hintergrund versetzt wird.

Der onSaveInstanceState()-Aufruf ist als Sicherheitsmaßnahme gedacht. Er gibt Ihnen die Möglichkeit, eine kleine Menge an Informationen in einem Bundle zu speichern, wenn Ihre Aktivität aus dem Vordergrund wechselt. Das System speichert diese Daten jetzt, da das Betriebssystem möglicherweise unter Ressourcenmangel leidet, wenn es bis zum Schließen der App wartet. Durch das Speichern der Daten wird sichergestellt, dass Aktualisierungsdaten im Bundle für die Wiederherstellung verfügbar sind, falls dies erforderlich ist.

  1. Überschreiben Sie in MainActivity den onSaveInstanceState()-Callback und fügen Sie eine Timber-Log-Anweisung hinzu.
override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)

   Timber.i("onSaveInstanceState Called")
}
  1. Kompiliere und führe die App aus und klicke auf die Schaltfläche Startbildschirm, um sie in den Hintergrund zu verschieben. Der onSaveInstanceState()-Callback erfolgt direkt nach onPause() und onStop():
  2. Fügen Sie oben in der Datei, direkt vor der Klassendefinition, diese Konstanten hinzu:
const val KEY_REVENUE = "revenue_key"
const val KEY_DESSERT_SOLD = "dessert_sold_key"
const val KEY_TIMER_SECONDS = "timer_seconds_key"

Sie verwenden diese Schlüssel sowohl zum Speichern als auch zum Abrufen von Daten aus dem Instanzstatus-Bundle.

  1. Scrollen Sie nach unten zu onSaveInstanceState() und beachten Sie den Parameter outState, der vom Typ Bundle ist.

    Ein Bundle ist eine Sammlung von Schlüssel/Wert-Paaren, wobei die Schlüssel immer Strings sind. Sie können primitive Werte wie int- und boolean-Werte in das Bundle einfügen.
    Da das System dieses Bundle im RAM behält, empfiehlt es sich, die Daten im Bundle klein zu halten. Die Größe dieses Bundles ist ebenfalls begrenzt, wobei die Größe je nach Gerät variiert. Im Allgemeinen sollten Sie weit weniger als 100.000 Elemente speichern, da Ihre App sonst mit dem Fehler TransactionTooLargeException abstürzen kann.
  2. Fügen Sie in onSaveInstanceState() den revenue-Wert (eine Ganzzahl) mit der Methode putInt() in das Bundle ein:
outState.putInt(KEY_REVENUE, revenue)

Die Methode putInt() (und ähnliche Methoden der Klasse Bundle wie putFloat() und putString()) verwendet zwei Argumente: einen String für den Schlüssel (die Konstante KEY_REVENUE) und den tatsächlichen Wert, der gespeichert werden soll.

  1. Wiederholen Sie den Vorgang mit der Anzahl der verkauften Desserts und dem Status des Timers:
outState.putInt(KEY_DESSERT_SOLD, dessertsSold)
outState.putInt(KEY_TIMER_SECONDS, dessertTimer.secondsCount)

Schritt 3: Bundle-Daten mit onCreate() wiederherstellen

  1. Scrollen Sie nach oben zu onCreate() und sehen Sie sich die Methodensignatur an:
override fun onCreate(savedInstanceState: Bundle) {

Beachten Sie, dass onCreate() bei jedem Aufruf ein Bundle erhält. Wenn Ihre Aktivität aufgrund eines Prozess-Shutdowns neu gestartet wird, wird das gespeicherte Bundle an onCreate() übergeben. Wenn Ihre Aktivität neu begonnen hat, ist dieses Paket in onCreate() null. Wenn das Bündel also nicht null ist, wird die Aktivität ab einem zuvor bekannten Punkt „neu erstellt“.

  1. Fügen Sie diesen Code nach der Einrichtung von DessertTimer zu onCreate() hinzu:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
}

Mit dem Test für null wird ermittelt, ob Daten im Bundle vorhanden sind oder ob das Bundle null ist. Daraus lässt sich wiederum ableiten, ob die App neu gestartet wurde oder nach dem Herunterfahren neu erstellt wurde. Dieser Test ist ein gängiges Muster zum Wiederherstellen von Daten aus dem Bundle.

Beachten Sie, dass der hier verwendete Schlüssel (KEY_REVENUE) derselbe ist wie der für putInt(). Damit Sie immer denselben Schlüssel verwenden, empfiehlt es sich, diese Schlüssel als Konstanten zu definieren. Mit getInt() rufen Sie Daten aus dem Bundle ab, so wie Sie mit putInt() Daten in das Bundle eingefügt haben. Die Methode getInt() verwendet zwei Argumente:

  • Ein String, der als Schlüssel dient, z. B. "key_revenue" für den Umsatzwert.
  • Ein Standardwert, falls für diesen Schlüssel im Bundle kein Wert vorhanden ist.

Die Ganzzahl, die Sie aus dem Bundle erhalten, wird dann der Variablen revenue zugewiesen und die Benutzeroberfläche verwendet diesen Wert.

  1. Fügen Sie getInt()-Methoden hinzu, um die Anzahl der verkauften Desserts und den Wert des Timers wiederherzustellen:
if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount =
       savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
}
  1. Kompilieren Sie die App und führen Sie sie aus. Drücken Sie mindestens fünfmal auf das Cupcake-Symbol, bis es sich in ein Donut-Symbol ändert. Klicken Sie auf „Home“, um die App in den Hintergrund zu verschieben.
  2. Führen Sie auf dem Tab Terminal in Android Studio adb aus, um den Prozess der App zu beenden.
adb shell am kill com.example.android.dessertclicker
  1. Kehren Sie über den Bildschirm „Zuletzt verwendet“ zur App zurück. Dieses Mal werden die richtigen Umsatz- und Dessertwerte aus dem Bundle zurückgegeben. Das Dessert ist aber wieder ein Cupcake. Es gibt noch eine Sache, die Sie tun müssen, damit die App nach dem Herunterfahren genau so wiederhergestellt wird, wie sie verlassen wurde.
  2. Sehen Sie sich in MainActivity die Methode showCurrentDessert() an. Bei dieser Methode wird anhand der aktuellen Anzahl der verkauften Desserts und der Liste der Desserts in der Variablen allDesserts bestimmt, welches Dessertbild in der Aktivität angezeigt werden soll.
for (dessert in allDesserts) {
   if (dessertsSold >= dessert.startProductionAmount) {
       newDessert = dessert
   }
    else break
}

Bei dieser Methode wird das richtige Bild anhand der Anzahl der verkauften Desserts ausgewählt. Daher müssen Sie nichts unternehmen, um einen Verweis auf das Bild im Bundle in onSaveInstanceState() zu speichern. In diesem Bundle speichern Sie bereits die Anzahl der verkauften Desserts.

  1. Rufen Sie in onCreate() im Block, der den Status aus dem Bundle wiederherstellt, showCurrentDessert() auf:
 if (savedInstanceState != null) {
   revenue = savedInstanceState.getInt(KEY_REVENUE, 0)
   dessertsSold = savedInstanceState.getInt(KEY_DESSERT_SOLD, 0)
   dessertTimer.secondsCount = 
      savedInstanceState.getInt(KEY_TIMER_SECONDS, 0)
   showCurrentDessert()                   
}
  1. Kompilieren Sie die App, führen Sie sie aus und minimieren Sie sie. Verwenden Sie adb, um den Vorgang zu beenden. Kehren Sie über den Bildschirm „Letzte Apps“ zur App zurück. Die Werte für „Desserts told“, „Total revenue“ und das Dessertbild werden jetzt korrekt wiederhergestellt.

Es gibt einen letzten Sonderfall bei der Verwaltung des Aktivitäts- und Fragmentlebenszyklus, den Sie kennen sollten: die Auswirkungen von Konfigurationsänderungen auf den Lebenszyklus Ihrer Aktivitäten und Fragmente.

Eine Konfigurationsänderung tritt ein, wenn sich der Status des Geräts so radikal ändert, dass das System die Änderung am einfachsten durch vollständiges Herunterfahren und Neuerstellen der Aktivität beheben kann. Wenn der Nutzer beispielsweise die Gerätesprache ändert, muss möglicherweise das gesamte Layout angepasst werden, um unterschiedliche Textrichtungen zu berücksichtigen. Wenn der Nutzer das Gerät an ein Dock anschließt oder eine physische Tastatur hinzufügt, muss das App-Layout möglicherweise eine andere Displaygröße oder ein anderes Layout nutzen. Wenn sich die Ausrichtung des Geräts ändert, z. B. wenn das Gerät vom Hoch- ins Querformat oder umgekehrt gedreht wird, muss sich das Layout möglicherweise an die neue Ausrichtung anpassen.

Schritt 1: Geräteausrichtung und Lifecycle-Callbacks kennenlernen

  1. Kompilieren und starten Sie Ihre App und öffnen Sie Logcat.
  2. Drehen Sie das Gerät oder den Emulator ins Querformat. Sie können den Emulator mit den Drehungstasten oder mit den Control- und Pfeiltasten (Command- und Pfeiltasten auf einem Mac) nach links oder rechts drehen.
  3. Sehen Sie sich die Ausgabe in Logcat an. Filtern Sie die Ausgabe nach MainActivity.
    Wenn das Gerät oder der Emulator den Bildschirm dreht, ruft das System alle Lebenszyklus-Callbacks auf, um die Aktivität zu beenden. Wenn die Aktivität neu erstellt wird, ruft das System alle Lebenszyklus-Callbacks auf, um die Aktivität zu starten.
  4. Kommentieren Sie in MainActivity die gesamte Methode onSaveInstanceState() aus.
  5. Kompilieren Sie Ihre App noch einmal und führen Sie sie aus. Klicken Sie mehrmals auf das Cupcake und drehen Sie das Gerät oder den Emulator. Wenn das Gerät gedreht und die Aktivität beendet und neu erstellt wird, wird die Aktivität dieses Mal mit Standardwerten gestartet.

    Wenn eine Konfigurationsänderung erfolgt, verwendet Android dasselbe Instanzstatus-Bundle, das Sie im vorherigen Schritt kennengelernt haben, um den Status der App zu speichern und wiederherzustellen. Wie bei einem Prozess-Shutdown verwenden Sie onSaveInstanceState(), um die Daten Ihrer App in das Bundle einzufügen. Stellen Sie dann die Daten in onCreate() wieder her, um zu vermeiden, dass Daten zum Aktivitätsstatus verloren gehen, wenn das Gerät gedreht wird.
  6. Entfernen Sie in MainActivity die Auskommentierung der onSaveInstanceState()-Methode, führen Sie die App aus, klicken Sie auf das Cupcake und drehen Sie die App oder das Gerät. Dieses Mal bleiben die Dessertdaten auch nach dem Rotieren der Aktivität erhalten.

Android Studio-Projekt: DessertClickerFinal

Tipps zum Lebenszyklus

  • Wenn Sie etwas in einem Lebenszyklus-Callback einrichten oder starten, müssen Sie es im entsprechenden Callback beenden oder entfernen. Wenn Sie das Gerät stoppen, wird es nicht weiter ausgeführt, wenn es nicht mehr benötigt wird. Wenn Sie beispielsweise einen Timer in onStart() einrichten, müssen Sie ihn in onStop() pausieren oder stoppen.
  • Verwenden Sie onCreate() nur, um die Teile Ihrer App zu initialisieren, die beim ersten Start der App einmal ausgeführt werden. Verwenden Sie onStart(), um die Teile Ihrer App zu starten, die sowohl beim Start der App als auch jedes Mal, wenn die App in den Vordergrund zurückkehrt, ausgeführt werden.

Lifecycle-Bibliothek

  • Mit der Android-Lifecycle-Bibliothek können Sie die Lebenszyklussteuerung von der Aktivität oder dem Fragment auf die tatsächliche Komponente verlagern, die lebenszyklusbewusst sein muss.
  • Lebenszyklus-Inhaber sind Komponenten, die Lebenszyklen haben (und daher „besitzen“), einschließlich Activity und Fragment. Lebenszyklusinhaber implementieren die LifecycleOwner-Schnittstelle.
  • Beobachter für den Lebenszyklus achten auf den aktuellen Lebenszyklusstatus und führen Aufgaben aus, wenn sich der Lebenszyklus ändert. Lifecycle-Beobachter implementieren die LifecycleObserver-Schnittstelle.
  • Lifecycle-Objekte enthalten die tatsächlichen Lebenszyklusstatus und lösen Ereignisse aus, wenn sich der Lebenszyklus ändert.

So erstellen Sie eine lebenszyklusbewusste Klasse:

  • Implementieren Sie die LifecycleObserver-Schnittstelle in Klassen, die den Lebenszyklus berücksichtigen müssen.
  • Initialisieren Sie eine Lifecycle-Observer-Klasse mit dem Lifecycle-Objekt aus der Aktivität oder dem Fragment.
  • Kennzeichnen Sie in der Klasse des Lebenszyklus-Observers Methoden, die den Lebenszyklus berücksichtigen, mit der Änderung des Lebenszyklusstatus, die sie beobachten.

    Die Annotation @OnLifecycleEvent(Lifecycle.Event.ON_START) gibt beispielsweise an, dass die Methode das Lebenszyklusereignis onStart beobachtet.

Prozessbeendigungen und Speichern des Aktivitätsstatus

  • Android regelt Apps, die im Hintergrund ausgeführt werden, damit die Vordergrund-App problemlos ausgeführt werden kann. Diese Verordnung umfasst die Begrenzung der Verarbeitung, die Apps im Hintergrund durchführen können, und manchmal sogar das Beenden des gesamten App-Prozesses.
  • Der Nutzer kann nicht erkennen, ob das System eine App im Hintergrund geschlossen hat. Die App wird weiterhin auf dem Bildschirm mit den zuletzt verwendeten Apps angezeigt und sollte im selben Zustand neu gestartet werden, in dem der Nutzer sie verlassen hat.
  • Die Android Debug Bridge (adb) ist ein Befehlszeilentool, mit dem Sie Anweisungen an Emulatoren und Geräte senden können, die an Ihren Computer angeschlossen sind. Mit adb können Sie das Herunterfahren eines Prozesses in Ihrer App simulieren.
  • Wenn Android den Prozess Ihrer App beendet, wird die onDestroy()-Lebenszyklusmethode nicht aufgerufen. Die App wird einfach beendet.

Aktivitäts- und Fragmentstatus beibehalten

  • Wenn Ihre App in den Hintergrund versetzt wird, werden die App-Daten kurz nach dem Aufrufen von onStop() in einem Bundle gespeichert. Einige App-Daten, z. B. der Inhalt eines EditText, werden automatisch für Sie gespeichert.
  • Das Bundle ist eine Instanz von Bundle, einer Sammlung von Schlüsseln und Werten. Die Schlüssel sind immer Strings.
  • Verwenden Sie den onSaveInstanceState()-Callback, um andere Daten im Bundle zu speichern, die Sie behalten möchten, auch wenn die App automatisch geschlossen wurde. Verwenden Sie die Bundle-Methoden, die mit put beginnen, z. B. putInt(), um Daten in das Bundle einzufügen.
  • Sie können Daten in der onRestoreInstanceState()-Methode oder häufiger in onCreate() aus dem Bundle abrufen. Die Methode onCreate() hat einen Parameter savedInstanceState, der das Bundle enthält.
  • Wenn die Variable savedInstanceState null enthält, wurde die Aktivität ohne ein Status-Bundle gestartet und es sind keine Statusdaten abzurufen.
  • Wenn Sie Daten aus dem Bundle mit einem Schlüssel abrufen möchten, verwenden Sie die Bundle-Methoden, die mit get beginnen, z. B. getInt().

Konfigurationsänderungen

  • Eine Konfigurationsänderung tritt ein, wenn sich der Status des Geräts so radikal ändert, dass das System die Änderung am einfachsten durch Herunterfahren und Neuerstellen der Aktivität beheben kann.
  • Das häufigste Beispiel für eine Konfigurationsänderung ist, wenn der Nutzer das Gerät vom Hoch- ins Querformat oder vom Quer- ins Hochformat dreht. Eine Konfigurationsänderung kann auch auftreten, wenn sich die Gerätesprache ändert oder eine Hardwaretastatur angeschlossen wird.
  • Wenn eine Konfigurationsänderung eintritt, ruft Android alle Shutdown-Callbacks des Aktivitätslebenszyklus auf. Android startet die Aktivität dann von Grund auf neu und führt alle Lifecycle-Start-Callbacks aus.
  • Wenn Android eine App aufgrund einer Konfigurationsänderung schließt, wird die Aktivität mit dem Status-Bundle neu gestartet, das für onCreate() verfügbar ist.
  • Wie beim Beenden des Prozesses müssen Sie den Zustand Ihrer App im Bundle in onSaveInstanceState() speichern.

Udacity-Kurs:

Android-Entwicklerdokumentation:

Sonstiges:

In diesem Abschnitt werden mögliche Hausaufgaben für Schüler und Studenten aufgeführt, die dieses Codelab im Rahmen eines von einem Kursleiter geleiteten Kurses durcharbeiten. Es liegt in der Verantwortung des Kursleiters, Folgendes zu tun:

  • Weisen Sie bei Bedarf Aufgaben zu.
  • Teilen Sie den Schülern/Studenten mit, wie sie Hausaufgaben abgeben können.
  • Benoten Sie die Hausaufgaben.

Lehrkräfte können diese Vorschläge nach Belieben nutzen und auch andere Hausaufgaben zuweisen, die sie für angemessen halten.

Wenn Sie dieses Codelab selbst durcharbeiten, können Sie mit diesen Hausaufgaben Ihr Wissen testen.

App ändern

Öffnen Sie die DiceRoller-App aus Lektion 1. Wenn Sie die App noch nicht haben, können Sie sie hier herunterladen. Kompilieren Sie die App und führen Sie sie aus. Wenn Sie das Gerät drehen, geht der aktuelle Wert des Würfels verloren. Implementieren Sie onSaveInstanceState(), um diesen Wert im Bundle beizubehalten und in onCreate() wiederherzustellen.

Beantworten Sie diese Fragen

Frage 1

Ihre App enthält eine Physiksimulation, für deren Darstellung umfangreiche Berechnungen erforderlich sind. Der Nutzer wird dann angerufen. Welche der folgenden Aussagen ist richtig?

  • Während des Telefonats sollten Sie die Positionen von Objekten in der Physiksimulation weiter berechnen.
  • Während des Anrufs sollten Sie die Berechnung der Positionen von Objekten in der Physiksimulation beenden.

Frage 2

Welche Lebenszyklusmethode sollten Sie überschreiben, um die Simulation zu pausieren, wenn die App nicht auf dem Bildschirm angezeigt wird?

  • onDestroy()
  • onStop()
  • onPause()
  • onSaveInstanceState()

Frage 3

Welche Schnittstelle muss eine Klasse implementieren, damit sie durch die Android-Lebenszyklusbibliothek lebenszyklusbewusst wird?

  • Lifecycle
  • LifecycleOwner
  • Lifecycle.Event
  • LifecycleObserver

Frage 4

Unter welchen Umständen erhält die Methode onCreate() in Ihrer Aktivität ein Bundle mit Daten (d. h. das Bundle ist nicht null)? Es kann mehr als eine Antwort zutreffen.

  • Die Aktivität wird neu gestartet, nachdem das Gerät gedreht wurde.
  • Die Aktivität wird von Grund auf neu gestartet.
  • Die Aktivität wird fortgesetzt, wenn die App aus dem Hintergrund zurückkehrt.
  • Das Gerät wird neu gestartet.

Nächste Lektion starten: 5.1: ViewModel und ViewModelFactory

Links zu anderen Codelabs in diesem Kurs finden Sie auf der Landingpage für Android Kotlin Fundamentals-Codelabs.