Efekty uboczne w funkcji tworzenia wiadomości

Efekt uboczny to zmiana stanu aplikacji spoza zakresu funkcji kompozycyjnej. Ze względu na cykl życia i właściwości takich jak nieprzewidywalne rekompozycje, ponowne kompozycje w różnych kolejnościach czy kompozycje, które można odrzucić, powinny być pozbawione efektów ubocznych.

Czasami jednak skutki uboczne są konieczne, aby wywołać jednorazowe zdarzenie, takie jak wyświetlenie paska powiadomień lub przejście na inny ekran w określonym stanie. Działania te należy wywoływać w kontrolowanym środowisku, które wie o cyklu życia elementu kompozycyjnego. Na tej stronie poznasz różne oferty dotyczące interfejsów API Jetpack Compose w celu uzyskania efektu ubocznego.

Stany i efekty użycia

Zgodnie z dokumentacją Thinking in Compose w komponencie kompozycyjnym elementy kompozycyjne nie powinny zawierać efektów ubocznych. Jeśli chcesz zmienić stan aplikacji (zgodnie z opisem w dokumencie Zarządzanie stanem w dokumentacji), użyj interfejsów Effect API, aby skutki uboczne były wykonywane w przewidywalny sposób.

Ze względu na różne możliwości dostępne w widoku tworzenia wiadomości łatwo jest je nadużywać. Zadbaj o to, by czynności wykonywane w nich były związane z interfejsem użytkownika i nie zakłócały jednokierunkowego przepływu danych, co zostało opisane w dokumentacji stanu zarządzania.

LaunchedEffect: uruchamianie funkcji zawieszania w zakresie elementu kompozycyjnego

Aby bezpiecznie wywołać funkcje zawieszania z poziomu funkcji kompozycyjnej, użyj funkcji LaunchedEffect. Gdy LaunchedEffect wchodzi do kompozycji, uruchamia współprogram z blokiem kodu przekazywanym jako parametr. Jeśli LaunchedEffect opuści kompozycję, współprogram zostanie anulowany. Jeśli LaunchedEffect zostanie skomponowany ponownie z innymi kluczami (zobacz sekcję Efekty ponownego uruchamiania poniżej), istniejąca współpraca zostanie anulowana, a nowa funkcja zawieszenia zostanie uruchomiona w nowej.

Na przykład do wyświetlenia elementu Snackbar w elemencie Scaffold służy funkcja SnackbarHostState.showSnackbar, która jest funkcją zawieszania.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    snackbarHostState: SnackbarHostState
) {

    // If the UI state contains an error, show snackbar
    if (state.hasError) {

        // `LaunchedEffect` will cancel and re-launch if
        // `scaffoldState.snackbarHostState` changes
        LaunchedEffect(snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `state.hasError` is false, and only start when `state.hasError` is true
            // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
            snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(
        snackbarHost = {
            SnackbarHost(hostState = snackbarHostState)
        }
    ) { contentPadding ->
        // ...
    }
}

W powyższym kodzie współprogram jest aktywowany, gdy stan zawiera błąd, a w przeciwnym razie jest usuwany. Witryna wywołania LaunchedEffect znajduje się wewnątrz instrukcji if, jeśli więc stwierdzenie ma wartość fałsz, jeśli element LaunchedEffect znajdował się w kompozycji, zostanie usunięty, a tym samym anulowana.

rememberCoroutineScope: uzyskaj zakres uwzględniający kompozycję w celu uruchomienia współpracy poza funkcją kompozycyjną

LaunchedEffect to funkcja kompozycyjna, więc można jej używać tylko w innych funkcjach kompozycyjnych. Jeśli chcesz uruchomić współpracę poza funkcją kompozycyjną, ale z określonym zakresem, tak aby była ona automatycznie anulowana po opuszczeniu kompozycji, użyj funkcji rememberCoroutineScope. rememberCoroutineScope używaj też wtedy, gdy musisz ręcznie kontrolować cykl życia co najmniej 1 korekty, na przykład anulować animację po wystąpieniu zdarzenia użytkownika.

rememberCoroutineScope to funkcja kompozycyjna, która zwraca CoroutineScope powiązany z punktem kompozycji, w którym jest wywołana. Zakres zostanie anulowany, gdy połączenie opuści kompozycję.

Korzystając z poprzedniego przykładu, możesz użyć tego kodu, aby wyświetlać 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 się wznowić w przypadku zmiany wartości.

LaunchedEffect uruchamia się ponownie, gdy zmieni się jeden z kluczowych parametrów. Jednak w niektórych sytuacjach możesz chcieć uchwycić w efekcie wartość, która gdy się zmieni, nie chcesz, aby efekt zaczął się powtarzać. Aby to zrobić, musisz użyć funkcji rememberUpdatedState i utworzyć odwołanie do tej wartości, które można przechwycić i zaktualizować. Ta metoda jest przydatna w przypadku efektów, które obejmują długotrwałe operacje, których odtworzenie i ponowne uruchomienie może być kosztowne lub trudne.

Załóżmy np., że aplikacja ma LandingScreen, który po pewnym czasie znika. Nawet jeśli właściwość LandingScreen zostanie skomponowana ponownie, efekt, który czeka trochę czasu i powiadamia, że czas nie powinien być uruchamiany ponownie:

@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 witryny generującej połączenia, jako parametr jest przekazywana stała niezmienna stała, np. Unit lub true. W powyższym kodzie używany jest LaunchedEffect(true). Aby mieć pewność, że lambda onTimeout zawsze zawiera najnowszą wartość, z którą skomponowano element LandingScreen, trzeba dodać do pliku onTimeout funkcję rememberUpdatedState. W efekcie należy użyć zwróconego w kodzie State (currentOnTimeout).

DisposableEffect: efekty wymagające wyczyszczenia

W przypadku efektów ubocznych, które trzeba wyczyścić po zmianie klawiszy lub jeśli funkcja kompozycyjna opuszcza kompozycję, użyj DisposableEffect. Jeśli klucze DisposableEffect zmienią się, funkcja kompozycyjna musi zrzucić (wyczyścić) swój bieżący efekt i zresetować go, ponownie wywołując ten efekt.

Możesz np. wysyłać zdarzenia analityczne na podstawie Lifecycle zdarzeń za pomocą metody LifecycleObserver. Aby nasłuchiwać tych zdarzeń w funkcji tworzenia, użyj DisposableEffect do zarejestrowania i wyrejestrowania obserwatora w razie potrzeby.

@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 spowoduje dodanie observer do elementu lifecycleOwner. Jeśli lifecycleOwner się zmieni, efekt zostanie usunięty i uruchomiony 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 podczas kompilacji.

SideEffect: publikowanie stanu tworzenia wiadomości do kodu, który nie służy do tworzenia wiadomości

Aby udostępnić stan tworzenia obiektom, które nie są zarządzane przez funkcję tworzenia, użyj funkcji kompozycyjnej SideEffect. Użycie właściwości SideEffect gwarantuje, że efekt zostanie wykonany po każdej udanej zmianie kompozycji. Z drugiej strony nie można uzyskać efektu przed zagwarantowaniem pomyślnej rekompozycji, co ma miejsce przy pisaniu efektu bezpośrednio w komponencie.

Na przykład biblioteka Analytics może umożliwiać podzielenie użytkowników na segmenty przez dołączanie niestandardowych metadanych (w tym przykładzie „właściwości użytkownika”) do wszystkich kolejnych zdarzeń Analytics. Aby przekazać informacje o typie bieżącego użytkownika do biblioteki Analytics, zaktualizuj jego wartość za pomocą funkcji 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: przekształć stan niezwiązany z tworzeniem w stan tworzenia wiadomości

produceState uruchamia współpracę o zakresie na poziomie kompozycji, która może przekazywać wartości do zwróconego elementu State. Służy on do przekształcania stanu nietworzenia w tworzenie wiadomości w stan tworzenia, np. do przeniesienia do kompozycji zewnętrznego stanu opartego na subskrypcji, takiego jak Flow, LiveData lub RxJava.

Producent jest uruchamiany, gdy produceState wejdzie do kompozycji, i zostanie anulowany, gdy opuści kompozycję. Zwracane wartości State są scalone. Ustawienie tej samej wartości nie spowoduje zmiany kompozycji.

Mimo że produceState tworzy współpracę, może też służyć do obserwowania niezawieszających źródeł danych. 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: konwertuj jeden lub wiele obiektów stanu na inny stan.

W widoku tworzenia rekompozycja ma miejsce za każdym razem, gdy obiekt stanu zaobserwowanego lub zmiany w danych wejściowych kompozycyjnych są modyfikowane. Obiekt stanu lub dane wejściowe mogą się zmieniać częściej niż w rzeczywistości interfejsu użytkownika, co prowadzi do niepotrzebnej zmiany kompozycji.

Funkcji derivedStateOf należy używać, gdy dane wejściowe funkcji kompozycyjnej zmieniają się częściej niż trzeba utworzyć ponownie. Dzieje się tak często, gdy coś się często zmienia, na przykład pozycja przewijania, ale funkcja kompozycyjna musi zareagować na nią dopiero wtedy, gdy przekroczy określony próg. derivedStateOf tworzy nowy obiekt stanu tworzenia wiadomości, który widać, że aktualizowana jest tylko tyle, ile potrzebujesz. W ten sposób działa podobnie do operatora Kotlin Flows distinctUntilChanged().

Prawidłowe użycie

Ten fragment kodu pokazuje odpowiedni przypadek użycia właściwości 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 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 itp. Zmiana ma jednak miejsce tylko wtedy, gdy wartość jest większa niż 0. Ta niezgodność w częstotliwości aktualizacji oznacza, że to dobry przypadek użycia dla funkcji derivedStateOf.

Nieprawidłowe użycie

Częstym błędem jest założenie, że łączenie 2 obiektów stanu tworzenia wymaga użycia derivedStateOf, ponieważ jest to „stan pochodny”. Nie jest to jednak wymagane, jak widać na przykładzie tego fragmentu 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 aplikacja fullName musi aktualizować się równie często jak firstName i lastName. W związku z tym nie występuje nadmierna zmiana kompozycji, a używanie właściwości derivedStateOf nie jest konieczne.

snapshotFlow: przekonwertuj stan tworzenia na przepływy

Za pomocą funkcji snapshotFlow przekonwertuj obiekty State<T> na zimny przepływ. Po zebraniu snapshotFlow uruchamia swój blok i emituje wynik odczytywanych w nim obiektów State. Gdy mutuje jeden z obiektów State odczytanych w bloku snapshotFlow, przepływ emituje nową wartość do swojego kolektora, jeśli nowa wartość nie równa się wcześniej wyemitowanej wartości (to zachowanie jest podobne do działania Flow.distinctUntilChanged).

Ten przykład przedstawia efekt uboczny, który rejestruje, gdy użytkownik przewinie stronę poza pierwszy element listy, aby zobaczyć statystyki:

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

Ponownie uruchamiam efekty

Niektóre efekty w trybie tworzenia, takie jak LaunchedEffect, produceState czy DisposableEffect, przyjmują zmienną liczbę argumentów (kluczy), które są używane do anulowania działania funkcji i rozpoczęcia nowego działania z nowymi kluczami.

Typowa postać tych interfejsów API to:

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

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

  • Mniejsze ponowne uruchamianie efektów może powodować błędy w aplikacji.
  • Ponowne uruchamianie efektów częściej niż powinno być nieskuteczne.

Ogólnie zmienne zmienne i stałe używane w bloku efektów kodu należy dodawać jako parametry do efektu kompozycyjnego. Aby wymusić ponowne uruchomienie efektu, można dodać więcej parametrów. Jeśli zmiana zmiennej nie powinna powodować ponownego uruchomienia efektu, umieść ją wewnątrz zakresu rememberUpdatedState. Jeśli zmienna nigdy się nie zmienia, ponieważ jest ujęta w element remember bez kluczy, nie musisz przekazywać zmiennej jako klucza do efektu.

W kodzie DisposableEffect pokazanym powyżej efekt przyjmuje jako parametr użyty w bloku przez 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 się nie zmienia w kompozycji z powodu użycia rememberUpdatedState. Jeśli nie prześlesz parametru lifecycleOwner jako parametru i zostanie on zmieniony, HomeScreen utworzy nową kompozycję, ale DisposableEffect nie zostanie usunięty i uruchomiony ponownie. Powoduje to problemy, ponieważ od tego momentu używany jest nieprawidłowy element lifecycleOwner.

Stałe jako klucze

Możesz użyć stałej typu true jako klucza efektu, aby przestrzegał cyklu życia witryny wywołania. Istnieją pewne przypadki użycia tej funkcji, jak w przykładzie powyżej LaunchedEffect. Jednak zanim to zrobisz, zastanów się, czy tego potrzebujesz.