Efek samping di Compose

Composable harus bebas efek samping. Namun, jika diperlukan untuk mengubah status aplikasi, composable harus dipanggil dari lingkungan terkontrol yang mengetahui siklus proses composable. Di halaman ini, Anda akan mempelajari berbagai API efek samping yang ditawarkan Jetpack Compose.

Kasus penggunaan status dan efek

Seperti yang dibahas dalam dokumentasi Paradigma Compose, composable harus bebas efek samping. Saat Anda perlu melakukan perubahan pada status aplikasi (seperti yang dijelaskan dalam dokumen dokumentasi Mengelola status), Anda harus menggunakan Effect API agar efek samping tersebut dieksekusi dengan cara yang dapat diprediksi.

Karena berbagai kemungkinan efek terbuka di Compose, efek tersebut dapat dengan mudah digunakan secara berlebihan. Pastikan tugas yang Anda lakukan di dalamnya berkaitan dengan UI dan tidak merusak aliran data searah seperti yang dijelaskan dalam dokumentasi Mengelola status.

LaunchedEffect: menjalankan fungsi penangguhan dalam cakupan composable

Untuk memanggil fungsi penangguhan dengan aman dari dalam composable, gunakan composable LaunchedEffect. Saat memasuki Komposisi, LaunchedEffect akan meluncurkan coroutine dengan blok kode yang diteruskan sebagai parameter. Coroutine akan dibatalkan jika LaunchedEffect keluar dari komposisi. Jika LaunchedEffect dikomposisi ulang dengan kunci yang berbeda (lihat bagian Memulai Ulang Efek di bawah), coroutine yang ada akan dibatalkan dan fungsi penangguhan yang baru akan diluncurkan dalam coroutine baru.

Misalnya, menampilkan Snackbar dalam Scaffold dilakukan dengan fungsi SnackbarHostState.showSnackbar, yang merupakan fungsi penangguhan.

@Composable
fun MyScreen(
    state: UiState<List<Movie>>,
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {

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

        // `LaunchedEffect` will cancel and re-launch if
        // `scaffoldState.snackbarHostState` changes
        LaunchedEffect(scaffoldState.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.
            scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
        }
    }

    Scaffold(scaffoldState = scaffoldState) {
        /* ... */
    }
}

Pada kode di atas, coroutine akan dipicu jika status berisi error dan akan dibatalkan jika tidak berisi error. Karena situs panggilan LaunchedEffect ada di dalam pernyataan if, jika pernyataan salah, LaunchedEffect if berada di Komposisi, situs akan dihapus dan coroutine akan dibatalkan.

rememberCoroutineScope: memperoleh cakupan yang mendukung komposisi untuk meluncurkan coroutine di luar composable

Karena LaunchedEffect adalah fungsi yang dapat dikomposisi, fungsi ini hanya dapat digunakan di dalam fungsi composable. Untuk meluncurkan coroutine di luar composable, tetapi disertakan agar otomatis dibatalkan setelah keluar dari komposisi, gunakan rememberCoroutineScope. Selain itu, gunakan rememberCoroutineScope setiap kali Anda perlu mengontrol siklus proses dari satu atau beberapa coroutine secara manual, misalnya, membatalkan animasi saat terjadi peristiwa pengguna.

rememberCoroutineScope adalah fungsi yang dapat dikomposisi yang menampilkan CoroutineScope yang terikat ke titik Komposisi tempatnya dipanggil. Cakupan akan dibatalkan saat panggilan keluar dari Komposisi.

Dengan mengikuti contoh sebelumnya, Anda dapat menggunakan kode ini untuk menampilkan Snackbar saat pengguna mengetuk Button:

@Composable
fun MoviesScreen(scaffoldState: ScaffoldState = rememberScaffoldState()) {

    // Creates a CoroutineScope bound to the MoviesScreen's lifecycle
    val scope = rememberCoroutineScope()

    Scaffold(scaffoldState = scaffoldState) {
        Column {
            /* ... */
            Button(
                onClick = {
                    // Create a new coroutine in the event handler
                    // to show a snackbar
                    scope.launch {
                        scaffoldState.snackbarHostState
                            .showSnackbar("Something happened!")
                    }
                }
            ) {
                Text("Press me")
            }
        }
    }
}

rememberUpdatedState: mereferensikan nilai dalam efek yang seharusnya tidak dimulai ulang jika nilai berubah

LaunchedEffect dimulai ulang saat salah satu parameter utama berubah. Namun, dalam beberapa situasi Anda mungkin ingin menangkap nilai dalam efek yang, jika berubah, Anda tidak ingin efek dimulai ulang. Untuk melakukannya, gunakan rememberUpdatedState untuk membuat referensi ke nilai ini yang dapat diambil dan diupdate. Pendekatan ini berguna untuk efek yang berisi operasi berdurasi panjang yang mungkin mahal atau sulit untuk dibuat ulang dan dimulai ulang.

Misalnya, aplikasi Anda memiliki LandingScreen yang hilang setelah beberapa saat. Meskipun LandingScreen dikomposisi ulang, efek yang menunggu beberapa saat dan memberi tahu bahwa waktu yang telah berlalu tidak boleh dimulai ulang:

@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 */
}

Untuk membuat efek yang cocok dengan siklus proses situs panggilan, konstanta yang tidak pernah berubah seperti Unit atau true diteruskan sebagai parameter. Pada kode di atas, LaunchedEffect(true) digunakan. Untuk memastikan bahwa lambda onTimeout selalu berisi nilai terbaru dengan LandingScreen dikomposisi ulang, onTimeout harus digabungkan dengan fungsi rememberUpdatedState. State yang ditampilkan, currentOnTimeout dalam kode, harus digunakan dalam efek.

DisposableEffect: efek yang perlu dibersihkan

Untuk efek samping yang perlu dibersihkan setelah kunci berubah atau jika composable keluar dari Komposisi, gunakan DisposableEffect. Jika kunci DisposableEffect berubah, composable harus membuang (melakukan pembersihan) efek saat ini, dan mereset dengan memanggil efek lagi.

Contohnya, Anda mungkin ingin mengirim peristiwa analisis berdasarkan peristiwa Lifecycle dengan menggunakan LifecycleObserver. Untuk memproses peristiwa tersebut di Compose, gunakan DisposableEffect untuk mendaftar dan membatalkan pendaftaran observer saat diperlukan.

@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 */
}

Pada kode di atas, efek akan menambahkan observer ke lifecycleOwner. Jika lifecycleOwner berubah, efek akan dibuang dan dimulai ulang dengan lifecycleOwner baru.

DisposableEffect harus menyertakan klausul onDispose sebagai pernyataan akhir dalam blok kodenya. Jika tidak, IDE akan menampilkan error waktu-build.

SideEffect: memublikasikan status Compose ke kode non-compose

Untuk membagikan status Compose dengan objek yang tidak dikelola oleh Compose, gunakan composable SideEffect, karena dipanggil pada setiap rekomposisi yang berhasil.

Contohnya, library analisis dapat digunakan untuk mengelompokkan populasi pengguna dengan melampirkan metadata khusus ("properti pengguna" pada contoh ini) ke semua peristiwa analisis berikutnya. Untuk memberitahukan jenis pengguna dari pengguna saat ini ke library analisis Anda, gunakan SideEffect untuk memperbarui nilainya.

@Composable
fun rememberAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        /* ... */
    }

    // On every successful composition, update the analytics library 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: mengonversi status non-Compose menjadi status Compose

produceState meluncurkan coroutine yang dicakupkan ke Komposisi yang dapat mendorong nilai ke dalam State yang ditampilkan. Gunakan coroutine tersebut untuk mengonversi status non-Compose menjadi status Compose, misalnya menerapkan status berbasis langganan eksternal seperti Flow, LiveData, atau RxJava ke dalam Komposisi.

Producer diluncurkan saat produceState memasuki Komposisi, dan akan dibatalkan saat keluar dari Komposisi. State yang ditampilkan bercampur; menyetel nilai yang sama tidak akan memicu rekomposisi.

Meskipun membuat coroutine, produceState juga dapat digunakan untuk mengamati sumber data yang tidak ditangguhkan. Untuk menghapus langganan ke sumber tersebut, gunakan fungsi awaitDispose.

Contoh berikut menunjukkan cara menggunakan produceState untuk memuat gambar dari jaringan. Fungsi yang dapat dikomposisi loadNetworkImage menampilkan State yang dapat digunakan di composable lain.

@Composable
fun loadNetworkImage(
    url: String,
    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 keys.
    return produceState(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: mengonversi satu atau beberapa objek status menjadi status lain

Gunakan derivedStateOf saat status tertentu dihitung atau diambil dari objek status lainnya. Penggunaan fungsi ini menjamin bahwa penghitungan hanya akan terjadi setiap kali salah satu status yang digunakan dalam penghitungan berubah.

Contoh berikut menampilkan Daftar Tugas dasar yang tugasnya dengan kata kunci prioritas tinggi buatan pengguna muncul terlebih dahulu:

@Composable
fun TodoList(
    highPriorityKeywords: List<String> = listOf("Review", "Unblock", "Compose")
) {
    val todoTasks = remember { mutableStateListOf<String>() }

    // Calculate high priority tasks only when the todoTasks or
    // highPriorityKeywords change, not on every recomposition
    val highPriorityTasks by remember {
        derivedStateOf {
            todoTasks.filter { it.containsWord(highPriorityKeywords) }
        }
    }

    Box(Modifier.fillMaxSize()) {
        LazyColumn {
            items(highPriorityTasks) { /* ... */ }
            items(todoTasks) { /* ... */ }
        }
        /* Rest of the UI where users can add elements to the list */
    }
}

Dalam kode di atas, derivedStateOf menjamin bahwa setiap todoTasks atau highPriorityKeywords berubah, penghitungan highPriorityTasks akan terjadi dan UI akan diupdate sesuai dengan itu. Karena pemfilteran untuk menghitung highPriorityTasks bisa menjadi mahal, pemfilteran hanya boleh dijalankan jika ada daftar yang berubah, bukan pada setiap rekomposisi.

Selain itu, pembaruan pada status yang dihasilkan oleh derivedStateOf tidak menyebabkan composable dideklarasikan untuk dikomposisi ulang, Compose hanya akan mengomposisi ulang composable dengan status yang ditampilkan dibaca, di dalam LazyColumn dalam contoh.

snapshotFlow: mengonversi Status Compose ke dalam Alur

Gunakan snapshotFlow untuk mengonversi objek State<T> ke dalam Alur dingin. snapshotFlow menjalankan bloknya saat dikumpulkan dan menampilkan hasil objek State yang dibaca di dalamnya. Saat salah satu objek State yang dibaca di dalam blok snapshotFlow berubah, Alur akan memunculkan nilai baru ke kolektornya jika nilai baru tidak sama dengan nilai yang muncul sebelumnya (perilaku ini mirip dengan Flow.distinctUntilChanged).

Contoh berikut menunjukkan efek samping yang dicatat saat pengguna men-scroll melewati item pertama dalam daftar ke analisis:

val listState = rememberLazyListState()

LazyColumn(state = listState) {
    // ...
}

LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .map { index -> index > 0 }
        .distinctUntilChanged()
        .filter { it == true }
        .collect {
            MyAnalyticsService.sendScrolledPastFirstItemEvent()
        }
}

Dalam kode di atas, listState.firstVisibleItemIndex dikonversi menjadi Alur yang dapat memanfaatkan kekuatan operator Alur.

Memulai ulang efek

Beberapa efek di Compose, seperti LaunchedEffect, produceState, atau DisposableEffect, mengambil sejumlah variabel argumen, kunci, yang digunakan untuk membatalkan efek yang sedang berjalan dan memulai yang baru dengan kunci baru.

Bentuk umum untuk API ini adalah:

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

Karena kerumitan perilaku ini, masalah dapat terjadi jika parameter yang digunakan untuk memulai ulang efek tidak tepat:

  • Memulai ulang efek kurang dari seharusnya dapat menyebabkan bug di aplikasi Anda.
  • Memulai ulang efek lebih dari seharusnya bisa menjadi tidak efisien.

Prinsipnya adalah variabel yang dapat diubah dan tidak dapat diubah yang digunakan dalam blok kode efek harus ditambahkan sebagai parameter ke composable efek. Selain itu, parameter lainnya dapat ditambahkan untuk memaksa efek dimulai ulang. Jika perubahan variabel tidak menyebabkan efek dimulai ulang, variabel harus digabungkan dalam rememberUpdatedState. Jika variabel tidak pernah berubah karena digabungkan dalam remember tanpa kunci, Anda tidak perlu meneruskan variabel sebagai kunci ke efek.

Pada kode DisposableEffect yang ditampilkan di atas, efek yang diambil sebagai parameter yang digunakan oleh lifecycleOwner dalam bloknya karena setiap perubahan pada kode tersebut harus menyebabkan efek dimulai ulang.

@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 dan currentOnStop tidak diperlukan sebagai kunci DisposableEffect karena nilainya tidak pernah berubah dalam Komposisi akibat penggunaan rememberUpdatedState. Jika lifecycleOwner tidak lulus sebagai parameter dan berubah, HomeScreen akan merekomposisi tetapi DisposableEffect tidak akan dibuang dan dimulai ulang. Ini menyebabkan masalah karena lifecycleOwner yang salah digunakan dari titik tersebut dan seterusnya.

Konstanta sebagai kunci

Anda dapat menggunakan konstanta seperti true sebagai kunci efek untuk membuatnya mengikuti siklus proses situs panggilan. Terdapat kasus penggunaan yang valid untuk hal ini, seperti contoh LaunchedEffect yang ditampilkan di atas. Namun, sebelum melakukannya, pikirkan kembali dan pastikan hal tersebut adalah yang Anda perlukan.