Bỏ qua để đến nội dung

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)”

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)
}
  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
}
}

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
)
}
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")
// ❌ 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")
}
}
}

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

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

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

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
@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) }
}
}

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

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
@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) }
}

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()
}

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
┌─────────────────────────────────────────┐
│ │
│ 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

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
  • 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)