Umgang mit Lebenszyklen mit lebenszyklusbewussten Komponenten (Ansichten)

Konzepte und Jetpack Compose-Implementierung

Lebenszyklusbezogene Komponenten führen Aktionen als Reaktion auf eine Änderung des Lebenszyklusstatus einer anderen Komponente aus, z. B. Aktivitäten und Fragmente. Mit diesen Komponenten können Sie besser organisierten und oft leichteren Code erstellen, der einfacher zu warten ist.

Ein häufiges Muster besteht darin, die Aktionen der abhängigen Komponenten in den Lebenszyklusmethoden von Aktivitäten und Fragmenten zu implementieren. Dieses Muster führt jedoch zu einer schlechten Organisation des Codes und zu einer Zunahme von Fehlern. Mit lebenszyklusbezogenen Komponenten können Sie den Code abhängiger Komponenten aus den Lebenszyklusmethoden in die Komponenten selbst verschieben.

Das androidx.lifecycle Paket enthält Klassen und Schnittstellen, mit denen Sie lebenszyklusbezogene Komponenten erstellen können. Diese Komponenten können ihr Verhalten automatisch an den aktuellen Lebenszyklusstatus einer Aktivität oder eines Fragments anpassen.

Die meisten in Android Framework definierten App-Komponenten haben Lebenszyklen. Lebenszyklen werden vom Betriebssystem oder vom Framework-Code verwaltet, der in Ihrem Prozess ausgeführt wird. Sie sind ein wichtiger Bestandteil der Funktionsweise von Android und müssen von Ihrer Anwendung berücksichtigt werden. Andernfalls kann es zu Speicherlecks oder sogar zu Abstürzen der Anwendung kommen.

Angenommen, wir haben eine Aktivität, die den Gerätestandort auf dem Bildschirm anzeigt. Eine gängige Implementierung könnte so aussehen:

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Java

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Obwohl dieses Beispiel gut aussieht, gibt es in einer echten App zu viele Aufrufe, die die UI und andere Komponenten als Reaktion auf den aktuellen Lebenszyklusstatus verwalten. Die Verwaltung mehrerer Komponenten führt dazu, dass eine beträchtliche Menge an Code in Lebenszyklusmethoden wie onStart() und onStop platziert wird, was die Wartung erschwert.

Außerdem gibt es keine Garantie, dass die Komponente gestartet wird, bevor die Aktivität oder das Fragment beendet wird. Das gilt insbesondere, wenn ein Vorgang mit langer Ausführungszeit ausgeführt werden muss, z. B. eine Konfigurationsprüfung in onStart. Dies kann zu einer Race-Bedingung führen, bei der die onStop() Methode vor dem onStart beendet wird, wodurch die Komponente länger als nötig aktiv bleibt.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

Das Paket androidx.lifecycle enthält Klassen und Schnittstellen, mit denen Sie diese Probleme auf robuste und isolierte Weise angehen können.

Lebenszyklus

Lifecycle ist eine Klasse, die die Informationen zum Lebenszyklusstatus einer Komponente (z. B. einer Aktivität oder eines Fragments) enthält und anderen Objekten ermöglicht, diesen Status zu beobachten.

Lifecycle verwendet zwei Hauptaufzählungen, um den Lebenszyklusstatus der zugehörigen Komponente zu verfolgen:

Veranstaltung

Die Lebenszyklusereignisse, die vom Framework und der Lifecycle Klasse gesendet werden. Diese Ereignisse werden den Callback-Ereignissen in Aktivitäten und Fragmenten zugeordnet.

Bundesland

Der aktuelle Status der Komponente, die vom Lifecycle Objekt verfolgt wird.

Abbildung 1 Status und Ereignisse des Android-Aktivitätslebenszyklus.

Stellen Sie sich die Status als Knoten eines Graphen und die Ereignisse als Kanten zwischen diesen Knoten vor.

Eine Klasse kann den Lebenszyklusstatus der Komponente überwachen, indem sie DefaultLifecycleObserver implementiert und entsprechende Methoden wie onCreate, onStart, usw. überschreibt. Dann können Sie einen Beobachter hinzufügen, indem Sie die Methode addObserver() der Klasse Lifecycle aufrufen und eine Instanz Ihres Beobachters übergeben, wie im folgenden Beispiel gezeigt:

Kotlin

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

Java

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

Im obigen Beispiel implementiert das myLifecycleOwner Objekt die LifecycleOwner Schnittstelle, die im folgenden Abschnitt erläutert wird.

LifecycleOwner

LifecycleOwner ist eine Schnittstelle mit einer einzigen Methode, die angibt, dass die Klasse einen Lifecycle hat. Sie hat eine Methode, getLifecycle, die von der Klasse implementiert werden muss. Wenn Sie stattdessen den Lebenszyklus eines gesamten Anwendungsprozesses verwalten möchten, sehen Sie sich ProcessLifecycleOwner an.

Diese Schnittstelle abstrahiert das Eigentum an einem Lifecycle von einzelnen Klassen wie Fragment und AppCompatActivity und ermöglicht das Schreiben von Komponenten, die mit ihnen funktionieren. Jede benutzerdefinierte Anwendungsklasse kann die LifecycleOwner Schnittstelle implementieren.

Komponenten, die DefaultLifecycleObserver implementieren, funktionieren nahtlos mit Komponenten, die LifecycleOwner implementieren, da ein Eigentümer einen Lebenszyklus bereitstellen kann, für den sich ein Beobachter registrieren kann.

Im Beispiel zur Standortverfolgung können wir die MyLocationListener Klasse so gestalten, dass sie DefaultLifecycleObserver implementiert, und sie dann in der Methode onCreate() mit dem Lifecycle der Aktivität initialisieren. Dadurch ist die Klasse MyLocationListener unabhängig. Das bedeutet, dass die Logik zum Reagieren auf Änderungen des Lebenszyklusstatus in MyLocationListener und nicht in der Aktivität deklariert wird. Wenn die einzelnen Komponenten ihre eigene Logik speichern, lässt sich die Logik von Aktivitäten und Fragmenten einfacher verwalten.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

Ein häufiger Anwendungsfall ist, bestimmte Callbacks nicht aufzurufen, wenn sich der Lifecycle gerade nicht in einem guten Zustand befindet. Wenn der Callback beispielsweise eine Fragmenttransaktion ausführt, nachdem der Aktivitätsstatus gespeichert wurde, würde dies einen Absturz auslösen. Daher sollten wir diesen Callback niemals aufrufen.

Um diesen Anwendungsfall zu vereinfachen, können andere Objekte mit der Lifecycle Klasse den aktuellen Status abfragen.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

Java

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

Mit dieser Implementierung ist unsere Klasse LocationListener vollständig lebenszyklusbezogen. Wenn wir unseren LocationListener in einer anderen Aktivität oder einem anderen Fragment verwenden möchten, müssen wir ihn nur initialisieren. Alle Einrichtungs- und Deaktivierungsvorgänge werden von der Klasse selbst verwaltet.

Wenn eine Bibliothek Klassen bereitstellt, die mit dem Android-Lebenszyklus funktionieren müssen, empfehlen wir die Verwendung von lebenszyklusbezogenen Komponenten. Ihre Bibliotheksclients können diese Komponenten einfach integrieren, ohne den Lebenszyklus auf Clientseite manuell verwalten zu müssen.

Benutzerdefinierten LifecycleOwner implementieren

Fragmente und Aktivitäten in der Support Library 26.1.0 und höher implementieren bereits die LifecycleOwner Schnittstelle.

Wenn Sie eine benutzerdefinierte Klasse haben, die Sie zu einem LifecycleOwner, machen möchten, können Sie die Klasse LifecycleRegistry verwenden. Sie müssen jedoch Ereignisse an diese Klasse weiterleiten, wie im folgenden Codebeispiel gezeigt:

Kotlin

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

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

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

Best Practices für lebenszyklusbezogene Komponenten

  • Halten Sie Ihre UI-Controller (Aktivitäten und Fragmente) so schlank wie möglich. Sie sollten nicht versuchen, eigene Daten abzurufen. Verwenden Sie stattdessen ein ViewModel und beobachten Sie ein LiveData-Objekt, um die Änderungen in den Ansichten widerzuspiegeln.
  • Versuchen Sie, datengesteuerte UIs zu schreiben, bei denen die Aufgabe des UI-Controllers darin besteht, die Ansichten zu aktualisieren, wenn sich Daten ändern, oder Nutzeraktionen an das ViewModel zu senden.
  • Platzieren Sie Ihre Datenlogik in der ViewModel Klasse. ViewModel sollte als Verbindung zwischen Ihrem UI-Controller und dem Rest Ihrer App dienen. Seien Sie jedoch vorsichtig: Es ist nicht die Aufgabe von ViewModel's, Daten abzurufen (z. B. aus einem Netzwerk). Stattdessen sollte ViewModel die entsprechende Komponente aufrufen, um die Daten abzurufen, und das Ergebnis dann an den UI-Controller zurückgeben.
  • Verwenden Sie Datenbindung, um eine saubere Schnittstelle zwischen Ihren Ansichten und dem UI-Controller zu gewährleisten. So können Sie Ihre Ansichten deklarativer gestalten und den Aktualisierungscode minimieren, den Sie in Ihren Aktivitäten und Fragmenten schreiben müssen. Wenn Sie dies lieber in der Programmiersprache Java tun möchten, verwenden Sie eine Bibliothek wie Butter Knife, um Boilerplate-Code zu vermeiden und eine bessere Abstraktion zu erhalten.
  • Wenn Ihre UI komplex ist, sollten Sie eine Presenter-Klasse erstellen, um UI-Änderungen zu verarbeiten. Das kann zwar mühsam sein, aber es kann das Testen Ihrer UI-Komponenten erleichtern.
  • Vermeiden Sie es, in Ihrem ViewModel auf einen View- oder Activity-Kontext zu verweisen. Wenn das ViewModel die Aktivität überdauert (bei Konfigurationsänderungen), kommt es zu einem Speicherleck in der Aktivität und sie wird nicht ordnungsgemäß vom Garbage Collector entsorgt.
  • Verwenden Sie Kotlin-Coroutinen, um zeitaufwendige Aufgaben und andere Vorgänge zu verwalten, die asynchron ausgeführt werden können.

Anwendungsfälle für lebenszyklusbezogene Komponenten

Lebenszyklusbezogene Komponenten können die Verwaltung von Lebenszyklen in einer Vielzahl von Fällen erheblich erleichtern. Einige Beispiele:

  • Zwischen groben und feinen Standortaktualisierungen wechseln. Verwenden Sie lebenszyklusbezogene Komponenten, um feine Standortaktualisierungen zu aktivieren, während Ihre Standort-App sichtbar ist, und wechseln Sie zu groben Aktualisierungen, wenn die App im Hintergrund ausgeführt wird. LiveData, eine lebenszyklusbezogene Komponente, ermöglicht es Ihrer App, die UI automatisch zu aktualisieren, wenn der Nutzer den Standort ändert.
  • Videopufferung beenden und starten. Verwenden Sie lebenszyklusbezogene Komponenten, um die Videopufferung so schnell wie möglich zu starten, die Wiedergabe jedoch zu verzögern, bis die App vollständig gestartet wurde. Sie können lebenszyklusbezogene Komponenten auch verwenden, um die Pufferung zu beenden, wenn Ihre App beendet wird.
  • Netzwerkverbindung starten und beenden. Verwenden Sie lebenszyklusbezogene Komponenten, um die Live-Aktualisierung (Streaming) von Netzwerkdaten zu aktivieren, während eine App im Vordergrund ausgeführt wird, und um sie automatisch zu pausieren, wenn die App in den Hintergrund wechselt.
  • Animierte Drawables pausieren und fortsetzen. Verwenden Sie lebenszyklusbezogene Komponenten, um das Pausieren animierter Drawables zu verarbeiten, wenn die App im Hintergrund ausgeführt wird, und um Drawables fortzusetzen, nachdem die App im Vordergrund ausgeführt wird.

Ereignisse beim Beenden verarbeiten

Wenn ein Lifecycle zu einer AppCompatActivity oder Fragment gehört, ändert sich der Status des Lifecycle's in CREATED und das Ereignis ON_STOP wird gesendet, wenn onSaveInstanceState() der AppCompatActivity oder Fragment's aufgerufen wird.

Wenn der Status eines Fragment oder AppCompatActivity's mit onSaveInstanceState gespeichert wird, gilt die UI als unveränderlich, bis ON_START aufgerufen wird. Wenn Sie versuchen, die UI zu ändern, nachdem der Status gespeichert wurde, kann dies zu Inkonsistenzen im Navigationsstatus Ihrer Anwendung führen. Daher löst FragmentManager eine Ausnahme aus, wenn die App eine FragmentTransaction ausführt, nachdem der Status gespeichert wurde. Weitere Informationen finden Sie unter commit() für Details.

LiveData verhindert diesen Grenzfall standardmäßig, indem der Beobachter nicht aufgerufen wird, wenn der zugehörige Lifecycle des Beobachters nicht mindestens STARTED ist. Im Hintergrund wird isAtLeast() aufgerufen, bevor entschieden wird, ob der Beobachter aufgerufen werden soll.

Leider wird die Methode AppCompatActivity's onStop() nach onSaveInstanceState aufgerufen. Dadurch entsteht eine Lücke, in der Änderungen am UI-Status nicht zulässig sind, der Lifecycle aber noch nicht in den CREATED Status versetzt wurde.

Um dieses Problem zu vermeiden, wird der Status in der Klasse Lifecycle in Version beta2 und niedriger als CREATED markiert, ohne das Ereignis zu senden. So erhalten alle Code, die den aktuellen Status prüfen, den tatsächlichen Wert, obwohl das Ereignis erst gesendet wird, wenn onStop() vom System aufgerufen wird.

Leider hat diese Lösung zwei große Probleme:

  • Auf API-Level 23 und niedriger speichert das Android-System den Status einer Aktivität auch dann, wenn sie teilweise von einer anderen Aktivität verdeckt wird. Mit anderen Worten: Das Android-System ruft onSaveInstanceState() auf, aber nicht unbedingt onStop. Dadurch entsteht ein potenziell langes Intervall, in dem der Beobachter immer noch davon ausgeht, dass der Lebenszyklus aktiv ist, obwohl der UI-Status nicht geändert werden kann.
  • Jede Klasse, die ein ähnliches Verhalten wie die LiveData Klasse aufweisen soll, muss den Workaround implementieren, der in der Version beta 2 und niedriger von Lifecycle bereitgestellt wird.

Zusätzliche Ressourcen

Weitere Informationen zum Verarbeiten von Lebenszyklen mit lebenszyklusbezogenen Komponenten finden Sie in den folgenden zusätzlichen Ressourcen.

Produktproben

  • Sunflower, eine Demo-App, die Best Practices mit Architekturkomponenten demonstriert

Codelabs

Blogs