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 niezbędne są efekty uboczne, np. do wywołania jednorazowego zdarzenia, takiego jak wyświetlenie paska powiadomień lub przejście do innego ekranu w przypadku spełnienia określonego warunku. Te działania należy wywoływać z kontrolowanego środowiska, które wie o cyklu życia zasobu kompozycyjnego. 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ć działania w czasie trwania funkcji typu composable i mieć możliwość wywoływania funkcji zawieszania, użyj funkcji typu 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 innymi kluczami (patrz sekcja Restartowanie efektów poniżej), dotychczasowa 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 zależny od kompozycji, aby uruchomić współpracę 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, np. 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ć element Snackbar
, gdy użytkownik kliknie element 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łuje się do wartości w wyniku, który nie powinien zostać uruchomiony ponownie, jeśli wartość się zmieni
LaunchedEffect
uruchamia się ponownie po zmianie jednego z kluczowych parametrów. Jednak w niektórych sytuacjach możesz chcieć zarejestrować w efekcie wartość, która w razie zmiany nie chcesz, aby efekt był ponownie uruchamiany. Aby to zrobić, musisz użyć parametru rememberUpdatedState
, by utworzyć odwołanie do tej wartości, które będzie można przechwytywać i aktualizować. 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 skomponowany, efekt, który będzie czekać przez jakiś czas i informuje, że nie należy wznawiać ustawionego czasu:
@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 trzeba wyczyścić po zmianie klawiszy lub jeśli funkcja kompozycyjna opuści kompozycję, użyj funkcji 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 zmieni się wartość lifecycleOwner
, efekt zostanie usunięty i rozpoczęty ponownie z nowym elementem lifecycleOwner
.
DisposableEffect
musi zawierać klauzulę onDispose
jako końcową instrukcję w swoim 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 tworzenia wiadomości obiektom, którym nie zarządza funkcja tworzenia wiadomości, użyj funkcji kompozycyjnej SideEffect
. Użycie właściwości SideEffect
gwarantuje, że efekt będzie wywoływany po każdej udanej zmianie kompozycji. 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 parametru 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
: konwertowanie stanu innego niż tworzenie na stan tworzenia
produceState
uruchamia coroutine ograniczoną do kompozycji, która może przekazywać wartości do zwracanej funkcji State
. Umożliwia przekształcenie stanu niezwiązanego z tworzeniem na stan Utwórz, na przykład przez wprowadzenie do Kompozycji stanu zewnętrznego zależnego od subskrypcji, takiego jak Flow
, LiveData
lub RxJava
.
Producent jest uruchamiany, gdy produceState
wchodzi do kompozycji, a jest 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 funkcji tworzenia wiadomości zmiana kompozycji następuje za każdym razem, gdy zmieni się zaobserwowany obiekt stanu lub element wejściowy kompozycyjny. Obiekt stanu lub dane wejściowe mogą się zmieniać częściej niż wymaga tego aktualizacja interfejsu, co prowadzi do niepotrzebnego ponownego tworzenia kompozycji.
Funkcji derivedStateOf
należy używać, gdy dane wejściowe do komponentu zmieniają się częściej niż trzeba je ponownie skompilować. 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
Ten fragment kodu przedstawia odpowiedni przypadek użycia dla 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 derivedStateOf
.
Nieprawidłowe użycie
Częstym błędem jest założenie, że do połączenia 2 obiektów stanu tworzenia wiadomości należy użyć właściwości derivedStateOf
, ponieważ określasz stan pobierania. 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 obiektów State
odczytanych w ramach tego bloku. 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 momentu, w którym użytkownik przewinie 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 obiekt listState.firstVisibleItemIndex
jest przekształcany w przepływ, który może korzystać z mocy operatorów przepływu.
Ponowne uruchamianie efektów
Niektóre efekty w funkcji Compose, np. LaunchedEffect
, produceState
i DisposableEffect
, przyjmują zmienną liczbę argumentów (kluczy), które są używane do anulowania bieżącego efektu i uruchamiania nowego z nowymi kluczami.
Typowa postać dla 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 spowodować 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 zmiennej nie powinna powodować ponownego uruchomienia efektu, należy uwzględnić zmienną w elemencie 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)
}
}
}
currentOnStart
i currentOnStop
nie są potrzebne jako klucze DisposableEffect
, ponieważ ich wartość nigdy nie zmienia się w Kompozycji z powodu użycia rememberUpdatedState
. Jeśli nie przekażesz parametru lifecycleOwner
, a on ulegnie zmianie, komponent HomeScreen
zostanie ponownie skompilowany, ale komponent DisposableEffect
nie zostanie usunięty i ponowicie uruchomiony. Spowoduje to problemy, ponieważ od tego momentu będzie używana niewłaściwa wartość lifecycleOwner
.
Stałe jako klucze
Możesz użyć stałej, takiej jak true
, jako klucza efektu, aby przestrzegał cyklu życia witryny wywołania. 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
- State i Jetpack Compose
- Kotlin w Jetpack Compose
- Korzystanie z widoków w sekcji Tworzenie wiadomości