beio Logobeio

【Android 架構系列 08】現代化 UI (Compose):打造「笨」元件——利用 Preview 與 Slot API 讓 AI 生成完美的無狀態 UI

發布於

01. 前言:AI 寫的 UI 太「聰明」了

這是系列文的第八篇。

在前幾篇,我們已經把 ViewModel 整理得服服貼貼,透過 Sealed Interface 輸出了乾淨的 UiState。現在,我們要進入 Jetpack Compose 的領域。

Compose 是一個宣告式的 UI 框架,它的彈性非常大。但也正因為太彈性了,AI 非常喜歡把所有東西都塞在一起。如果你不加限制,AI 會寫出那種「直接在 Button onClick 裡面呼叫 ViewModel」的耦合代碼,導致你的 UI 元件無法預覽 (Preview),也無法重用。

這篇將教你如何指揮 AI,打造出 「笨」 (Dumb)「無狀態」 (Stateless) 的 UI 元件,讓你的 Compose 代碼達到完美的解耦。

當你要求 AI:「寫一個新聞列表畫面,點擊後跳轉。」

AI 通常會給你這樣的 Composable:

// ❌ AI 預設生成的「聰明」元件
@Composable
fun NewsScreen(
    viewModel: NewsViewModel = hiltViewModel(), // 😱 直接依賴 ViewModel
    navController: NavController // 😱 直接依賴導航控制器
) {
    val state by viewModel.uiState.collectAsState()

    LazyColumn {
        items(state.news) { article ->
            NewsItem(
                article = article,
                onClick = { 
                    // 😱 邏輯洩漏:直接在這裡處理導航參數
                    navController.navigate("detail/${article.id}") 
                }
            )
        }
    }
}

這段程式碼有什麼問題?

  1. 無法預覽 (No Preview): 你想用 Android Studio 的 @Preview 功能看畫面?不行,因為預覽器無法提供 NewsViewModelNavController
  2. 無法重用 (No Reusability): 這個 NewsScreen 被鎖死在特定的 ViewModel 上,你不能在別的地方(例如收藏頁面)重用它顯示新聞列表。
  3. 測試困難: 你必須 Mock 一整個 ViewModel 才能測 UI。

為了讓 AI 寫出高品質的 UI,我們必須強迫它寫 「笨元件」(Dumb Components)

02. 核心策略:Stateful vs Stateless

我們要教 AI 把 UI 拆成兩層:

  1. Stateful (聰明層): 負責與 ViewModel 和 Navigation 溝通。
  2. Stateless (笨蛋層): 只負責「拿資料顯示」和「回報點擊事件」。這裡嚴禁出現 ViewModel。

Prompt 策略:

"請將 NewsScreen 拆分為兩個 Composable:

  1. NewsRoute (Stateful): 負責從 ViewModel 收集狀態,並處理導航。
  2. NewsScreen (Stateless): 只接受 NewsUiState 和 lambda callback (onNewsClick)。不包含任何 ViewModel 或 NavController。"

AI 生成的代碼:

// ✅ 1. Stateful 層 (負責接線)
@Composable
fun NewsRoute(
    viewModel: NewsViewModel = hiltViewModel(),
    onNavigateToDetail: (String) -> Unit
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    // 將狀態與事件往下傳
    NewsScreen(
        uiState = uiState,
        onNewsClick = onNavigateToDetail
    )
}

// ✅ 2. Stateless 層 (純粹顯示,可以 Preview!)
@Composable
fun NewsScreen(
    uiState: NewsUiState, // 只依賴純資料
    onNewsClick: (String) -> Unit // 事件透過 Lambda 往上報
) {
    when (uiState) {
        is NewsUiState.Success -> {
            LazyColumn {
                items(uiState.news) { article ->
                    NewsItem(
                        article = article,
                        onClick = { onNewsClick(article.id) }
                    )
                }
            }
        }
        // ... 其他狀態處理
    }
}

筆者點評:

現在,NewsScreen 變得很「笨」。它不知道資料從哪來(ViewModel? Room? 假的?),也不知道點擊後會去哪。它只知道「顯示這個」和「回報點擊」。

這意味著我們可以隨意地餵假資料給它,進行 Preview 或 Screenshot Test。

03. Slot API:讓 AI 寫出積木

如果你的 UI 比較複雜,例如有一個共用的 Scaffold (包含 TopBar, FAB),AI 往往會把它寫死。

我們要利用 Compose 的 Slot API (插槽模式),讓 AI 生成可插拔的元件。

Prompt 策略:

"請設計一個 NewsLayout。 使用 Slot API,接受 topBar, content, bottomBar 三個 Composable 參數。 這樣我可以自由替換標題列或內容。"

AI 生成的代碼:

@Composable
fun NewsLayout(
    topBar: @Composable () -> Unit,
    content: @Composable (PaddingValues) -> Unit,
    bottomBar: @Composable () -> Unit = {}
) {
    Scaffold(
        topBar = topBar,
        bottomBar = bottomBar,
        content = content
    )
}

這樣一來,AI 就不會把 TopAppBar 的標題寫死成 "新聞",你可以隨時傳入不同的 TopBar。

04. Preview 作為驗收:AI 寫得好不好,看預覽就知道

我們怎麼確定 AI 真的沒有在 UI 裡偷藏邏輯?

要求它寫 Preview。

如果 AI 在 Composable 裡偷用了 ViewModel,它寫出的 @Preview 就會報錯(因為 Preview 無法注入 Hilt)。

如果它寫的是純正的 Stateless Composable,Preview 就能完美呈現。

Prompt 策略:

"請為 NewsScreen 建立多個 Preview。

  1. PreviewLoading: 顯示 Loading 狀態。
  2. PreviewSuccess: 顯示含有 3 筆假資料的列表。
  3. PreviewError: 顯示錯誤訊息。 使用 PreviewParameterProvider 提供假資料。"

AI 生成的驗收代碼:

@Preview(showBackground = true)
@Composable
fun PreviewSuccess() {
    MyTheme {
        // 因為是 Stateless,我們可以輕鬆餵入假資料
        NewsScreen(
            uiState = NewsUiState.Success(
                listOf(
                    Article("1", "AI 統治世界", "Gemini", Date()),
                    Article("2", "Compose 真好用", "Google", Date())
                )
            ),
            onNewsClick = {}
        )
    }
}

當你在 Android Studio 看到這三個 Preview 都能正常顯示時,你就知道你的 UI 層架構是乾淨的。

05. 結論:笨一點,走得遠

在 Compose 的世界裡,「笨」是一種美德

透過強制區分 Stateful (Route)Stateless (Screen),我們達成以下目標:

  1. 解耦: UI 不再綁死 ViewModel。
  2. 可預覽: 開發者(和設計師)可以直接看 Preview,不用跑模擬器。
  3. 可測試: 不需要 Mock 複雜的依賴,只需要傳入 Data Class。

現在,我們的新專案已經有了完美的架構。但這世界並不完美,大多數時候,我們面對的是一坨已經存在的 Legacy Code (遺產代碼)。我們該如何用 AI 來重構一座運作中的「屎山」而不把它弄垮?

下一篇,我們將探討高階重構技巧:絞殺榕模式

下集預告:【Part 9】遺產繼承 (Legacy Code):絞殺榕模式 (Strangler Fig)——如何利用 AI 安全地重構一座屎山


Ken Huang

關於作者

Ken Huang

熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。

最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。

這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。

Android APP DevelopmentiOS APP DevelopmentBePTT CreatorFull Stack Learner