Skip to Content

Advanced Compose

Lưu ý: Các chủ đề Side Effects, Animation, và CompositionLocal đã được tách thành các bài riêng:

Bài này tập trung vào các kỹ thuật nâng cao khác.

1. CompositionLocal là gì?

Vấn đề: Truyền data qua nhiều tầng

Hãy tưởng tượng bạn có một ứng dụng với cấu trúc như sau:

App └── MainScreen └── UserProfile └── UserAvatar └── AvatarImage ← Cần biết theme color

Nếu AvatarImage cần biết theme color, bạn phải truyền qua 4 tầng composables! Điều này rất bất tiện và tạo ra “prop drilling”.

Giải pháp: CompositionLocal

CompositionLocal cho phép truyền data ngầm định xuống cây composables mà không cần truyền qua từng tầng.

// Bước 1: Tạo CompositionLocal val LocalAppThemeColor = staticCompositionLocalOf { Color.Blue } // Bước 2: Provide giá trị ở tầng cao @Composable fun App() { CompositionLocalProvider( LocalAppThemeColor provides Color.Red ) { MainScreen() // Tất cả con đều có thể đọc LocalAppThemeColor } } // Bước 3: Đọc giá trị ở bất kỳ đâu bên trong @Composable fun AvatarImage() { val themeColor = LocalAppThemeColor.current // Lấy được Color.Red! Box(modifier = Modifier.background(themeColor)) }

Các CompositionLocal có sẵn

Android đã cung cấp một số CompositionLocal phổ biến:

// Context val context = LocalContext.current // Lifecycle val lifecycleOwner = LocalLifecycleOwner.current // Configuration (orientation, locale) val config = LocalConfiguration.current // Density (for dp ↔ px conversions) val density = LocalDensity.current

2. Side Effects là gì?

Tại sao cần Side Effects?

Composable function nên pure (không có side effects) - chỉ nên mô tả UI dựa trên input.

Nhưng đôi khi bạn cần:

  • Gọi API khi màn hình mở
  • Đăng ký/hủy đăng ký listener
  • Cập nhật code bên ngoài Compose

Compose cung cấp các Side Effect APIs để xử lý các tình huống này một cách an toàn.

LaunchedEffect

Mục đích: Chạy suspend code khi composable được tạo.

@Composable fun UserScreen(userId: Int) { var user by remember { mutableStateOf<User?>(null) } // LaunchedEffect chạy khi userId thay đổi LaunchedEffect(userId) { user = api.getUser(userId) // suspend function } user?.let { UserProfile(it) } }

Giải thích:

  • LaunchedEffect(userId) tạo một coroutine
  • Coroutine tự động cancel khi composable rời khỏi composition
  • Nếu userId thay đổi, coroutine cũ bị cancel và chạy lại

DisposableEffect

Mục đích: Setup và cleanup resources (như đăng ký listeners).

@Composable fun LifecycleLogger() { val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner) { // Setup: Đăng ký observer val observer = LifecycleEventObserver { _, event -> Log.d("Lifecycle", "Event: $event") } lifecycleOwner.lifecycle.addObserver(observer) // Cleanup: Hủy đăng ký khi composable bị remove onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } }

Tại sao cần onDispose? Nếu không cleanup, observer vẫn tồn tại → memory leak!

SideEffect

Mục đích: Sync Compose state với code bên ngoài Compose.

@Composable fun AnalyticsScreen(screenName: String) { // Chạy sau mỗi successful recomposition SideEffect { analytics.logScreenView(screenName) } }

3. Animations trong Compose

Animation làm cho app trở nên sống động và chuyên nghiệp hơn. Compose cung cấp các API đơn giản để tạo animations.

animate*AsState - Animation đơn giản nhất

@Composable fun ExpandableCard() { var expanded by remember { mutableStateOf(false) } // Tự động animate khi expanded thay đổi val cardHeight by animateDpAsState( targetValue = if (expanded) 200.dp else 100.dp, animationSpec = tween(durationMillis = 300) ) Card( modifier = Modifier .fillMaxWidth() .height(cardHeight) .clickable { expanded = !expanded } ) { Text("Click to expand/collapse") } }

Giải thích:

  • animateDpAsState tự động animate giá trị từ current → target
  • Khi expanded thay đổi, cardHeight sẽ mượt mà chuyển đổi
  • tween(300) = animation trong 300ms

Các loại animate*AsState

// Animate số Dp val size by animateDpAsState(if (big) 200.dp else 100.dp) // Animate màu sắc val color by animateColorAsState(if (selected) Color.Red else Color.Gray) // Animate số Float (0 -> 1) val alpha by animateFloatAsState(if (visible) 1f else 0f) // Animate offset val offset by animateIntOffsetAsState(...)

AnimatedVisibility - Hiện/ẩn với animation

@Composable fun ToastMessage(show: Boolean, message: String) { AnimatedVisibility( visible = show, enter = fadeIn() + slideInVertically(), exit = fadeOut() + slideOutVertically() ) { Card(modifier = Modifier.padding(16.dp)) { Text(message) } } }

Giải thích:

  • AnimatedVisibility tự động animate khi visible thay đổi
  • enter: animation khi xuất hiện (fade in + slide from top)
  • exit: animation khi biến mất (fade out + slide to top)

Crossfade - Chuyển đổi giữa 2 content

@Composable fun TabContent(selectedTab: Int) { Crossfade(targetState = selectedTab) { tab -> when (tab) { 0 -> HomeContent() 1 -> SearchContent() 2 -> ProfileContent() } } }

Khi selectedTab thay đổi, content cũ fade out và content mới fade in.

Animation phức tạp với updateTransition

@Composable fun AnimatedButton(selected: Boolean) { val transition = updateTransition(targetState = selected, label = "button") val backgroundColor by transition.animateColor(label = "bg") { isSelected -> if (isSelected) Color.Blue else Color.Gray } val borderWidth by transition.animateDp(label = "border") { isSelected -> if (isSelected) 2.dp else 0.dp } val scale by transition.animateFloat(label = "scale") { isSelected -> if (isSelected) 1.1f else 1f } Box( modifier = Modifier .scale(scale) .background(backgroundColor, RoundedCornerShape(8.dp)) .border(borderWidth, Color.White, RoundedCornerShape(8.dp)) ) }

Tại sao dùng updateTransition?

  • Sync nhiều animations cùng lúc
  • Tất cả chạy song song và hoàn thành cùng lúc
  • Dễ debug với Android Studio Animation Preview

Infinite Animation

@Composable fun PulsingDot() { val infiniteTransition = rememberInfiniteTransition() val scale by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 1.5f, animationSpec = infiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse ) ) Box( modifier = Modifier .size(20.dp) .scale(scale) .background(Color.Red, CircleShape) ) }

Đây là animation vô hạn - dot sẽ pulse liên tục.


📝 Tóm tắt cho người mới

Khái niệmKhi nào dùngVí dụ
CompositionLocalTruyền data qua nhiều tầngTheme, User settings
LaunchedEffectGọi API, async workLoad data khi mở màn hình
DisposableEffectSetup/cleanup resourcesĐăng ký listeners
animate*AsStateAnimation đơn giảnSize, color changes
AnimatedVisibilityShow/hide với animationToast, dialogs
updateTransitionNhiều animations cùng lúcComplex button states
Last updated on