Jetpack Compose UI 設計 Android App

AgriLinq AdminAgriLinq Admin
7 min read

本文撰寫時間為 2025 年 7 月,請注意該文章的介紹是否與閱讀時有落差。

甚麼是 Jetpack Compose

Jetpack Compose 是為了簡化 UI 開發而設計的新型工具包,目的是解決 XML 開發 UI 上與邏輯割裂與不夠及時響應的問題,負責處理 App 中的 Activity 部分是目前官方最推薦開發 Android UI 的方法。

官方在 https://github.com/android/compose-samples 有提供多個精美 UI參考。

他的核心優勢在於宣告式的 UI,傳統的 XML 會遇到以下問題

  • 使用 XML 建立 UI,然後在 Activity/Fragment 透過 findViewById 取得元件。

  • UI 與邏輯分散在不同檔案,當狀態改變時,需要手動更新元件,例如:

      textView.text = "New value"
      button.isEnabled = false
    

這容易導致:

  • 重複程式碼

  • 狀態與畫面不同步

  • 維護困難


Compose 的解法

Compose 採用 宣告式 UI

  • UI 是資料的函數 (UI = f(State))。

  • 只要狀態改變,UI 會自動重新組合 (Recompose)。

  • 不需要手動更新 UI。

@Composable
fun Greeting() {
    var name by remember { mutableStateOf("World") }

    Column {
        Text("Hello $name")
        Button(onClick = { name = "Compose" }) {
            Text("Change Name")
        }
    }
}

優勢:

  • UI 與狀態自動同步

  • 不需要「命令式」更新 UI

  • 大幅降低 UI bug


完全用 Kotlin 撰寫 UI

傳統 View 系統

  • UI 在 XML

  • 邏輯在 Kotlin/Java

  • 需要額外的 R.id 來連結 UI 元件

  • 重構容易斷鏈(改了 XML id 要同步改 Kotlin)


Compose 的解法

  • UI 直接寫在 Kotlin 裡

  • 沒有 R.id、沒有 XML,UI 元件就是 Kotlin 函式

  • 任何 IDE refactor 都可直接應用在 UI

@Composable
fun LoginButton(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Login")
    }
}

優勢:

  • 單一語言開發(不用在 XML 和 Kotlin 之間切換)

  • IDE 自動提示、型別安全

  • 開發流程更順暢


內建狀態管理

傳統 View 系統

  • 狀態管理需要:

    • findViewById 更新 UI

    • LiveData 或 DataBinding 搭配觀察者

    • 維護 UI 狀態同步的額外程式碼


Compose 的解法

Compose 內建狀態機制:

  • remembermutableStateOf

  • UI 會自動因狀態更新而重新產生

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) { Text("Increment") }
    }
}

優勢:

  • 不需要額外的 View Binding

  • 不需要手動呼叫 setText()

  • 狀態管理簡單且直覺


可組合與可重複使用元件

傳統 View 系統

  • UI 重複時需要建立 include layout

  • 或建立自訂 View Class,增加開發複雜度


Compose 的解法

  • UI 是函式 → 可直接重複使用

  • 支援參數化 → 輕鬆建立可客製化元件

@Composable
fun StyledButton(label: String, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text(label)
    }
}

@Composable
fun Example() {
    Column {
        StyledButton("Save") { /* save action */ }
        StyledButton("Cancel") { /* cancel action */ }
    }
}

優勢:

  • 高度模組化

  • 減少重複程式碼

  • 容易維護與測試


更簡單的 UI 更新與動畫

傳統 View 系統

  • 需要使用 ObjectAnimatorAnimatorSet

  • 需管理 View 屬性與狀態轉換

  • 動畫與 UI 綁定複雜


Compose 的解法

  • 內建動畫 API

  • 直接用 animate*AsState 即可:

@Composable
fun AnimatedBox() {
    var isExpanded by remember { mutableStateOf(false) }
    val size by animateDpAsState(if (isExpanded) 200.dp else 100.dp)

    Box(
        modifier = Modifier
            .size(size)
            .background(Color.Red)
            .clickable { isExpanded = !isExpanded }
    )
}

優勢:

  • 無需額外動畫物件

  • 以狀態驅動動畫 → 簡單直覺


更快的 UI 預覽與開發循環

傳統 View 系統

  • 需在 XML Layout Editor 或跑實機看效果

  • 常常要重建專案才能看到 UI 變化


Compose 的解法

  • @Preview 可直接在 IDE 預覽

  • 支援多種裝置尺寸與主題

@Preview(showBackground = true)
@Composable
fun PreviewGreeting() {
    Greeting()
}

優勢:

  • 快速調整 UI

  • 大幅縮短開發迭代時間


簡化 Navigation 與架構整合

傳統 View 系統

  • 需要 FragmentManager

  • Transaction API 冗長

  • BackStack 管理複雜


Compose 的解法

  • 使用 Navigation Compose

  • 以簡單的 NavHost 管理畫面

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable("detail/{id}") { backStackEntry ->
        DetailScreen(backStackEntry.arguments?.getString("id"))
    }
}

優勢:

  • 減少 Fragment 與 Transaction 的負擔

  • UI 與導航整合一致


可漸進式採用

  • Compose 可以和傳統 View 並存。

  • 可以在現有專案中逐步導入,不必完全重寫。

setContentView(ComposeView(this).apply {
    setContent { Greeting() }
})

優勢:

  • 無需一次性大改專案

  • 平滑遷移成本低

Android 轉向 Jetpack Compose:

  • 取代 XML 與 View 系統

  • 宣告式 UI

  • 內建狀態管理

  • API 24 以上無需額外設定

文章目標:

  • 了解基本 Composable

  • 能管理狀態

  • 建立多畫面導航

Compose UI 架構

Compose 架構圖

Activity → setContent { NavHost() }
           ↓
       Composables
           ↓
       State (ViewModel)

核心概念

  • @Composable 函式:UI 元件

  • State 驅動 UI → 單向資料流

  • Navigation Compose → 畫面切換

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello Compose")
        }
    }
}

Compose 基礎語法

Text

用於顯示文字。

@Composable
fun TextExample() {
    Text("Hello Compose")
}

常見屬性

  • text:要顯示的文字。

  • fontSize:文字大小(使用 sp)。

  • fontWeight:字體粗細。

  • color:文字顏色。

  • modifier:修飾(如間距、對齊)。

@Composable
fun TextStyledExample() {
    Text(
        text = "Welcome to Jetpack Compose",
        fontSize = 20.sp,
        fontWeight = FontWeight.Bold,
        color = Color.Blue,
        modifier = Modifier.padding(16.dp)
    )
}

Button

用於建立可點擊的按鈕。

@Composable
fun ButtonExample() {
    Button(onClick = { println("Button clicked") }) {
        Text("Click Me")
    }
}

常見屬性

  • onClick:按下時的動作。

  • modifier:設定按鈕大小、間距。

  • enabled:控制按鈕是否可點擊。

進階範例

@Composable
fun ButtonStyledExample() {
    Button(
        onClick = { println("Saved!") },
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp),
        enabled = true
    ) {
        Text("Save", fontSize = 18.sp)
    }
}

Column

是垂直排列的容器,類似於傳統 Android 的 LinearLayout (vertical)。

基本用法

@Composable
fun ColumnExample() {
    Column {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
}

常見屬性

  • modifier:控制整體大小、間距。

  • horizontalAlignment:控制水平對齊方式。

  • verticalArrangement:控制垂直間距。

進階範例

@Composable
fun ColumnStyledExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Top")
        Text("Middle")
        Text("Bottom")
    }
}

Row

是水平排列的容器,類似於 LinearLayout (horizontal)。

基本用法

@Composable
fun RowExample() {
    Row {
        Text("Left")
        Text("Center")
        Text("Right")
    }
}

常見屬性

  • horizontalArrangement:控制水平間距(例如 Arrangement.SpaceBetween)。

  • verticalAlignment:垂直對齊方式。

進階範例

@Composable
fun RowStyledExample() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("Left")
        Button(onClick = {}) { Text("Action") }
        Text("Right")
    }
}

Modifier

是用來修飾 Composable 的強大工具,可以調整大小、間距、背景、點擊事件等。

常見用法

@Composable
fun ModifierExample() {
    Text(
        text = "Styled Text",
        modifier = Modifier
            .padding(16.dp)
            .background(Color.Yellow)
            .fillMaxWidth()
            .clickable { println("Text clicked!") }
    )
}

常見函式

  • padding():內距。

  • background():背景色。

  • fillMaxWidth() / fillMaxSize():填滿寬度或整個畫面。

  • size():設定固定大小。

  • clickable():增加點擊事件。

佈局與修飾符

  • Modifier.padding()

  • Modifier.fillMaxWidth()

@Composable
fun GreetingScreen() {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text("Hello Compose", fontSize = 20.sp)
        Button(onClick = { println("Clicked!") }) {
            Text("Click Me")
        }
    }
}

整合範例

以下是把 TextButtonColumnRowModifier 整合在一起的範例:

@Composable
fun FullExample() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Text("Hello Jetpack Compose", fontSize = 22.sp, fontWeight = FontWeight.Bold)

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Button(onClick = { println("Left clicked") }) {
                Text("Left")
            }
            Button(onClick = { println("Right clicked") }) {
                Text("Right")
            }
        }

        Text(
            text = "Click Me!",
            modifier = Modifier
                .background(Color.LightGray)
                .fillMaxWidth()
                .padding(8.dp)
                .clickable { println("Text clicked") }
        )
    }
}

狀態管理

狀態基礎

  • remembermutableStateOf

  • State 驅動 UI

實作:計數器

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Column {
        Text("Count: $count")
        Button(onClick = { count++ }) { Text("Increment") }
    }
}

畫面切換與可重複使用元件

  • Navigation Compose

  • 可組合 UI 元件

@Composable
fun StyledButton(label: String, onClick: () -> Unit) {
    Button(onClick = onClick) { Text(label) }
}
  • 導航示範
NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable("detail/{id}") { backStackEntry ->
        DetailScreen(backStackEntry.arguments?.getString("id"))
    }
}

動態 Text (顯示狀態)

  • remembermutableStateOf 來記錄狀態。

  • Text 會根據狀態變化自動重新組合(Recompose)。

@Composable
fun DynamicTextExample() {
    var message by remember { mutableStateOf("Hello Compose") }

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = message, fontSize = 20.sp)

        Button(onClick = { message = "Button Clicked!" }) {
            Text("Change Text")
        }
    }
}

重點
message 變動時,Compose 會自動重新繪製 Text,不需要像傳統 Android 手動呼叫 findViewByIdsetText()


Button + Counter (計數器)

說明

  • 透過 Button 點擊次數更新狀態。

  • Text 即時顯示最新值。

@Composable
fun CounterExample() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(text = "Count: $count", fontSize = 24.sp)

        Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = { count++ }) {
                Text("Increment")
            }
            Button(onClick = { count = 0 }) {
                Text("Reset")
            }
        }
    }
}

重點

  • 使用 remember 讓狀態在組合期間持續存在。

  • 任何狀態變化都會自動更新 UI。


Column + 動態清單

說明

  • 動態清單 UI:按下按鈕新增項目。

  • 展示 Column 與狀態結合的效果。

@Composable
fun DynamicListExample() {
    var items by remember { mutableStateOf(listOf("Item 1", "Item 2")) }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items.forEach { item ->
            Text(text = item, fontSize = 18.sp)
        }

        Button(onClick = {
            items = items + "Item ${items.size + 1}"
        }) {
            Text("Add Item")
        }
    }
}

重點

  • 狀態改變(items 新增元素) → UI 自動重繪。

  • forEach 可直接動態產生多個 Text 元件。


Row + 狀態互動

範例說明

  • 使用 Row 水平排列元件。

  • 讓使用者可以「加一」或「減一」。

@Composable
fun RowStateExample() {
    var value by remember { mutableStateOf(0) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Button(onClick = { value-- }) { Text("-") }
        Text("Value: $value", fontSize = 20.sp)
        Button(onClick = { value++ }) { Text("+") }
    }
}

重點

  • Row 搭配 Arrangement.SpaceBetween,能清楚分配按鈕與文字的位置。

Modifier + 動態 UI

範例說明

  • 利用狀態切換背景顏色。

  • 示範 Modifier 動態控制 UI 外觀。

@Composable
fun ModifierStateExample() {
    var isActive by remember { mutableStateOf(false) }

    Text(
        text = if (isActive) "Active" else "Inactive",
        color = Color.White,
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .background(if (isActive) Color.Green else Color.Gray)
            .clickable { isActive = !isActive }
            .padding(16.dp)
    )
}

重點

  • Modifier.background() 可根據狀態動態切換。

  • 點擊文字即能改變狀態 → UI 自動更新。

0
Subscribe to my newsletter

Read articles from AgriLinq Admin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

AgriLinq Admin
AgriLinq Admin