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 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 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 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.