Skip to Content

Thinking in Compose - Tư duy trong Jetpack 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)

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 ID val textView = findViewById<TextView>(R.id.greeting) val button = findViewById<Button>(R.id.button) // Thao tác trực tiếp trên widget button.setOnClickListener { textView.text = "Clicked!" textView.setTextColor(Color.RED) }

Các vấn đề phát sinh

  1. 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ố đó
  2. State không đồng bộ: Hai updates có thể xung đột với nhau
  3. Illegal states: Cố gắng cập nhật một view đã bị remove khỏi UI
  4. Độ 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 view fun updateUserName(name: String) { headerTextView.text = name // Quên cập nhật profileTextView! } // ❌ State conflicts fun 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

Ý 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 @Composable fun 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

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 propertiesGọi function với data mới
Quản lý state thủ côngFramework tự động cập nhật
textView.text = "Hello"Text("Hello")

Ví dụ đầy đủ

// ❌ Imperative - Thao tác từng bước class 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 @Composable fun 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

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.

@Composable fun ClickCounter(clicks: Int, onClick: () -> Unit) { Button(onClick = onClick) { Text("I've been clicked $clicks times") } }
  • Khi clicks thay đổi từ 0 → 1
  • Compose gọi lại ClickCounter với clicks = 1
  • Text được vẽ lại với nội dung mới
  • Button có thể được skip nếu không thay đổi

Smart Recomposition

Compose thông minh - chỉ recompose những phần thực sự thay đổi:

@Composable fun 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 UserAvatarUserBio.


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)

// ❌ SAI: Side effect trong composable @Composable fun 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 @Composable fun 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

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 @Composable fun 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 @Composable fun 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

Compose có thể chạy nhiều composables cùng lúc trên các thread khác nhau:

// ❌ SAI: Shared mutable state var sharedCounter = 0 @Composable fun BadCounter() { sharedCounter++ // Race condition! Text("Count: $sharedCounter") } // ✅ ĐÚNG: State được quản lý bởi Compose @Composable fun 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

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 @Composable fun 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ộ @Composable fun GoodScreen() { var data by remember { mutableStateOf<String?>(null) } LaunchedEffect(Unit) { data = loadData() } data?.let { DataDisplay(it) } }

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 @Composable fun 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 @Composable fun 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

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

┌─────────────────────────────────────────┐ │ │ │ State ─────────▶ UI (Composables) │ │ ▲ │ │ │ │ ▼ │ │ Logic ◀─────────── Events │ │ │ └─────────────────────────────────────────┘
  1. State flows down → UI hiển thị state
  2. Events flow up → User actions được gửi lên logic
  3. Logic xử lý và cập nhật state
  4. Cycle lặp lại

📝 Tóm tắt

Nguyên tắcGiải thích
DeclarativeMô tả UI là gì, không phải làm thế nào
Pure functionsKhông có side effects trong composables
Fast executionComposables phải nhanh vì có thể chạy mỗi frame
No shared stateKhông dùng shared mutable variables
No order dependencyKhông phụ thuộc vào thứ tự thực thi
UI = f(State)UI được generate từ state

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)
Last updated on