Efekty uboczne w funkcji tworzenia wiadomości

Efekt uboczny to zmiana stanu aplikacji, która następuje poza zakresem funkcji kompozycyjnej. Ze względu na cykl życia funkcji kompozycyjnych i ich właściwości, takie jak nieprzewidywalne ponowne kompozycje, wykonywanie ponownych kompozycji funkcji kompozycyjnych w różnej kolejności lub ponowne kompozycje, które można odrzucić, funkcje kompozycyjne powinny być w idealnym przypadku 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ślonych warunkach. Te działania powinny być wywoływane z kontrolowanego środowiska, które zna cykl życia komponentu. Na tej stronie dowiesz się więcej o różnych interfejsach API efektów ubocznych, które oferuje Jetpack Compose.

Przypadki użycia dotyczące stanu i efektu

Zgodnie z dokumentacją Thinking in Compose funkcje kompozycyjne nie powinny wywoływać efektów ubocznych. Gdy musisz wprowadzić zmiany w stanie aplikacji (zgodnie z opisem w dokumencie Zarządzanie stanem), używaj interfejsów API efektów, aby efekty uboczne były wykonywane w przewidywalny sposób.

Ze względu na różne możliwości, jakie efekty otwierają w Compose, można ich łatwo nadużywać. Upewnij się, że Twoja praca dotyczy interfejsu i nie narusza jednokierunkowego przepływu danych, jak wyjaśniono w dokumentacji dotyczącej zarządzania stanem.

LaunchedEffect: uruchamianie funkcji zawieszających w zakresie funkcji typu „composable”

Aby wykonać pracę w trakcie działania funkcji typu „composable” i mieć możliwość wywoływania funkcji zawieszania, użyj funkcji typu „composable” LaunchedEffect. Gdy LaunchedEffect wchodzi w skład kompozycji, uruchamia korutynę z blokiem kodu przekazanym jako parametr. Jeśli LaunchedEffect opuści kompozycję, korutyna zostanie anulowana. Jeśli funkcja LaunchedEffect zostanie ponownie skomponowana z użyciem innych kluczy (patrz sekcja Ponowne uruchamianie efektów poniżej), istniejąca korutyna zostanie anulowana, a nowa funkcja zawieszająca zostanie uruchomiona w nowej korutynie.

Oto na przykład animacja, 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 { 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 zawieszającej delay do odczekania określonego czasu. Następnie sekwencyjnie animuje wartość alfa do zera i z powrotem, używając animateTo. Będzie się to powtarzać przez cały czas życia komponentu.

rememberCoroutineScope: uzyskiwanie zakresu uwzględniającego kompozycję w celu uruchamiania korutyny poza funkcją kompozycyjną;

Funkcja LaunchedEffect jest funkcją kompozycyjną, więc można jej używać tylko w innych funkcjach kompozycyjnych. Aby uruchomić korutynę poza funkcją kompozycyjną, ale w zakresie, który spowoduje jej automatyczne anulowanie po opuszczeniu kompozycji, użyj funkcji rememberCoroutineScope. Używaj też rememberCoroutineScope, gdy musisz ręcznie kontrolować cykl życia co najmniej 1 korutyny, np. anulować animację, gdy wystąpi zdarzenie użytkownika.

rememberCoroutineScope to funkcja kompozycyjna, która zwraca CoroutineScope powiązany z punktem kompozycji, w którym jest wywoływana. Zakres zostanie anulowany, gdy połączenie 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ływanie się do wartości w efekcie, który nie powinien się ponownie uruchamiać, jeśli wartość się zmieni

LaunchedEffect rozpoczyna się od nowa, gdy zmieni się jeden z kluczowych parametrów. W niektórych sytuacjach możesz jednak chcieć przechwycić w efekcie wartość, która po zmianie nie powinna powodować ponownego uruchomienia efektu. Aby to zrobić, musisz użyć rememberUpdatedState, aby utworzyć odwołanie do tej wartości, które można przechwycić i zaktualizować. To podejście jest przydatne w przypadku efektów, które zawierają długotrwałe operacje, których ponowne utworzenie i uruchomienie może być kosztowne lub niemożliwe.

Załóżmy na przykład, że w aplikacji jest LandingScreen, który po pewnym czasie znika. Nawet jeśli LandingScreen zostanie ponownie skomponowany, efekt, który czeka przez pewien czas i powiadamia o jego upływie, nie powinien zostać ponownie uruchomiony:

@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 uzyskać efekt zgodny z cyklem życia witryny wywołującej, jako parametr przekazywana jest stała, która nigdy się nie zmienia, np. Unit lub true. W powyższym kodzie użyto LaunchedEffect(true). Aby mieć pewność, że funkcja onTimeout lambda zawsze zawiera najnowszą wartość, która została ponownie skomponowana za pomocą funkcji LandingScreen, należy ją umieścić w funkcji rememberUpdatedState.onTimeout Zwrócone State, currentOnTimeout w kodzie powinny być użyte w efekcie.

DisposableEffect: efekty wymagające oczyszczenia

W przypadku efektów ubocznych, które wymagają wyczyszczenia po zmianie kluczy lub gdy funkcja kompozycyjna opuści kompozycję, użyj DisposableEffect. Jeśli klucze DisposableEffect się zmienią, funkcja kompozycyjna musi usunąć (wykonać czyszczenie) bieżący efekt i zresetować się, 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 Compose, użyj DisposableEffect, aby w razie potrzeby zarejestrować i wyrejestrować 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 powyższym kodzie efekt doda znak observer do elementu lifecycleOwner. Jeśli lifecycleOwner się zmieni, efekt zostanie usunięty i uruchomiony ponownie z nową wartością lifecycleOwner.

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

SideEffect: publikowanie stanu Compose w kodzie innym niż Compose

Aby udostępnić stan funkcji Compose obiektom, którymi nie zarządza Compose, użyj funkcji kompozycyjnej SideEffect. Użycie SideEffect gwarantuje, że efekt zostanie wykonany po każdej udanej rekompozycji. Z drugiej strony nie należy wykonywać efektu przed zagwarantowaniem pomyślnej ponownej kompozycji, co ma miejsce w przypadku zapisywania efektu bezpośrednio w funkcji kompozycyjnej.

Na przykład biblioteka analityczna może umożliwiać segmentowanie użytkowników przez dołączanie do wszystkich kolejnych zdarzeń analitycznych niestandardowych metadanych (w tym przykładzie „właściwości użytkownika”). Aby przekazać do biblioteki analitycznej typ użytkownika, który obecnie korzysta z aplikacji, użyj funkcji 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łcanie stanu innego niż Compose w stan Compose

produceState uruchamia korutynę w zakresie kompozycji, która może przekazywać wartości do zwracanego obiektu State. Używaj go do przekształcania stanu innego niż Compose w stan Compose, np. do przenoszenia do kompozycji zewnętrznego stanu opartego na subskrypcji, takiego jak Flow, LiveData lub RxJava.

Producent jest uruchamiany, gdy produceState wchodzi w skład kompozycji, i anulowany, gdy ją opuszcza. Zwrócona wartość State jest łączona. Ustawienie tej samej wartości nie spowoduje ponownego komponowania.

Mimo że funkcja produceState tworzy korutynę, można jej też używać do obserwowania źródeł danych, które nie wstrzymują działania. 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żywać w innych funkcjach 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: przekształcanie jednego lub wielu obiektów stanu w inny stan;

W Compose ponowne komponowanie następuje za każdym razem, gdy zmieni się obserwowany obiekt stanu lub dane wejściowe funkcji kompozycyjnej. Obiekt stanu lub dane wejściowe mogą się zmieniać częściej, niż jest to konieczne do aktualizacji interfejsu, co prowadzi do niepotrzebnego ponownego komponowania.

Funkcji derivedStateOf należy używać, gdy dane wejściowe funkcji kompozycyjnej zmieniają się częściej niż jest to potrzebne do ponownego skomponowania. Często zdarza się to, gdy coś często się zmienia, np. pozycja przewijania, ale komponent musi reagować na tę zmianę 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 potrzebne. Działa podobnie do operatora Kotlin FlowsdistinctUntilChanged().

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. Podczas przewijania wartość zmienia się na 0, 1, 2, 3, 4, 5 itd. Kompozycja musi jednak zostać ponownie utworzona tylko wtedy, gdy wartość jest większa niż 0. Ta różnica 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 podczas łączenia dwóch obiektów stanu Compose należy użyć derivedStateOf, ponieważ „wyprowadzasz stan”. Jest to jednak tylko dodatkowy kod, który nie jest wymagany, co widać w tym fragmencie:

// 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 firstNamelastName. Dlatego nie dochodzi do nadmiernego ponownego komponowania i nie ma potrzeby używania derivedStateOf.

snapshotFlow: przekształcanie stanu funkcji Compose w przepływy

Użyj snapshotFlow, aby przekształcić obiekty State<T> w zimny przepływ. snapshotFlow uruchamia swój blok po zebraniu i emituje wynik odczytanych w nim obiektów State. Gdy jeden z StateobiektówsnapshotFlow odczytanych w bloku ulegnie zmianie, przepływ wyśle nową wartość do kolektora, jeśli nie jest ona równa poprzedniej wysłanej wartości (działanie podobne do Flow.distinctUntilChanged).

Poniższy przykład pokazuje efekt uboczny, który rejestruje w Analytics, kiedy 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 przekształcane w Flow, które może korzystać z operatorów Flow.

Uruchamianie efektów

Niektóre efekty w Compose, takie jak LaunchedEffect, produceState lub DisposableEffect, przyjmują zmienną liczbę argumentów (kluczy), które służą do anulowania bieżącego efektu i rozpoczynania 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żyte do ponownego uruchomienia efektu nie są odpowiednie:

  • Ponowne uruchamianie efektów rzadziej niż powinno 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 efektu kodu należy dodać jako parametry do funkcji kompozycyjnej efektu. Oprócz nich 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 umieścić w tagu rememberUpdatedState. Jeśli zmienna nigdy się nie zmienia, ponieważ jest zawarta w funkcji remember bez kluczy, nie musisz przekazywać jej jako klucza do efektu.

W pokazanym powyżej kodzie DisposableEffect efekt przyjmuje jako parametr wartość lifecycleOwner używaną w jego bloku, ponieważ każda zmiana tych wartości 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)
        }
    }
}

currentOnStartcurrentOnStop nie są potrzebne jako klucze DisposableEffect, ponieważ ich wartość nigdy się nie zmienia w kompozycji ze względu na użycie rememberUpdatedState. Jeśli nie przekażesz parametru lifecycleOwner, a jego wartość się zmieni, HomeScreen ponownie utworzy kompozycję, ale DisposableEffect nie zostanie usunięty i ponownie uruchomiony. Powoduje to problemy, ponieważ od tego momentu używany jest nieprawidłowy lifecycleOwner.

Stałe jako klucze

Możesz użyć stałej, np. true, jako klucza efektu, aby śledzić cykl życia miejsca wywołania. Istnieją uzasadnione przypadki użycia, takie jak LaunchedEffect przykład pokazany powyżej. Zanim to zrobisz, zastanów się dwa razy i upewnij się, że tego potrzebujesz.