Bỏ qua để đến nội dung

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.

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”.

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))
}

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

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.

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

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!

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)
}
}

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.

@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
// 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(...)
@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)
@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.

@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
@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.


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