Stand: 29.07.2018

10.1 Das große Ganze: Tasks und der Back-Stack

Auf einem mobilen Endgerät laufen meistens mehrere Apps parallel. Ein Task ist nur der technische Begriff für eine laufende App. Wir benutzen den Letzte Applikationen Button, um zwischen den Tasks zu wechseln.

Innerhalb eines Tasks (also innerhalb einer App) befinden wir uns in einer Activity. Manche Apps starten z.B. auf Knopfdruck eine zweite Activity, aber was passiert dann mit der ersten Activity?

Wie Sie vielleicht wissen, kann man mit dem Back-Button jederzeit zurück zur ersten Activity. Technisch speichert Android für jeden Task alle vergangenen Activities in einem Back-Stack.

Als Beispiel nehmen wir eine Adessbuch-App mit einer Liste aller Adressen (Activity A), der Detailansicht einer Adresse (Activity B) und der Edit-Ansicht einer Adresse (Activity C). Wir starten in A. Der Back-Stack enthält nur A. Wir klicken auf eine bestimmte Adresse, die App geht in Activity B über. Der Back-Stack enthält B-A. Dabei ist B oben und somit im Vordergrund:

Back-Stack mit zwei Activities

Würden wir jetzt auf den Back-Button drücken, würden wir wieder in Activity A landen. Denn ein "Back" bedeutet, die oberste Activität vom Stack zu nehmen und die darunter liegende zu reaktivieren.

Back-Stack mit Back

Nehmen wir aber an, wir sind in A, klicken auf eine Adresse und kommen in B und dann drücken wir auf "Edit" und kommen in C. Der Back-Stack enthält jetzt C-B-A. Activity C ist im Vordergrund, die beiden anderen liegen darunter im Stack.

Back-Stack mit drei Activities

Jetzt ist es wichtig, dass bei Beendigung der Activity C (normalerweise über einen Button wie "fertig") die Activity mit finish beendet wird. Das bewirkt - ähnlich wie beim Back-Button - dass die aktuelle Activity beendet und vom Stack genommen wird.

Back-Stack und Beenden von Activities mit finish

Sie haben in Modul Intents I gelernt, wie man Activities startet und beendet. Das Wissen über den Back-Stack sollte Ihnen ermöglichen zu entscheiden, wann Sie finish einsetzen und wann Sie mit startActivity eine neue Aktivität starten.

10.2 Warum Lifecycle?

Mit dem Lifecycle (engl. für Lebenszyklus) einer Activity bezeichnet man verschiedene Phasen dieser Activity, wie z.B. das erste Starten, das Sichtbarwerden, das Wechseln in eine andere Activity, das Wechseln in eine andere App und das Schließen der App.

Man spricht auch davon, dass eine App im Vordergrund ist oder im Hintergrund (aber nicht beendet).

Aus Sicht des Programmierers ist dies wichtig für Fragen wie: Sind meine Daten noch da? Aus Sicht von Android ist dies wichtig, um die Performance des Geräts optimal zu halten, denn für eine App gibt es potentiell viele Activities und für das Gerät gibt es potentiell viele Apps, die alle gleichzeitig "laufen" und Ressourcen verbrauchen können (Rechenzeit, Lese-/Schreibzugriffe auf Speicher und Netz). Diesen Verbrauch möchte man so gering wie möglich halten.

Wenn eine Activity einer App bereits läuft, gibt es mindestens vier Verläufe, bei denen man sich fragen kann, was mit der Activity und Ihren Daten - genauer gesagt: ihren Instanzvariablen - passiert:

Mögliche Ereignisse im Leben einer App

10.3 Callback-Methoden

Android stellt den Programmierern mehrere Methoden bereit, die vom System aufgerufen werden, wenn bestimmte Ereignisse (engl. events) im Leben einer Activity eintreten. Methoden, die bei bestimmten Events aufgerufen werden, nennt man auch Callback-Methoden oder Callbacks. Als Programmiererin kann man sich "einklinken", indem man die entsprechenden Methoden *überschreibt.

Sie kennen bereits die Methode onCreate(), die Sie überschreiben müssen und wo Sie Code zu Initialisierung Ihrer Activity hineinschreiben. Es gibt entsprechend noch weitere Methoden, die von der Activity-Klasse bereit gestellt werden, und die man optional überschreiben kann, um z.B. Daten zu sichern, zu laden oder um ressourcenfressende Animationen zu pausieren.

10.3.1 Zustände

Jede Activity einer App hat einen Lifecycle, d.h. jede Activity befindet sich zur Laufzeit, sofern sie mindestens einmal vom Benutzer geöffnet wurde, in einem von sechs Zuständen:

Im letzten Zustand verschwindet die Activity.

Den Übergang von einem Zustand in den nächsten (oder ganz zu Beginn in den ersten Zustand) nennt man auch ein Lifecycle-Event und dafür gibt es mehrere Callback-Methoden, die man als Programmierer*in überschreiben kann:

Welchen Zustand die Callback-Methoden jeweils einleiten, sehen Sie in folgendem Graphen:

Graph mit Callbacks und Zuständen im Lifecycle einer Activity

(Quelle: Google / Android Developers Training, Lizenz CC-BY-NC)

10.3.2 Callbacks

Wir sehen uns die verschiedenen Abläufe von Lifecycle-Events und entsprechenden Aufrufen der Callback-Methoden für typische Fälle an.

Activity starten

Beim ersten Starten einer Activity werden die folgenden Methoden in dieser Reihenfolge aufgerufen.

Lebenszyklus: Activity starten

Sie überschreiben normalerweise onCreate, um UI-Komponenten hinzuzufügen, zu ändern oder mit Listenern auszustatten. Die App ist beim Abarbeiten dieser Methode nocht nicht sichtbar.

Die Methode onStart wird anschließend aufgerufen und i.d.R. nicht überschrieben.

Zum Schluss kommt die Methode onResume, die auch in einigen anderen Kontexten aufgerufen wird (siehe Abschnitt "Activity wiederaufnehmen"). Hier kommt Code rein, der z.B. die Instanzvariablen auf den aktuellen Zustand bringen soll. Beim Abarbeiten dieser Methode ist die App bereits sichtbar. Diese Methode ist z.B. dann interessant, wenn Sie beim jedem "Wiedereintritt" in die App Dinge tun möchten.

Etwas unerwartet ist vielleicht, dass auch beim Ändern der Konfiguration die Activity komplett neu gestartet wird. Dazu gehört:

Der Grund ist, dass hier komplett neue Layouts (in Form von speziellen XML-Layout-Dateien) aufgerufen werden können (siehe Modul Layout II).

Diese Reihe wird immer in folgenden Situationen aufgerufen:

Activity pausieren

Wenn Sie in eine andere Activity wechseln, wird die aktuelle Activity gestoppt, aber nicht zerstört. Die Activity durchläuft die folgenden Methoden:

Lebenszyklus: Activity wechseln

Die Methode onPause leitet einen Zustand ein, wo die Activity nur teilweise sichtbar ist. Zum Beispiel, wenn ein teiltransparentes Dialogfenster die Activity teilweise verdeckt. Es ist ein Übergangszustand. Es kann z.B. sinnvoll sein, hier bereits ein laufendes Video zu pausieren. Bei Animationen kann es genauso sinnvoll sein, sie weiter laufen zu lassen und erst später zu stoppen. Hier ist es nur wichtig, dass Sie keine langwierigen Prozesse (z.B. Speichern) ausführen, um den Fluss der Übergänge nicht zu belasten.

Sobald onStop aufgerufen ist, ist die Activity nicht mehr sichtbar. Spätestens hier kann man also Videos/Animationen stoppen/pausieren. Auch Prozesse wie das Sichern von Daten, die evtl. mehr Zeit benötigen, können hier statt in onPause ausgeführt werden.

Diese Reihe wird immer in folgenden Situationen aufgerufen:

Activity wiederaufnehmen

Wenn die Activity schon mal aufgerufen wurde und man aus einer anderen Activity zurückkehrt, werden folgende Methoden durchlaufen:

Lebenszyklus: Activity wieder aufnehmen

Diese Reihe wird immer in folgenden Situationen aufgerufen (unter der Voraussetzung, dass die Activity schon 1x vorher aufgerufen worden war):

Activity beenden

Wenn Sie die App schließen, passiert in allen bisher aufgerufenen Activities der App folgendes:

Lebenszyklus: App schließen

Wenn Sie also eine App mit drei Activities haben, die Sie alle mindestens einmal aufgerufen haben, dann werden für alle drei Activities diese Methoden aufgerufen.

Zu beachten ist, dass eine Sub-Activity, sobald sie mit finish beendet wird, auch zerstört wird, d.h. diese Reihe wird dann schon durchlaufen.

Da es vorkommen kann, dass onDestroy nicht aufgerufen wird, sollten wichtige Aktionen wie das Speichern von Daten, lieber in onStop durchgeführt werden.

Diese Reihe wird immer in folgenden Situationen aufgerufen:

Überschreiben

Wie erwähnt können Sie als Programmiererin jede der erwähnten Methoden *überschreiben, um Code an Zustandsübergängen auszuführen. Das sieht das ungefähr so aus:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // Ihr Code
}

protected void onStart() {
    super.onStart();
    // Ihr Code
}

protected void onResume() {
    super.onResume();
    // Ihr Code
}

protected void onRestart() {
    super.onRestart();
    // Ihr Code
}

protected void onPause() {
    super.onPause();
    // Ihr Code
}

protected void onStop() {
    super.onStop();
    // Ihr Code
}

protected void onDestroy() {
    super.onDestroy();
    // Ihr Code
}

Außer bei onCreate bleibt es Ihnen überlassen, ob Sie die jeweilige Methode überschreiben oder nicht. Der Aufruf von z.B. super.onStart bedeutet, dass die entsprechende Methode der Oberklasse aufgerufen wird und sollte man zur Sicherheit im Code lassen.

Überblick

Hier sehen wir die möglichen Abfolgen im Überblick:

Lebenszyklus-Überblick

Wir haben hier nicht alle Fälle besprochen. Ein sehr guter und vollständiger Überblick zum Thema findet man in folgendem Video: The Activity Lifecycle Explained

10.4 Activity-Zustandsobjekt (Bundle)

Es wurde schon erwähnt, dass das Ändern der "Konfiguration" die Activity schließt und erneut onCreate aufruft. Zur Erinnerung, das Ändern der Konfiguration heißt

Dabei gehen z.B. die Werte aller Instanzvariablen verloren. Wir haben bereits in den Modulen Daten I und Daten II Lösungen gesehen, wie man Daten zwischenzuspeichern kann. Für kleinteilige Informationen wie Zahlen und Texte, die man sich merken möchte, um sie nach der Konfigurationsänderung wieder aufzunehmen, gibt es zusätzlich in jeder Activity ein Zustandsobjekt vom Typ Bundle und eine zugehörige Methode onSaveInstanceState.

Die Callback-Methode onSaveInstanceState wird immer dann aufgerufen, wenn z.B. eine Orientierungsänderung eintritt. Dort bekommt man ein Objekt vom Type Bundle, das man benutzen kann, um Informationen zu speichern. Es handelt sich - wie schon bei den Preferences - um eine einfache Tabelle.

Wenn Sie z.B. eine Instanzvariable counter vom Typ int haben (für eine einfache Zähler-App), dann können Sie diese in der Tabelle im Bundle ablegen:

protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("zählstand", counter);
}

Wann genau rufen Sie diese Info wieder auf? Hier gibt es zwei Möglichkeiten. Entweder Sie nutzen eine weitere Callback-Methode namens onRestoreInstanceState oder - noch besser (und empfohlen) - Sie erledigen das direkt in onCreate. Auch dort haben Sie ein Objekt vom Typ Bundle und genau dort steht die Info drin. Beim ersten Aufruf von onCreate ist dies Objekt allerdings null, d.h. Sie testen das Objekt erst auf null und lesen gegebenenfalls die Infos aus.

In unserem Beispiel:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (savedInstanceState != null) {
        counter = savedInstanceState.getInt("zählstand");
    }

    ...
}

10.5 Zusammenfassung

Jede Activity einer App hat einen Lifecycle, d.h. jede Activity befindet sich zur Laufzeit, sofern sie mindestens einmal vom Benutzer geöffnet wurde, in einem von sechs Zuständen:

  1. Created
  2. Started
  3. Resumed
  4. Paused
  5. Stopped
  6. Destroyed

Im Zustand "Destroyed" verschwindet die Activity.

Als Programmierer kann man sich in jeden Übergang zu einem der Zustände "einklinken", indem man eine Callback-Methode in der entsprechenden Activity-Klasse überschreibt (wobei onCreate immer überschrieben wird):

Weiterhin kann man den Zustand einer Activity bei Konfigurationsänderungen (z.B. Änderung der Orientierung oder Spracheinstellung) speichern, indem man die Methode onSaveInstanceState überschreibt und die dort in einem Bundle-Objekt gespeicherten Informationen z.B. in onCreate verwendet.

10.6 Übungen

(A) ZählerApp v3

Implementieren Sie eine dritte Version der ZählerApp (siehe auch Module Daten I und Daten II), diesmal mit Hilfe von des Zustandsobjekts vom Typ Bundle und der Callback-Methode onSaveInstanceState.

Zähler-App

(B) Lifecycle testen

Um den Lifecycle zu testen, empflieht es sich, eine App zu schreiben, in der man alle Methoden in MainActivity überschreibt und mit Log-Output versieht (siehe Modul Hilfe zum Thema Logging).

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onCreate");
}

protected void onStart() {
    super.onStart();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onStart");
}

protected void onResume() {
    super.onResume();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onResume");
}

protected void onRestart() {
    super.onRestart();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onRestart");
}

protected void onPause() {
    super.onPause();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onPause");
}

protected void onStop() {
    super.onStop();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onStop");
}

protected void onDestroy() {
    super.onDestroy();
    Log.i("App", "++++++++++++++++++++++++++++++++++++ MAIN onDestroy");
}

Sinnvoll ist weiterhin, eine zweite Activity zu schreiben mit analogen Log-Nachrichten und jede Activity mit einem Button zum Übergang zu versehen.

Lebenszyklus-Übung

Sie können mit einer solchen App auch prüfen, was passiert, wenn Sie auf die Buttons Home, Letzte Applikationen oder Back drucken.

In den Screenshots oben sehen Sie einen Button "Make Line in Log", mit dem Sie eine Trennzeile ins Log-Fenster schreiben:

findViewById(R.id.buttonLog).setOnClickListener(v -> {
  Log.i("App", "#######################################################");
});