Skip to Content

Side Effects trong Jetpack Compose

Side effects là các thao tác ảnh hưởng đến bên ngoài scope của composable function. Compose cung cấp các APIs đặc biệt để xử lý side effects một cách an toàn.

1. Side Effects là gì?

Định nghĩa

Side effect là bất kỳ thay đổi nào visible bên ngoài composable function:

  • Gọi API hoặc database
  • Cập nhật shared state
  • Ghi log, analytics
  • Đăng ký/hủy đăng ký listeners
  • Navigate

Tại sao Composable không nên có Side Effects?

Composables có thể:

  • Chạy nhiều lần (recomposition)
  • Chạy song song trên nhiều threads
  • Bị hủy giữa chừng
  • Chạy theo bất kỳ thứ tự nào
// ❌ SAI: Side effect trực tiếp trong composable @Composable fun BadExample(viewModel: MyViewModel) { viewModel.loadData() // Gọi mỗi recomposition! Text("Loading...") } // ✅ ĐÚNG: Dùng Effect API @Composable fun GoodExample(viewModel: MyViewModel) { LaunchedEffect(Unit) { viewModel.loadData() // Chỉ gọi 1 lần } Text("Loading...") }

2. LaunchedEffect - Chạy suspend functions

Cách sử dụng

LaunchedEffect tạo coroutine scope và chạy suspend functions:

@Composable fun SnackbarDemo(message: String?) { val snackbarHostState = remember { SnackbarHostState() } // Chạy khi message thay đổi (và không null) LaunchedEffect(message) { message?.let { snackbarHostState.showSnackbar(it) } } Scaffold( snackbarHost = { SnackbarHost(snackbarHostState) } ) { } }

Lifecycle của LaunchedEffect

LaunchedEffect enters composition ├── Coroutine bắt đầu ├── Key thay đổi? │ ├── Yes: Cancel coroutine cũ → Start mới │ └── No: Tiếp tục chạy └── Leave composition → Cancel coroutine

Các loại Keys

// Key là Unit: Chỉ chạy 1 lần khi enter composition LaunchedEffect(Unit) { loadInitialData() } // Key là state: Restart khi state thay đổi LaunchedEffect(userId) { loadUserData(userId) } // Nhiều keys: Restart khi bất kỳ key nào thay đổi LaunchedEffect(userId, filter) { loadFilteredData(userId, filter) } // Key là true: "Bật/tắt" effect LaunchedEffect(isPolling) { if (isPolling) { while (true) { pollData() delay(5000) } } }

Ví dụ: Animation loop

@Composable fun PulsingDot(pulseRateMs: Long) { val alpha = remember { Animatable(1f) } LaunchedEffect(pulseRateMs) { while (isActive) { delay(pulseRateMs) alpha.animateTo(0.3f) alpha.animateTo(1f) } } Box( modifier = Modifier .size(50.dp) .alpha(alpha.value) .background(Color.Red, CircleShape) ) }

3. rememberCoroutineScope - Coroutine từ callbacks

Khi nào dùng?

Khi bạn cần launch coroutine từ event callback (onClick, etc.):

@Composable fun ScrollToTopButton(listState: LazyListState) { // Tạo scope gắn với lifecycle của composable này val scope = rememberCoroutineScope() Button(onClick = { // Launch từ callback, không phải từ composition scope.launch { listState.animateScrollToItem(0) } }) { Text("Scroll to Top") } }

So sánh LaunchedEffect vs rememberCoroutineScope

LaunchedEffectrememberCoroutineScope
Chạy trong compositionChạy từ callbacks
Tự động trigger bởi keyManual trigger
Không dùng được trong onClickDùng trong onClick

4. DisposableEffect - Setup và Cleanup

Khi nào dùng?

Khi bạn cần cleanup resources khi composable leave composition:

@Composable fun LifecycleObserver(onStart: () -> Unit, onStop: () -> Unit) { val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_START -> onStart() Lifecycle.Event.ON_STOP -> onStop() else -> {} } } // Setup: đăng ký observer lifecycleOwner.lifecycle.addObserver(observer) // Cleanup: hủy đăng ký khi leave composition hoặc key thay đổi onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } }

Ví dụ: Back press handler

@Composable fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) { val currentOnBack by rememberUpdatedState(onBack) val backCallback = remember { object : OnBackPressedCallback(enabled) { override fun handleOnBackPressed() { currentOnBack() } } } val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher DisposableEffect(backDispatcher) { backDispatcher?.addCallback(backCallback) onDispose { backCallback.remove() // Cleanup! } } // Update enabled state LaunchedEffect(enabled) { backCallback.isEnabled = enabled } }

5. SideEffect - Sync với non-Compose code

Khi nào dùng?

Khi bạn cần publish Compose state ra non-Compose code sau mỗi successful recomposition:

@Composable fun AnalyticsScreen(screenName: String) { // Chỉ chạy sau recomposition thành công SideEffect { Analytics.setCurrentScreen(screenName) } // UI content }

Ví dụ: Logging

@Composable fun CounterWithLogging() { var count by remember { mutableStateOf(0) } // Log sau mỗi successful recomposition SideEffect { Log.d("Counter", "Current count: $count") } Button(onClick = { count++ }) { Text("Count: $count") } }

6. rememberUpdatedState - Giữ reference mới nhất

Vấn đề

Khi LaunchedEffect chạy lâu, nó có thể giữ reference cũ:

// ❌ SAI: onTimeout là closure capture -> có thể outdated @Composable fun Timer(onTimeout: () -> Unit) { LaunchedEffect(Unit) { delay(5000) onTimeout() // Có thể gọi callback cũ! } }

Giải pháp

// ✅ ĐÚNG: Dùng rememberUpdatedState @Composable fun Timer(onTimeout: () -> Unit) { val currentOnTimeout by rememberUpdatedState(onTimeout) LaunchedEffect(Unit) { delay(5000) currentOnTimeout() // Luôn gọi callback mới nhất! } }

Khi nào dùng?

  • Callback có thể thay đổi nhưng bạn không muốn restart effect
  • Long-running effects cần giữ reference mới nhất

7. produceState - Chuyển đổi sang Compose State

Cách dùng

produceState chuyển đổi Flow, callback-based APIs, etc. thành Compose State:

@Composable fun NetworkImage(url: String): State<ImageResult> { return produceState<ImageResult>( initialValue = ImageResult.Loading, key1 = url ) { value = try { val image = imageLoader.load(url) ImageResult.Success(image) } catch (e: Exception) { ImageResult.Error(e) } } } // Sử dụng @Composable fun Avatar(url: String) { val imageState by NetworkImage(url) when (val result = imageState) { is ImageResult.Loading -> CircularProgressIndicator() is ImageResult.Success -> Image(result.image) is ImageResult.Error -> ErrorPlaceholder() } }

Với Flow

@Composable fun <T> Flow<T>.collectAsStateWithLifecycle( initial: T ): State<T> { val lifecycleOwner = LocalLifecycleOwner.current return produceState(initialValue = initial) { lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { value = it } } } }

8. derivedStateOf - Tính toán từ state khác

Vấn đề

// ❌ SAI: Tính toán mỗi recomposition (dù items không đổi) @Composable fun ShoppingCart(items: List<Item>) { val total = items.sumOf { it.price } // Expensive! Text("Total: $total") }

Giải pháp

// ✅ ĐÚNG: Chỉ tính lại khi items thay đổi @Composable fun ShoppingCart(items: List<Item>) { val total by remember { derivedStateOf { items.sumOf { it.price } } } Text("Total: $total") }

Ví dụ: Show FAB khi scroll

@Composable fun ScrollableFab() { val listState = rememberLazyListState() // Chỉ update khi điều kiện thay đổi, không phải mỗi scroll pixel val showFab by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } Scaffold( floatingActionButton = { if (showFab) { FloatingActionButton(onClick = { }) { Icon(Icons.Default.KeyboardArrowUp, null) } } } ) { LazyColumn(state = listState) { items(100) { Text("Item $it") } } } }

9. snapshotFlow - Chuyển State thành Flow

Cách dùng

@Composable fun ScrollAnalytics(listState: LazyListState) { LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { index -> Analytics.logScroll(index) } } }
@Composable fun SearchScreen() { var query by remember { mutableStateOf("") } var results by remember { mutableStateOf<List<Result>>(emptyList()) } LaunchedEffect(Unit) { snapshotFlow { query } .debounce(300) // Debounce 300ms .filter { it.length >= 3 } .distinctUntilChanged() .collectLatest { searchQuery -> results = searchApi.search(searchQuery) } } Column { TextField(value = query, onValueChange = { query = it }) LazyColumn { items(results) { ResultItem(it) } } } }

📝 Tóm tắt

EffectMục đíchKey behavior
LaunchedEffectSuspend functionsRestart khi key thay đổi
rememberCoroutineScopeCoroutine từ callbacksManual trigger
DisposableEffectSetup/cleanup resourcesonDispose được gọi
SideEffectSync với non-ComposeSau mỗi successful recomposition
rememberUpdatedStateGiữ reference mới nhấtKhông trigger restart
produceStateChuyển thành Compose StateAutomatic State
derivedStateOfTính toán từ statesLazy evaluation
snapshotFlowState → FlowEmit khi state thay đổi

Quyết định dùng Effect nào?

Cần chạy suspend function? ├── Từ composition → LaunchedEffect └── Từ callback → rememberCoroutineScope Cần cleanup? └── DisposableEffect với onDispose Cần sync state ra ngoài? └── SideEffect Cần chuyển đổi data sources? ├── Non-Compose → Compose State → produceState ├── State → State (derived) → derivedStateOf └── State → Flow → snapshotFlow
Last updated on