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
- 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 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 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 đủ
// ❌ 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
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
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 UserAvatar và UserBio.
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 │
│ │
└─────────────────────────────────────────┘- 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
| 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
- 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)