Efekty uboczne w funkcji tworzenia wiadomości

Efekt uboczny to zmiana stanu aplikacji, która występuje poza zakresem funkcji typu „composable”. Ze względu na cykl życia funkcji kompozycyjnych i ich właściwości, takie jak nieprzewidywalne rekompozycje, wykonywanie rekompozycji funkcji kompozycyjnych w różnych kolejnościach lub rekompozycje, które można odrzucić, funkcje kompozycyjne powinny być wolne od efektów ubocznych.

Czasami jednak efekty uboczne są konieczne, np. do wywołania jednorazowego zdarzenia, takiego jak wyświetlenie paska powiadomień lub przejście do innego ekranu w określonym stanie. Te działania powinny być wywoływane z kontrolowanego środowiska, które jest świadome cyklu życia funkcji kompozycyjnej. Na tej stronie dowiesz się o różnych interfejsach API efektów ubocznych, które oferuje Jetpack Compose.

Przypadki użycia dotyczące stanu i efektu

Jak opisano w dokumentacji Myślenie w Compose, funkcje kompozycyjne nie powinny mieć efektów ubocznych. Jeśli chcesz wprowadzić zmiany w stanie aplikacji (zgodnie z opisem w dokumentacji dotyczącej zarządzania stanem), użyj interfejsów Effect API, aby te efekty uboczne były wykonywane w przewidywalny sposób.

Ze względu na różne możliwości, jakie dają efekty w Compose, można je łatwo nadużywać. Upewnij się, że wykonywane w nich działania są związane z interfejsem i nie zakłócają jednokierunkowego przepływu danych, jak wyjaśniono w dokumentacji dotyczącej zarządzania stanem.

LaunchedEffect: uruchamianie funkcji zawieszania w zakresie funkcji typu „composable”

Aby wykonywać pracę przez cały cykl życia funkcji kompozycyjnej i mieć możliwość wywoływania funkcji zawieszania, użyj funkcji kompozycyjnej LaunchedEffect. Gdy LaunchedEffect wchodzi do kompozycji, uruchamia współprogram z blokiem kodu przekazanym jako parametr. Jeśli LaunchedEffect opuści kompozycję, współprogram zostanie anulowany. Jeśli LaunchedEffect zostanie ponownie skomponowany z innymi kluczami (patrz sekcja Ponowne uruchamianie efektów poniżej), istniejąca współprocedura zostanie anulowana, a nowa funkcja zawieszenia zostanie uruchomiona w nowej współprocedurze.

Oto przykład animacji, która pulsuje wartością alfa z konfigurowalnym opóźnieniem:

// 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 { mutableLongStateOf(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 kodzie powyżej animacja używa funkcji zawieszającej delay, aby odczekać określony czas. Następnie sekwencyjnie animuje kanał alfa do zera i z powrotem za pomocą animateTo. Będzie to powtarzane przez cały okres istnienia funkcji kompozycyjnej.

rememberCoroutineScope: uzyskiwanie zakresu uwzględniającego funkcję kompozycyjną, aby uruchomić współprogram poza funkcją kompozycyjną

LaunchedEffect jest funkcją typu „composable”, więc można jej używać tylko w innych funkcjach tego typu. Aby uruchomić współprogram poza funkcją kompozycyjną, ale w zakresie, który zostanie automatycznie anulowany po opuszczeniu kompozycji, użyj rememberCoroutineScope. Używaj rememberCoroutineScope również wtedy, gdy musisz ręcznie kontrolować cykl życia co najmniej 1 współprogramu, np. anulować animację, gdy wystąpi zdarzenie użytkownika.

rememberCoroutineScope to funkcja typu „composable”, która zwraca obiekt CoroutineScope powiązany z punktem kompozycji, w którym jest wywoływana. Zakres zostanie anulowany, gdy wywołanie opuści kompozycję.

W nawiązaniu do poprzedniego przykładu – 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 być uruchamiany ponownie, jeśli wartość się zmieni

LaunchedEffect uruchamia się ponownie, gdy zmieni się jeden z kluczowych parametrów. Jednak w niektórych sytuacjach możesz chcieć przechwycić wartość w efekcie, ale nie chcesz, aby efekt uruchamiał się ponownie, gdy ta wartość się zmieni. Aby to zrobić, musisz użyć rememberUpdatedState, aby utworzyć odwołanie do tej wartości, które można przechwycić i zaktualizować. Takie 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 aplikacja zawiera element LandingScreen, który po pewnym czasie znika. Nawet jeśli LandingScreen zostanie ponownie skomponowany, efekt, który czeka przez pewien czas i powiadamia, że czas minął, 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 przekazywana jest niezmienna stała, np. Unit lub true. W powyższym kodzie użyto LaunchedEffect(true). Aby mieć pewność, że funkcja lambda onTimeout zawsze zawiera najnowszą wartość, z którą funkcja LandingScreen została ponownie skomponowana, funkcję onTimeout należy opakować funkcją rememberUpdatedState. Zwrócony obiekt State, currentOnTimeout w kodzie, powinien być używany w efekcie.

DisposableEffect: efekty, które wymagają czyszczenia

W przypadku efektów ubocznych, które trzeba wyczyścić po zmianie kluczy lub gdy funkcja kompozycyjna opuszcza kompozycję, użyj DisposableEffect. Jeśli klucze DisposableEffect ulegną zmianie, funkcja kompozycyjna musi usunąć (wyczyścić) bieżący efekt i zresetować go, wywołując go ponownie.

Możesz na przykład wysyłać zdarzenia Analytics na podstawie zdarzeń Lifecycle za pomocą LifecycleObserver. Aby nasłuchiwać tych zdarzeń w Compose, użyj DisposableEffect, aby zarejestrować i wyrejestrować obserwatora, gdy jest to potrzebne.

@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 powyższym kodzie efekt doda observer do lifecycleOwner. Jeśli lifecycleOwner się zmieni, efekt zostanie usunięty i uruchomiony ponownie z nowym lifecycleOwner.

Element DisposableEffect musi zawierać klauzulę onDispose jako ostatnią instrukcję w bloku kodu. W przeciwnym razie IDE wyświetli błąd czasu kompilacji.

SideEffect: publikowanie stanu Compose w kodzie innym niż Compose

Aby udostępnić stan Compose obiektom, które nie są zarządzane przez Compose, użyj funkcji kompozycyjnej SideEffect. Użycie SideEffect gwarantuje, że efekt zostanie wykonany po każdej udanej rekompozycji. Z drugiej strony nieprawidłowe jest wykonywanie efektu, zanim zagwarantowana zostanie udana rekompozycja, co ma miejsce, gdy efekt jest zapisywany bezpośrednio w funkcji kompozycyjnej.

Na przykład biblioteka analityczna może umożliwiać segmentowanie populacji użytkowników przez dołączanie niestandardowych metadanych („właściwości użytkownika” w tym przykładzie) do wszystkich kolejnych zdarzeń analitycznych. Aby przekazać typ bieżącego użytkownika do biblioteki Analytics, użyj 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 spoza Compose na stan Compose

produceState uruchamia współprogram ograniczony do kompozycji, który może przekazywać wartości do zwracanego obiektu State. Użyj go, aby przekonwertować stan spoza Compose na stan Compose, na przykład aby wprowadzić do kompozycji zewnętrzny stan oparty na subskrypcji, taki jak Flow, LiveData lub RxJava.

Producent jest uruchamiany, gdy produceState wchodzi do kompozycji, i zostanie anulowany, gdy ją opuści. Zwrócony obiekt State łączy wartości. Ustawienie tej samej wartości nie spowoduje ponownego tworzenia kompozycji.

Chociaż produceState tworzy współprogram, można go też używać 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 używać produceState do wczytywania obrazu z sieci. Funkcja typu „composable” loadNetworkImage zwraca obiekt State, którego można używać w innych funkcjach tego typu.

@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: przekształcanie jednego lub kilku obiektów stanu w inny stan.

W Compose rekompozycja występuje za każdym razem, gdy zmienia się obserwowany obiekt stanu lub dane wejściowe funkcji kompozycyjnej. Obiekt stanu lub dane wejściowe mogą zmieniać się częściej, niż jest to konieczne do aktualizacji interfejsu, co prowadzi do niepotrzebnej rekompozycji.

Funkcji derivedStateOf należy używać, gdy dane wejściowe funkcji kompozycyjnej zmieniają się częściej, niż jest to potrzebne do rekompozycji. Często dzieje się tak, gdy coś często się zmienia, np. pozycja przewijania, ale funkcja kompozycyjna musi zareagować dopiero po przekroczeniu określonego progu. derivedStateOf tworzy nowy obiekt stanu Compose, który możesz obserwować i który aktualizuje się tylko wtedy, gdy jest to konieczne. Działa podobnie do operatora distinctUntilChanged() w Kotlin Flow.

Prawidłowe użycie

Poniższy fragment kodu pokazuje odpowiedni przypadek użycia 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 wartość firstVisibleItemIndex zmienia się za każdym razem, gdy zmienia się pierwszy widoczny element. Podczas przewijania 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 zakładanie, że podczas łączenia 2 obiektów stanu Compose należy użyć derivedStateOf, ponieważ „wywodzisz stan”. Jest to jednak tylko narzut i nie jest wymagany, jak pokazano 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ć aktualizowany tak samo często jak firstName i lastName. Dlatego nie dochodzi do nadmiernej rekompozycji i nie trzeba używać derivedStateOf.

snapshotFlow: konwertowanie stanu Compose na przepływy

Użyj snapshotFlow, aby przekonwertować obiekty State<T> na zimny Flow. snapshotFlow uruchamia swój blok po zebraniu i emituje wynik odczytanych w nim obiektów State. Gdy jeden z obiektów State odczytywanych w bloku snapshotFlow ulegnie zmianie, Flow wyemituje nową wartość do swojego kolektora, jeśli nowa wartość nie jest równa poprzedniej wyemitowanej wartości (to zachowanie jest podobne do Flow.distinctUntilChanged).

Poniższy przykład pokazuje efekt uboczny, który rejestruje, gdy użytkownik przewija listę poza pierwszy element:

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 listState.firstVisibleItemIndex jest konwertowany na Flow, który może korzystać z możliwości operatorów Flow.

Ponowne uruchamianie efektów

Niektóre efekty w Compose, takie jak LaunchedEffect, produceState lub DisposableEffect, przyjmują zmienną liczbę argumentów, kluczy, które są używane do anulowania uruchomionego efektu i uruchomienia nowego z nowymi kluczami.

Typowa postać tych interfejsów API to:

EffectName(restartIfThisKeyChanges, orThisKey, orThisKey, ...) { block }

Ze względu na subtelności tego zachowania mogą wystąpić problemy, jeśli parametry użyte do ponownego uruchomienia efektu nie są prawidłowe:

  • Zbyt rzadkie ponowne uruchamianie efektów może powodować błędy w aplikacji.
  • Zbyt częste ponowne uruchamianie efektów może być nieefektywne.

Zasadniczo zmienne modyfikowalne i niemodyfikowalne używane w bloku kodu efektu powinny być dodawane jako parametry do funkcji kompozycyjnej efektu. Oprócz tego można dodać więcej parametrów, aby wymusić ponowne uruchomienie efektu. Jeśli zmiana zmiennej nie powinna powodować ponownego uruchomienia efektu, zmienną należy opakować w rememberUpdatedState. Jeśli zmienna nigdy się nie zmienia, ponieważ jest opakowana w remember bez kluczy, nie musisz przekazywać jej jako klucza do efektu.

W pokazanym powyżej kodzie DisposableEffect efekt przyjmuje jako parametr lifecycleOwner używany w jego bloku, ponieważ każda zmiana 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ści nigdy nie zmieniają się w funkcji Composition ze względu na użycie rememberUpdatedState. Jeśli nie przekażesz lifecycleOwner jako parametru i nastąpi zmiana, HomeScreen zostanie ponownie skomponowany, ale DisposableEffect nie zostanie usunięty i uruchomiony ponownie. Powoduje to problemy, ponieważ od tego momentu używany jest nieprawidłowy lifecycleOwner.

Stałe jako klucze

Możesz użyć stałej, takiej jak true, jako klucza efektu, aby podążał za cyklem życia miejsca wywołania. Istnieją uzasadnione przypadki użycia, takie jak przykład LaunchedEffect pokazany powyżej. Zanim jednak to zrobisz, zastanów się, czy na pewno tego potrzebujesz.