Thinking in Compose
Để sử dụng Jetpack Compose hiệu quả, bạn cần thay đổi cách suy nghĩ về việc xây dựng UI. Bài này giải thích sự khác biệt giữa lập trình imperative (truyền thống) và declarative (Compose).
1. Vấn đề với cách làm truyền thống (Imperative)
Phần tiêu đề “1. Vấn đề với cách làm truyền thống (Imperative)”Cách XML Views hoạt động
Phần tiêu đề “Cách XML Views hoạt động”Trong Android truyền thống, UI được biểu diễn dưới dạng cây widget. Khi state thay đổi, bạn phải thao tác trực tiếp trên các widget:
// Tìm view theo IDval textView = findViewById<TextView>(R.id.greeting)val button = findViewById<Button>(R.id.button)
// Thao tác trực tiếp trên widgetbutton.setOnClickListener { textView.text = "Clicked!" textView.setTextColor(Color.RED)}Các vấn đề phát sinh
Phần tiêu đề “Các vấn đề phát sinh”- Dễ quên cập nhật: Nếu data hiển thị ở nhiều nơi, bạn có thể quên cập nhật một trong số đó
- State không đồng bộ: Hai updates có thể xung đột với nhau
- Illegal states: Cố gắng cập nhật một view đã bị remove khỏi UI
- Độ phức tạp tăng: Càng nhiều views cần cập nhật, càng khó bảo trì
// ❌ Lỗi thường gặp: Quên cập nhật một viewfun updateUserName(name: String) { headerTextView.text = name // Quên cập nhật profileTextView!}
// ❌ State conflictsfun refreshData() { loadingView.visibility = View.VISIBLE // Nếu user navigate away trong lúc loading... fetchData { result -> loadingView.visibility = View.GONE // Crash nếu view đã bị destroy! dataView.text = result }}2. Declarative UI - Cách tiếp cận mới
Phần tiêu đề “2. Declarative UI - Cách tiếp cận mới”Ý tưởng cốt lõi
Phần tiêu đề “Ý tưởng cốt lõi”Thay vì thao tác trên UI, bạn mô tả UI dựa trên state hiện tại:
// ✅ Compose: Mô tả UI là gì, không phải làm thế nào@Composablefun Greeting(name: String, isClicked: Boolean) { Text( text = if (isClicked) "Clicked!" else "Hello, $name", color = if (isClicked) Color.Red else Color.Black )}So sánh hai cách tiếp cận
Phần tiêu đề “So sánh hai cách tiếp cận”| Imperative (XML Views) | Declarative (Compose) |
|---|---|
| “Làm thế nào” - Thao tác từng bước | “Là gì” - Mô tả kết quả |
| Tìm view → Thay đổi properties | Gọi function với data mới |
| Quản lý state thủ công | Framework tự động cập nhật |
textView.text = "Hello" |
Text("Hello") |
Ví dụ đầy đủ
Phần tiêu đề “Ví dụ đầy đủ”// ❌ Imperative - Thao tác từng bướcclass CounterActivity : AppCompatActivity() { private var count = 0
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_counter)
val countText = findViewById<TextView>(R.id.count) val button = findViewById<Button>(R.id.increment)
button.setOnClickListener { count++ countText.text = "Count: $count" // Phải tự cập nhật } }}
// ✅ Declarative - Mô tả UI dựa trên state@Composablefun Counter() { var count by remember { mutableStateOf(0) }
Column { Text("Count: $count") // UI tự động cập nhật khi count thay đổi Button(onClick = { count++ }) { Text("Increment") } }}3. Recomposition - Cách Compose cập nhật UI
Phần tiêu đề “3. Recomposition - Cách Compose cập nhật UI”Recomposition là gì?
Phần tiêu đề “Recomposition là gì?”Khi state thay đổi, Compose gọi lại composable function với data mới. Quá trình này gọi là recomposition.
@Composablefun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") }}- Khi
clicksthay đổi từ 0 → 1 - Compose gọi lại
ClickCountervớiclicks = 1 Textđược vẽ lại với nội dung mớiButtoncó thể được skip nếu không thay đổi
Smart Recomposition
Phần tiêu đề “Smart Recomposition”Compose thông minh - chỉ recompose những phần thực sự thay đổi:
@Composablefun UserScreen(user: User) { Column { UserAvatar(user.avatarUrl) // Chỉ recompose nếu avatarUrl thay đổi UserName(user.name) // Chỉ recompose nếu name thay đổi UserBio(user.bio) // Chỉ recompose nếu bio thay đổi }}Nếu chỉ user.name thay đổi, Compose chỉ recompose UserName, bỏ qua UserAvatar và UserBio.
4. Các nguyên tắc quan trọng khi viết Composables
Phần tiêu đề “4. Các nguyên tắc quan trọng khi viết Composables”4.1. Composables phải “pure” (không có side effects)
Phần tiêu đề “4.1. Composables phải “pure” (không có side effects)”// ❌ SAI: Side effect trong composable@Composablefun BadExample(viewModel: MyViewModel) { viewModel.loadData() // Side effect! Sẽ chạy lại mỗi lần recompose Text("Loading...")}
// ✅ ĐÚNG: Dùng LaunchedEffect cho side effects@Composablefun GoodExample(viewModel: MyViewModel) { LaunchedEffect(Unit) { viewModel.loadData() // Chỉ chạy một lần } Text("Loading...")}4.2. Composables có thể chạy với tần suất cao
Phần tiêu đề “4.2. Composables có thể chạy với tần suất cao”Composable functions có thể được gọi mỗi frame (60 lần/giây khi animate). Vì vậy:
// ❌ SAI: Tính toán nặng trong composable@Composablefun ExpensiveScreen(items: List<Item>) { val sorted = items.sortedBy { it.name } // Chạy mỗi recomposition! LazyColumn { items(sorted) { ItemRow(it) } }}
// ✅ ĐÚNG: Cache kết quả với remember@Composablefun OptimizedScreen(items: List<Item>) { val sorted = remember(items) { items.sortedBy { it.name } } LazyColumn { items(sorted) { ItemRow(it) } }}4.3. Composables có thể chạy song song
Phần tiêu đề “4.3. Composables có thể chạy song song”Compose có thể chạy nhiều composables cùng lúc trên các thread khác nhau:
// ❌ SAI: Shared mutable statevar sharedCounter = 0
@Composablefun BadCounter() { sharedCounter++ // Race condition! Text("Count: $sharedCounter")}
// ✅ ĐÚNG: State được quản lý bởi Compose@Composablefun GoodCounter() { var count by remember { mutableStateOf(0) } Text("Count: $count")}4.4. Composables có thể chạy theo bất kỳ thứ tự nào
Phần tiêu đề “4.4. Composables có thể chạy theo bất kỳ thứ tự nào”Không đảm bảo thứ tự thực thi giữa các composables:
// ❌ SAI: Phụ thuộc vào thứ tự thực thi@Composablefun BadScreen() { var data: String? = null
DataLoader { data = it } // Có thể chạy sau DataDisplay! DataDisplay(data!!) // Crash nếu data chưa được set}
// ✅ ĐÚNG: Dùng state để đồng bộ@Composablefun GoodScreen() { var data by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) { data = loadData() }
data?.let { DataDisplay(it) }}4.5. Recomposition có thể bị hủy
Phần tiêu đề “4.5. Recomposition có thể bị hủy”Compose có quyền hủy recomposition nếu state thay đổi giữa chừng:
// ❌ SAI: Giả định recomposition luôn hoàn thành@Composablefun BadProgress() { // Nếu recomposition bị hủy, analytics có thể không được gọi analytics.logEvent("screen_viewed") ProgressIndicator()}
// ✅ ĐÚNG: Dùng SideEffect cho analytics@Composablefun GoodProgress() { SideEffect { analytics.logEvent("screen_viewed") // Chỉ chạy khi recomposition thành công } ProgressIndicator()}5. Mental Model - Cách suy nghĩ về Compose
Phần tiêu đề “5. Mental Model - Cách suy nghĩ về Compose”UI = f(State)
Phần tiêu đề “UI = f(State)”Trong Compose, UI là function của State:
UI = Composable(State)- Khi State thay đổi → Gọi lại Composable → UI mới
- Bạn không bao giờ “update” UI, bạn “generate” UI mới từ state mới
Unidirectional Data Flow
Phần tiêu đề “Unidirectional Data Flow”┌─────────────────────────────────────────┐│ ││ State ─────────▶ UI (Composables) ││ ▲ │ ││ │ ▼ ││ Logic ◀─────────── Events ││ │└─────────────────────────────────────────┘- State flows down → UI hiển thị state
- Events flow up → User actions được gửi lên logic
- Logic xử lý và cập nhật state
- Cycle lặp lại
📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Nguyên tắc | Giải thích |
|---|---|
| Declarative | Mô tả UI là gì, không phải làm thế nào |
| Pure functions | Không có side effects trong composables |
| Fast execution | Composables phải nhanh vì có thể chạy mỗi frame |
| No shared state | Không dùng shared mutable variables |
| No order dependency | Không phụ thuộc vào thứ tự thực thi |
| UI = f(State) | UI được generate từ state |
Checklist khi viết Composables
Phần tiêu đề “Checklist khi viết Composables”- Function có side effects không? → Dùng LaunchedEffect/SideEffect
- Có tính toán nặng không? → Dùng remember
- Có đọc/ghi shared state không? → Dùng mutableStateOf
- Function có idempotent không? (gọi nhiều lần cho kết quả giống nhau)
- Function có nhanh không? (< 1ms)