Hi~ 我是 Eric!
Day 1 我先把 Flutter Learning Lab 的專案地圖看完,知道 README.md、main.dart、app.dart、router.dart、home_page.dart 各自在做什麼。
今天進入 Day 2。
這一天的主題是 Flutter 初學者幾乎一定會聽到的一句話:
Everything is a Widget.
老實說,第一次看到這句話時,我的反應是:好,聽起來很厲害,然後呢?
因為如果只是把它當成口號,其實幫助不大。真正有幫助的是把它翻成一個可以拿來讀程式碼的心智模型:
Flutter UI 不是一堆畫面控制項,而是一棵用 Widget 描述出來的畫面樹。
Day 2 我想完成的事情,就是把「巢狀括號很多的 Flutter 程式碼」翻成「我看得懂的 Widget tree」。
Day 2 的目標:把括號看成樹
今天我對照的專案內容主要是:
docs/lessons/WIDGET_MENTAL_MODEL.mddocs/lessons/FLUTTER_UI_GUIDE.mddocs/lessons/UI_COMPONENT_LIBRARY.mdlib/views/home_page.dartlib/01_basic_widgets.dartlib/02_state_management.dartlib/views/ui_kit_view.dart
Day 2 的目標不是一次學完所有 Flutter UI 元件。
我只想先做到這幾件事:
- 能用自己的話解釋 Everything is a Widget
- 能把一段 Flutter UI 程式碼翻成 Widget tree
- 能分辨
StatelessWidget和StatefulWidget - 能知道
build()不是畫一次畫面,而是描述目前狀態下的畫面 - 能判斷什麼時候該把一段 UI 拆成小 Widget
這一天的關鍵不是背元件,而是建立閱讀方式。
先從首頁開始,不要從抽象概念開始
我沒有先打開一大堆 Widget 文件。
我先回到 Day 1 看過的首頁:
lib/views/home_page.dart
這個檔案很適合當 Day 2 的入口,因為它不複雜,但又剛好包含 Flutter 畫面常見的結構:
ScaffoldAppBarListViewColumnPaddingTextListTileIconDivider
如果只看程式碼,Flutter 很容易變成一串括號。
但如果把它翻成樹,就會變成這樣:
HomePage
-> Scaffold
-> AppBar
-> Text
-> ListView
-> Column section
-> Padding
-> Text
-> ListTile
-> Text
-> Icon
-> Divider
-> Column section
-> CounterWidget
-> ListTile
-> ListTile
-> Divider
這一刻我才比較能理解「Everything is a Widget」。
它不是在說 Flutter 很愛把東西都叫 Widget。
它是在說:畫面的骨架、文字、按鈕、間距、排列方式、捲動方式,全部都用 Widget 這個共同語言描述。
Widget 不是畫面本身,而是畫面的描述
這句話是 Day 2 我最想記住的概念。
在 Flutter 裡,Widget 比較像是「畫面應該長什麼樣子」的描述,而不是一個你手動操作的畫面物件。
例如:
Text('Flutter 學習路徑')
它不是叫你直接去螢幕上畫字。
它是在描述:「這裡應該有一段文字,內容是 Flutter 學習路徑。」
再例如:
ListView(
children: [
ListTile(title: Text('01 基礎元件')),
ListTile(title: Text('UI 元件庫')),
],
)
它是在描述:「這裡應該有一個可以捲動的列表,裡面有兩個清單項目。」
這也是為什麼 Flutter 的 build() 很重要。
build() 不是「把畫面畫一次就結束」。
它比較像是在回答:
根據目前的資料與狀態,這個畫面現在應該被描述成什麼樣子?
這個理解一建立,後面看 state、API loading、表單錯誤、theme 切換時,就不會覺得畫面更新是一件很神祕的事。
StatelessWidget:資料進來,畫面畫出去
接著我用 lib/02_state_management.dart 來看 StatelessWidget 和 StatefulWidget 的差別。
專案裡有一個很小的範例:
class StaticText extends StatelessWidget {
final String text;
const StaticText(this.text, {super.key});
@override
Widget build(BuildContext context) {
return Text(text);
}
}
這段很適合用一句話理解:
資料從外面進來,Widget 負責把它顯示出去。
StaticText 自己不保存可變狀態。它收到 text,然後在 build() 裡回傳 Text(text)。
像這種情境就很適合 StatelessWidget:
- 顯示標題
- 顯示資訊卡
- 顯示清單項目
- 顯示從父層傳進來的資料
- 拆出一小塊純展示 UI
回頭看 HomePage,它也是 StatelessWidget。
首頁本身只是把學習項目列出來,點擊後交給 go_router 切頁。它沒有自己保存「目前選到哪一頁」這種狀態,所以用 StatelessWidget 很合理。
StatefulWidget:畫面需要保存短期狀態
同一個檔案裡也有 CounterWidget:
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
真正保存狀態的是對應的 State:
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
void _increment() {
setState(() {
_counter++;
});
}
}
這裡的 _counter 就是畫面自己的短期狀態。
使用者點按鈕,_counter 改變,然後 setState() 告訴 Flutter:
這個 Widget 的狀態變了,請重新執行
build(),用新的狀態描述畫面。
所以 StatefulWidget 適合這種情境:
- counter
- 展開 / 收合
- tab 當前選取狀態
- animation controller
- 表單 controller
- 使用者正在輸入、還不一定要同步到全域的暫時狀態
但這裡也有一個重要提醒:不是所有狀態都該丟進 StatefulWidget。
如果狀態需要跨頁共享、需要被測試、來自 API、會影響整個 app,就應該考慮 ViewModel、Riverpod 或 Repository。
這也是後面 Day 4、Day 5 會碰到的內容。
build() 不是生命週期儀式,而是畫面描述器
以前我看 Flutter 程式碼時,很容易把 build() 當成某種固定要覆寫的儀式。
每個 Widget 都有它,好像只是語法規定。
但 Day 2 我想換一個說法:
build() 是畫面描述器。
它做的不是「命令式地修改畫面」,而是回傳一棵 Widget tree。
例如 HomePage 的 build() 回傳 Scaffold:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter 學習路徑')),
body: ListView(...),
);
}
這段的白話是:
這個頁面現在應該是一個 Scaffold。
上面有 AppBar。
內容是一個可以捲動的 ListView。
ListView 裡有學習區塊與清單項目。
當狀態改變時,Flutter 會重新呼叫 build(),重新拿到一份新的畫面描述。
這也是為什麼我們在 build() 裡要避免做太重的事情。它可能會被呼叫很多次,所以它應該專心描述 UI,而不是偷偷做 API request 或複雜副作用。
Row、Column、Stack:先理解排列方式
Day 2 我沒有急著把所有 Material 3 元件背起來。
我先把最常見的 layout Widget 分成三種:
| Widget | 白話理解 | 適合情境 |
|---|---|---|
Row | 橫向排一排 | icon + 文字、左右兩側資訊 |
Column | 直向疊起來 | 表單欄位、卡片內容、區塊列表 |
Stack | 疊在同一個平面 | 圖片上疊文字、badge、浮動元素 |
docs/lessons/FLUTTER_UI_GUIDE.md 裡也提醒了一個重要概念:Flutter layout 不是你想放哪就放哪,而是會受到 constraints 影響。
我先用很粗的方式記:
父層給限制
子層決定尺寸
父層決定位置
這句話 Day 2 不需要完全吃透,但先知道它存在很重要。
因為很多 Flutter UI 爆版,不是因為你不會用 Row,而是因為你還沒理解限制如何往下傳。
例如 Row 裡放太長的文字,如果沒有 Expanded 或換行策略,就很容易 overflow。
這就是 Day 2 我先想記下來的第一個 UI 雷點。
UI Kit:把常見元件集中看一次
接著我打開:
lib/views/ui_kit_view.dart
這個頁面不是業務功能,它比較像「常用元件展示區」。
對初學者來說,UI Kit 很有用,因為它可以把常見元件放在同一個畫面裡觀察:
- buttons
- inputs
- cards
- chips
- dialogs
- snackbars
- lists
這裡我不是要背每個 Widget 的 API。
我比較想觀察的是:這些元件放在畫面上時,通常會搭配哪些外層 Widget?
例如:
- button 常常需要
onPressed - card 通常需要
Padding - list item 常常用
ListTile - dialog / snackbar 是 feedback
- input 會牽涉 controller、validator 或 state
這讓我開始把 Widget 分類,而不是只記名字。
Day 2 的重點不是「我知道很多 Widget」,而是「我知道看到一個 Widget 時,要問它負責哪一種 UI 角色」。
什麼時候該拆 Widget?
Flutter 初學時很容易出現一個狀況:整個頁面都塞在同一個 build() 裡。
短期看起來很快。
長期會變成括號地獄。
專案的 WIDGET_MENTAL_MODEL.md 給了一個很好用的判斷方式。當我看到這些訊號,就可以考慮拆 Widget:
build()超過一個螢幕還看不完- 同一段 UI 重複出現
- 某一小塊 UI 有清楚名稱
- 想單獨測某一塊 UI
- parent widget 同時處理 layout、資料、事件、樣式
拆 Widget 不是為了炫技。
它的目的很實際:讓畫面變得可讀、可維護、可測試。
例如首頁裡的 _LearningItem 目前只是一個小資料類別,但如果首頁清單項目之後變複雜,就可以考慮拆成 LearningItemTile。
這樣 HomePage 就不需要知道每個 list item 的細節,只需要負責「首頁有哪些學習區塊」。
我怎麼用 AI 學 Widget?
Day 2 我覺得 AI 最有幫助的地方,不是叫它幫我產生更多 UI。
而是叫它幫我翻譯 Widget tree。
我可以直接問:
請把 lib/views/home_page.dart 的 build() 轉成樹狀結構,
並用初學者聽得懂的方式說明每一層 Widget 的責任。
也可以問:
這段 UI 什麼地方適合拆成小 Widget?
哪些 state 應該留在 StatefulWidget?
哪些 state 之後應該交給 Riverpod?
這種問法比「幫我寫一個漂亮 UI」更有學習價值。
因為我不是把 AI 當外包,而是把它當成一個能看著程式碼講解的 Flutter 導師。
它可以幫我把括號翻成樹,把樹翻成責任,再把責任翻成架構判斷。
我實際怎麼追問 Codex?
Day 2 的學習過程其實很像在跟 Codex 一起看 code review。
我不是一次問:「請教我 Flutter Widget。」
這種問題太大,AI 很容易回答成教科書。
我比較有效的問法是拿著一個具體檔案,請它先幫我拆最小單位。例如:
請先只看 lib/views/home_page.dart。
不要急著講全部 Flutter Widget。
請用初學者能理解的方式,告訴我這個頁面的 Widget tree 長什麼樣子。
這樣 Codex 的回答就會貼著專案走,而不是泛泛介紹 Scaffold、AppBar、ListView。
接著我會繼續追問:
這個 HomePage 為什麼適合用 StatelessWidget?
它有沒有任何狀態需要自己保存?
如果未來清單項目變複雜,哪一段適合拆成新的 Widget?
這一輪問完,我才比較知道 StatelessWidget 不是「比較簡單的 Widget」,而是「自己不保存可變狀態的 Widget」。
然後我會換到 lib/02_state_management.dart:
請比較 StaticText 和 CounterWidget。
不要只說一個是 StatelessWidget、一個是 StatefulWidget。
請用狀態責任的角度解釋,為什麼 CounterWidget 需要 State。
這裡 Codex 會把重點拉回 _counter 和 setState()。
我再追問:
如果這個 counter 的數字需要被其他頁面使用,
它還適合只放在 StatefulWidget 裡嗎?
什麼時候應該改用 Riverpod 或 ViewModel?
這個問題很重要,因為它把 Day 2 的 Widget 狀態,先接到後面 Day 4、Day 5 會學的 Riverpod 與 ViewModel。
我發現跟 Codex 學 Flutter 時,最有效的節奏是這樣:
- 先指定一個檔案,不要讓問題發散
- 請它把程式碼翻成樹狀結構
- 追問每一層 Widget 的責任
- 再問「這裡為什麼不用另一種寫法」
- 最後請它給我一個最小修改練習
例如 Day 2 我最後會問:
請根據目前的 HomePage,設計一個不會破壞架構的 Day 2 最小練習。
練習要能幫我確認自己真的理解 Widget tree、StatelessWidget 和 StatefulWidget。
這種對話讓我比較像是在被一位老師引導,而不是只是收到一份答案。
AI 生成答案很快,但真正讓我學會的,是我可以針對同一段程式碼一直問「為什麼」。
Day 2 常見卡點:我用問答把概念釐清
看完 Widget tree 之後,我發現自己還是會有一些很初學者的問題。
這些問題如果只自己悶著看,很容易越看越抽象。但丟給 Codex 之後,我可以用一問一答的方式,把每個卡點拆小。
Q1:Everything is a Widget 是不是代表所有東西都一樣?
我問 Codex:
Flutter 說 Everything is a Widget,
這是不是代表 Text、Padding、ListView、Scaffold 都是同一種東西?
那我怎麼知道每個 Widget 的責任?
Codex 幫我整理成一個比較好懂的說法:
它們都叫 Widget,但責任不同。
Scaffold 是頁面骨架。
AppBar 是頂部列。
ListView 是可捲動內容。
Padding 是空間規則。
Text 是內容。
所以我後來不再只問「這是不是 Widget」,而是改問:
這個 Widget 在這棵樹裡負責什麼?
它是在處理結構、排版、內容、互動,還是回饋?
這個問法比背 Widget 名稱有用很多。
Q2:StatelessWidget 真的完全不會變嗎?
這是我一開始很容易誤會的地方。
我問:
StatelessWidget 叫做 stateless,
那它是不是代表畫面永遠不會改變?
如果父層傳進來的資料變了,它會不會重建?
Codex 的解釋是:StatelessWidget 不是「畫面永遠不變」,而是「自己不保存可變狀態」。
父層如果用新的資料重建它,它仍然可以顯示不同內容。
差別在於它自己沒有 _counter、controller、animation 這種內部 state。
所以我改成這樣記:
StatelessWidget:
自己不記狀態,但可以根據外部傳入的資料重新畫出不同畫面。
這讓我比較能理解 HomePage 為什麼適合 StatelessWidget。
首頁會顯示清單,也會讓我點擊切頁,但它自己不需要保存一份會變動的內部狀態。
Q3:什麼時候才需要 StatefulWidget?
我接著問:
如果 StatelessWidget 也會因為父層資料改變而重建,
那 StatefulWidget 到底什麼時候才需要?
Codex 叫我回去看 CounterWidget。
因為 _counter 不是父層傳進來的資料,而是這個 widget 自己管理的短期 UI 狀態。
使用者點按鈕,_counter 改變,setState() 觸發重建。
這讓我把判斷標準改成:
這個狀態是不是只屬於這個畫面的一小段互動?
如果是,StatefulWidget 可能合理。
這個狀態是不是需要跨頁共享、被測試、來自 API、或影響整個 app?
如果是,就不要急著塞進 StatefulWidget。
這個問題也剛好銜接到後面 Day 4、Day 5 的 ViewModel 和 Riverpod。
Q4:build() 被重複執行會不會很可怕?
我以前看到 Flutter 一直 rebuild,會直覺覺得很危險。
於是我問:
如果 setState 會重新執行 build(),
那 build() 被呼叫很多次是不是代表效能很差?
我應該避免 rebuild 嗎?
Codex 的回答讓我比較安心:
build() 本來就可能被呼叫很多次。重點不是「完全避免 rebuild」,而是讓 build() 保持單純,專心回傳 Widget tree。
所以我記下兩個原則:
build() 裡適合描述 UI。
build() 裡不適合偷偷打 API、寫檔案、做很重的計算或副作用。
這也讓我比較能理解,為什麼後面 Posts API 載入會放到 ViewModel / Repository,而不是直接塞在 Widget 的 build() 裡。
Q5:什麼時候該拆 Widget?
我問 Codex:
如果我把 UI 拆成很多小 Widget,
會不會反而更難找?
什麼時候拆才合理?
Codex 給我的判斷很實用:
當一段 UI 有清楚名稱、會重複出現、需要單獨測試,
或讓 parent build() 變得太長時,就可以拆。
所以 Day 2 我先不追求「拆越多越好」。
我只問:
這一段 UI 能不能被命名?
拆出去後,原本的 build() 會不會更好讀?
如果答案是 yes,再拆就比較合理。
Q6:Row overflow 到底是誰的錯?
Day 2 還有一個很常見的卡點:Row 裡文字太長,畫面爆掉。
我問:
為什麼 Row 裡放長文字會 overflow?
我是不是只要包 Container 設寬度就好?
Codex 把問題拉回 layout constraints:
Row 會讓 children 橫向排列。
如果某個 child 想要的寬度太大,
又沒有 Expanded、Flexible 或換行策略,
就可能超出父層限制。
所以我先記一個簡單規則:
看到 overflow,不要先亂包固定寬高。
先問:父層給了什麼限制?子層有沒有彈性空間?
這讓我知道 Day 2 還不需要完全精通 layout,但至少要開始用 constraints 的角度看問題。
這些問答看起來都很小。
但它們讓我從「我知道一些名詞」變成「我知道遇到卡點時可以怎麼問」。
今日最小練習:畫出首頁 Widget Tree
Day 2 的最小練習很單純:
- 打開
lib/views/home_page.dart - 找到
build() - 先不要改程式碼
- 把整個畫面手動寫成 Widget tree
- 再挑一個清單項目,追它點擊後會去哪個 route
- 最後打開
lib/02_state_management.dart,比較StaticText和CounterWidget
我會用這樣的格式寫筆記:
HomePage
-> Scaffold:頁面骨架
-> AppBar:頁面標題
-> ListView:可捲動內容
-> ListTile:單一學習入口
-> context.push:交給 go_router 切頁
然後回答三個問題:
- 這個頁面哪些 Widget 只是顯示資料?
- 這個頁面有沒有自己保存狀態?
- 如果清單項目變複雜,我會拆出哪個小 Widget?
這個練習看起來很小,但它會直接訓練 Flutter 最重要的閱讀能力。
Day 2 結論:我開始看得懂 Flutter 的畫面語言
Day 2 我沒有寫出很華麗的 UI。
但我開始看懂 Flutter 的畫面語言。
我現在可以說出:
- Everything is a Widget 代表 Flutter 用 Widget tree 描述 UI
- Widget 不是畫面本身,而是畫面描述
build()會根據目前狀態回傳 Widget treeStatelessWidget適合純展示或接收外部資料StatefulWidget適合保存畫面短期互動狀態- 不是所有狀態都該放在
StatefulWidget Row、Column、Stack先從排列方式理解- 拆 Widget 是為了可讀、可維護、可測試
- AI 很適合幫我把巢狀 UI 轉成 Widget tree
這一天的成果不是「我會做漂亮畫面了」。
而是:「我終於不再只看到一堆括號。」
下一篇,我會把 Widget 接回 App Shell,繼續看 Form、Router、Theme 怎麼一起組成一個比較完整的 Flutter app。