Efekty uboczne w funkcji tworzenia wiadomości

Efekt uboczny to zmiana stanu aplikacji, która zachodzi poza zakresem funkcji kompozycyjnej. 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 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 wspomnieliśmy w dokumentacji Thinking in Compose, elementy kompozycyjne powinny być wolne od efektó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 się to powtarzać przez cały okres istnienia elementu kompozycyjnego.

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ć współprogram poza kompozycją, ale o zakresie ograniczonym do automatycznego anulowania po jej opuszczeniu, użyj funkcji rememberCoroutineScope. rememberCoroutineScope przydaje się też wtedy, gdy chcesz ręcznie kontrolować cykl życia co najmniej 1 współrzędnej, na przykład anulować animację w przypadku 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ę.

Zgodnie z poprzednim przykładem możesz użyć tego kodu, by 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ł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. 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ć. Ta metoda jest przydatna w przypadku efektów wymagających długotrwałych operacji, które mogą być kosztowne lub niemożliwe do odtworzenia i ponownego uruchomienia.

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 klawisze DisposableEffect ulegną zmianie, funkcja kompozycyjna musi usunąć (wyczyścić) jej bieżący efekt i zresetować go, 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 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 czasu 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ć do biblioteki Analytics typ bieżącego użytkownika, zaktualizuj jego wartość za pomocą SideEffect.

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

Narzędzie Producer jest uruchamiane, gdy produceState znajdzie się w kompozycji, i zostanie anulowane, gdy opuści kompozycję. Zwrócona wartość State jest zróżnicowana. Ustawienie tej samej wartości nie spowoduje ponownego skompilowania.

Mimo że produceState tworzy współrzędną, może też służyć do obserwowania źródeł danych, które nie są zawieszone. 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żyć w innych obiektach 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: 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 musi faktycznie aktualizować, co prowadzi do niepotrzebnej zmiany 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

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 fullName musi być aktualizowana tak samo często jak 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 odczytanych w bloku snapshotFlow ulegnie mutacji, przepływ wyśle nową wartość do swojego kolektora, jeśli nowa wartość nie jest równa poprzedniej wartości (działa to podobnie jak w przypadku 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 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, 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:

  • Ponowne uruchamianie efektów rzadziej niż powinno 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)
        }
    }
}

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.