---
# System prepended metadata

title: Kotlin / Ch05 - Jetpack Compose

---

# Kotlin / Ch05 - Jetpack Compose

## 第一章：Jetpack Compose 簡介與核心概念

### 1.1 什麼是 Jetpack Compose

Jetpack Compose 是 Google 推出的現代化 Android UI 工具包，它採用 Declarative Programming 的方式來建構使用者介面。這個概念比較容易聯想到 React 或 Vue.js 這類前端框架，它們都強調透過描述「UI 應該是什麼樣子」而非「如何改變 UI」來開發介面。

傳統的 Android 開發使用 XML 來定義佈局，並透過 Java 或 Kotlin 程式碼操作 View 物件；Imperative Programming 的方式就會需要開發者手動管理 UI 狀態的同步，容易產生錯誤。Compose 則徹底改變了這個模式，它採用 Kotlin 函數來描述 UI，當資料改變時，框架會自動重新渲染相關的 UI 部分。

### 1.2 為什麼選擇 Compose

從傳統 View 系統遷移到 Compose 帶來了多個顯著優勢。首先是程式碼量的大幅減少。原本需要在 XML 中定義佈局，在 Activity 或 Fragment 中綁定 View，再設定各種監聽器的流程，現在可以在單一個 Composable 函數中就可以完成。這種整合性讓程式碼更容易閱讀和維護。

其次是型別安全，XML 佈局在編譯時期無法完全驗證，許多錯誤只能在執行時期發現。Compose 完全使用 Kotlin 編寫，享有完整的型別檢查、IDE 自動完成和重構支援。

第三是效能優化。Compose 的編譯器會進行智慧型差異比對（Smart Recomposition），只更新實際改變的 UI 元素。這個機制類似於 React 的 Virtual DOM，但更加高效。框架會追蹤資料依賴關係，確保不必要的 Re-render 被省略。

### 1.3 Declarative UI 的思維轉換

理解 Declarative Programming 是掌握 Compose 的關鍵。在 Imperative Programming 中，你會寫出「將按鈕的文字設為 X」、「隱藏這個 TextView」這樣的指令序列。不過在 Declarative Programming 中，你則是描述「當狀態為 A 時，按鈕顯示文字 X」、「當條件 B 成立時，這個元件不可見」。

> 這種思維方式在 Spring 開發中也有體現。例如 Spring Security 的設定，你不是寫「檢查使用者，如果是管理員則允許存取」，而是聲明「這個端點需要 ADMIN 角色」。Compose 將這個概念徹底應用到 UI 開發。

讓我們看一個簡單的對比。傳統方式可能需要這樣寫：

```kotlin=
val textView = findViewById<TextView>(R.id.message)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
    counter++
    textView.text = "點擊次數: $counter"
}
```

而在 Compose 中，同樣的功能變成：

```kotlin=
@Composable
fun CounterScreen() {
    var counter by remember { mutableStateOf(0) }
    Column {
        Text("點擊次數: $counter")
        Button(onClick = { counter++ }) {
            Text("點擊我")
        }
    }
}
```

**你不需要手動更新 UI，只需要改變狀態（`counter`），UI 會自動反映這個改變。**

### 1.4 Compose 的核心原則

#### 1.4.1 Composable 函數

Composable 函數是 Compose 的基本建構單元。它們是標註了 `@Composable` annotation 的普通 Kotlin 函數，用來描述 UI 的一部分。這個概念類似於 Spring 中的 `@Component` 或 `@Bean`，告訴框架這個函數有特殊的處理方式。

Composable 函數可以呼叫其他 Composable 函數，形成 UI 樹狀結構。每個函數可以接收參數來客製化其行為，就像普通函數一樣。重要的是，Composable 函數應該是純函數（Pure Function）的概念，相同的輸入應該產生相同的輸出。

#### 1.4.2 狀態與重組（Recomposition）

狀態（State）是驅動 UI 變化的核心。在 Compose 中，當狀態改變時，依賴該狀態的 Composable 函數會被重新執行，這個過程稱為重組（Recomposition）。框架會智慧地決定哪些部分需要重組，哪些可以跳過；**這個機制讓你可以專注於資料流，而非手動同步 UI。**

#### 1.4.3 單向資料流

Compose 遵循單向資料流（Unidirectional Data Flow）模式。資料從父組件流向子組件，事件從子組件向上傳遞到父組件。這個模式讓資料流動變得可預測和可追蹤，減少了狀態管理的複雜度。

在實作上，這通常意味著父組件持有狀態，並將狀態和回調函數傳遞給子組件。子組件是無狀態的（Stateless），只負責顯示接收到的資料和觸發事件。

---

## 第二章：Compose 的建構基礎

### 2.1 開發環境設定

開始 Compose 開發前需要確保開發環境符合要求。首先是 Android Studio，建議使用 Arctic Fox（2020.3.1）或更新版本，這些版本對 Compose 有完整的支援，包括即時預覽（Live Preview）和互動式預覽功能。

在專案的 `build.gradle.kts`（如果你使用 Kotlin DSL）或 `build.gradle` 中，需要設定 Compose 相關的依賴。以下是基本的設定範例：

```kotlin=
android {
    compileSdk = 34
    
    defaultConfig {
        minSdk = 21
        targetSdk = 34
    }
    
    buildFeatures {
        compose = true
    }
    
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.3"
    }
    
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2024.02.00")
    implementation(composeBom)
    
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.activity:activity-compose:1.8.2")
    
    debugImplementation("androidx.compose.ui:ui-tooling")
}
```

這裡使用了 BOM（Bill of Materials）來管理 Compose 版本，就不需要為每個依賴指定版本號。

### 2.2 第一個 Composable 函數

讓我們建立第一個簡單的 Composable 函數。在 Compose 中，所有的 UI 元件都是函數，不是類別。這是一個重要的概念轉變，特別是對於習慣物件導向設計的 Java 開發者。

```kotlin=
@Composable
fun Greeting(name: String) {
    Text(text = "你好, $name!")
}
```

這個函數接收一個字串參數，並顯示一段文字。`@Composable` annotation 告訴編譯器這個函數會發射 UI。`Text` 是 Compose 提供的基礎元件，類似於傳統 Android 的 TextView。

要在 Activity 中使用這個函數，你需要在 `setContent` 區塊中呼叫它：

```kotlin=
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Greeting(name = "開發者")
            }
        }
    }
}
```

`setContent` 是 Compose 的進入點，它替代了傳統的 `setContentView`。`MaterialTheme` 提供了 Material Design 的主題樣式，包含顏色、字型和形狀定義。

### 2.3 預覽功能

Compose 的一大優勢是即時預覽功能。你可以在不執行應用的情況下，直接在 Android Studio 中查看 UI 的樣子。這需要建立預覽函數：

```kotlin=
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MaterialTheme {
        Greeting(name = "預覽模式")
    }
}
```

`@Preview` annotation 告訴 IDE 這是一個預覽函數。你可以設定多個參數來客製化預覽效果，例如 `showBackground` 顯示背景，`uiMode` 切換深色模式，`device` 指定裝置類型等。

預覽函數不會出現在最終的 APK 中，它們純粹是開發時的輔助工具。你可以為同一個 Composable 建立多個預覽，展示不同的狀態或配置。

### 2.4 Modifier：樣式與行為的調整器

Modifier 是 Compose 中用來調整 Composable 外觀和行為的機制。它採用鏈式呼叫（Chaining）的方式，讓你可以依序套用多個修飾。這個設計模式類似於 Java Stream API 或 Kotlin 的集合操作。

```kotlin=
@Composable
fun StyledText() {
    Text(
        text = "樣式化的文字",
        modifier = Modifier
            .padding(16.dp)
            .background(Color.LightGray)
            .fillMaxWidth()
            .height(100.dp)
    )
}
```

這裡的 Modifier 鏈依序套用了內邊距、背景色、寬度填滿和固定高度。**順序很重要：例如先套用 padding 再套用 background，背景就不會包含 padding 區域；反之則會。**

Modifier 還可以處理互動行為。例如 `clickable` 讓元件可點擊，`draggable` 讓元件可拖曳。這些修飾器將行為與外觀統一在同一個 API 中處理。

### 2.5 組合與重用

**Compose 鼓勵建立小而專注的 Composable 函數，然後組合它們來建構複雜的 UI。** 這個理念與主流前端框架的「元件」設計邏輯一致，都是為了提高程式碼的可維護性和可測試性。

例如，你可以建立一個自訂的卡片元件：

```kotlin=
@Composable
fun UserCard(name: String, email: String) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = name,
                style = MaterialTheme.typography.titleLarge
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = email,
                style = MaterialTheme.typography.bodyMedium,
                color = Color.Gray
            )
        }
    }
}
```

這個 `UserCard` 可以在應用的多個地方重複使用，只需傳入不同的參數。這種組合式的設計讓 UI 變得模組化，每個元件都有清晰的責任界線。

---

## 第三章：狀態管理基礎

### 3.1 理解狀態

在 Compose 中，狀態（State）是指任何會隨時間變化並影響 UI 顯示的資料。這可能是使用者輸入、網路請求結果、定時器數值等等。理解狀態管理是掌握 Compose 的關鍵，因為 **Compose 的核心理念就是「UI = f(State)」**，也就是說，UI 是狀態的函數。

### 3.2 remember 與 mutableStateOf

Compose 提供了 `remember` 和 `mutableStateOf` 來建立和儲存狀態。讓我們深入理解這兩個 API 的運作方式。

`mutableStateOf` 建立一個可觀察的狀態物件。當這個物件的值改變時，所有讀取它的 Composable 函數都會被通知並重組。這類似於觀察者模式（Observer Pattern），是 Reactive Programming 的體現。

```kotlin=
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0) }
    
    Column {
        Text("目前計數: ${count.value}")
        Button(onClick = { count.value++ }) {
            Text("增加")
        }
    }
}
```

在這個範例中，`mutableStateOf(0)` 建立了一個初始值為 0 的狀態。`remember` 確保這個狀態在重組過程中被保留。沒有 `remember` 的話，每次重組都會重新建立狀態物件，導致值被重置。

`remember` 的作用類似於在 Spring 中使用 `@Scope("singleton")`，確保物件在特定範圍內只有一個實例。不同的是，Compose 的 remember 是按 Composable 的位置來記憶，而非全域單例。

### 3.3 委派屬性語法

直接使用 `count.value` 讀寫狀態是可行的，但 Kotlin 提供了更優雅的委派屬性（Delegated Property）語法：

```kotlin=
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("目前計數: $count")
        Button(onClick = { count++ }) {
            Text("增加")
        }
    }
}
```

使用 `by` 關鍵字後，你可以直接讀寫 `count` 而不需要透過 `.value`。這讓程式碼更簡潔，更接近普通變數的使用方式。在幕後，Kotlin 編譯器會自動生成必要的 getter 和 setter。

這個語法需要匯入 `import androidx.compose.runtime.getValue` 和 `import androidx.compose.runtime.setValue`，不過 Android Studio  通常會自動處理這些匯入。

### 3.4 狀態提升（State Hoisting）

Hoisting 是 Compose 中一個重要的模式，它指的是**將狀態從子元件移到父元件，讓子元件變成無狀態的（Stateless）**。這樣做有幾個好處：提高可重用性、簡化測試、讓資料流更清晰。

這邊可以直接看範例，首先是有狀態的版本：

```kotlin=
@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    CounterDisplay(
        count = count,
        onIncrement = { count++ }
    )
}
```

然後是無狀態的子元件：

```kotlin=
@Composable
fun CounterDisplay(
    count: Int,
    onIncrement: () -> Unit
) {
    Column {
        Text("目前計數: $count")
        Button(onClick = onIncrement) {
            Text("增加")
        }
    }
}
```

Hoisting 讓 `CounterDisplay` 變得可預測且易於測試，你可以傳入任意的 count 值和 onIncrement 實作，不需要 Mock 任何東西。這個優勢比較能在單元測試中體現，類似在 Spring 中測試 Service 層比測試整個 Controller 更容易。

### 3.5 rememberSaveable

`remember` 只能在 Composable 的生命週期中保存狀態。當配置改變（例如螢幕旋轉）導致 Activity 重建時，remember 的狀態會遺失。為了處理這種情況，Compose 提供了 `rememberSaveable`。

```kotlin=
@Composable
fun PersistentCounter() {
    var count by rememberSaveable { mutableStateOf(0) }
    
    Column {
        Text("計數（會保存）: $count")
        Button(onClick = { count++ }) {
            Text("增加")
        }
    }
}
```

`rememberSaveable` 會將狀態儲存到 Android 的 saved instance state 機制中，這類似於傳統 Android 開發中的 `onSaveInstanceState` 和 `onRestoreInstanceState`。它只能儲存可序列化的基本類型或實作了 Parcelable 的物件。

那對於複雜的物件，你會需要提供自訂的 Saver：

```kotlin=
data class User(val name: String, val age: Int)

val UserSaver = Saver<User, List<Any>>(
    save = { listOf(it.name, it.age) },
    restore = { User(it[0] as String, it[1] as Int) }
)

@Composable
fun UserProfile() {
    var user by rememberSaveable(stateSaver = UserSaver) {
        mutableStateOf(User("訪客", 0))
    }
    // ... UI 實作
}
```

這個機制與 Spring Session 的序列化類似，都是為了在不同的生命週期階段保持狀態一致性。

## 第四章：Layout 與 UI 組件

### 4.1 基礎 Layout 元件

Compose 提供了幾個基礎的 Layout 元件來組織 UI 結構。這些元件類似於傳統 Android 的 LinearLayout、FrameLayout 等，但設計更加簡潔和靈活。

#### 4.1.1 Column 與 Row

`Column` 和 `Row` 是最常用的佈局元件，分別用於垂直和水平排列子元素。它們的使用非常直觀，對 CSS 熟悉的朋友們是直接無痛上手的：

```kotlin=
@Composable
fun ColumnExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("第一行文字")
        Text("第二行文字")
        Button(onClick = { }) {
            Text("按鈕")
        }
    }
}
```

`verticalArrangement` 控制子元素在垂直方向的排列方式，可以是：
- `spacedBy`（等距分布）
- `SpaceBetween`（兩端對齊）
- `SpaceAround`（周圍留空）
 
`horizontalAlignment` 則控制子元素的水平對齊方式。這些參數讓你可以精確控制佈局行為，而不需要複雜的 XML 設定。

Row 的使用方式類似，只是方向相反：

```kotlin=
@Composable
fun RowExample() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(Icons.Default.Favorite, contentDescription = "喜歡")
        Text("項目名稱")
        Text("$99", fontWeight = FontWeight.Bold)
    }
}
```

#### 4.1.2 Box

`Box` 用於堆疊元素，類似於 FrameLayout。子元素會按照宣告順序疊加，後面的元素會覆蓋前面的元素。這在需要重疊效果時非常有用：

```kotlin=
@Composable
fun OverlayExample() {
    Box(modifier = Modifier.size(200.dp)) {
        Image(
            painter = painterResource(id = R.drawable.background),
            contentDescription = "背景圖片",
            modifier = Modifier.fillMaxSize()
        )
        Text(
            text = "覆蓋文字",
            modifier = Modifier.align(Alignment.Center),
            color = Color.White,
            fontSize = 20.sp
        )
    }
}
```

Box 的 `contentAlignment` 參數可以控制預設的對齊方式，而每個子元素也可以透過 `Modifier.align()` 單獨指定對齊位置。

### 4.2 LazyColumn 與 LazyRow

當需要顯示大量項目時，使用 `LazyColumn` 或 `LazyRow` 可以獲得類似 RecyclerView 的效能優化。這些元件只會渲染螢幕上可見的項目，當使用者滾動時才載入新的項目。

```kotlin=
@Composable
fun UserList(users: List<User>) {
    LazyColumn(
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(users) { user ->
            UserCard(user = user)
        }
    }
}

@Composable
fun UserCard(user: User) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp)
        ) {
            // 使用者頭像
            Box(
                modifier = Modifier
                    .size(48.dp)
                    .background(Color.LightGray, CircleShape)
            )
            Column {
                Text(
                    text = user.name,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = user.email,
                    style = MaterialTheme.typography.bodySmall,
                    color = Color.Gray
                )
            }
        }
    }
}
```

`items` 函數是 LazyColumn 的 DSL 建構器，它接受一個集合並為每個項目產生一個 Composable。這比傳統的 RecyclerView Adapter 簡單許多，不需要建立 ViewHolder 或實作複雜的 Adapter 邏輯。

對於更複雜的列表需求，LazyColumn 還提供了其他有用的函數：

```kotlin=
LazyColumn {
    // 單一項目
    item {
        HeaderComponent()
    }
    
    // 多個項目
    items(userList) { user ->
        UserCard(user)
    }
    
    // 帶索引的項目
    itemsIndexed(userList) { index, user ->
        UserCardWithIndex(index, user)
    }
    
    // 頁尾
    item {
        FooterComponent()
    }
}
```

### 4.3 Scaffold：應用程式結構框架

`Scaffold` 是 Material Design 的核心佈局元件，它提供了一個標準的應用程式結構，包含 Top Bar、Bottom Bar、Floating Action Button 等常見元素。使用 Scaffold 可以快速建立符合 Material Design 規範的介面：

```kotlin=
@Composable
fun MainScreen() {
    var selectedItem by remember { mutableStateOf(0) }
    val items = listOf("首頁", "搜尋", "設定")
    
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("我的應用") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer
                )
            )
        },
        bottomBar = {
            NavigationBar {
                items.forEachIndexed { index, item ->
                    NavigationBarItem(
                        icon = { Icon(Icons.Filled.Home, contentDescription = item) },
                        label = { Text(item) },
                        selected = selectedItem == index,
                        onClick = { selectedItem = index }
                    )
                }
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* 新增動作 */ }) {
                Icon(Icons.Filled.Add, contentDescription = "新增")
            }
        }
    ) { paddingValues ->
        // 主要內容區域
        Box(modifier = Modifier.padding(paddingValues)) {
            when (selectedItem) {
                0 -> HomeContent()
                1 -> SearchContent()
                2 -> SettingsContent()
            }
        }
    }
}
```

注意 Scaffold 的 content 參數會接收一個 `PaddingValues`，這包含了 top bar 和 bottom bar 佔用的空間。你需要將這個 padding 套用到主要內容上，以避免內容被系統 UI 遮擋。

### 4.4 自訂 Layout

雖然內建的 Layout 元件已經能滿足大部分需求，但有時你需要建立自訂的佈局邏輯。Compose 允許實現完全客製化的佈局演算法：

```kotlin=
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 測量所有子元素
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        
        // 計算佈局大小
        val width = placeables.maxOf { it.width }
        val height = placeables.sumOf { it.height }
        
        // 放置子元素
        layout(width, height) {
            var yPosition = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)
                yPosition += placeable.height
            }
        }
    }
}
```

### 4.5 ConstraintLayout

對於複雜的相對佈局需求，Compose 也提供了 `ConstraintLayout`。雖然它不像在 XML 中那麼常用，但在某些情況下仍然方便：

```kotlin=
@Composable
fun ConstraintLayoutExample() {
    ConstraintLayout(modifier = Modifier.fillMaxSize()) {
        // 建立引用
        val (title, description, button) = createRefs()
        
        Text(
            text = "標題",
            modifier = Modifier.constrainAs(title) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(parent.start, margin = 16.dp)
            }
        )
        
        Text(
            text = "這是一段描述文字",
            modifier = Modifier.constrainAs(description) {
                top.linkTo(title.bottom, margin = 8.dp)
                start.linkTo(parent.start, margin = 16.dp)
            }
        )
        
        Button(
            onClick = { },
            modifier = Modifier.constrainAs(button) {
                bottom.linkTo(parent.bottom, margin = 16.dp)
                end.linkTo(parent.end, margin = 16.dp)
            }
        ) {
            Text("確認")
        }
    }
}
```

使用 ConstraintLayout 需要額外的依賴：`implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")`。

---

## 第五章：進階狀態管理與架構

### 5.1 ViewModel 整合

對於複雜的應用程式，將所有狀態邏輯都放在 Composable 中會導致程式碼難以維護。這時就需要引入架構元件，特別是 ViewModel。ViewModel 的概念對於 Spring 開發者來說應該不陌生，它類似於 Service 層，負責處理業務邏輯和資料管理。

首先需要新增依賴：

```kotlin
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
```

然後建立 ViewModel：

```kotlin=
class UserViewModel : ViewModel() {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()
    
    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
    
    fun loadUsers() {
        viewModelScope.launch {
            _isLoading.value = true
            try {
                val result = userRepository.getUsers()
                _users.value = result
            } catch (e: Exception) {
                // 處理錯誤
            } finally {
                _isLoading.value = false
            }
        }
    }
    
    fun addUser(user: User) {
        _users.value = _users.value + user
    }
}
```

這裡使用了 Kotlin Flow 的 `StateFlow`，它是一個狀態持有者，類似於 LiveData 但更適合 Kotlin Coroutines。`viewModelScope` 是 ViewModel 提供的協程作用域，當 ViewModel 被清除時會自動取消所有協程，避免記憶體洩漏。

在 Composable 中使用 ViewModel：

```kotlin=
@Composable
fun UserScreen(
    viewModel: UserViewModel = viewModel()
) {
    val users by viewModel.users.collectAsStateWithLifecycle()
    val isLoading by viewModel.isLoading.collectAsStateWithLifecycle()
    
    LaunchedEffect(Unit) {
        viewModel.loadUsers()
    }
    
    if (isLoading) {
        LoadingIndicator()
    } else {
        UserList(users = users)
    }
}
```

`collectAsStateWithLifecycle()` 是一個擴充函數，它將 Flow 轉換為 Compose State，並自動處理生命週期。當 Composable 離開畫面時，它會停止收集；回到畫面時會重新開始。避免了不必要的資源消耗。

### 5.2 Side Effects 與 Effect Handlers

Compose 是一個純函數式的系統，但應用程式總是需要執行 [Side Effects](https://developer.android.com/develop/ui/compose/side-effects)（非貶義），例如網路請求、資料庫操作或導航。Compose 提供了幾個 Effect API 來安全地處理這些情況。

#### 5.2.1 LaunchedEffect

`LaunchedEffect` 用於在 Composable 進入組成時啟動協程。它接受一個或多個 key，當這些 key 改變時，之前的協程會被取消並啟動新的協程：

```kotlin
@Composable
fun SearchScreen(query: String) {
    var results by remember { mutableStateOf<List<SearchResult>>(emptyList()) }
    
    LaunchedEffect(query) {
        if (query.isNotEmpty()) {
            delay(300) // 防抖動
            results = searchRepository.search(query)
        }
    }
    
    SearchResults(results)
}
```

這個範例實作了搜尋防抖動（Debouncing）。當 query 改變時，之前的搜尋會被取消，300 毫秒後才執行新的搜尋。這個模式在 Spring 中也相當常見，例如使用 `@Async` 執行非同步任務。

#### 5.2.2 DisposableEffect

`DisposableEffect` 用於需要清理資源的情況，類似於傳統 Android 的 `onStart`/`onStop`：

```kotlin=
@Composable
fun SensorMonitor() {
    val context = LocalContext.current
    val sensorManager = remember {
        context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }
    
    DisposableEffect(Unit) {
        val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                // 處理感應器資料
            }
            override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
        }
        
        sensorManager.registerListener(
            listener,
            sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_NORMAL
        )
        
        onDispose {
            sensorManager.unregisterListener(listener)
        }
    }
}
```

`onDispose` 區塊會在 Composable 離開組成或 key 改變時執行，確保資源被正確釋放。

#### 5.2.3 rememberCoroutineScope

當你需要在事件處理器中啟動協程時，可以使用 `rememberCoroutineScope`：

```kotlin
@Composable
fun AnimatedButton() {
    val scope = rememberCoroutineScope()
    var isExpanded by remember { mutableStateOf(false) }
    
    Button(
        onClick = {
            scope.launch {
                isExpanded = true
                delay(2000)
                isExpanded = false
            }
        }
    ) {
        Text(if (isExpanded) "已展開" else "點擊展開")
    }
}
```

這個協程作用域綁定到 Composable 的生命週期，當 Composable 離開組成時會自動取消。

### 5.3 CompositionLocal

`CompositionLocal` 是 Compose 的依賴注入機制，類似於 React 的 Context API 或 Spring 的 DI。它允許你在組成樹的任何層級提供值，讓子元件可以存取這些值而不需要透過參數傳遞。

定義 CompositionLocal：

```kotlin=
data class AppConfiguration(
    val apiEndpoint: String,
    val enableDebugMode: Boolean
)

val LocalAppConfig = compositionLocalOf<AppConfiguration> {
    error("未提供 AppConfiguration")
}
```

提供值：

```kotlin=
@Composable
fun App() {
    val config = AppConfiguration(
        apiEndpoint = "https://api.example.com",
        enableDebugMode = BuildConfig.DEBUG
    )
    
    CompositionLocalProvider(LocalAppConfig provides config) {
        MainScreen()
    }
}
```

使用值：

```kotlin=
@Composable
fun ApiCallButton() {
    val config = LocalAppConfig.current
    
    Button(onClick = {
        makeApiCall(config.apiEndpoint)
    }) {
        Text("呼叫 API")
    }
}
```

Compose 內建了幾個 CompositionLocal，例如 `LocalContext`（提供 Android Context）和 `LocalConfiguration`（提供裝置配置資訊）。這個機制讓你可以避免在整個組成樹中傳遞相同的參數，類似於 Spring 的 `@Autowired`。

### 5.4 單向資料流架構

實現良好的架構需要遵循單向資料流（UDF）原則。在這個模式中，狀態向下流動，事件向上傳遞。可以直接看完整範例：

```kotlin=
// UI State
data class ProductListUiState(
    val products: List<Product> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null,
    val selectedCategory: Category? = null
)

// UI Events
sealed class ProductListEvent {
    data class SelectCategory(val category: Category) : ProductListEvent()
    object RefreshProducts : ProductListEvent()
    data class AddToCart(val productId: String) : ProductListEvent()
}

// ViewModel
class ProductListViewModel(
    private val productRepository: ProductRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(ProductListUiState())
    val uiState: StateFlow<ProductListUiState> = _uiState.asStateFlow()
    
    fun onEvent(event: ProductListEvent) {
        when (event) {
            is ProductListEvent.SelectCategory -> {
                selectCategory(event.category)
            }
            is ProductListEvent.RefreshProducts -> {
                loadProducts()
            }
            is ProductListEvent.AddToCart -> {
                addToCart(event.productId)
            }
        }
    }
    
    private fun selectCategory(category: Category) {
        _uiState.update { it.copy(selectedCategory = category) }
        loadProducts()
    }
    
    private fun loadProducts() {
        viewModelScope.launch {
            _uiState.update { it.copy(isLoading = true) }
            try {
                val products = productRepository.getProducts(
                    _uiState.value.selectedCategory
                )
                _uiState.update { it.copy(products = products, isLoading = false) }
            } catch (e: Exception) {
                _uiState.update { 
                    it.copy(error = e.message, isLoading = false) 
                }
            }
        }
    }
    
    private fun addToCart(productId: String) {
        viewModelScope.launch {
            productRepository.addToCart(productId)
        }
    }
}

// UI Layer
@Composable
fun ProductListScreen(
    viewModel: ProductListViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    ProductListContent(
        uiState = uiState,
        onEvent = viewModel::onEvent
    )
}

@Composable
fun ProductListContent(
    uiState: ProductListUiState,
    onEvent: (ProductListEvent) -> Unit
) {
    Column {
        CategoryTabs(
            selectedCategory = uiState.selectedCategory,
            onCategorySelected = { category ->
                onEvent(ProductListEvent.SelectCategory(category))
            }
        )
        
        when {
            uiState.isLoading -> LoadingIndicator()
            uiState.error != null -> ErrorMessage(uiState.error)
            else -> ProductGrid(
                products = uiState.products,
                onAddToCart = { productId ->
                    onEvent(ProductListEvent.AddToCart(productId))
                }
            )
        }
    }
}
```

這個架構模式清楚地展示了關注點分離：ViewModel 負責業務邏輯、UI 只負責渲染和事件傳遞。

## 第六章：導航與路由

### 6.1 Navigation Compose 入門

Navigation Compose 是 Jetpack Navigation 元件的 Compose 版本，它提供了一個型別安全的方式來處理應用程式內的頁面跳轉，與定義 router & path 非常類似。

首先需要新增依賴：

```kotlin
implementation("androidx.navigation:navigation-compose:2.7.7")
```

基本的導航設定包含三個核心概念：NavController（導航控制器）、NavHost（導航宿主）和導航路由。讓我們看一個完整的範例：

```kotlin=
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate("profile/$userId")
                },
                onNavigateToSettings = {
                    navController.navigate("settings")
                }
            )
        }
        
        composable(
            route = "profile/{userId}",
            arguments = listOf(
                navArgument("userId") { type = NavType.StringType }
            )
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId")
            ProfileScreen(
                userId = userId,
                onNavigateBack = {
                    navController.popBackStack()
                }
            )
        }
        
        composable("settings") {
            SettingsScreen(
                onNavigateBack = {
                    navController.popBackStack()
                }
            )
        }
    }
}
```

在這個範例中，我們定義了三個目的地：首頁、個人資料頁和設定頁。個人資料頁接受一個 `userId` 參數，這類似於 Spring MVC 中的 `@PathVariable`。

### 6.2 參數傳遞與深層連結

除了路徑參數，Navigation Compose 還支援查詢參數和複雜物件的傳遞。對於簡單的參數，可以使用路徑或查詢字串：

```kotlin=
// 路徑參數
composable("article/{articleId}") { backStackEntry ->
    val articleId = backStackEntry.arguments?.getString("articleId")
    ArticleScreen(articleId = articleId)
}

// 查詢參數
composable(
    route = "search?query={query}&category={category}",
    arguments = listOf(
        navArgument("query") { 
            type = NavType.StringType
            defaultValue = ""
        },
        navArgument("category") {
            type = NavType.StringType
            nullable = true
            defaultValue = null
        }
    )
) { backStackEntry ->
    val query = backStackEntry.arguments?.getString("query") ?: ""
    val category = backStackEntry.arguments?.getString("category")
    SearchScreen(query = query, category = category)
}
```

對於複雜物件，建議使用 ViewModel 或持久化儲存來共享資料，而不是透過導航參數傳遞。

另外，深層連結（Deep Links）允許從外部（例如通知或網頁連結）直接開啟應用的特定頁面：

```kotlin=
composable(
    route = "article/{articleId}",
    deepLinks = listOf(
        navDeepLink {
            uriPattern = "myapp://article/{articleId}"
            action = Intent.ACTION_VIEW
        },
        navDeepLink {
            uriPattern = "https://myapp.com/article/{articleId}"
            action = Intent.ACTION_VIEW
        }
    )
) { backStackEntry ->
    val articleId = backStackEntry.arguments?.getString("articleId")
    ArticleScreen(articleId = articleId)
}
```

### 6.3 巢狀導航與底部導航列

複雜的應用通常需要巢狀導航結構。例如，底部導航列的每個分頁可能有自己的導航堆疊。Navigation Compose 透過巢狀圖表（Nested Graph）來處理這種情況：

```kotlin=
@Composable
fun MainScreen() {
    val navController = rememberNavController()
    
    Scaffold(
        bottomBar = {
            BottomNavigationBar(navController = navController)
        }
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = "home_graph",
            modifier = Modifier.padding(paddingValues)
        ) {
            // 首頁導航圖
            navigation(
                startDestination = "home",
                route = "home_graph"
            ) {
                composable("home") {
                    HomeScreen(
                        onNavigateToDetail = { id ->
                            navController.navigate("home_detail/$id")
                        }
                    )
                }
                composable("home_detail/{id}") { backStackEntry ->
                    val id = backStackEntry.arguments?.getString("id")
                    DetailScreen(id = id)
                }
            }
            
            // 搜尋導航圖
            navigation(
                startDestination = "search",
                route = "search_graph"
            ) {
                composable("search") {
                    SearchScreen()
                }
            }
            
            // 個人資料導航圖
            navigation(
                startDestination = "profile",
                route = "profile_graph"
            ) {
                composable("profile") {
                    ProfileScreen()
                }
            }
        }
    }
}

@Composable
fun BottomNavigationBar(navController: NavController) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    
    NavigationBar {
        NavigationBarItem(
            icon = { Icon(Icons.Default.Home, contentDescription = "首頁") },
            label = { Text("首頁") },
            selected = currentRoute?.startsWith("home_graph") == true,
            onClick = {
                navController.navigate("home_graph") {
                    popUpTo(navController.graph.findStartDestination().id) {
                        saveState = true
                    }
                    launchSingleTop = true
                    restoreState = true
                }
            }
        )
        
        NavigationBarItem(
            icon = { Icon(Icons.Default.Search, contentDescription = "搜尋") },
            label = { Text("搜尋") },
            selected = currentRoute?.startsWith("search_graph") == true,
            onClick = {
                navController.navigate("search_graph") {
                    popUpTo(navController.graph.findStartDestination().id) {
                        saveState = true
                    }
                    launchSingleTop = true
                    restoreState = true
                }
            }
        )
        
        NavigationBarItem(
            icon = { Icon(Icons.Default.Person, contentDescription = "個人") },
            label = { Text("個人") },
            selected = currentRoute?.startsWith("profile_graph") == true,
            onClick = {
                navController.navigate("profile_graph") {
                    popUpTo(navController.graph.findStartDestination().id) {
                        saveState = true
                    }
                    launchSingleTop = true
                    restoreState = true
                }
            }
        )
    }
}
```

這個設定確保了每個分頁都有自己獨立的導航堆疊，並且在切換分頁時會保存和恢復狀態。`launchSingleTop` 防止重複導航到相同的目的地。

### 6.4 型別安全的導航

傳統的字串路由容易出錯，Navigation Compose 2.8.0 引入了型別安全的導航 API：

```kotlin=
// 定義導航目的地
@Serializable
object Home

@Serializable
data class Profile(val userId: String)

@Serializable
data class Settings(val section: String? = null)

// 設定導航
@Composable
fun TypeSafeNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = Home
    ) {
        composable<Home> {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    navController.navigate(Profile(userId))
                }
            )
        }
        
        composable<Profile> { backStackEntry ->
            val profile = backStackEntry.toRoute<Profile>()
            ProfileScreen(userId = profile.userId)
        }
        
        composable<Settings> { backStackEntry ->
            val settings = backStackEntry.toRoute<Settings>()
            SettingsScreen(section = settings.section)
        }
    }
}
```

---

## 第七章：最佳實踐

### 7.1 效能最佳化

#### 7.1.1 避免不必要的重組

Compose 的智慧重組機制已經很高效，但還是需要注意一些常見陷阱：

```kotlin=
// 不好的實踐：每次重組都建立新的 lambda
@Composable
fun BadExample(items: List<String>) {
    LazyColumn {
        items(items) { item ->
            Button(onClick = { handleClick(item) }) {
                Text(item)
            }
        }
    }
}

// 好的實踐：穩定的回調函數
@Composable
fun GoodExample(
    items: List<String>,
    onItemClick: (String) -> Unit
) {
    LazyColumn {
        items(items) { item ->
            Button(onClick = { onItemClick(item) }) {
                Text(item)
            }
        }
    }
}
```

#### 7.1.2 使用 derivedStateOf

當計算型狀態的依賴沒有改變時，可以使用 `derivedStateOf` 避免重新計算：

```kotlin=
@Composable
fun FilteredList(items: List<Item>, searchQuery: String) {
    // 只有當 items 或 searchQuery 改變時才重新篩選
    val filteredItems by remember(items, searchQuery) {
        derivedStateOf {
            items.filter { it.name.contains(searchQuery, ignoreCase = true) }
        }
    }
    
    LazyColumn {
        items(filteredItems) { item ->
            ItemRow(item)
        }
    }
}
```

#### 7.1.3 延遲初始化

對於開銷比較大的物件建立，使用 `remember` 配合延遲初始化：

```kotlin
@Composable
fun ExpensiveComponent() {
    val expensiveObject = remember {
        // 這只會在第一次組成時執行
        ExpensiveClass()
    }
    
    // 使用 expensiveObject
}
```

### 7.2 專案結構建議

對於大型專案，良好的專案結構至關重要。建議如下：

```
app/
├── ui/
│   ├── components/          # 可重用的 UI 元件
│   │   ├── buttons/
│   │   ├── cards/
│   │   └── inputs/
│   ├── screens/             # 畫面級別的 Composable
│   │   ├── home/
│   │   │   ├── HomeScreen.kt
│   │   │   ├── HomeViewModel.kt
│   │   │   └── HomeUiState.kt
│   │   ├── profile/
│   │   └── settings/
│   ├── navigation/          # 導航相關
│   └── theme/               # 主題和樣式
├── data/                    # 資料層
│   ├── repository/
│   ├── local/
│   └── remote/
└── domain/                  # 領域層
    ├── model/
    └── usecase/
```

### 7.3 可訪問性（Accessibility）

確保應用程式對所有使用者友善是專業開發的重要部分，Compose 提供了內建的可訪問性支援：

```kotlin
@Composable
fun AccessibleButton() {
    Button(
        onClick = { /* 動作 */ },
        modifier = Modifier.semantics {
            contentDescription = "確認按鈕，點擊以提交表單"
            role = Role.Button
        }
    ) {
        Icon(Icons.Default.Check, contentDescription = null)
        Spacer(modifier = Modifier.width(8.dp))
        Text("確認")
    }
}
```

使用 `contentDescription` 為非文字元素提供描述，使用 `semantics` block 添加額外的語義資訊。這讓視障使用者可以透過螢幕閱讀器理解和操作你的應用程式。

---

## 附錄：Mermaid 架構圖

### Compose 組成樹結構

```mermaid
graph TD
    A[MaterialTheme] --> B[Scaffold]
    B --> C[TopAppBar]
    B --> D[Content]
    B --> E[BottomNavigationBar]
    D --> F[LazyColumn]
    F --> G[Item 1]
    F --> H[Item 2]
    F --> I[Item 3]
    G --> J[Card]
    J --> K[Row]
    K --> L[Image]
    K --> M[Column]
    M --> N[Text: Title]
    M --> O[Text: Description]
```

---

### 單向資料流模型

```mermaid
graph LR
    A[ViewModel State] -->|向下| B[UI Layer]
    B -->|事件向上| C[Event Handler]
    C -->|更新狀態| A
    
    style A fill:#e1f5ff
    style B fill:#fff4e6
    style C fill:#f3e5f5
```

---

### Compose 重組流程

```mermaid
sequenceDiagram
    participant State
    participant Compose
    participant UI
    
    State->>Compose: 狀態改變
    Compose->>Compose: 標記需要重組的節點
    Compose->>Compose: 執行重組
    Compose->>UI: 更新受影響的 UI
    Note over Compose: 智慧跳過未改變的部分
```

---

### 導航架構

```mermaid
graph TB
    A[NavController] --> B[NavHost]
    B --> C[Home Graph]
    B --> D[Search Graph]
    B --> E[Profile Graph]
    C --> C1[Home Screen]
    C --> C2[Detail Screen]
    D --> D1[Search Screen]
    D --> D2[Result Screen]
    E --> E1[Profile Screen]
    E --> E2[Settings Screen]
    
    style A fill:#4caf50
    style B fill:#2196f3
    style C fill:#ff9800
    style D fill:#ff9800
    style E fill:#ff9800
```

---

## 結語

Jetpack Compose 代表了 Android UI 開發的新時代。透過 Declarative Programming、強大的狀態管理和豐富的工具支援，它讓開發者可以更快速、更可靠地建構現代化的應用程式介面。對於有 Java 和 Spring 背景的開發者，許多概念都是熟悉的：DI、生命週期管理、分層架構等。

從傳統的 View 系統過渡到 Compose 需要一些思維轉換，但一旦掌握了核心概念，你會發現開發效率和程式碼品質都有顯著提升。**關鍵原則不外乎是：保持 Composable 函數純淨、Hoisting 到適當的層級、善用 Effect API 處理 Side Effects、遵循 UDF。**