Efekt uboczny to zmiana stanu aplikacji, która występuje poza zakresem funkcji kompozytowej. Ze względu na cykl życia i właściwości kompozytów, takie jak nieprzewidywalne rekompozycje, wykonywanie rekompozycji kompozytów w różnych kolejnościach czy rekompozycje, które można odrzucić, kompozyty powinny być w miarę możliwości pozbawione efektów ubocznych.
Czasami jednak efekty uboczne są konieczne, na przykład do wywołania jednorazowego zdarzenia, takiego jak wyświetlenie paska informacji lub przejście do innego ekranu w określonych warunkach stanu. Te działania powinny być wywoływane z kontrolowanego środowiska, które jest świadome cyklu życia kompozytu. Na tej stronie dowiesz się więcej o różnych interfejsach API Jetpack Compose.
Przypadki użycia stanu i efektów
Jak opisano w dokumentacji Myślenie w komponowaniu, komponenty powinny być wolne od skutków ubocznych. Jeśli chcesz wprowadzić zmiany w stanie aplikacji (jak opisano w dokumentacji zarządzania stanem), użyj interfejsów API efektów, aby te efekty uboczne były wykonywane w przewidywalny sposób.
Ze względu na różne możliwości, jakie dają efekty w sekcji Tworzenie wiadomości, można ich łatwo nadużywać. Upewnij się, że wykonywane przez Ciebie czynności dotyczą interfejsu użytkownika i nie zakłócają jednokierunkowego przepływu danych, jak wyjaśniono w dokumentacji zarządzania stanem.
LaunchedEffect
: uruchamianie funkcji zawieszania w zakresie funkcji typu composable
Aby wykonywać zadania w czasie działania funkcji typu „composable” i mieć możliwość wywoływania funkcji zawieszania, użyj funkcji typu „composable” LaunchedEffect
. Gdy LaunchedEffect
wejdzie do kompozycji, uruchomi coroutine z blokiem kodu przekazanym jako parametr. Jeśli LaunchedEffect
opuści kompozycję, współbieżność zostanie anulowana. Jeśli LaunchedEffect
zostanie zrekonstruowany z użyciem innych kluczy (patrz sekcja Ponowne uruchamianie efektów poniżej), istniejąca coroutine zostanie anulowana, a nowa funkcja zawieszenia zostanie uruchomiona w nowej coroutine.
Oto przykład animacji, która pulsuje wartością alfa z możliwością ustawienia opóźnienia:
// 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 zawieszania
delay
, aby poczekać przez określony czas. Następnie sekwencyjnie animuje przezroczystość od 1 do 0 i z powrotem, używając funkcji animateTo
.
Będzie to powtarzać się przez cały czas istnienia kompozytu.
rememberCoroutineScope
: uzyskaj zakres uwzględniający kompozycję, aby uruchomić coroutine poza kompozycją.
Funkcja LaunchedEffect
jest funkcją składającą, więc można jej używać tylko wewnątrz innych funkcji składających. Aby uruchomić coroutine poza kompozycją, ale tak, aby została ona automatycznie anulowana po opuszczeniu kompozycji, użyj funkcji rememberCoroutineScope
.
Użyj też funkcji rememberCoroutineScope
, gdy chcesz ręcznie kontrolować cykl życia co najmniej 1 korobocznej funkcji, na przykład anulować animację po wystąpieniu zdarzenia użytkownika.
rememberCoroutineScope
to funkcja składana, która zwraca CoroutineScope
powiązany z miejscem w kompozycji, w którym jest wywoływana. Zakres zostanie anulowany, gdy połączenie opuści kompozycję.
W poprzednim przykładzie 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łanie do wartości w efekcie, który nie powinien się uruchamiać ponownie, jeśli wartość się zmieni
LaunchedEffect
uruchamia się ponownie, gdy zmieni się jeden z kluczowych parametrów. W niektórych przypadkach możesz jednak chcieć zarejestrować wartość efektu, która w razie zmiany nie powinna powodować jego ponownego uruchamiania. Aby to zrobić, musisz użyć rememberUpdatedState
, aby utworzyć odwołanie do tej wartości, którą można przechwycić i zaktualizować. To podejście jest przydatne w przypadku efektów, które zawierają operacje długotrwałe, których odtworzenie i ponowne uruchomienie może być kosztowne lub niemożliwe.
Załóżmy na przykład, że w aplikacji jest element LandingScreen
, który po pewnym czasie znika. Nawet jeśli LandingScreen
zostanie ponownie skompilowany, efekt, który czeka przez jakiś czas i powiadamia, że upłynął czas, nie powinien być ponownie uruchamiany:
@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 utworzyć efekt, który odpowiada cyklowi życia miejsca wywołania, jako parametr należy podać stałą wartość, która się nigdy nie zmienia, np. Unit
lub true
. W powyższym kodzie użyto wartości LaunchedEffect(true)
. Aby mieć pewność, że funkcja lambda onTimeout
zawsze zawiera najnowszą wartość, z którą została zrekonstruowana funkcja LandingScreen
, musisz ją otoczyć funkcją rememberUpdatedState
.onTimeout
Zwrócona wartość State
, currentOnTimeout
w kodzie, powinna być użyta w efekcie.
DisposableEffect
: efekty, które wymagają oczyszczenia
W przypadku efektów ubocznych, które należy usunąć po zmianie kluczy lub jeśli kompozyt wychodzi z kompozycji, użyj DisposableEffect
.
Jeśli klucze DisposableEffect
ulegną zmianie, komponent musi usunąć (czyli wykonać czyszczenie dla) bieżącego efektu i zresetować go, wywołując go ponownie.
Możesz na przykład wysyłać zdarzenia analityczne na podstawie zdarzeń Lifecycle
za pomocą tagu LifecycleObserver
.
Aby nasłuchiwać tych zdarzeń w komponencie Compose, użyj elementu DisposableEffect
, aby zarejestrować i w razie potrzeby anulować rejestrację 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 kodzie powyżej efekt doda observer
do lifecycleOwner
. Jeśli lifecycleOwner
się zmieni, efekt zostanie usunięty i ponownie uruchomiony z nowym lifecycleOwner
.
Element DisposableEffect
musi zawierać klauzulę onDispose
jako ostatnie polecenie w bloku kodu. W przeciwnym razie IDE wyświetli błąd kompilacji.
SideEffect
: publikowanie stanu tworzenia wiadomości w kodzie niebędącym w trybie tworzenia wiadomości
Aby udostępnić stan Compose obiektom, które nie są zarządzane przez Compose, użyj komponentu SideEffect
. Użycie SideEffect
gwarantuje, że efekt zostanie wykonany po każdej pomyślnej rekompozycji. Z drugiej strony, nie należy stosować efektu przed zagwarantowaniem pomyślnego przekształcenia, co ma miejsce podczas zapisywania efektu bezpośrednio w komponowalnym.
Twoja biblioteka analityczna może na przykład umożliwiać dzielenie populacji użytkowników na segmenty przez dołączanie niestandardowych metadanych (w tym przykładzie „właściwości użytkownika”) do wszystkich kolejnych zdarzeń analitycznych. Aby przekazać typ użytkownika do biblioteki analitycznej, użyj zmiennej SideEffect
do zaktualizowania jej wartości.
@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łcenie stanu innego niż Compose w stan Compose
produceState
uruchamia coroutine ograniczoną do kompozycji, która może przekazywać wartości do zwracanej funkcji State
. Użyj go, aby przekształcić stan inny niż kompozycja w stan kompozycja, na przykład wprowadzić do kompozycji zewnętrzny stan oparty na subskrypcji, taki jak Flow
, LiveData
lub RxJava
.
Producent jest uruchamiany, gdy produceState
wchodzi do kompozycji, a anulowany, gdy ją opuszcza. Zwrócona wartość State
jest zróżnicowana. Ustawienie tej samej wartości nie spowoduje ponownego skompilowania.
Mimo że produceState
tworzy coroutine, można go też używać do obserwowania źródeł danych, które nie są zawieszane. Aby usunąć subskrypcję tego źródła, użyj funkcji awaitDispose
.
Ten przykład pokazuje, jak za pomocą funkcji produceState
wczytać obraz z sieci. Funkcja kompozytowa loadNetworkImage
zwraca wartość State
, która może być używana w innych kompozytach.
@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
: konwertowanie jednego lub wielu obiektów stanu na inny stan
W komponowaniu za każdym razem, gdy zmienia się obserwowany stan obiektu lub kompozybilny element wejściowy, następuje rekompozycja. Obiekt stanu lub dane wejściowe mogą się zmieniać częściej niż interfejs wymaga aktualizacji, co prowadzi do niepotrzebnego ponownego tworzenia kompozycji.
Funkcji derivedStateOf
należy używać, gdy dane wejściowe do komponentu zmieniają się częściej niż konieczne jest ponowne skompilowanie. Zdarza się to często, gdy coś zmienia się często, np. pozycja przewijania, ale komponent musi reagować tylko wtedy, gdy przekroczy określony próg. derivedStateOf
tworzy nowy obiekt stanu Compose, który możesz obserwować i aktualizować tylko wtedy, gdy jest to konieczne. W ten sposób działa on podobnie do operatora Kotlin Flows
distinctUntilChanged()
.
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. Gdy przewijasz, wartość zmienia się na 0
, 1
, 2
, 3
, 4
, 5
itd. Jednak rekompozycja musi nastąpić tylko wtedy, gdy wartość jest większa niż 0
.
Ta rozbieżność w częstotliwości aktualizacji oznacza, że jest to dobry przypadek użycia dla funkcji derivedStateOf
.
Nieprawidłowe użycie
Typowym błędem jest założenie, że podczas łączenia 2 obiektów stanu w komponencie należy użyć funkcji derivedStateOf
, ponieważ „wywodzisz stan”. Jest to jednak tylko nadmiar informacji i nie jest wymagany, jak widać w tym fragmencie kodu:
// 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 element fullName
musi być aktualizowany tak samo często jak elementy firstName
i lastName
. Dlatego nie dochodzi do nadmiernego przekształcania, więc użycie parametru derivedStateOf
nie jest konieczne.
snapshotFlow
: przekształcenie stanu usługi Compose na stan usługi Flows
Użyj snapshotFlow
, aby przekształcić obiekty State<T>
w zimny przepływ. snapshotFlow
wykonuje swój blok po zebraniu danych i wysyła wynik odczytu obiektów State
. Gdy jeden z obiektów State
odczytywanych w bloku snapshotFlow
ulegnie mutacji, przepływ wyemituje nową wartość do swojego kolektora, jeśli nowa wartość nie jest równa poprzedniej wyemitowanej wartości (to zachowanie jest podobne do zachowania funkcji Flow.distinctUntilChanged
).
Ten przykład pokazuje efekt uboczny, który polega na rejestrowaniu przez Analytics, gdy użytkownik przewinie poza pierwszy element na liście:
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 element listState.firstVisibleItemIndex
jest przekształcany w proces, który może korzystać z potęgi operatorów Flow.
Ponowne uruchamianie efektów
Niektóre efekty w sekcji Komponuj, takie jak LaunchedEffect
, produceState
lub DisposableEffect
, przyjmują zmienną liczbę argumentów, czyli kluczy, które służą do anulowania bieżącego efektu i uruchomienia 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żywane do ponownego uruchamiania efektu są nieprawidłowe:
- Restartowanie efektów rzadziej niż powinno może powodować błędy w aplikacji.
- Ponowne uruchamianie efektów częściej niż to konieczne może być nieefektywne.
Zmienne zmienne i niezmienne używane w bloku efektów kodu powinny być dodawane jako parametry do efektu kompozytowego. Oprócz tych parametrów możesz dodać więcej parametrów, aby wymusić ponowne uruchomienie efektu. Jeśli zmiana wartości zmiennej nie powinna powodować ponownego uruchamiania efektu, zmienną należy umieścić w elementach rememberUpdatedState
. Jeśli zmienna nigdy się nie zmienia, ponieważ jest zapakowana w element remember
bez kluczy, nie musisz przekazywać jej jako klucza efektu.
W powyższym kodzie DisposableEffect
efekt przyjmuje jako parametr bloku lifecycleOwner
, ponieważ każda zmiana w nich 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)
}
}
}
Klucze currentOnStart
i currentOnStop
nie są potrzebne jako klucze DisposableEffect
, ponieważ ich wartość nigdy nie zmienia się w składniku ze względu na użycie rememberUpdatedState
. Jeśli nie przekażesz parametru lifecycleOwner
, a wartość tego parametru się zmieni, komponent HomeScreen
zostanie ponownie skompilowany, ale komponent DisposableEffect
nie zostanie usunięty i ponowicie uruchomiony. To powoduje problemy, ponieważ od tego momentu używana jest niewłaściwa wartość lifecycleOwner
.
Stałe jako klucze
Jako klucza efektu możesz użyć stałej wartości, np. true
, aby podążała ona za cyklem życia miejsca wywołania funkcji. Istnieją uzasadnione przypadki użycia, takie jak przykład LaunchedEffect
pokazany powyżej. Zanim to zrobisz, zastanów się, czy na pewno tego potrzebujesz.
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy obsługa JavaScript jest wyłączona
- Stan i Jetpack Compose
- Kotlin w Jetpack Compose
- Korzystanie z widoków w sekcji Tworzenie wiadomości