Hi~ 我是 Eric!

上一篇我先說明了為什麼我要用 AI 陪自己重新學 Flutter。今天正式進入 Day 1。

不過,第一天我沒有急著打開 Flutter 然後開始堆畫面。

這件事很反直覺。因為很多人學前端或 App 開發時,第一個反應通常是:「先做一個畫面吧!」按鈕、卡片、列表、切頁,做出來才有成就感。

但這次我想換一種方式。

我不是要證明自己第一天就能寫很多 Flutter UI。我真正要完成的是:看得懂這個 AI 幫我整理出來的 Flutter Learning Lab 專案地圖

只要我今天能從 README.md 走到 main.dart,再從 app.dartrouter.dart 追到首頁和某個 feature 頁面,我就已經跨過 Flutter 初學最容易迷路的第一關。


Day 1 的目標:不是寫很多,而是不迷路

我這次拿來學習的專案,已經公開放在 GitHub:

Flutter-Learning-Sample

這個專案不是單純的 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 我先把目標壓小:

  1. 補上 Dart 最基本的語法概念
  2. 看懂 Flutter app 從 main() 啟動後怎麼進到 MyApp
  3. 理解 app.dartrouter.darthome_page.dart 的分工
  4. 建立 Everything is a Widget 的第一層理解
  5. 知道 lib/core/lib/services/lib/features/docs/test/ 各自負責什麼
  6. 能追蹤首頁上的一個學習項目連到哪個 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,就走這個專案目前採用的流程:

  1. 本機先閱讀與維護 docs/lib/test/
  2. 推到 GitHub 後由 GitHub Actions 執行 flutter analyze
  3. 再由 GitHub Actions 執行 flutter test --coverage
  4. 把本機無法驗證的限制記錄在 PROJECT_STATUS.md

這也是我覺得 AI 輔助學習很適合這個場景的原因。它可以先帶我讀懂專案架構、文件與程式碼意圖,等環境補齊或 CI 跑完後,再回頭處理 analyzer / test 的實際結果。


第三步:Dart 先補地基

Flutter 使用 Dart。雖然我有 C# 和 JavaScript 背景,但 Dart 還是有幾個地方需要先對齊。

這個專案把 Dart 基礎放在兩個地方:

  • docs/lessons/DART_BASICS_GUIDE.md
  • dart_foundation/

Day 1 我最需要看的不是全部進階語法,而是這幾個基本概念:

  • 變數與型別
  • finalconst
  • class、constructor、method
  • nullable 型別,例如 String?
  • required
  • asyncawaitFuture
  • List、Map 的基本操作

這裡我最有感的是 null safety。

在 Dart 裡,StringString? 是兩種不同的承諾。

String name = 'Eric';
String? nickname;

String 代表這個值不應該是 nullString? 則是在提醒你:「這個值可能還沒有資料,你要處理它。」

這對 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 可以使用 await
  • await 會等待非同步結果回來
  • Flutter 裡 API request、本機儲存、初始化資料都會碰到這套模型

這個地基打好,後面看 posts feature 的 API 載入流程才不會霧煞煞。


第四步:看 Flutter app 怎麼啟動

接著我打開 lib/main.dart

新版專案把 app 啟動流程拆得很乾淨:

  • lib/main.dart:只負責啟動 app
  • lib/app.dart:負責組裝 MaterialApp.router、theme、router 和全域狀態
  • lib/core/router.dart:集中管理 route
  • lib/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.dartMyAppConsumerWidget,它會監聽 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/ 示範的是 FormTextFormField、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.dartapp.dart 到底差在哪?

請用初學者能理解的方式解釋:
為什麼 main.dart 只放 runApp,
而 app.dart 要放 MaterialApp.router、theme、router?

這個問答幫我建立一個比喻:

main.dart 像按下開機鍵。
app.dart 像 app 的總開關面板。

有了這個比喻後,我再看 main()ProviderScopeMyApp 就比較不會混在一起。

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,也不是新增功能。

我只要做這件事:

  1. 打開 lib/views/home_page.dart
  2. 找到 _LearningItem
  3. 修改其中一個學習項目的文字
  4. 找到它的 route path
  5. 打開 lib/core/router.dart
  6. 找到這個 path 對應的 Widget
  7. 用自己的話寫下這個頁面由哪些 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 只負責啟動 app
  • app.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 analyzeflutter test --coverage

這就是 Day 1 的成果。

不是「我會寫 Flutter 了」。

而是:「我打開這個專案時,終於知道自己站在哪裡。」

下一篇,我會開始更靠近 Flutter UI 本身,從 Widget、Layout 和 UI Kit 繼續往下拆。