Obsługa cyklu życia za pomocą komponentów uwzględniających cykl życia Część stanowiąca część Androida Jetpack.

Komponenty uwzględniające cykl życia wykonują działania w odpowiedzi na zmianę stanu cyklu życia innego komponentu, np. aktywności lub fragmentów. Te komponenty pomagają w tworzeniu lepiej uporządkowanego, a często także lżejszego kodu, który jest łatwiejszy w utrzymaniu.

Częstym wzorcem jest implementacja działań zależnych komponentów w metodach cyklu życia działań i fragmentów. Taki schemat prowadzi jednak do kiepskiej organizacji kodu i liczby błędów. Korzystając z komponentów uwzględniających cykl życia, możesz przenieść kod komponentów zależnych z metod cyklu życia do samych komponentów.

Pakiet androidx.lifecycle zawiera klasy i interfejsy umożliwiające tworzenie komponentów zorientowanych na cykl życia, czyli komponentów mogących automatycznie dostosowywać swoje działanie na podstawie bieżącego stanu cyklu życia aktywności lub fragmentu.

Większość komponentów aplikacji zdefiniowanych w Android Framework ma przypisane cykle życia. Cyklami życia zarządza system operacyjny lub uruchamiany w nim kod platformy. Są kluczem do działania Androida a Twoje aplikacje muszą je respektować. W przeciwnym razie mogą wystąpić wycieki pamięci, a nawet awarie aplikacji.

Wyobraź sobie, że mamy aktywność, która pokazuje 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
    }
}

Mimo że ten przykład wygląda dobrze, w rzeczywistej aplikacji pojawia się w takiej sytuacji zbyt wiele wywołań zarządzających interfejsem i innymi komponentami w odpowiedzi na bieżący stan cyklu życia aplikacji. Zarządzanie wieloma komponentami powoduje umieszczenie znacznej części kodu w metodach cyklu życia, np. onStart() i onStop(), co utrudnia ich obsługę.

Nie ma też gwarancji, że komponent uruchomi się przed zatrzymaniem działania lub fragmentu. Zwłaszcza wtedy, gdy musimy wykonać długotrwałą operację, taką jak sprawdzenie konfiguracji w onStart(). Może to spowodować warunek wyścigu, w którym metoda onStop() kończy się przed zdarzeniem onStart(), przez co komponent utrzymuje się dłużej, niż jest to potrzebne.

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 zawiera klasy i interfejsy, które pomagają rozwiązywać te problemy w odporny, odizolowany sposób.

Cykl życia

Lifecycle to klasa przechowująca informacje o stanie cyklu życia komponentu (np. aktywność lub fragment) i pozwala innym obiektom obserwować ten stan.

Lifecycle używa 2 głównych wyliczeń do śledzenia stanu cyklu życia powiązanego komponentu:

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

Stany są węzłami wykresu, a zdarzenia – jak krawędziami między tymi węzłami.

Klasa może monitorować stan cyklu życia komponentu przez wdrożenie metody DefaultLifecycleObserver i zastępowanie odpowiednich metod, takich jak onCreate, onStart itp. Następnie możesz dodać obserwatora, wywołując metodę addObserver() klasy Lifecycle i przekazując instancję obserwatora, tak 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 został opisany w następnej sekcji.

Właściciel cyklu życia

LifecycleOwner to interfejs pojedynczej metody, który wskazuje, że klasa ma Lifecycle. Ma jedną metodę getLifecycle(), którą musi wdrożyć klasa. Jeśli zamiast tego chcesz zarządzać cyklem życia całego procesu aplikacji, przeczytaj artykuł ProcessLifecycleOwner.

Interfejs ten wyodrębnia własność elementu Lifecycle z poszczególnych klas, takich jak Fragment czy AppCompatActivity, i umożliwia pisanie komponentów, które z nimi współpracują. Każda niestandardowa klasa aplikacji może implementować interfejs LifecycleOwner.

Komponenty implementujące DefaultLifecycleObserver bezproblemowo współpracują z komponentami implementującymi LifecycleOwner, ponieważ właściciel może zapewnić cykl życia, który może zarejestrować obserwator.

Na potrzeby przykładu śledzenia lokalizacji możemy wprowadzić klasę MyLocationListener wdrożoną DefaultLifecycleObserver, a następnie zainicjować ją za pomocą metody Lifecycle aktywności w metodzie onCreate(). Dzięki temu klasa MyLocationListener jest samowystarczająca, co oznacza, że logika reagowania na zmiany stanu cyklu życia jest deklarowana w MyLocationListener, a nie w aktywności. Przechowywanie własnych mechanizmów logicznych przez poszczególne komponenty ułatwia zarządzanie działaniami i logiką 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 przypadkiem użycia jest unikanie wywoływania niektórych wywołań zwrotnych, jeśli element Lifecycle nie jest obecnie w dobrym stanie. Jeśli na przykład wywołanie zwrotne uruchomi transakcję dotyczącą fragmentu po zapisaniu stanu aktywności, spowoduje to awarię i nie będzie trzeba wywoływać tego wywołania zwrotnego.

Aby ułatwić ten przypadek użycia, klasa Lifecycle umożliwia innym obiektom wysyłanie zapytań dotyczących bieżącego stanu.

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 takiej implementacji nasza klasa LocationListener jest w pełni świadoma cyklu życia. Jeśli musimy użyć obiektu LocationListener z innej aktywności lub fragmentu, wystarczy go zainicjować. Wszystkimi operacjami konfiguracji i demontażu zarządza sama klasa.

Jeśli biblioteka udostępnia zajęcia, które muszą być zgodne z cyklem życia Androida, zalecamy korzystanie z komponentów uwzględniających cykl życia. Klienty biblioteki mogą łatwo zintegrować te komponenty bez ręcznego zarządzania cyklem życia po stronie klienta.

Wdrażanie niestandardowego właściciela cyklu życia

Fragmenty i działania w bibliotece pomocy w wersji 26.1.0 i nowszych implementują już interfejs LifecycleOwner.

Jeśli masz klasę niestandardową, którą chcesz utworzyć LifecycleOwner, możesz użyć klasy LifecycleRegistry, ale musisz przekazywać do niej zdarzenia, tak jak w tym przykładowym kodzie:

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

  • Zadbaj o jak najmniejszą ilość kontrolerów interfejsu (aktywności i fragmentów). Nie należy próbować pozyskać własnych danych. W tym celu użyj narzędzia ViewModel i obserwuj obiekt LiveData, aby odzwierciedlić zmiany w widokach danych.
  • Spróbuj pisać interfejsy użytkownika oparte na danych, w których kontroler UI odpowiada za aktualizowanie widoków po zmianie danych lub powiadamiaj o działaniach użytkowników z powrotem do ViewModel.
  • Przenieś logikę danych do klasy ViewModel. Usługa ViewModel powinna służyć jako łącznik między kontrolerem UI a resztą aplikacji. Uważaj jednak, że za pobieranie danych (np. z sieci) nie odpowiada usługa ViewModel. Zamiast tego ViewModel powinien wywołać odpowiedni komponent, aby pobrać dane, a następnie przekazać wynik z powrotem do kontrolera interfejsu.
  • Używaj powiązywania danych, aby utrzymać czysty interfejs między widokami i kontrolerem interfejsu. Pozwala to zwiększyć deklaratywne widoki widoków i zminimalizować ilość kodu aktualizacji, który trzeba pisać w działaniach i fragmentach. Jeśli wolisz robić to w języku programowania Java, użyj biblioteki takiej jak butter Knife, by uniknąć powtarzającego się kodu i uzyskać lepszą abstrakcję.
  • Jeśli Twój interfejs użytkownika jest złożony, rozważ utworzenie klasy Prezentera do obsługi modyfikacji interfejsu. Może to być pracochłonne, ale może ułatwić testowanie komponentów UI.
  • Unikaj odwoływania się do kontekstu View lub Activity w ViewModel. Jeśli element ViewModel przetrwa działanie (w przypadku zmiany konfiguracji), aktywność wycieknie i nie zostanie poprawnie usunięta przez kolektor śmieci.
  • Używaj korekty Kotlin do zarządzania długotrwałymi zadaniami i innymi operacjami, które mogą być uruchamiane asynchronicznie.

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 wielu przypadkach. Oto kilka przykładów:

  • Przełączanie się między szczegółowym i precyzyjnym aktualizowaniem lokalizacji. Korzystaj z komponentów uwzględniających cykl życia, aby umożliwić szczegółową aktualizację lokalizacji, gdy aplikacja z lokalizacją jest widoczna, a także przełączyć się na aktualizacje o dużej ziarnistości, gdy aplikacja działa w tle. LiveData – komponent identyfikujący cykl życia, umożliwia aplikacji automatyczne aktualizowanie interfejsu, gdy użytkownik zmieni lokalizację.
  • Zatrzymuję i uruchamiam buforowanie filmu. Użyj komponentów uwzględniających cykl życia, aby jak najszybciej rozpocząć buforowanie filmu, ale opóźnić odtwarzanie do momentu pełnego uruchomienia aplikacji. Możesz też użyć komponentów uwzględniających cykl życia, aby wyłączyć buforowanie po zniszczeniu aplikacji.
  • Uruchamiam i zatrzymuję połączenia sieciowe. Użyj komponentów uwzględniających cykl życia, aby umożliwić aktualizowanie na żywo (strumieniowe) danych sieciowych, gdy aplikacja działa na pierwszym planie, oraz automatyczne wstrzymywanie jej działania w tle.
  • Wstrzymywanie i wznawianie animowanych elementów rysowanych. Za pomocą komponentów uwzględniających cykl życia możesz wstrzymywać animowane elementy rysowane, gdy aplikacja działa w tle, i wznawiać te elementy, gdy aplikacja działa na pierwszym planie.

Obsługa zdarzeń zatrzymania

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

Gdy stan elementu Fragment lub AppCompatActivity jest zapisywany za pomocą onSaveInstanceState(), interfejs jest uznawany za niezmienny, dopóki nie zostanie wywołany element ON_START. Próba zmodyfikowania interfejsu po zapisaniu stanu prawdopodobnie spowoduje niespójności w stanie nawigacji w Twojej aplikacji, dlatego FragmentManager zgłasza wyjątek, jeśli po zapisaniu stanu aplikacja uruchamia wyjątek FragmentTransaction. Aby dowiedzieć się więcej, wejdź na commit().

LiveData zapobiega wyjściu z urządzenia, powstrzymując się od wywoływania jego obserwatora, jeśli powiązane z nim Lifecycle nie wynoszą przynajmniej STARTED. W tle wywołuje metodę isAtLeast(), zanim zdecyduje się wywołać swojego obserwatora.

Niestety metoda onStop() w AppCompatActivity jest wywoływana po onSaveInstanceState(), co powoduje lukę, w wyniku której zmiany stanu interfejsu są niedozwolone, ale element Lifecycle nie został jeszcze przeniesiony do stanu CREATED.

Aby zapobiec temu problemowi, klasa Lifecycle w wersji beta2 i niższym oznacza stan jako CREATED bez wysyłania zdarzenia. Dzięki temu każdy kod, który sprawdza bieżący stan, otrzymuje wartość rzeczywistą, mimo że zdarzenie nie jest wysyłane, dopóki system nie zostanie wywołany przez system onStop().

Niestety w tym rozwiązaniu występują 2 główne problemy:

  • W przypadku interfejsu API na poziomie 23 i niższym system Android faktycznie zapisuje stan działania, nawet jeśli częściowo jest on objęty inną aktywnością. Inaczej mówiąc, system Android wywołuje metodę onSaveInstanceState(), ale niekoniecznie onStop(). Powoduje to potencjalnie długi przedział czasu, w którym obserwator w dalszym ciągu uważa, że cykl życia jest aktywny, mimo że nie można zmienić jego stanu interfejsu.
  • Każda klasa, która chce zaprezentować podobne działanie klasie LiveData, musi stosować obejście dostępne w Lifecycle w wersji beta 2 i starszej.

Dodatkowe materiały

Aby dowiedzieć się więcej o obsłudze cyklu życia z użyciem komponentów uwzględniających cykl życia, zapoznaj się z dodatkowymi materiałami.

Próbki

Ćwiczenia z programowania

Blogi