Efek samping di Compose adalah perubahan pada status aplikasi yang terjadi di luar cakupan fungsi composable. Karena siklus proses dan properti composable seperti rekomposisi yang tidak dapat diprediksi, menjalankan rekomposisi composable dalam urutan yang berbeda, atau rekomposisi yang dapat dihapus, composable yang ideal sebaiknya bebas efek samping.
Namun, terkadang efek samping diperlukan, misalnya, untuk memicu peristiwa satu kali seperti menampilkan snackbar atau membuka layar lain mengingat kondisi status tertentu. Tindakan ini harus dipanggil dari lingkungan terkontrol yang mendukung 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 melakukan pekerjaan selama masa pakai composable dan memiliki kemampuan untuk memanggil
fungsi penangguhan, gunakan
LaunchedEffect
composable. 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, berikut ini animasi yang menggerakkan nilai alfa dengan penundaan yang dapat dikonfigurasi:
// 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) } }
Pada kode di atas, animasi menggunakan fungsi penangguhan
delay
untuk menunggu selama waktu yang ditetapkan. Kemudian, model ini secara berurutan membuat animasi
ke nol dan kembali lagi menggunakan
animateTo
Tindakan ini akan diulangi selama masa pakai composable.
rememberCoroutineScope
: mendapatkan 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(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
: mereferensikan nilai dalam efek yang tidak akan 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 memerlukan pembersihan
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
SideEffect
composable. Penggunaan SideEffect
menjamin bahwa efek akan dieksekusi setelah setiap
rekomposisi berhasil. Di sisi lain, Anda tidak akan
melakukan efek sebelum rekomposisi berhasil dijamin, yang merupakan
terjadi saat menulis efek secara langsung dalam composable.
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 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
: 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 = 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
: mengonversi satu atau beberapa objek status menjadi status lain
Di Compose, rekomposisi terjadi setiap kali objek status yang diamati atau input composable berubah. Objek status atau {i>input<i} mungkin lebih sering berubah daripada yang perlu diperbarui UI, yang menyebabkan rekomposisi yang tidak perlu.
Anda harus menggunakan derivedStateOf
jika input ke composable lebih sering berubah dari yang Anda perlukan
untuk merekomposisi. Hal ini sering terjadi ketika ada sesuatu yang sering berubah, seperti
posisi scroll, tetapi composable hanya perlu bereaksi setelah melewatinya
batas tertentu. derivedStateOf
membuat objek status Compose baru yang Anda
dapat mengamati bahwa hanya
pembaruan sebanyak yang Anda butuhkan. Dengan cara ini, komponen itu bertindak
mirip dengan Flow Kotlin
distinctUntilChanged()
operator.
Penggunaan yang benar
Cuplikan berikut menunjukkan kasus penggunaan yang sesuai untuk 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() } } }
Dalam cuplikan ini, firstVisibleItemIndex
berubah kapan saja item pertama yang terlihat
perubahan. Saat Anda men-scroll, nilai menjadi 0
, 1
, 2
, 3
, 4
, 5
, dll.
Namun, rekomposisi hanya perlu terjadi jika nilai lebih besar dari 0
.
Ketidakcocokan dalam frekuensi pembaruan ini berarti bahwa ini adalah kasus penggunaan yang baik untuk
derivedStateOf
.
Penggunaan yang salah
Kesalahan yang umum adalah mengasumsikan bahwa, saat Anda menggabungkan dua objek status Compose,
Anda harus menggunakan derivedStateOf
karena Anda "mendapatkan status". Namun,
hanya merupakan overhead dan tidak diperlukan, seperti ditunjukkan dalam cuplikan berikut:
// 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
Dalam cuplikan ini, fullName
harus diperbarui sesering firstName
dan
lastName
. Oleh karena itu, tidak ada rekomposisi berlebih yang terjadi, dan menggunakan
derivedStateOf
tidak diperlukan.
snapshotFlow
: mengonversi Status Compose menjadi Flow
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 berfungsi sebagai parameter
yang digunakan oleh lifecycleOwner
dalam bloknya karena setiap perubahan pada kode tersebut akan 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 Anda tidak meneruskan lifecycleOwner
sebagai parameter dan
berubah, HomeScreen
akan merekomposisi, tetapi DisposableEffect
tidak akan dibuang
dan dimulai ulang. Hal 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.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Status dan Jetpack Compose
- Kotlin untuk Jetpack Compose
- Menggunakan View di Compose