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
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ì?
Composition là cây UI mà Compose xây dựng từ các composable functions:
@Composable
fun MyScreen() {
Column { // Node trong Composition
Text("Hello") // Node con
Text("World") // Node con khác
}
}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
Khi nào Recomposition xảy ra?
Recomposition được trigger khi State<T> mà composable đọc bị thay đổi:
@Composable
fun 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
Compose tự động skip recomposition cho composables mà inputs không thay đổi:
@Composable
fun 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
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ể skip
data class User(val id: Int, val name: String) // data class có equals()
val number: Int = 42
val text: String = "Hello"
val list: List<String> = listOf("a", "b") // Immutable list
// ❌ Unstable types - không skip được
class User(var name: String) // Mutable class
val mutableList: MutableList<String> // Mutable collection4. Call Site Identity
Compose nhận diện composable instance bằng call site (vị trí gọi trong code):
@Composable
fun 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
Khi có list, call site giống nhau cho mỗi item → Compose có thể nhầm lẫn:
@Composable
fun 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
Dùng key() để giúp Compose phân biệt các items trong list:
@Composable
fun 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()?
// ❌ Không cần key - LazyColumn tự handle
LazyColumn {
items(
items = todos,
key = { it.id } // Truyền key trực tiếp
) { todo ->
TodoItem(todo)
}
}
// ✅ Cần key - forEach thường
Column {
todos.forEach { todo ->
key(todo.id) { // Cần bọc key()
TodoItem(todo)
}
}
}
// ✅ Cần key - Khi có logic phức tạp
@Composable
fun AnimatedList(items: List<Item>) {
Column {
items.forEach { item ->
key(item.id) { // Giữ animation state đúng
AnimatedVisibility(visible = true) {
ItemCard(item)
}
}
}
}
}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 reorder
items.forEachIndexed { index, item ->
key(index) { // Reorder sẽ giữ sai state!
ItemWithState(item)
}
}
// ✅ ĐÚNG: Unique ID làm key
items.forEach { item ->
key(item.id) { // Luôn đúng dù reorder
ItemWithState(item)
}
}6. remember và Lifecycle
remember lưu giá trị trong suốt lifecycle của composable:
@Composable
fun 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
@Composable
fun UserAvatar(userId: String) {
// Reset giá trị khi userId thay đổi
val avatar by remember(userId) {
mutableStateOf(loadAvatar(userId))
}
Image(avatar)
}7. Lifecycle-aware Effects
LaunchedEffect - Chạy khi enter/restart
@Composable
fun 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
@Composable
fun AnalyticsScreen(screenName: String) {
DisposableEffect(screenName) {
Analytics.screenViewed(screenName)
onDispose {
Analytics.screenLeft(screenName) // Cleanup
}
}
}SideEffect - Sau mỗi successful recomposition
@Composable
fun 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ế
Lifecycle trong Navigation
@Composable
fun 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
@Composable
fun 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)
}
}
}
@Composable
fun 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
| 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
- 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