Efekty uboczne w funkcji tworzenia wiadomości

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 skutku

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 w taki sposób, 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ą długotrwałe operacje, których odtworzenie i ponowne uruchomienie może być kosztowne lub niemożliwe.

Załóżmy na przykład, że Twoja aplikacja zawiera 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.

to właśnie tego potrzebujesz.

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 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: 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 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 komponowaniu za każdym razem, gdy zmienia się obserwowany obiekt stanu lub kompozytywny 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ć 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 firstNamelastName. 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 momentu, w którym użytkownik przewinął 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 Compose, np. 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 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 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 currentOnStartcurrentOnStop 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 on ulegnie zmianie, 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.