Efekt uboczny to zmiana stanu aplikacji, która następuje poza zakresem funkcji kompozycyjnej. Ze względu na cykl życia funkcji kompozycyjnych i ich właściwości, takie jak nieprzewidywalne ponowne kompozycje, wykonywanie ponownych kompozycji funkcji kompozycyjnych w różnej kolejności lub ponowne kompozycje, które można odrzucić, funkcje kompozycyjne powinny być w idealnym przypadku wolne od efektów ubocznych.
Czasami jednak efekty uboczne są konieczne, np. do wywołania jednorazowego zdarzenia, takiego jak wyświetlenie paska powiadomień lub przejście do innego ekranu w określonych warunkach. Te działania powinny być wywoływane z kontrolowanego środowiska, które zna cykl życia komponentu. Na tej stronie dowiesz się więcej o różnych interfejsach API efektów ubocznych, które oferuje Jetpack Compose.
Przypadki użycia dotyczące stanu i efektu
Zgodnie z dokumentacją Thinking in Compose funkcje kompozycyjne nie powinny wywoływać efektów ubocznych. Gdy musisz wprowadzić zmiany w stanie aplikacji (zgodnie z opisem w dokumencie Zarządzanie stanem), używaj interfejsów API efektów, aby efekty uboczne były wykonywane w przewidywalny sposób.
Ze względu na różne możliwości, jakie efekty otwierają w Compose, można ich łatwo nadużywać. Upewnij się, że Twoja praca dotyczy interfejsu i nie narusza jednokierunkowego przepływu danych, jak wyjaśniono w dokumentacji dotyczącej zarządzania stanem.
LaunchedEffect
: uruchamianie funkcji zawieszających w zakresie funkcji typu „composable”
Aby wykonać pracę w trakcie działania funkcji typu „composable” i mieć możliwość wywoływania funkcji zawieszania, użyj funkcji typu „composable” LaunchedEffect
. Gdy LaunchedEffect
wchodzi w skład kompozycji, uruchamia korutynę z blokiem kodu przekazanym jako parametr. Jeśli LaunchedEffect
opuści kompozycję, korutyna zostanie anulowana. Jeśli funkcja LaunchedEffect
zostanie ponownie skomponowana z użyciem innych kluczy (patrz sekcja Ponowne uruchamianie efektów poniżej), istniejąca korutyna zostanie anulowana, a nowa funkcja zawieszająca zostanie uruchomiona w nowej korutynie.
Oto na przykład animacja, która pulsuje wartością alfa z konfigurowalnym opóźnieniem:
// Allow the pulse rate to be configured, so it can be sped up if the user is running // out of time var pulseRateMs by remember { mutableStateOf(3000L) } val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { // Restart the effect when the pulse rate changes while (isActive) { delay(pulseRateMs) // Pulse the alpha every pulseRateMs to alert the user alpha.animateTo(0f) alpha.animateTo(1f) } }
W powyższym kodzie animacja używa funkcji zawieszającej delay
do odczekania określonego czasu. Następnie sekwencyjnie animuje wartość alfa do zera i z powrotem, używając animateTo
.
Będzie się to powtarzać przez cały czas życia komponentu.
rememberCoroutineScope
: uzyskiwanie zakresu uwzględniającego kompozycję w celu uruchamiania korutyny poza funkcją kompozycyjną;
Funkcja LaunchedEffect
jest funkcją kompozycyjną, więc można jej używać tylko w innych funkcjach kompozycyjnych. Aby uruchomić korutynę poza funkcją kompozycyjną, ale w zakresie, który spowoduje jej automatyczne anulowanie po opuszczeniu kompozycji, użyj funkcji rememberCoroutineScope
.
Używaj też rememberCoroutineScope
, gdy musisz ręcznie kontrolować cykl życia co najmniej 1 korutyny, np. anulować animację, gdy wystąpi zdarzenie użytkownika.
rememberCoroutineScope
to funkcja kompozycyjna, która zwraca CoroutineScope
powiązany z punktem kompozycji, w którym jest wywoływana. Zakres zostanie anulowany, gdy połączenie opuści kompozycję.
W nawiązaniu do poprzedniego przykładu możesz użyć tego kodu, aby wyświetlić Snackbar
, gdy użytkownik kliknie Button
:
@Composable fun MoviesScreen(snackbarHostState: SnackbarHostState) { // Creates a CoroutineScope bound to the MoviesScreen's lifecycle val scope = rememberCoroutineScope() Scaffold( snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) { contentPadding -> Column(Modifier.padding(contentPadding)) { Button( onClick = { // Create a new coroutine in the event handler to show a snackbar scope.launch { snackbarHostState.showSnackbar("Something happened!") } } ) { Text("Press me") } } } }
rememberUpdatedState
: odwoływanie się do wartości w efekcie, który nie powinien się ponownie uruchamiać, jeśli wartość się zmieni
LaunchedEffect
rozpoczyna się od nowa, gdy zmieni się jeden z kluczowych parametrów. W niektórych sytuacjach możesz jednak chcieć przechwycić w efekcie wartość, która po zmianie nie powinna powodować ponownego uruchomienia efektu. Aby to zrobić, musisz użyć rememberUpdatedState
, aby utworzyć odwołanie do tej wartości, które można przechwycić i zaktualizować. To podejście jest przydatne w przypadku efektów, które zawierają długotrwałe operacje, których ponowne utworzenie i uruchomienie może być kosztowne lub niemożliwe.
Załóżmy na przykład, że w aplikacji jest LandingScreen
, który po pewnym czasie znika. Nawet jeśli LandingScreen
zostanie ponownie skomponowany, efekt, który czeka przez pewien czas i powiadamia o jego upływie, nie powinien zostać ponownie uruchomiony:
@Composable fun LandingScreen(onTimeout: () -> Unit) { // This will always refer to the latest onTimeout function that // LandingScreen was recomposed with val currentOnTimeout by rememberUpdatedState(onTimeout) // Create an effect that matches the lifecycle of LandingScreen. // If LandingScreen recomposes, the delay shouldn't start again. LaunchedEffect(true) { delay(SplashWaitTimeMillis) currentOnTimeout() } /* Landing screen content */ }
Aby uzyskać efekt zgodny z cyklem życia witryny wywołującej, jako parametr przekazywana jest stała, która nigdy się nie zmienia, np. Unit
lub true
. W powyższym kodzie użyto LaunchedEffect(true)
. Aby mieć pewność, że funkcja onTimeout
lambda zawsze zawiera najnowszą wartość, która została ponownie skomponowana za pomocą funkcji LandingScreen
, należy ją umieścić w funkcji rememberUpdatedState
.onTimeout
Zwrócone State
, currentOnTimeout
w kodzie powinny być użyte w efekcie.
DisposableEffect
: efekty wymagające oczyszczenia
W przypadku efektów ubocznych, które wymagają wyczyszczenia po zmianie kluczy lub gdy funkcja kompozycyjna opuści kompozycję, użyj DisposableEffect
.
Jeśli klucze DisposableEffect
się zmienią, funkcja kompozycyjna musi usunąć (wykonać czyszczenie) bieżący efekt i zresetować się, ponownie wywołując efekt.
Możesz na przykład wysyłać zdarzenia Analytics na podstawie zdarzeń Lifecycle
za pomocą LifecycleObserver
.
Aby nasłuchiwać tych zdarzeń w Compose, użyj DisposableEffect
, aby w razie potrzeby zarejestrować i wyrejestrować obserwatora.
@Composable fun HomeScreen( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, onStart: () -> Unit, // Send the 'started' analytics event onStop: () -> Unit // Send the 'stopped' analytics event ) { // Safely update the current lambdas when a new one is provided val currentOnStart by rememberUpdatedState(onStart) val currentOnStop by rememberUpdatedState(onStop) // If `lifecycleOwner` changes, dispose and reset the effect DisposableEffect(lifecycleOwner) { // Create an observer that triggers our remembered callbacks // for sending analytics events val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { currentOnStart() } else if (event == Lifecycle.Event.ON_STOP) { currentOnStop() } } // Add the observer to the lifecycle lifecycleOwner.lifecycle.addObserver(observer) // When the effect leaves the Composition, remove the observer onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } /* Home screen content */ }
W powyższym kodzie efekt doda znak observer
do elementu lifecycleOwner
. Jeśli lifecycleOwner
się zmieni, efekt zostanie usunięty i uruchomiony ponownie z nową wartością lifecycleOwner
.
Element DisposableEffect
musi zawierać klauzulę onDispose
jako ostatnie stwierdzenie w bloku kodu. W przeciwnym razie środowisko IDE wyświetli błąd czasu kompilacji.
SideEffect
: publikowanie stanu Compose w kodzie innym niż Compose
Aby udostępnić stan funkcji Compose obiektom, którymi nie zarządza Compose, użyj funkcji kompozycyjnej
SideEffect
. Użycie SideEffect
gwarantuje, że efekt zostanie wykonany po każdej udanej rekompozycji. Z drugiej strony nie należy wykonywać efektu przed zagwarantowaniem pomyślnej ponownej kompozycji, co ma miejsce w przypadku zapisywania efektu bezpośrednio w funkcji kompozycyjnej.
Na przykład biblioteka analityczna może umożliwiać segmentowanie użytkowników przez dołączanie do wszystkich kolejnych zdarzeń analitycznych niestandardowych metadanych (w tym przykładzie „właściwości użytkownika”). Aby przekazać do biblioteki analitycznej typ użytkownika, który obecnie korzysta z aplikacji, użyj funkcji SideEffect
, aby zaktualizować jego wartość.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
produceState
: przekształcanie stanu innego niż Compose w stan Compose
produceState
uruchamia korutynę w zakresie kompozycji, która może przekazywać wartości do zwracanego obiektu State
. Używaj go do przekształcania stanu innego niż Compose w stan Compose, np. do przenoszenia do kompozycji zewnętrznego stanu opartego na subskrypcji, takiego jak Flow
, LiveData
lub RxJava
.
Producent jest uruchamiany, gdy produceState
wchodzi w skład kompozycji, i anulowany, gdy ją opuszcza. Zwrócona wartość State
jest łączona. Ustawienie tej samej wartości nie spowoduje ponownego komponowania.
Mimo że funkcja produceState
tworzy korutynę, można jej też używać do obserwowania źródeł danych, które nie wstrzymują działania. Aby usunąć subskrypcję tego źródła, użyj funkcji awaitDispose
.
Poniższy przykład pokazuje, jak za pomocą produceState
wczytać obraz z sieci. Funkcja kompozycyjna loadNetworkImage
zwraca element State
, którego można używać w innych funkcjach kompozycyjnych.
@Composable fun loadNetworkImage( url: String, imageRepository: ImageRepository = ImageRepository() ): State<Result<Image>> { // Creates a State<T> with Result.Loading as initial value // If either `url` or `imageRepository` changes, the running producer // will cancel and will be re-launched with the new inputs. return produceState<Result<Image>>(initialValue = Result.Loading, url, imageRepository) { // In a coroutine, can make suspend calls val image = imageRepository.load(url) // Update State with either an Error or Success result. // This will trigger a recomposition where this State is read value = if (image == null) { Result.Error } else { Result.Success(image) } } }
derivedStateOf
: przekształcanie jednego lub wielu obiektów stanu w inny stan;
W Compose ponowne komponowanie następuje za każdym razem, gdy zmieni się obserwowany obiekt stanu lub dane wejściowe funkcji kompozycyjnej. Obiekt stanu lub dane wejściowe mogą się zmieniać częściej, niż jest to konieczne do aktualizacji interfejsu, co prowadzi do niepotrzebnego ponownego komponowania.
Funkcji derivedStateOf
należy używać, gdy dane wejściowe funkcji kompozycyjnej zmieniają się częściej niż jest to potrzebne do ponownego skomponowania. Często zdarza się to, gdy coś często się zmienia, np. pozycja przewijania, ale komponent musi reagować na tę zmianę dopiero po przekroczeniu określonego progu. derivedStateOf
tworzy nowy obiekt stanu Compose, który możesz obserwować i który aktualizuje się tylko wtedy, gdy jest to potrzebne. Działa podobnie do operatora Kotlin FlowsdistinctUntilChanged()
.
Prawidłowe użycie
Poniższy fragment kodu pokazuje odpowiedni przypadek użycia funkcji derivedStateOf
:
@Composable // When the messages parameter changes, the MessageList // composable recomposes. derivedStateOf does not // affect this recomposition. fun MessageList(messages: List<Message>) { Box { val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } // Show the button if the first visible item is past // the first item. We use a remembered derived state to // minimize unnecessary compositions val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() } } }
W tym fragmencie kodu firstVisibleItemIndex
zmienia się za każdym razem, gdy zmienia się pierwszy widoczny element. Podczas przewijania wartość zmienia się na 0
, 1
, 2
, 3
, 4
, 5
itd. Kompozycja musi jednak zostać ponownie utworzona tylko wtedy, gdy wartość jest większa niż 0
.
Ta różnica w częstotliwości aktualizacji oznacza, że jest to dobry przypadek użycia dla derivedStateOf
.
Nieprawidłowe użycie
Częstym błędem jest założenie, że podczas łączenia dwóch obiektów stanu Compose należy użyć derivedStateOf
, ponieważ „wyprowadzasz stan”. Jest to jednak tylko dodatkowy kod, który nie jest wymagany, co widać w tym fragmencie:
// DO NOT USE. Incorrect usage of derivedStateOf. var firstName by remember { mutableStateOf("") } var lastName by remember { mutableStateOf("") } val fullNameBad by remember { derivedStateOf { "$firstName $lastName" } } // This is bad!!! val fullNameCorrect = "$firstName $lastName" // This is correct
W tym fragmencie kodu fullName
musi być aktualizowany tak samo często jak firstName
i lastName
. Dlatego nie dochodzi do nadmiernego ponownego komponowania i nie ma potrzeby używania
derivedStateOf
.
snapshotFlow
: przekształcanie stanu funkcji Compose w przepływy
Użyj snapshotFlow
, aby przekształcić obiekty State<T>
w zimny przepływ. snapshotFlow
uruchamia swój blok po zebraniu i emituje wynik odczytanych w nim obiektów State
. Gdy jeden z State
obiektówsnapshotFlow
odczytanych w bloku ulegnie zmianie, przepływ wyśle nową wartość do kolektora, jeśli nie jest ona równa poprzedniej wysłanej wartości (działanie podobne do Flow.distinctUntilChanged
).
Poniższy przykład pokazuje efekt uboczny, który rejestruje w Analytics, kiedy użytkownik przewija listę poza pierwszy element:
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.map { index -> index > 0 }
.distinctUntilChanged()
.filter { it == true }
.collect {
MyAnalyticsService.sendScrolledPastFirstItemEvent()
}
}
W powyższym kodzie listState.firstVisibleItemIndex
jest przekształcane w Flow, które może korzystać z operatorów Flow.
Uruchamianie efektów
Niektóre efekty w Compose, takie jak LaunchedEffect
, produceState
lub DisposableEffect
, przyjmują zmienną liczbę argumentów (kluczy), które służą do anulowania bieżącego efektu i rozpoczynania nowego z nowymi kluczami.
Typowa forma tych interfejsów API to:
EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }
Ze względu na subtelność tego zachowania mogą wystąpić problemy, jeśli parametry użyte do ponownego uruchomienia efektu nie są odpowiednie:
- Ponowne uruchamianie efektów rzadziej niż powinno może powodować błędy w aplikacji.
- Zbyt częste ponowne uruchamianie efektów może być nieefektywne.
Zasadniczo zmienne modyfikowalne i niemodyfikowalne używane w bloku efektu kodu należy dodać jako parametry do funkcji kompozycyjnej efektu. Oprócz nich można dodać więcej parametrów, aby wymusić ponowne uruchomienie efektu. Jeśli zmiana zmiennej nie powinna powodować ponownego uruchomienia efektu, zmienną należy umieścić w tagu rememberUpdatedState
. Jeśli zmienna nigdy się nie zmienia, ponieważ jest zawarta w funkcji remember
bez kluczy, nie musisz przekazywać jej jako klucza do efektu.
W pokazanym powyżej kodzie DisposableEffect
efekt przyjmuje jako parametr wartość lifecycleOwner
używaną w jego bloku, ponieważ każda zmiana tych wartości powinna spowodować ponowne uruchomienie efektu.
@Composable
fun HomeScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
onStart: () -> Unit, // Send the 'started' analytics event
onStop: () -> Unit // Send the 'stopped' analytics event
) {
// These values never change in Composition
val currentOnStart by rememberUpdatedState(onStart)
val currentOnStop by rememberUpdatedState(onStop)
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
/* ... */
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}
currentOnStart
i currentOnStop
nie są potrzebne jako klucze DisposableEffect
, ponieważ ich wartość nigdy się nie zmienia w kompozycji ze względu na użycie rememberUpdatedState
. Jeśli nie przekażesz parametru lifecycleOwner
, a jego wartość się zmieni, HomeScreen
ponownie utworzy kompozycję, ale DisposableEffect
nie zostanie usunięty i ponownie uruchomiony. Powoduje to problemy, ponieważ od tego momentu używany jest nieprawidłowy lifecycleOwner
.
Stałe jako klucze
Możesz użyć stałej, np. true
, jako klucza efektu, aby śledzić cykl życia miejsca wywołania. Istnieją uzasadnione przypadki użycia, takie jak LaunchedEffect
przykład pokazany powyżej. Zanim to zrobisz, zastanów się dwa razy i upewnij się, że tego potrzebujesz.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony.
- Stan i Jetpack Compose
- Kotlin w Jetpack Compose
- Korzystanie z widoków w Compose