【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}")
}
)
}
}
}
這段程式碼有什麼問題?
- 無法預覽 (No Preview): 你想用 Android Studio 的
@Preview功能看畫面?不行,因為預覽器無法提供NewsViewModel和NavController。 - 無法重用 (No Reusability): 這個
NewsScreen被鎖死在特定的 ViewModel 上,你不能在別的地方(例如收藏頁面)重用它顯示新聞列表。 - 測試困難: 你必須 Mock 一整個 ViewModel 才能測 UI。
為了讓 AI 寫出高品質的 UI,我們必須強迫它寫 「笨元件」(Dumb Components)。
02. 核心策略:Stateful vs Stateless
我們要教 AI 把 UI 拆成兩層:
- Stateful (聰明層): 負責與 ViewModel 和 Navigation 溝通。
- Stateless (笨蛋層): 只負責「拿資料顯示」和「回報點擊事件」。這裡嚴禁出現 ViewModel。
Prompt 策略:
"請將 NewsScreen 拆分為兩個 Composable:
- NewsRoute (Stateful): 負責從 ViewModel 收集狀態,並處理導航。
- 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。
- PreviewLoading: 顯示 Loading 狀態。
- PreviewSuccess: 顯示含有 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),我們達成以下目標:
- 解耦: UI 不再綁死 ViewModel。
- 可預覽: 開發者(和設計師)可以直接看 Preview,不用跑模擬器。
- 可測試: 不需要 Mock 複雜的依賴,只需要傳入 Data Class。
現在,我們的新專案已經有了完美的架構。但這世界並不完美,大多數時候,我們面對的是一坨已經存在的 Legacy Code (遺產代碼)。我們該如何用 AI 來重構一座運作中的「屎山」而不把它弄垮?
下一篇,我們將探討高階重構技巧:絞殺榕模式。
下集預告:【Part 9】遺產繼承 (Legacy Code):絞殺榕模式 (Strangler Fig)——如何利用 AI 安全地重構一座屎山

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【Android 架構系列 06】錯誤處理 (Error Handling):別讓 AI 吞掉你的 Exception
- 【Android 架構系列 07】狀態的藝術:Presentation Layer 設計——用 Sealed Interface 終結變數大爆炸
- 【Android 架構系列 08】現代化 UI (Compose):打造「笨」元件——利用 Preview 與 Slot API 讓 AI 生成完美的無狀態 UI (本文)
- 【Android 架構系列 09】遺產繼承 (Legacy Code):絞殺榕模式 (Strangler Fig)——如何利用 AI 安全地重構一座屎山
- 【Android 架構系列 10】最終驗收:測試驅動 AI (TDAD)——讓單元測試成為你的自動化質檢員