Pojęcia i implementacja w Jetpack Compose
Komponenty uwzględniające cykl życia wykonują działania w odpowiedzi na zmianę stanu cyklu życia innego komponentu, np. aktywności i fragmentów. Te komponenty pomagają tworzyć lepiej zorganizowany, a często lżejszy kod, który jest łatwiejszy w utrzymaniu.
Często stosowanym wzorcem jest implementowanie działań komponentów zależnych w metodach cyklu życia aktywności i fragmentów. Ten wzorzec prowadzi jednak do słabej organizacji kodu i rozpowszechniania 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 zawiera klasy i interfejsy, które umożliwiają tworzenie komponentów uwzględniających cykl życia, czyli komponentów, które mogą 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 kod platformy działający w Twoim procesie. Są one kluczowe dla działania Androida i Twoja aplikacja musi ich przestrzegać. Jeśli tego nie zrobisz, może to spowodować wycieki pamięci lub nawet awarie aplikacji.
Wyobraź sobie, że mamy aktywność, która wyświetla na ekranie lokalizację urządzenia. 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 prawdziwej aplikacji masz zbyt wiele wywołań, które zarządzają interfejsem i innymi komponentami w odpowiedzi na bieżący stan cyklu życia. Zarządzanie wieloma komponentami powoduje, że w metodach cyklu życia, takich jak onStart() i onStop, znajduje się znaczna ilość kodu, co utrudnia ich utrzymanie.
Nie ma też gwarancji, że komponent zostanie uruchomiony przed zatrzymaniem aktywności lub fragmentu. Dotyczy to zwłaszcza sytuacji, gdy musimy przeprowadzić długo trwającą operację, np. sprawdzanie konfiguracji w onStart. Może to spowodować sytuację wyścigu, w której metoda onStop() zakończy się przed metodą onStart, co spowoduje, że komponent będzie aktywny 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 i izolowany sposób.
Cykl życia
Lifecycle to klasa, która zawiera informacje o stanie cyklu życia komponentu (np. aktywności lub fragmentu) i umożliwia innym obiektom obserwowanie tego stanu.
Lifecycle używa 2 głównych wyliczeń do śledzenia stanu cyklu życia powiązanego komponentu:
Wydarzenie
Zdarzenia cyklu życia wysyłane z ram i klasy Lifecycle. Te zdarzenia są mapowane na zdarzenia wywołania zwrotnego w aktywnościach i fragmentach.
Województwo
Bieżący stan komponentu śledzonego przez obiekt Lifecycle.
Stany można traktować 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 pokazano 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 powyższym przykładzie obiekt myLifecycleOwner implementuje interfejs LifecycleOwner, który jest opisany w następnej sekcji.
LifecycleOwner
LifecycleOwner to interfejs z jedną metodą, który oznacza, że klasa ma Lifecycle. Ma jedną metodę, getLifecycle, która musi być zaimplementowana przez klasę. Jeśli chcesz zarządzać całym procesem składania wniosku, zapoznaj się z tym artykułem: ProcessLifecycleOwner.
Ten interfejs oddziela własność Lifecycle od poszczególnych klas, takich jak Fragment i AppCompatActivity, i umożliwia pisanie komponentów, które z nimi współpracują. Każda niestandardowa klasa aplikacji może implementować interfejs LifecycleOwner.
Komponenty, które implementują interfejs DefaultLifecycleObserver, działają bezproblemowo z komponentami, które implementują interfejs LifecycleOwner, ponieważ właściciel może udostępnić cykl życia, który obserwator może zarejestrować, aby go śledzić.
W przykładzie śledzenia lokalizacji możemy sprawić, że klasa MyLocationListener będzie implementować interfejs DefaultLifecycleObserver, a następnie zainicjować ją za pomocą metody Lifecycle aktywności 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. Przechowywanie logiki w poszczególnych komponentach ułatwia zarządzanie logiką aktywności 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();
}
});
}
}
Częstym przypadkiem użycia jest unikanie wywoływania niektórych wywołań zwrotnych, jeśli Lifecycle
nie jest obecnie w dobrym stanie. Jeśli na przykład wywołanie zwrotne uruchamia transakcję fragmentu po zapisaniu stanu działania, spowoduje to awarię, więc nigdy nie chcemy wywoływać tego wywołania zwrotnego.
Aby ułatwić ten przypadek użycia, klasa Lifecycle umożliwia innym obiektom sprawdzanie 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 temu nasza klasa LocationListener jest w pełni świadoma cyklu życia. Jeśli musimy użyć LocationListener z innego działania lub fragmentu, wystarczy go zainicjować. Wszystkie operacje konfigurowania i zamykania są zarządzane przez samą klasę.
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 zintegrować te komponenty bez ręcznego zarządzania cyklem życia po stronie klienta.
Implementowanie niestandardowego obiektu LifecycleOwner
Fragmenty i aktywności w Bibliotece pomocy w wersji 26.1.0 i nowszych implementują już interfejs LifecycleOwner.
Jeśli masz klasę niestandardową, którą chcesz przekształcić w LifecycleOwner, możesz użyć klasy LifecycleRegistry, ale musisz przekazywać 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
- Staraj się, aby kontrolery interfejsu (aktywności i fragmenty) były jak najprostsze.
Nie powinny one próbować pozyskiwać własnych danych. Zamiast tego powinny używać do tego
ViewModeli obserwować obiektLiveData, aby odzwierciedlać zmiany w widokach. - Staraj się tworzyć interfejsy oparte na danych, w których kontroler interfejsu odpowiada za aktualizowanie widoków w miarę zmian danych lub powiadamianie
ViewModelo działaniach użytkownika. - Umieść logikę danych w klasie
ViewModel.ViewModelpowinien pełnić rolę łącznika między kontrolerem interfejsu a pozostałą częścią aplikacji. Pamiętaj jednak, że pobieranie danych (np. z sieci) nie jest obowiązkiemViewModel. Zamiast tegoViewModelpowinien wywołać odpowiedni komponent, aby pobrać dane, a następnie przekazać wynik z powrotem do kontrolera interfejsu. - Używaj powiązań danych, aby zachować przejrzysty interfejs między widokami a kontrolerem interfejsu. Dzięki temu możesz tworzyć bardziej deklaratywne widoki i minimalizować kod aktualizacji, który musisz pisać w aktywnościach i fragmentach. Jeśli wolisz to zrobić w języku programowania Java, użyj biblioteki takiej jak Butter Knife, aby uniknąć powtarzalnego kodu i uzyskać lepszą abstrakcję.
- Jeśli interfejs jest złożony, rozważ utworzenie klasy prezentera, która będzie obsługiwać modyfikacje interfejsu. Może to być pracochłonne, ale ułatwi testowanie komponentów interfejsu.
- Unikaj odwoływania się do kontekstu
ViewlubActivitywViewModel. JeśliViewModelprzetrwa aktywność (w przypadku zmian konfiguracji), aktywność wycieknie i nie zostanie prawidłowo usunięta przez moduł odśmiecania. - Używaj współprogramów Kotlin do zarządzania długotrwałymi zadaniami i innymi operacjami, które mogą być wykonywane 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 różnych przypadkach. Oto kilka przykładów:
- Przełączanie się między przybliżonymi a dokładnymi aktualizacjami lokalizacji. Używaj komponentów uwzględniających cykl życia, aby włączać precyzyjne aktualizacje lokalizacji, gdy aplikacja do lokalizacji jest widoczna, i przełączać się na przybliżone aktualizacje, gdy aplikacja działa w tle.
LiveData, czyli komponent uwzględniający cykl życia, umożliwia automatyczne aktualizowanie interfejsu aplikacji, gdy użytkownik zmienia lokalizację. - wstrzymywanie i wznawianie buforowania filmu; Używaj komponentów uwzględniających cykl życia, aby jak najszybciej rozpocząć buforowanie filmu, ale odłożyć odtwarzanie do momentu pełnego uruchomienia aplikacji. Możesz też używać komponentów uwzględniających cykl życia, aby zakończyć buforowanie, gdy aplikacja zostanie zamknięta.
- włączanie i wyłączanie połączenia sieciowego; Używaj komponentów uwzględniających cykl życia, aby włączać aktualizowanie na żywo (streaming) danych sieciowych, gdy aplikacja jest na pierwszym planie, a także automatycznie wstrzymywać je, gdy aplikacja przechodzi w tło.
- Wstrzymywanie i wznawianie animowanych elementów rysowalnych. Używaj komponentów uwzględniających cykl życia, aby wstrzymywać animowane obiekty rysowalne, gdy aplikacja działa w tle, i wznawiać je, gdy aplikacja działa na pierwszym planie.
Obsługa zdarzeń zatrzymania
Gdy element Lifecycle należy do elementu AppCompatActivity lub Fragment, stan elementu Lifecycle zmienia się na CREATED, a zdarzenie ON_STOP jest wysyłane, gdy wywoływana jest funkcja AppCompatActivity lub Fragment onSaveInstanceState().
Gdy stan Fragment lub AppCompatActivity jest zapisywany za pomocą onSaveInstanceState, interfejs użytkownika jest uznawany za niezmienny do momentu wywołania ON_START. Próba zmodyfikowania interfejsu po zapisaniu stanu może spowodować niespójności w stanie nawigacji aplikacji, dlatego FragmentManager zgłasza wyjątek, jeśli aplikacja uruchomi FragmentTransaction po zapisaniu stanu. Więcej informacji znajdziesz w commit().
LiveData zapobiega temu przypadkowi brzegowemu, nie wywołując obserwatora, jeśli powiązany z nim Lifecycle nie jest co najmniej STARTED. Za kulisami wywołuje funkcję isAtLeast(), zanim zdecyduje się wywołać obserwatora.
Niestety metoda AppCompatActivity onStop() jest wywoływana po onSaveInstanceState, co powoduje lukę, w której zmiany stanu interfejsu są niedozwolone, ale Lifecycle nie został jeszcze przeniesiony do stanu CREATED.
Aby zapobiec temu problemowi, klasa Lifecycle w wersji beta2 i starszych
oznacza stan jako CREATED bez wysyłania zdarzenia, dzięki czemu każdy kod,
który sprawdza bieżący stan, otrzymuje rzeczywistą wartość, mimo że zdarzenie nie jest
wysyłane, dopóki system nie wywoła funkcji onStop().
To rozwiązanie ma jednak 2 poważne wady:
- W przypadku interfejsu API na poziomie 23 i niższym system Android zapisuje stan aktywności, nawet jeśli jest ona częściowo zasłonięta przez inną aktywność. Innymi słowy, system Android wywołuje funkcję
onSaveInstanceState(), ale niekoniecznie wywołuje funkcjęonStop. Może to spowodować długi przedział czasu, w którym obserwator nadal uważa, że cykl życia jest aktywny, mimo że nie można modyfikować stanu interfejsu. - Każda klasa, która chce udostępniać podobne działanie do klasy
LiveData, musi wdrożyć obejście udostępnione w wersjiLifecyclebeta 2i starszych.
Dodatkowe materiały
Więcej informacji o obsłudze cykli życia za pomocą komponentów uwzględniających cykl życia znajdziesz w tych materiałach.
Próbki
- Sunflower, aplikacja w wersji demonstracyjnej pokazująca sprawdzone metody korzystania z komponentów architektury.