# 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。**