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ì?
Phần tiêu đề “1. CompositionLocal là gì?”Vấn đề: Truyền data qua nhiều tầng
Phần tiêu đề “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 colorNế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
Phần tiêu đề “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 CompositionLocalval LocalAppThemeColor = staticCompositionLocalOf { Color.Blue }
// Bước 2: Provide giá trị ở tầng cao@Composablefun 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@Composablefun AvatarImage() { val themeColor = LocalAppThemeColor.current // Lấy được Color.Red! Box(modifier = Modifier.background(themeColor))}Các CompositionLocal có sẵn
Phần tiêu đề “Các CompositionLocal có sẵn”Android đã cung cấp một số CompositionLocal phổ biến:
// Contextval context = LocalContext.current
// Lifecycleval lifecycleOwner = LocalLifecycleOwner.current
// Configuration (orientation, locale)val config = LocalConfiguration.current
// Density (for dp ↔ px conversions)val density = LocalDensity.current2. Side Effects là gì?
Phần tiêu đề “2. Side Effects là gì?”Tại sao cần Side Effects?
Phần tiêu đề “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
Phần tiêu đề “LaunchedEffect”Mục đích: Chạy suspend code khi composable được tạo.
@Composablefun 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
userIdthay đổi, coroutine cũ bị cancel và chạy lại
DisposableEffect
Phần tiêu đề “DisposableEffect”Mục đích: Setup và cleanup resources (như đăng ký listeners).
@Composablefun 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
Phần tiêu đề “SideEffect”Mục đích: Sync Compose state với code bên ngoài Compose.
@Composablefun AnalyticsScreen(screenName: String) { // Chạy sau mỗi successful recomposition SideEffect { analytics.logScreenView(screenName) }}3. Animations trong Compose
Phần tiêu đề “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
Phần tiêu đề “animate*AsState - Animation đơn giản nhất”@Composablefun 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:
animateDpAsStatetự động animate giá trị từ current → target- Khi
expandedthay đổi,cardHeightsẽ mượt mà chuyển đổi tween(300)= animation trong 300ms
Các loại animate*AsState
Phần tiêu đề “Các loại animate*AsState”// Animate số Dpval size by animateDpAsState(if (big) 200.dp else 100.dp)
// Animate màu sắcval 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 offsetval offset by animateIntOffsetAsState(...)AnimatedVisibility - Hiện/ẩn với animation
Phần tiêu đề “AnimatedVisibility - Hiện/ẩn với animation”@Composablefun 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:
AnimatedVisibilitytự động animate khivisiblethay đổienter: 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
Phần tiêu đề “Crossfade - Chuyển đổi giữa 2 content”@Composablefun 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
Phần tiêu đề “Animation phức tạp với updateTransition”@Composablefun 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
Phần tiêu đề “Infinite Animation”@Composablefun 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
Phần tiêu đề “📝 Tóm tắt cho người mới”| Khái niệm | Khi nào dùng | Ví dụ |
|---|---|---|
| CompositionLocal | Truyền data qua nhiều tầng | Theme, User settings |
| LaunchedEffect | Gọi API, async work | Load data khi mở màn hình |
| DisposableEffect | Setup/cleanup resources | Đăng ký listeners |
| animate*AsState | Animation đơn giản | Size, color changes |
| AnimatedVisibility | Show/hide với animation | Toast, dialogs |
| updateTransition | Nhiều animations cùng lúc | Complex button states |