Zarządzanie cyklami życia za pomocą komponentów uwzględniających cykl życia   Element Jetpacka na Androida.

Komponenty uwzględniające cykl życia wykonują działania w odpowiedzi na zmianę stanu cyklu życia innego komponentu, np. aktywności lub fragmentu. Te komponenty pomagają tworzyć lepiej zorganizowany i często lżejszy kod, który jest łatwiejszy w utrzymaniu.

Typowym wzorcem jest implementowanie działań komponentów zależnych w metodach cyklu życia aktywności i fragmentów. Jednak taki schemat prowadzi do złej organizacji kodu i rozprzestrzeniania się błędów. Dzięki komponentom uwzględniającym cykl życia możesz przenieść kod komponentów zależnych z metod cyklu życia do samych komponentów.

Pakiet androidx.lifecycle udostępnia klasy i interfejsy, które umożliwiają tworzenie komponentów świadomych cyklu życia. Są to komponenty, które mogą automatycznie dostosowywać swoje działanie do bieżącego stanu cyklu życia aktywności lub fragmentu.

Większość komponentów aplikacji zdefiniowanych w ramach Android Framework ma przypisane cykle życia. Cykl życia jest zarządzany przez system operacyjny lub kod frameworka działający w procesie. Są one podstawą działania Androida i Twoja aplikacja musi je uwzględniać. Niezastosowanie się do tej zasady może spowodować wyciek pamięci lub awarię aplikacji.

Załóżmy, że mamy aktywność, która wyświetla lokalizację urządzenia na ekranie. Typowa implementacja może wyglądać tak:

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

Chociaż ten przykład wygląda dobrze, w rzeczywistej aplikacji kończy się na tym, że masz zbyt wiele wywołań, które zarządzają interfejsem użytkownika i innymi komponentami w odpowiedzi na bieżący stan cyklu życia. Zarządzanie wieloma komponentami powoduje, że znaczna część kodu znajduje się w metodach cyklu życia, takich jak onStart()onStop(), co utrudnia ich utrzymanie.

Nie ma też gwarancji, że komponent zostanie uruchomiony przed zatrzymaniem aktywności lub fragmentu. Jest to szczególnie ważne, jeśli musimy wykonać długotrwałą operację, taką jak sprawdzenie konfiguracji w onStart(). Może to spowodować stan wyścigu, w którym metoda onStop() kończy działanie przed metodą onStart(), co powoduje, że komponent pozostaje aktywny dłużej niż to konieczne.

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

Pakiet androidx.lifecycle udostępnia klasy i interfejsy, które pomagają rozwiązywać te problemy w sposób odporny i odporny na błędy.

Cykl życia

Lifecycle to klasa, która przechowuje informacje o stanie cyklu życia komponentu (np. aktywności lub fragmentu) i umożliwia innym obiektom obserwowanie tego stanu.

Lifecycle korzysta z 2 głównych zbiorów do śledzenia stanu cyklu życia powiązanego komponentu:

Wydarzenie
Zdarzenia cyklu życia wysyłane z ramy i klasy Lifecycle. Te zdarzenia są mapowane na zdarzenia wywołania zwrotnego w aktywnościach i fragmentach.
Region
Bieżący stan komponentu śledzonego przez obiekt Lifecycle.
Diagram stanów cyklu życia
Rysunek 1. Stany i zdarzenia, które składają się na cykl życia działania na Androidzie

Traktuj stany jako węzły grafu, a zdarzenia jako krawędzie między tymi węzłami.

Klasa może monitorować stan cyklu życia komponentu, implementując interfejs DefaultLifecycleObserver i zastępując odpowiednie metody, takie jak onCreate, onStart itp. Następnie możesz dodać obserwatora, wywołując metodę addObserver() klasy Lifecycle i przekazując instancję obserwatora, jak w tym przykładzie:

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

W przykładzie powyżej obiekt myLifecycleOwner implementuje interfejs LifecycleOwner, który jest opisany w sekcji poniżej.

LifecycleOwner

LifecycleOwner to interfejs z jedną metodą, który oznacza, że zajęcia mają Lifecycle. Zawiera jedną metodę getLifecycle(), którą klasa musi zaimplementować. Jeśli chcesz zarządzać cyklem życia całego procesu aplikacji, zapoznaj się z artykułem ProcessLifecycleOwner.

Ten interfejs abstrakcyjnie traktuje własność Lifecycle w poszczególnych klasach, takich jak FragmentAppCompatActivity, i pozwala pisać komponenty, które z nimi współpracują. Interfejs LifecycleOwner może być implementowany przez dowolną niestandardową klasę aplikacji.

Komponenty, które implementują DefaultLifecycleObserver działają bezproblemowo z komponentami, które implementują LifecycleOwner ponieważ właściciel może zapewnić cykl życia, który obserwator może zarejestrować, aby go obserwować.

W przypadku śledzenia lokalizacji możemy w klasie MyLocationListener zaimplementować metodę DefaultLifecycleObserver, a potem zainicjować ją za pomocą zmiennej Lifecycle w metodzie onCreate(). Dzięki temu klasa MyLocationListener jest samowystarczalna, co oznacza, że logika reagowania na zmiany stanu cyklu życia jest deklarowana w klasie MyLocationListener, a nie w aktywności. Dzięki temu, że poszczególne komponenty przechowują własną logikę, łatwiej jest zarządzać logiką działań i fragmentów.

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

Typowym zastosowaniem jest unikanie wywoływania niektórych funkcji zwracających dane, jeśli Lifecycle nie jest w danym momencie w dobrym stanie. Jeśli na przykład wywołanie zwrotne uruchamia transakcję fragmentu po zapisaniu stanu aktywności, spowoduje awarię, dlatego nigdy nie należy wywoływać tego wywołania zwrotnego.

Aby ułatwić to zadanie, klasa Lifecycle umożliwia innym obiektom wysyłanie zapytań o obecny stan.

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

Dzięki tej implementacji klasa LocationListener jest w pełni świadoma cyklu życia. Jeśli chcemy użyć obiektu LocationListener z innej aktywności lub fragmentu, musimy go zainicjować. Wszystkie operacje konfiguracji i demontażu są zarządzane przez zajęcia.

Jeśli biblioteka udostępnia klasy, które muszą współpracować z cyklem życia Androida, zalecamy używanie komponentów uwzględniających cykl życia. Klienci biblioteki mogą łatwo integrować te komponenty bez ręcznego zarządzania ich cyklem życia po stronie klienta.

Implementacja niestandardowego obiektu LifecycleOwner

Fragmenty i aktywności w Bibliotece wsparcia w wersji 26.1.0 i nowszych już implementują interfejs LifecycleOwner.

Jeśli masz klasę niestandardową, którą chcesz użyć w miejscu klasy LifecycleOwner, możesz użyć klasy LifecycleRegistry, ale musisz przekazać do niej zdarzenia, jak pokazano w tym przykładzie kodu:

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

Sprawdzone metody dotyczące komponentów uwzględniających cykl życia

  • Upewnij się, że kontrolery interfejsu użytkownika (aktywności i fragmenty) są jak najbardziej zwięzłe. Nie powinny one samodzielnie pozyskiwać danych, ale zamiast tego używać do tego metody ViewModel i obserwować obiekt LiveData, aby odzwierciedlić zmiany w widokach.
  • Spróbuj napisać interfejs użytkownika sterowany danymi, w którym zadaniem kontrolera interfejsu jest aktualizowanie widoków w miarę zmian danych lub powiadamianie ViewModel o działaniach użytkownika.
  • Umieść logikę danych w klasie ViewModel. ViewModel powinna pełnić funkcję łącznika między kontrolerem interfejsu użytkownika a resztą aplikacji. Pamiętaj jednak, że nie jest odpowiedzialna za pobieranie danych (np. z sieci).ViewModel Zamiast tego ViewModel powinna wywołać odpowiedni komponent, aby pobrać dane, a potem przekazać wynik do kontrolera interfejsu użytkownika.
  • Użyj wiązania danych, aby zachować przejrzystość interfejsu między widokami a kontrolerem interfejsu użytkownika. Dzięki temu możesz tworzyć bardziej deklaracyjne widoki i minimalizować kod aktualizacji, który musisz napisać w swoich aktywnościach i fragmentach. Jeśli wolisz to zrobić w języku programowania Java, użyj biblioteki takiej jak Butter Knife, aby uniknąć szablonowego kodu i uzyskać lepszą abstrakcję.
  • Jeśli interfejs jest złożony, rozważ utworzenie klasy prezenter, która będzie obsługiwać modyfikacje interfejsu. Może to być pracochłonne, ale ułatwia testowanie komponentów interfejsu użytkownika.
  • Unikaj odwoływania się do kontekstu View lub ActivityViewModel. Jeśli ViewModel przetrwa aktywność (w przypadku zmian konfiguracji), Twoja aktywność wycieka i nie jest prawidłowo usuwana przez zbieracza.
  • Do zarządzania długotrwałymi zadaniami i innymi operacjami, które mogą być wykonywane asynchronicznie, używaj korobon w Kotlinie.

Przypadki użycia komponentów uwzględniających cykl życia

Komponenty uwzględniające cykl życia mogą znacznie ułatwić zarządzanie cyklami życia w różnych przypadkach. Oto kilka przykładów:

  • Przełączanie się między ogólnymi a szczegółowymi aktualizacjami lokalizacji. Używaj komponentów uwzględniających cykl życia, aby umożliwić szczegółowe aktualizacje lokalizacji, gdy aplikacja jest widoczna, i przełączać się na aktualizacje ogólne, gdy aplikacja działa w tle. LiveData, komponent uwzględniający cykl życia, pozwala aplikacji automatycznie aktualizować interfejs użytkownika, gdy ten zmienia lokalizację.
  • Zatrzymywanie i rozpoczynanie buforowania filmu. Używaj komponentów uwzględniających cykl życia, aby jak najszybciej rozpocząć buforowanie wideo, ale opóźnić odtwarzanie do czasu pełnego uruchomienia aplikacji. Możesz też użyć komponentów uwzględniających cykl życia, aby zakończyć buforowanie, gdy aplikacja zostanie zniszczona.
  • inicjowanie i zatrzymywanie połączenia sieciowego; Używaj komponentów uwzględniających cykl życia, aby umożliwić aktualizowanie na żywo (przesyłanie strumieniowe) danych sieciowych, gdy aplikacja jest na pierwszym planie, a także automatyczne wstrzymywanie, gdy aplikacja przechodzi do działania w tle.
  • Wstrzymywanie i wznawianie animowanych obiektów do rysowania. Używaj komponentów uwzględniających cykl życia, aby obsługiwać wstrzymywanie animowanych obiektów rysowanych, gdy aplikacja jest w tle, oraz wznawianie ich rysowania, gdy aplikacja jest na pierwszym planie.

Obsługa zdarzeń zatrzymania

Gdy Lifecycle należy do AppCompatActivity lub Fragment, stan Lifecycle zmienia się na CREATED, a zdarzenie ON_STOP jest wysyłane, gdy wywoływane jest AppCompatActivity lub Fragment.onSaveInstanceState()

Gdy stan Fragment lub AppCompatActivity jest zapisywany za pomocą funkcji onSaveInstanceState(), jego interfejs jest uważany za niezmienny, dopóki nie zostanie wywołana funkcja ON_START. Próba zmodyfikowania interfejsu po zapisaniu stanu prawdopodobnie spowoduje niespójności w stanie nawigacji aplikacji. Dlatego funkcja FragmentManager wyrzuca wyjątek, jeśli aplikacja uruchomi funkcję FragmentTransaction po zapisaniu stanu. Więcej informacji znajdziesz na stronie commit().

LiveData zapobiega temu brzegowemu przypadkowi, nie wywołując obserwatora, jeśli powiązany z nim Lifecycle nie jest co najmniej STARTED. W tle wywołuje ona funkcję isAtLeast(), zanim zdecyduje się wywołać swojego obserwatora.

Niestety metoda onStop() klasy AppCompatActivity jest wywoływana po onSaveInstanceState(), co powoduje, że zmiany stanu interfejsu nie są dozwolone, ale obiekt Lifecycle nie został jeszcze przeniesiony do stanu CREATED.

Aby uniknąć tego problemu, klasa Lifecycle w wersji beta2 i niżej oznacza stan jako CREATED bez wysyłania zdarzenia, dzięki czemu każdy kod sprawdzający bieżący stan otrzymuje rzeczywistą wartość, mimo że zdarzenie nie jest wysyłane, dopóki system nie wywoła funkcji onStop().

Niestety, to rozwiązanie ma 2 główne problemy:

  • Na poziomie interfejsu API 23 lub niższym system Android zapisuje stan aktywności, nawet jeśli jest on częściowo objęty inną aktywnością. Innymi słowy, system Androida wywołuje funkcję onSaveInstanceState(), ale niekoniecznie onStop(). Powoduje to potencjalnie długi przedział czasu, w którym obserwator nadal uważa, że cykl życia jest aktywny, mimo że stanu interfejsu nie można zmodyfikować.
  • Każda klasa, która ma działać podobnie do klasy LiveData, musi zaimplementować obejście dostępne w wersji Lifecycle beta 2 lub niższej.

Dodatkowe materiały

Aby dowiedzieć się więcej o obsługiwaniu cykli życia za pomocą komponentów uwzględniających cykl życia, zapoznaj się z tymi dodatkowymi materiałami.

Próbki

  • Sunflower, aplikacja demonstracyjna prezentująca sprawdzone metody korzystania z Architecture Components

Ćwiczenia z programowania

Blogi