Lifecycle của Composables
Hiểu lifecycle của composables giúp bạn viết code hiệu quả và tránh các bugs khó phát hiện. Bài này giải thích cách Compose quản lý lifecycle của các UI components.
1. Lifecycle Overview
Phần tiêu đề “1. Lifecycle Overview”Một composable có lifecycle đơn giản với 3 giai đoạn:
┌─────────────────────────────────────────────┐│ ││ Enter Composition ──▶ Recompose (n lần) ││ │ │ ││ └───────────┬─────────┘ ││ ▼ ││ Leave Composition ││ │└─────────────────────────────────────────────┘- Enter Composition: Composable được gọi lần đầu tiên
- Recompose: Composable được gọi lại khi state thay đổi (có thể 0 đến n lần)
- Leave Composition: Composable không còn cần thiết và bị remove
2. Composition là gì?
Phần tiêu đề “2. Composition là gì?”Composition là cây UI mà Compose xây dựng từ các composable functions:
@Composablefun MyScreen() { Column { // Node trong Composition Text("Hello") // Node con Text("World") // Node con khác }}Initial Composition vs Recomposition
Phần tiêu đề “Initial Composition vs Recomposition”| Initial Composition | Recomposition |
|---|---|
| Chạy composable lần đầu | Chạy lại khi state thay đổi |
| Tạo cây Composition | Cập nhật cây Composition |
| Tất cả composables được gọi | Chỉ composables affected được gọi |
3. Recomposition chi tiết
Phần tiêu đề “3. Recomposition chi tiết”Khi nào Recomposition xảy ra?
Phần tiêu đề “Khi nào Recomposition xảy ra?”Recomposition được trigger khi State<T> mà composable đọc bị thay đổi:
@Composablefun Counter() { var count by remember { mutableStateOf(0) } // State
Column { Text("Count: $count") // Đọc count → sẽ recompose khi count thay đổi
Button(onClick = { count++ }) { Text("Increment") // Không đọc count → không recompose } }}Smart Recomposition - Skipping
Phần tiêu đề “Smart Recomposition - Skipping”Compose tự động skip recomposition cho composables mà inputs không thay đổi:
@Composablefun UserProfile(user: User) { Column { Avatar(user.imageUrl) // Skip nếu imageUrl không đổi Name(user.name) // Skip nếu name không đổi Bio(user.bio) // Skip nếu bio không đổi }}Điều kiện để Skip
Phần tiêu đề “Điều kiện để Skip”Một composable được skip nếu:
- Tất cả parameters là stable (không thay đổi)
- Parameters implement
equals()đúng cách
// ✅ Stable types - có thể skipdata class User(val id: Int, val name: String) // data class có equals()val number: Int = 42val text: String = "Hello"val list: List<String> = listOf("a", "b") // Immutable list
// ❌ Unstable types - không skip đượcclass User(var name: String) // Mutable classval mutableList: MutableList<String> // Mutable collection4. Call Site Identity
Phần tiêu đề “4. Call Site Identity”Compose nhận diện composable instance bằng call site (vị trí gọi trong code):
@Composablefun LoginScreen(showError: Boolean) { if (showError) { LoginError() // Call site 1 } LoginInput() // Call site 2 - luôn là LoginInput khác, không bị ảnh hưởng}Dù LoginInput đứng ở vị trí khác trong UI tree (có hoặc không có LoginError phía trên), Compose vẫn nhận diện nó nhờ call site.
Vấn đề với Lists
Phần tiêu đề “Vấn đề với Lists”Khi có list, call site giống nhau cho mỗi item → Compose có thể nhầm lẫn:
@Composablefun NameList(names: List<String>) { Column { names.forEach { name -> Text(name) // Cùng call site cho tất cả items! } }}Nếu list thay đổi thứ tự, Compose có thể không biết item nào là item nào.
5. key() - Giúp Compose nhận diện đúng
Phần tiêu đề “5. key() - Giúp Compose nhận diện đúng”Dùng key() để giúp Compose phân biệt các items trong list:
@Composablefun TodoList(todos: List<Todo>) { Column { todos.forEach { todo -> key(todo.id) { // Dùng unique ID làm key TodoItem(todo) } } }}Khi nào cần key()?
Phần tiêu đề “Khi nào cần key()?”// ❌ Không cần key - LazyColumn tự handleLazyColumn { items( items = todos, key = { it.id } // Truyền key trực tiếp ) { todo -> TodoItem(todo) }}
// ✅ Cần key - forEach thườngColumn { todos.forEach { todo -> key(todo.id) { // Cần bọc key() TodoItem(todo) } }}
// ✅ Cần key - Khi có logic phức tạp@Composablefun AnimatedList(items: List<Item>) { Column { items.forEach { item -> key(item.id) { // Giữ animation state đúng AnimatedVisibility(visible = true) { ItemCard(item) } } } }}Key Rules
Phần tiêu đề “Key Rules”- Key phải unique trong scope đó
- Key phải stable - cùng item luôn có cùng key
- Không dùng index làm key nếu list có thể reorder
// ❌ SAI: Index làm key - sẽ sai khi reorderitems.forEachIndexed { index, item -> key(index) { // Reorder sẽ giữ sai state! ItemWithState(item) }}
// ✅ ĐÚNG: Unique ID làm keyitems.forEach { item -> key(item.id) { // Luôn đúng dù reorder ItemWithState(item) }}6. remember và Lifecycle
Phần tiêu đề “6. remember và Lifecycle”remember lưu giá trị trong suốt lifecycle của composable:
@Composablefun Timer() { // Giữ giá trị qua recompositions, reset khi leave composition var time by remember { mutableStateOf(0) }
LaunchedEffect(Unit) { while (true) { delay(1000) time++ } }
Text("Time: $time seconds")}remember với key
Phần tiêu đề “remember với key”@Composablefun UserAvatar(userId: String) { // Reset giá trị khi userId thay đổi val avatar by remember(userId) { mutableStateOf(loadAvatar(userId)) }
Image(avatar)}7. Lifecycle-aware Effects
Phần tiêu đề “7. Lifecycle-aware Effects”LaunchedEffect - Chạy khi enter/restart
Phần tiêu đề “LaunchedEffect - Chạy khi enter/restart”@Composablefun DataScreen(dataId: String) { var data by remember { mutableStateOf<Data?>(null) }
// Chạy khi enter composition // Restart khi dataId thay đổi // Cancel khi leave composition LaunchedEffect(dataId) { data = fetchData(dataId) }
data?.let { DataContent(it) }}DisposableEffect - Cleanup khi leave
Phần tiêu đề “DisposableEffect - Cleanup khi leave”@Composablefun AnalyticsScreen(screenName: String) { DisposableEffect(screenName) { Analytics.screenViewed(screenName)
onDispose { Analytics.screenLeft(screenName) // Cleanup } }}SideEffect - Sau mỗi successful recomposition
Phần tiêu đề “SideEffect - Sau mỗi successful recomposition”@Composablefun LoggingText(text: String) { SideEffect { // Chạy sau mỗi recomposition thành công Log.d("UI", "Text displayed: $text") }
Text(text)}8. Ví dụ thực tế
Phần tiêu đề “8. Ví dụ thực tế”Lifecycle trong Navigation
Phần tiêu đề “Lifecycle trong Navigation”@Composablefun AppNavigation() { val navController = rememberNavController()
NavHost(navController, startDestination = "home") { composable("home") { HomeScreen() // Enter khi navigate đến, Leave khi navigate đi } composable("detail/{id}") { backStackEntry -> val id = backStackEntry.arguments?.getString("id") DetailScreen(id) // Lifecycle riêng cho mỗi instance } }}Lifecycle với Lists
Phần tiêu đề “Lifecycle với Lists”@Composablefun MessageList(messages: List<Message>) { LazyColumn { items( items = messages, key = { it.id } // Important: giữ đúng state khi list thay đổi ) { message -> // Mỗi MessageItem có lifecycle riêng // Enter khi scroll vào view // Leave khi scroll ra khỏi view MessageItem(message) } }}
@Composablefun MessageItem(message: Message) { // State được giữ trong suốt lifecycle của item này var expanded by remember { mutableStateOf(false) }
// LaunchedEffect bound với lifecycle của MessageItem LaunchedEffect(message.id) { markAsRead(message.id) }
Card(onClick = { expanded = !expanded }) { Text(message.content) if (expanded) { Text(message.details) } }}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Concept | Mô tả |
|---|---|
| Enter Composition | Composable được gọi lần đầu |
| Recomposition | Gọi lại khi state thay đổi |
| Leave Composition | Composable bị remove |
| Smart Skipping | Compose skip composables với unchanged stable inputs |
| Call Site | Compose identify composable bằng vị trí gọi |
| key() | Giúp Compose phân biệt items trong list |
| remember | Lưu giá trị trong suốt lifecycle |
| Effects | LaunchedEffect, DisposableEffect, SideEffect |
Best Practices
Phần tiêu đề “Best Practices”- Dùng
key()cho list items với state - Dùng unique, stable IDs làm key (không dùng index)
- Dùng
remembercho expensive calculations - Dùng
LaunchedEffectcho async operations - Dùng
DisposableEffectkhi cần cleanup