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 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
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.current2. 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
userIdthay đổ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:
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
// 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:
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
@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ệ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 |