Efekty uboczne w funkcji tworzenia wiadomości

Efekt uboczny to zmiana stanu aplikacji, która następuje poza zakresem funkcji typu „composable”. Ze względu na cykl życia funkcji typu „composable” i ich właściwości, takie jak nieprzewidywalne ponowne komponowanie, wykonywanie ponownego komponowania funkcji typu „composable” w różnej kolejności lub ponowne komponowanie, które można odrzucić, funkcje typu „composable” powinny być idealnie wolne od efektów ubocznych free.

Czasami jednak efekty uboczne są konieczne, np. aby uruchomić jednorazowe zdarzenie, takie jak wyświetlenie paska powiadomień, lub przejść do innego ekranu w określonym stanie. Te działania powinny być wywoływane w kontrolowanym środowisku, które zna cykl życia funkcji typu „composable”. Na tej stronie dowiesz się więcej o różnych interfejsach API efektów ubocznych oferowanych przez Jetpack Compose.

Przypadki użycia stanu i efektów

Jak wspomnieliśmy w dokumentacji Thinking in Compose, funkcje typu „composable” powinny być wolne od efektów ubocznych. Gdy musisz wprowadzić zmiany w stanie aplikacji (jak opisano w dokumencie Zarządzanie stanem), użyj 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 praca wykonywana w nich jest związana z interfejsem i nie narusza jednokierunkowego przepływu danych , jak wyjaśniono w dokumentacji Zarządzanie stanem.

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

Aby wykonywać pracę przez cały cykl życia funkcji typu „composable” i mieć możliwość wywoływania funkcji zawieszających, użyj funkcji typu „composable” LaunchedEffect. Gdy LaunchedEffect wejdzie w skład, uruchomi współprogram z blokiem kodu przekazanym jako parametr. Współprogram zostanie anulowany, jeśli LaunchedEffect opuści skład. Jeśli LaunchedEffect zostanie ponownie skomponowany z innymi kluczami (patrz sekcja Ponowne uruchamianie efektów poniżej), dotychczasowy współprogram zostanie anulowany, a nowa funkcja zawieszająca zostanie uruchomiona w nowym współprogramie.

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 { 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 powyższym kodzie animacja używa funkcji zawieszającej delay aby poczekać przez określony czas. Następnie sekwencyjnie animuje wartość alfa do zera i z powrotem za pomocą animateTo. Będzie się to powtarzać przez cały cykl życia funkcji typu „composable”.

rememberCoroutineScope: uzyskiwanie zakresu uwzględniającego skład, aby uruchomić współprogram poza funkcją typu „composable”

Ponieważ LaunchedEffect jest funkcją typu „composable”, można jej używać tylko w innych funkcjach typu „composable”. Aby uruchomić współprogram poza funkcją typu „composable”, ale w zakresie, który zostanie automatycznie anulowany po opuszczeniu składu, użyj rememberCoroutineScope. Używaj też funkcji rememberCoroutineScope, gdy musisz ręcznie kontrolować cykl życia co najmniej 1 współprogramu, np. anulować animację po wystąpieniu zdarzenia użytkownika.

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

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 być ponownie uruchamiany, jeśli wartość się zmieni

LaunchedEffect jest ponownie uruchamiany, gdy zmieni się jeden z parametrów klucza. W niektórych sytuacjach możesz jednak chcieć przechwycić w efekcie wartość, która, jeśli się zmieni, nie powinna powodować ponownego uruchomienia efektu. Aby to zrobić, musisz użyć funkcji 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 Twoja aplikacja ma ekran LandingScreen, który znika po pewnym czasie. Nawet jeśli LandingScreen zostanie ponownie skomponowany, efekt, który czeka przez pewien czas i powiadamia o upływie czasu, 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 pasuje do cyklu ż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 lambda onTimeout zawsze zawiera najnowszą wartość, z którą LandingScreen został ponownie skomponowany, należy ją opakować funkcją rememberUpdatedState.onTimeout W efekcie należy użyć zwróconego State, czyli currentOnTimeout w kodzie.

DisposableEffect: efekty wymagające czyszczenia

W przypadku efektów ubocznych, które wymagają wyczyszczenia po zmianie kluczy lub jeśli funkcja typu „composable” opuści skład, użyj funkcji DisposableEffect. Jeśli klucze DisposableEffect ulegną zmianie, funkcja typu „composable” musi usunąć (wyczyścić) swój bieżący efekt i zresetować go, ponownie wywołując efekt.

Możesz na przykład wysyłać zdarzenia Analytics na podstawie Lifecycle zdarzeń za pomocą funkcji LifecycleObserver. Aby nasłuchiwać tych zdarzeń w Compose, użyj funkcji 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 observer do lifecycleOwner. Jeśli lifecycleOwner się zmieni, efekt zostanie usunięty i ponownie uruchomiony z nowym lifecycleOwner.

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ępniać stan Compose obiektom niezarządzanym przez Compose, użyj funkcji typu „composable” SideEffect. Użycie funkcji SideEffect gwarantuje, że efekt zostanie wykonany po każdym udanym ponownym skomponowaniu. Z drugiej strony, wykonywanie efektu przed zagwarantowaniem udanego ponownego skomponowania jest nieprawidłowe, co ma miejsce w przypadku pisania efektu bezpośrednio w funkcji typu „composable”.

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

produceState uruchamia współprogram w zakresie składu, który może przekazywać wartości do zwróconego State. Użyj go, aby przekonwertować stan inny niż Compose na stan Compose, np. aby przenieść do składu stan zewnętrzny oparty na subskrypcji, taki jak Flow, LiveData lub RxJava.

Producent jest uruchamiany, gdy produceState wejdzie w skład, i zostanie anulowany, gdy opuści skład. Zwrócony State jest spójny. Ustawienie tej samej wartości nie spowoduje rekompozycji.

Chociaż produceState tworzy współprogram, 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.

Poniższy przykład pokazuje, jak użyć funkcji produceState, aby wczytać obraz z sieci. Funkcja typu „composable” loadNetworkImage zwraca State, którego można używać w innych funkcjach typu „composable”.

@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 co najmniej 1 obiektu stanu na inny stan

W Compose rekompozycja 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 potrzebne do zaktualizowania interfejsu, co prowadzi do niepotrzebnej rekompozycji.

Gdy dane wejściowe funkcji typu „composable” zmieniają się częściej niż jest to potrzebne do ponownego skomponowania, użyj funkcji derivedStateOf. Często zdarza się to, gdy coś się często zmienia, np. pozycja przewijania, ale funkcja typu „composable” musi reagować na to tylko wtedy, gdy przekroczy określony próg. derivedStateOf tworzy nowy obiekt stanu Compose, który możesz obserwować i który będzie się aktualizować tylko wtedy, gdy będzie to konieczne. W ten sposób działa podobnie do operatora distinctUntilChanged() w Kotlin Flow.

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 zmieni się pierwszy widoczny element. Podczas przewijania wartość zmienia się na 0, 1, 2, 3, 4, 5 itd. Ponowna kompozycja musi jednak nastąpić tylko wtedy, gdy wartość jest większa niż 0. Ta niezgodność częstotliwości aktualizacji oznacza, że jest to dobry przypadek użycia funkcji derivedStateOf.

Nieprawidłowe użycie

Częstym błędem jest założenie, że gdy łączysz 2 obiekty stanu Compose, należy użyć funkcji derivedStateOf, ponieważ „wyprowadzasz stan”. Jest to jednak czysto narzut i nie jest wymagane, 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 się aktualizować tak samo często jak firstName i lastName. Dlatego nie występuje nadmierna rekompozycja i używanie funkcji derivedStateOf nie jest konieczne.

snapshotFlow: konwertowanie stanu Compose na przepływy

Użyj snapshotFlow , aby przekonwertować obiekty State<T> na zimny przepływ. snapshotFlow uruchamia swój blok po zebraniu i emituje wynik odczytanych w nim obiektów State. Gdy jeden z obiektów State odczytanych w bloku snapshotFlow ulegnie zmianie, 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).

Poniższy przykład pokazuje efekt uboczny, który rejestruje w Analytics, gdy użytkownik przewinie 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 przepływ, który może korzystać z możliwości operatorów przepływu.

Ponowne uruchamianie efektów

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

Typowa forma 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żywane do ponownego uruchomienia efektu nie są odpowiednie:

  • Ponowne uruchamianie efektów rzadziej niż powinno powoduje błędy w aplikacji.
  • Ponowne uruchamianie efektów częściej niż powinno może być nieefektywne.

Z reguły zmienne modyfikowalne i niemodyfikowalne używane w bloku kodu efektu należy dodać jako parametry do funkcji typu „composable” 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 opakować w rememberUpdatedState. Jeśli zmienna nigdy się nie zmienia, ponieważ jest opakowana funkcją remember bez kluczy, nie musisz przekazywać jej jako klucza do efektu.

W powyższym 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)
        }
    }
}

Funkcje currentOnStart i currentOnStop nie są potrzebne jako klucze DisposableEffect, ponieważ ich wartości nigdy się nie zmieniają w składzie ze względu na użycie funkcji rememberUpdatedState. Jeśli nie przekażesz funkcji lifecycleOwner jako parametru i ulegnie ona zmianie, funkcja HomeScreen zostanie ponownie skomponowana, ale funkcja DisposableEffect nie zostanie usunięta ani ponownie uruchomiona. Powoduje to problemy, ponieważ od tego momentu używana jest nieprawidłowa funkcja lifecycleOwner.

Stałe jako klucze

Jako klucz efektu możesz użyć stałej, np. true, aby śledzić cykl życia miejsca wywołania. Istnieją uzasadnione przypadki użycia, np. pokazany powyżej przykład LaunchedEffect. Zanim to zrobisz, zastanów się i upewnij, że tego właśnie potrzebujesz.