Skip to Content
Android📚 Học lập trình AndroidLifecycle của Composables

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 │ │ │ └─────────────────────────────────────────────┘
  1. Enter Composition: Composable được gọi lần đầu tiên
  2. Recompose: Composable được gọi lại khi state thay đổi (có thể 0 đến n lần)
  3. 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 CompositionRecomposition
Chạy composable lần đầuChạy lại khi state thay đổi
Tạo cây CompositionCập nhật cây Composition
Tất cả composables được gọiChỉ 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:

  1. Tất cả parameters là stable (không thay đổi)
  2. 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 collection

4. 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 }

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

  1. Key phải unique trong scope đó
  2. Key phải stable - cùng item luôn có cùng key
  3. 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

ConceptMô tả
Enter CompositionComposable được gọi lần đầu
RecompositionGọi lại khi state thay đổi
Leave CompositionComposable bị remove
Smart SkippingCompose skip composables với unchanged stable inputs
Call SiteCompose identify composable bằng vị trí gọi
key()Giúp Compose phân biệt items trong list
rememberLưu giá trị trong suốt lifecycle
EffectsLaunchedEffect, 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 remember cho expensive calculations
  • Dùng LaunchedEffect cho async operations
  • Dùng DisposableEffect khi cần cleanup
Last updated on