Hi~ 我是 Eric!
上一篇我先說明了為什麼我要用 AI 陪自己重新學 Flutter。今天正式進入 Day 1。
不過,第一天我沒有急著打開 Flutter 然後開始堆畫面。
這件事很反直覺。因為很多人學前端或 App 開發時,第一個反應通常是:「先做一個畫面吧!」按鈕、卡片、列表、切頁,做出來才有成就感。
但這次我想換一種方式。
我不是要證明自己第一天就能寫很多 Flutter UI。我真正要完成的是:看得懂這個 AI 幫我整理出來的 Flutter Learning Lab 專案地圖。
只要我今天能從 README.md 走到 main.dart,再從 app.dart、router.dart 追到首頁和某個 feature 頁面,我就已經跨過 Flutter 初學最容易迷路的第一關。
Day 1 的目標:不是寫很多,而是不迷路
我這次拿來學習的專案,已經公開放在 GitHub:
這個專案不是單純的 Flutter demo。它現在更像是一份 Flutter 百科教學範例專案,我把它定位成 Flutter Learning Lab:用文章列表、表單與設定頁,學會現代 Flutter App 架構。
裡面包含:
- Dart 基礎練習
- Flutter UI 範例
- app 入口與 router
- feature-first 專案結構
- Riverpod、Dio、go_router、SharedPreferences
- docs 教學文件
- unit test、widget test、integration test
- GitHub Actions 驗證流程
換句話說,這不是「一個 App」,而是「一份可以拿來拆解的 Flutter 學習地圖」。
所以 Day 1 我先把目標壓小:
- 補上 Dart 最基本的語法概念
- 看懂 Flutter app 從
main()啟動後怎麼進到MyApp - 理解
app.dart、router.dart、home_page.dart的分工 - 建立 Everything is a Widget 的第一層理解
- 知道
lib/core/、lib/services/、lib/features/、docs/、test/各自負責什麼 - 能追蹤首頁上的一個學習項目連到哪個 route、哪個頁面
今天不是要成為 Flutter 工程師。
今天只是要讓我明天打開專案時,不會像走進地下迷宮一樣,連入口在哪都不知道。
第一步:先讀 README,不要先衝 main.dart
我以前看新專案有個壞習慣:直接找入口檔。
在 Flutter 裡就是立刻打開 lib/main.dart。這當然很快,但問題是,如果我還不知道專案作者想教什麼,直接看入口檔很容易只看到語法,卻看不到意圖。
所以這次我先讀 README.md。
這份 README 很明確地把專案定位成 Flutter 百科教學範例,而且它已經幫我畫出一張 30 秒專案地圖:
dart_foundation/ Dart 基礎練習
docs/ 學習文件與導讀
lib/core/ router、theme 這類 app 全域設定
lib/services/ API 與 storage service
lib/views/ 首頁與 UI kit
lib/features/ posts、settings、profile_form 等功能範例
test/ unit / widget test
integration_test/ integration smoke test
這張圖對初學者很重要。
因為 Flutter 專案一開始最容易卡住的不是語法,而是你不知道自己看到的檔案屬於哪一層。
例如:
lib/core/router.dart不是某個畫面,它是全站切頁規則lib/core/theme.dart是全域視覺規則,不應該散落在每個 Widget 裡lib/services/是跨 feature 共用的 API 或本機儲存服務lib/views/home_page.dart是首頁,也是學習項目的入口lib/features/posts/是一個完整 feature 範例,不只是列表畫面docs/lessons/是觀念文件docs/features/是功能導讀test/則是在示範怎麼驗證這些範例
先讀 README 的好處是,我還沒開始看程式碼,就先知道「我正在看哪一種東西」。
第二步:確認我的學習環境
新版專案有一個很真實的限制:我這台本機目前沒有 Flutter SDK。
這件事一開始聽起來很尷尬。學 Flutter,結果本機不能跑 Flutter?
但這反而讓我更清楚 Day 1 要學什麼。第一天不是先追求「我可以按下 run 看到畫面」,而是先確認整個專案的驗證責任放在哪裡。
如果本機可以安裝 Flutter,Day 1 應該先確認:
flutter --version
flutter doctor
flutter pub get
如果本機暫時不能安裝 Flutter,就走這個專案目前採用的流程:
- 本機先閱讀與維護
docs/、lib/、test/ - 推到 GitHub 後由 GitHub Actions 執行
flutter analyze - 再由 GitHub Actions 執行
flutter test --coverage - 把本機無法驗證的限制記錄在
PROJECT_STATUS.md
這也是我覺得 AI 輔助學習很適合這個場景的原因。它可以先帶我讀懂專案架構、文件與程式碼意圖,等環境補齊或 CI 跑完後,再回頭處理 analyzer / test 的實際結果。
第三步:Dart 先補地基
Flutter 使用 Dart。雖然我有 C# 和 JavaScript 背景,但 Dart 還是有幾個地方需要先對齊。
這個專案把 Dart 基礎放在兩個地方:
docs/lessons/DART_BASICS_GUIDE.mddart_foundation/
Day 1 我最需要看的不是全部進階語法,而是這幾個基本概念:
- 變數與型別
final和const- class、constructor、method
- nullable 型別,例如
String? requiredasync、await、Future- List、Map 的基本操作
這裡我最有感的是 null safety。
在 Dart 裡,String 和 String? 是兩種不同的承諾。
String name = 'Eric';
String? nickname;
String 代表這個值不應該是 null。String? 則是在提醒你:「這個值可能還沒有資料,你要處理它。」
這對 Flutter 很重要,因為 UI 會一直面對「資料還沒來」、「資料是空的」、「使用者還沒輸入」這些狀態。
如果我不先理解 nullable 型別,後面看表單、API、ViewModel 時,很容易被 ?、!、?? 這些符號追著跑。
另外一個重點是 Future。
Future<String> fetchUserData() async {
await Future.delayed(const Duration(seconds: 2));
return 'User: Eric';
}
Day 1 我不需要把 Dart async 學到很深,但至少要知道:
Future表示「未來會完成的一個結果」async讓 function 可以使用awaitawait會等待非同步結果回來- Flutter 裡 API request、本機儲存、初始化資料都會碰到這套模型
這個地基打好,後面看 posts feature 的 API 載入流程才不會霧煞煞。
第四步:看 Flutter app 怎麼啟動
接著我打開 lib/main.dart。
新版專案把 app 啟動流程拆得很乾淨:
lib/main.dart:只負責啟動 applib/app.dart:負責組裝MaterialApp.router、theme、router 和全域狀態lib/core/router.dart:集中管理 routelib/core/theme.dart:集中管理亮色與深色主題
它的啟動流程可以簡化成這樣:
main()
-> runApp()
-> ProviderScope
-> MyApp
-> MaterialApp.router
-> appRouter
-> HomePage
入口是:
void main() {
runApp(const ProviderScope(child: MyApp()));
}
這裡有兩個關鍵字。
第一個是 runApp()。這是 Flutter app 的起點,意思是把某個 Widget 當成整個 App 的根。
第二個是 ProviderScope。這是 Riverpod 的根容器,讓後面的 Provider、Repository、ViewModel 可以被整個 App 使用。
接著看 lib/app.dart。MyApp 是 ConsumerWidget,它會監聽 settings feature 的狀態,讓使用者在設定頁切換主題時,整個 app shell 可以跟著更新。
這裡我第一次感覺到 app shell 的價值。
main.dart 像是按下開機鍵,app.dart 才是整個 app 的總開關面板。
再往下看 MyApp,它回傳的是 MaterialApp.router。
這代表這個 App 不是用傳統 Navigator.push() 手動堆頁面,而是用 go_router 管理路由。
這時候我知道下一站要看哪裡了:
lib/core/router.dart
第五步:從 router 找到首頁和 feature
lib/core/router.dart 是這個專案的切頁地圖。
目前它定義了這些 route:
/ HomePage
/basic-widgets BasicWidgetsDemo
/layout LayoutPrinciplesDemo
/responsive ResponsiveDemo
/ui-kit UIKitView
/profile-form ProfileFormView
/posts PostListView
/settings SettingsView
這裡我第一次把 Flutter 的「頁面」和「路由」接起來。
首頁不是憑空出現的。它是因為 router 裡有這段規則:
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
)
當 app 啟動時,initialLocation 是 /,所以畫面會進到 HomePage。
這個發現很重要。
因為初學 Flutter 時,我常常會只盯著某個 Widget 看,卻忘記問:「這個 Widget 是誰帶我進來的?」
Router 就是在回答這個問題。
第六步:首頁其實是一張學習索引
接著看 lib/views/home_page.dart。
首頁是 StatelessWidget,它主要做一件事:顯示學習項目列表。
裡面有一個小資料結構:
class _LearningItem {
const _LearningItem(this.title, this.path);
final String title;
final String path;
}
首頁會把 _LearningItem 變成 ListTile。每個 ListTile 被點擊時,會呼叫:
context.push(item.path)
這樣就會從首頁跳到對應的 route。
這裡就是 Day 1 最小練習的核心。
我可以挑首頁上的任一項,例如 Riverpod + Dio,然後一路追:
HomePage
-> _LearningItem('Riverpod + Dio 文章列表', '/posts')
-> context.push('/posts')
-> router.dart
-> PostListView
這樣我就不是「看一堆檔案」,而是在走一條有方向的路。
第七步:Everything is a Widget,先懂第一層就好
接著讀 docs/lessons/WIDGET_MENTAL_MODEL.md。
Flutter 最常聽到的一句話是:
Everything is a Widget.
但 Day 1 我不想把這句話理解得太玄。
我先用最簡單的方式記:
Widget 不是傳統 UI 控制項,而是畫面的描述。
也就是說,Flutter 不是叫你直接操作一個畫面物件,而是叫你用 Widget tree 描述「現在畫面應該長什麼樣子」。
例如首頁大概可以想成:
Scaffold
-> AppBar
-> ListView
-> Section title
-> ListTile
-> ListTile
-> Divider
這也是為什麼 build() 很重要。
build() 不是「只執行一次,畫出畫面」。
它比較像是在說:「根據目前狀態,重新描述這個畫面應該長什麼樣子。」
Day 1 我先記住兩種 Widget:
StatelessWidget
StatelessWidget 適合資料進來、畫面畫出去,中間不保存互動狀態的情境。
像 HomePage 就是很好的例子。它只是顯示學習清單,點擊後交給 router 切頁。
StatefulWidget
StatefulWidget 適合畫面本身需要保存互動狀態。
例如表單欄位的 controller、簡單 counter、animation controller,這些通常需要生命週期和狀態保存。
在這個專案裡,ProfileFormView 就是 ConsumerStatefulWidget,因為它需要保存 _formKey 和多個 TextEditingController。
第八步:讀懂 feature-first 結構
Day 1 不需要深入每個 feature 的實作,但我要先知道它們各自在教什麼。
這個專案的 lib/features/ 下面有三個主要範例:
features/posts/
features/settings/
features/profile_form/
posts:API 與非同步狀態
features/posts/ 示範的是 Riverpod、Dio、Repository、ViewModel。
它的資料流可以簡化成:
PostListView
-> PostListViewModel
-> PostRepository
-> PostApiService
-> ApiClient
這個 feature 的學習重點不是「怎麼顯示列表」而已,而是:
- View 不直接碰 API
- ViewModel 管理 loading、data、error
- Repository 隔離資料來源
- Widget test 可以用 fake repository 測 UI
settings:本機設定與 app shell 狀態
features/settings/ 示範的是 SharedPreferences 和 ThemeMode。
它讓我看到一件事:設定頁不是只有自己的畫面,它會影響整個 App。
流程大概是:
SettingsView
-> SettingsViewModel.setThemeMode
-> SettingsRepository
-> StorageService
-> MyApp rebuild
-> MaterialApp.router(themeMode: ...)
這讓我開始理解,為什麼 app shell 需要讀取 settings state。
profile_form:表單、驗證與送出狀態
features/profile_form/ 示範的是 Form、TextFormField、validator 和 submit state。
這裡我可以學到:
- 欄位 controller 怎麼管理
- validator 怎麼拆出來
- submit loading state 怎麼顯示
- error message 應該回到 UI,而不是只印在 console
這三個 feature 剛好代表三種常見 App 問題:
- API 資料
- 本機設定
- 使用者輸入
第一天只要知道它們分別在教什麼,就已經很夠了。
Day 1 常見卡點:我怎麼問 Codex?
Day 1 最容易卡住的地方,不是某一段程式碼很難,而是「不知道該從哪裡開始看」。
所以我沒有問 Codex:
請教我 Flutter。
這個問題太大,回答通常會變成一份概念列表。
我改成問比較具體的問題。
Q1:這個專案第一個該看的檔案是什麼?
我今天是 Flutter Day 1。
請只根據這個 repo,告訴我第一個小時應該照什麼順序讀文件和程式碼。
不要列太多,只給我最重要的 5 個入口。
這讓 Codex 把順序收斂成:
README.md
docs/7_day_flutter_learning_plan.md
docs/lessons/DART_BASICS_GUIDE.md
lib/main.dart
lib/app.dart
我才沒有一開始就掉進 lib/features/ 裡迷路。
Q2:main.dart 和 app.dart 到底差在哪?
請用初學者能理解的方式解釋:
為什麼 main.dart 只放 runApp,
而 app.dart 要放 MaterialApp.router、theme、router?
這個問答幫我建立一個比喻:
main.dart 像按下開機鍵。
app.dart 像 app 的總開關面板。
有了這個比喻後,我再看 main()、ProviderScope、MyApp 就比較不會混在一起。
Q3:本機沒有 Flutter SDK,Day 1 還能學嗎?
如果我本機不能跑 flutter analyze 和 flutter test,
Day 1 還有哪些事情是有學習價值的?
哪些驗證應該交給 GitHub Actions?
Codex 幫我把工作拆成兩邊:
本機:讀文件、讀程式碼、理解架構、檢查 diff。
CI:flutter pub get、flutter analyze、flutter test --coverage。
這讓我比較不焦慮。第一天的重點不是一定要跑起 App,而是先知道這個專案怎麼被閱讀、怎麼被驗證。
Q4:feature-first 架構要先看哪一層?
我看到 features/posts、features/settings、features/profile_form。
Day 1 要不要深入看每個 feature?
還是先只理解它們各自在教什麼?
Codex 的建議是先不要深入。
Day 1 只要先知道:
posts:API 資料與列表狀態
settings:本地偏好與 ThemeMode
profile_form:表單、驗證與 submit state
這個回答很實用,因為它幫我把 Day 1 的範圍收住。
今日最小練習:改首頁的一個學習項目
Day 1 的練習我刻意設計得很小。
不是做完整 App,也不是新增功能。
我只要做這件事:
- 打開
lib/views/home_page.dart - 找到
_LearningItem - 修改其中一個學習項目的文字
- 找到它的 route path
- 打開
lib/core/router.dart - 找到這個 path 對應的 Widget
- 用自己的話寫下這個頁面由哪些 Widget 組成
例如我可以追 /posts:
HomePage
-> _LearningItem
-> /posts
-> PostListView
-> Scaffold
-> AppBar
-> AsyncValue.when
-> loading / data / error UI
這個練習看起來很簡單,但它其實同時練了四件事:
- 看懂首頁 Widget
- 看懂 route
- 找到 feature 頁面
- 練習拆 Widget tree
這比第一天硬寫一堆 UI 更重要。
因為我開始知道自己在專案裡的位置。
Day 1 結論:先拿到地圖,再開始走路
今天我沒有寫出什麼厲害的 Flutter 畫面。
但我完成了一件更重要的事:我知道這個 AI 生成的 Flutter 學習專案是怎麼組起來的。
我現在可以說出:
- Flutter app 從
main()開始,透過runApp()啟動 ProviderScope是 Riverpod 的根容器main.dart只負責啟動 appapp.dart負責組裝MaterialApp.router、theme、router 與全域狀態appRouter決定路徑對應哪個頁面/會進到HomePage- 首頁的
_LearningItem會透過context.push()切到 feature - Widget 是畫面描述,
build()是根據狀態重新描述畫面 StatelessWidget適合單純顯示,StatefulWidget適合保存互動狀態features/posts/、features/settings/、features/profile_form/分別示範 API、本機設定與表單- 本機不能跑 Flutter 時,可以先靠文件與程式碼閱讀學習,再交給 GitHub Actions 跑
flutter analyze和flutter test --coverage
這就是 Day 1 的成果。
不是「我會寫 Flutter 了」。
而是:「我打開這個專案時,終於知道自己站在哪裡。」
下一篇,我會開始更靠近 Flutter UI 本身,從 Widget、Layout 和 UI Kit 繼續往下拆。