flutter-dart-code-review

flutter-dart-code-review

熱門

與函式庫無關的 Flutter/Dart 程式碼審查清單,涵蓋 Widget 最佳實踐、狀態管理模式(BLoC、Riverpod、Provider、GetX、MobX、Signals)、Dart 慣用語法、效能、無障礙、安全性與乾淨架構。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
flutter-dart-code-review
description

與函式庫無關的 Flutter/Dart 程式碼審查清單,涵蓋 Widget 最佳實踐、狀態管理模式(BLoC、Riverpod、Provider、GetX、MobX、Signals)、Dart 慣用語法、效能、無障礙、安全性與乾淨架構。

Flutter/Dart 程式碼審查最佳實踐

全面且與函式庫無關的 Flutter/Dart 應用程式審查清單。這些原則適用於任何狀態管理解決方案、路由函式庫或依賴注入框架。


1. 專案整體健康度

  • [ ] 專案遵循一致的資料夾結構(功能優先或分層優先)
  • [ ] 關注點適當分離:UI、商業邏輯、資料層
  • [ ] Widget 中不含商業邏輯;Widget 純粹負責呈現
  • [ ] pubspec.yaml 乾淨 — 無未使用的依賴,版本適當鎖定
  • [ ] analysis_options.yaml 包含嚴格的 lint 規則集,並啟用嚴格的分析器設定
  • [ ] 正式環境程式碼中無 print() 陳述式 — 使用 dart:developerlog() 或日誌套件
  • [ ] 產生的檔案(.g.dart.freezed.dart.gr.dart)為最新版本或已加入 .gitignore
  • [ ] 平台特定程式碼隔離在抽象層之後

2. Dart 語言陷阱

  • [ ] 隱含的 dynamic:缺少型別註解導致 dynamic — 啟用 strict-castsstrict-inferencestrict-raw-types
  • [ ] 空安全誤用:過度使用 !(強制運算子)而非適當的空值檢查或 Dart 3 模式匹配(if (value case var v?)
  • [ ] 型別提升失敗:使用 this.field 而本機變數提升即可達成
  • [ ] 捕捉範圍過廣catch (e) 未加 on 子句;務必指定例外型別
  • [ ] 捕捉 ErrorError 子型別代表程式錯誤,不應被捕捉
  • [ ] 未使用的 async:標記為 async 但從未 await 的函式 — 不必要的開銷
  • [ ] 過度使用 late:在可為空或建構子初始化更安全的地方使用 late,將錯誤延遲到執行時期
  • [ ] 迴圈中的字串串接:使用 StringBuffer 而非 + 進行迭代字串建構
  • [ ] const 上下文中的可變狀態const 建構子類別中的欄位不應是可變的
  • [ ] 忽略 Future 回傳值:使用 await 或明確呼叫 unawaited() 來表達意圖
  • [ ] 使用 varfinal 即可:偏好對區域變數使用 final,對編譯期常數使用 const
  • [ ] 相對匯入:使用 package: 匯入以保持一致性
  • [ ] 暴露可變集合:公開 API 應回傳不可修改的視圖,而非原始 List/Map
  • [ ] 缺少 Dart 3 模式匹配:偏好 switch 表達式和 if-case 而非冗長的 is 檢查與手動轉型
  • [ ] 為多重回傳建立一次性類別:使用 Dart 3 記錄 (String, int) 而非一次性 DTO
  • [ ] 正式環境程式碼中的 print():使用 dart:developerlog() 或專案的日誌套件;print() 沒有日誌層級且無法過濾

3. Widget 最佳實踐

Widget 分解:

  • [ ] 單一 Widget 的 build() 方法不超過約 80-100 行
  • [ ] Widget 根據封裝性以及變更方式(重建邊界)進行拆分
  • [ ] 回傳 Widget 的私有 _build*() 輔助方法應提取為獨立的 Widget 類別(啟用元素重用、const 傳播與框架最佳化)
  • [ ] 在不需要可變區域狀態時,優先使用 StatelessWidget 而非 StatefulWidget
  • [ ] 可重用的提取 Widget 應放在獨立檔案中

Const 使用:

  • [ ] 盡可能使用 const 建構子 — 防止不必要的重建
  • [ ] 對不變的集合使用 const 字面值(const []const {}
  • [ ] 當所有欄位皆為 final 時,建構子宣告為 const

Key 使用:

  • [ ] 在列表/網格中使用 ValueKey 以在重新排序時保留狀態
  • [ ] 謹慎使用 GlobalKey — 僅在真正需要跨樹存取狀態時使用
  • [ ] 避免在 build() 中使用 UniqueKey — 它會強制每一幀重建
  • [ ] 當身份基於資料物件而非單一值時使用 ObjectKey

主題與設計系統:

  • [ ] 顏色來自 Theme.of(context).colorScheme — 不硬編碼 Colors.red 或十六進位值
  • [ ] 文字樣式來自 Theme.of(context).textTheme — 不使用內聯 TextStyle 搭配原始字型大小
  • [ ] 驗證深色模式相容性 — 不假設淺色背景
  • [ ] 間距與尺寸使用一致的設計 Token 或常數,而非魔術數字

Build 方法複雜度:

  • [ ] build() 中不進行網路呼叫、檔案 I/O 或大量計算
  • [ ] build() 中不使用 Future.then()async 工作
  • [ ] build() 中不建立訂閱(.listen()
  • [ ] setState() 限制在最小的子樹範圍內

4. 狀態管理(與函式庫無關)

這些原則適用於所有 Flutter 狀態管理解決方案(BLoC、Riverpod、Provider、GetX、MobX、Signals、ValueNotifier 等)。

架構:

  • [ ] 商業邏輯存在於 Widget 層之外 — 在狀態管理元件中(BLoC、Notifier、Controller、Store、ViewModel 等)
  • [ ] 狀態管理器透過注入接收依賴,而非在內部自行建構
  • [ ] 服務或儲存庫層抽象化資料來源 — Widget 和狀態管理器不應直接呼叫 API 或資料庫
  • [ ] 狀態管理器具有單一職責 — 沒有處理不相關關注點的「上帝」管理器
  • [ ] 跨元件依賴遵循該解決方案的慣例:
    • Riverpod 中:provider 依賴其他 provider(透過 ref.watch)是預期的 — 僅標記循環或過度糾纏的鏈結
    • BLoC 中:bloc 不應直接依賴其他 bloc — 偏好共享儲存庫或呈現層協調
    • 在其他解決方案中:遵循文件記載的跨元件通訊慣例

不可變性與值相等性(適用於不可變狀態解決方案:BLoC、Riverpod、Redux):

  • [ ] 狀態物件是不可變的 — 透過 copyWith() 或建構子建立新實例,絕不原地修改
  • [ ] 狀態類別正確實作 ==hashCode(所有欄位都納入比較)
  • [ ] 機制在整個專案中保持一致 — 手動覆寫、Equatablefreezed、Dart 記錄或其他
  • [ ] 狀態物件內的集合不應以原始可變 List/Map 形式暴露

反應式紀律(適用於反應式變異解決方案:MobX、GetX、Signals):

  • [ ] 狀態僅透過該解決方案的反應式 API 進行變異(MobX 的 @action、signal 的 .value、GetX 的 .obs)— 直接欄位變異會繞過變更追蹤
  • [ ] 衍生值使用該解決方案的計算機制,而非冗餘儲存
  • [ ] 反應與釋放器被妥善清理(MobX 的 ReactionDisposer、Signals 的 effect 清理)

狀態形狀設計:

  • [ ] 互斥狀態使用密封型別、聯合變體或該解決方案的內建非同步狀態型別(例如 Riverpod 的 AsyncValue)— 而非布林旗標(isLoadingisErrorhasData
  • [ ] 每個非同步操作將載入、成功和錯誤建模為不同的狀態
  • [ ] 所有狀態變體在 UI 中都被窮舉處理 — 沒有被靜默忽略的情況
  • [ ] 錯誤狀態攜帶錯誤資訊以供顯示;載入狀態不攜帶過時資料
  • [ ] 可為空資料不用作載入指示器 — 狀態是明確的
// 不好 — 布林旗標混雜允許不可能狀態
class UserState {
  bool isLoading = false;
  bool hasError = false; // isLoading && hasError 是可表示的!
  User? user;
}

// 好(不可變方法)— 密封型別使不可能狀態無法表示
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
  final User user;
  const UserLoaded(this.user);
}
class UserError extends UserState {
  final String message;
  const UserError(this.message);
}

// 好(反應式方法)— 可觀察列舉 + 資料,透過反應式 API 進行變異
// enum UserStatus { initial, loading, loaded, error }
// 使用您的解決方案的 observable/signal 分別包裝狀態和資料

重建最佳化:

  • [ ] 狀態消費者 Widget(Builder、Consumer、Observer、Obx、Watch 等)範圍盡可能窄
  • [ ] 使用選擇器僅在特定欄位變更時重建 — 而非每次狀態發射時
  • [ ] 使用 const Widget 阻止重建在樹中傳播
  • [ ] 計算/衍生狀態以反應式方式計算,而非冗餘儲存

訂閱與釋放:

  • [ ] 所有手動訂閱(.listen())在 dispose() / close() 中取消
  • [ ] StreamController 在不再需要時關閉
  • [ ] 計時器在釋放生命週期中取消
  • [ ] 偏好框架管理的生命週期而非手動訂閱(宣告式 builder 優於 .listen()
  • [ ] 在非同步回呼中呼叫 setState 前檢查 mounted
  • [ ] 在 await 後不使用 BuildContext,除非檢查 context.mounted(Flutter 3.7+)— 過時的 context 會導致崩潰
  • [ ] 在非同步間隔後,未確認 Widget 仍掛載時,不進行導航、對話框或 Scaffold 訊息
  • [ ] BuildContext 絕不儲存在單例、狀態管理器或靜態欄位中

區域狀態 vs 全域狀態:

  • [ ] 短暫 UI 狀態(核取方塊、滑桿、動畫)使用區域狀態(setStateValueNotifier
  • [ ] 共享狀態僅提升到必要的高度 — 不過度全域化
  • [ ] 功能範圍的狀態在功能不再活躍時被妥善釋放

5. 效能

不必要的重建:

  • [ ] setState() 不在根 Widget 層級呼叫 — 將狀態變更局部化
  • [ ] 使用 const Widget 阻止重建傳播
  • [ ] 在獨立重繪的複雜子樹周圍使用 RepaintBoundary
  • [ ] 對獨立於動畫的子樹使用 AnimatedBuilder 的 child 參數

build() 中的昂貴操作:

  • [ ] 不在 build() 中對大型集合進行排序、過濾或映射 — 在狀態管理層計算
  • [ ] 不在 build() 中編譯正規表達式
  • [ ] MediaQuery.of(context) 的使用是具體的(例如 MediaQuery.sizeOf(context)

圖片最佳化:

  • [ ] 網路圖片使用快取(任何適合專案的快取解決方案)
  • [ ] 針對目標裝置使用適當的圖片解析度(不為縮圖載入 4K 圖片)
  • [ ] Image.asset 搭配 cacheWidth/cacheHeight 以顯示尺寸解碼
  • [ ] 為網路圖片提供佔位符和錯誤 Widget

延遲載入:

  • [ ] 對大型或動態列表使用 ListView.builder / GridView.builder 而非 ListView(children: [...])(具體建構子適用於小型靜態列表)
  • [ ] 對大型資料集實作分頁
  • [ ] 在 Web 建置中對大型函式庫使用延遲載入(deferred as

其他:

  • [ ] 避免在動畫中使用 Opacity Widget — 使用 AnimatedOpacityFadeTransition
  • [ ] 避免在動畫中裁剪 — 預先裁剪圖片
  • [ ] 不在 Widget 上覆寫 operator == — 改用 const 建構子
  • [ ] 謹慎使用內在尺寸 Widget(IntrinsicHeightIntrinsicWidth)(額外佈局傳遞)

6. 測試

測試類型與期望:

  • [ ] 單元測試:涵蓋所有商業邏輯(狀態管理器、儲存庫、工具函式)
  • [ ] Widget 測試:涵蓋個別 Widget 行為、互動與視覺輸出
  • [ ] 整合測試:端到端涵蓋關鍵使用者流程
  • [ ] 黃金測試:對設計關鍵的 UI 元件進行像素完美比較

涵蓋率目標:

  • [ ] 目標商業邏輯行涵蓋率 80% 以上
  • [ ] 所有狀態轉換都有對應的測試(載入→成功、載入→錯誤、重試等)
  • [ ] 測試邊界情況:空狀態、錯誤狀態、載入狀態、邊界值

測試隔離:

  • [ ] 外部依賴(API 客戶端、資料庫、服務)被模擬或偽造
  • [ ] 每個測試檔案僅測試一個類別/單元
  • [ ] 測試驗證行為,而非實作細節
  • [ ] Stub 僅定義每個測試所需的行為(最小 stub)
  • [ ] 測試案例之間不共享可變狀態

Widget 測試品質:

  • [ ] 正確使用 pumpWidgetpump 處理非同步操作
  • [ ] 適當使用 find.byTypefind.textfind.byKey
  • [ ] 沒有依賴時間的不穩定測試 — 使用 pumpAndSettle 或明確的 pump(Duration)
  • [ ] 測試在 CI 中執行,失敗會阻止合併

7. 無障礙

語意 Widget:

  • [ ] 在自動標籤不足時,使用 Semantics Widget 提供螢幕閱讀器標籤
  • [ ] 對純裝飾性元素使用 ExcludeSemantics
  • [ ] 使用 MergeSemantics 將相關 Widget 合併為單一可存取元素
  • [ ] 圖片設定 semanticLabel 屬性

螢幕閱讀器支援:

  • [ ] 所有互動元素都可聚焦並具有有意義的描述
  • [ ] 焦點順序符合邏輯(遵循視覺閱讀順序)

視覺無障礙:

  • [ ] 文字與背景的對比度 >= 4.5:1
  • [ ] 可點擊目標至少 48x48 像素
  • [ ] 顏色不是狀態的唯一指示器(同時使用圖示/文字)
  • [ ] 文字隨系統字型大小設定縮放

互動無障礙:

  • [ ] 沒有無作用的 onPressed 回呼 — 每個按鈕都有功能或被停用
  • [ ] 錯誤欄位建議修正
  • [ ] 使用者輸入資料時,上下文不會意外變更

8. 平台特定考量

iOS/Android 差異:

  • [ ] 在適當情況下使用平台適應性 Widget
  • [ ] 正確處理返回導航(Android 返回按鈕、iOS 滑動返回)
  • [ ] 透過 SafeArea Widget 處理狀態列和安全區域
  • [ ] 在 AndroidManifest.xmlInfo.plist 中宣告平台特定權限

響應式設計:

  • [ ] 使用 LayoutBuilderMediaQuery 進行響應式佈局
  • [ ] 一致地定義斷點(手機、平板、桌面)
  • [ ] 文字在小螢幕上不溢出 — 使用 FlexibleExpandedFittedBox
  • [ ] 測試橫向模式或明確鎖定
  • [ ] Web 特定:支援滑鼠/鍵盤互動,存在懸浮狀態

9. 安全性

安全儲存:

  • [ ] 敏感資料(Token、憑證)使用平台安全儲存(iOS 的 Keychain、Android 的 EncryptedSharedPreferences)
  • [ ] 絕不以純文字儲存機密
  • [ ] 考慮對敏感操作使用生物辨識驗證閘道

API 金鑰處理:

  • [ ] API 金鑰不硬編碼在 Dart 原始碼中 — 使用 --dart-define、排除在 VCS 外的 .env 檔案,或編譯期設定
  • [ ] 機密不提交到 git — 檢查 .gitignore
  • [ ] 對真正機密的金鑰使用後端代理(客戶端不應持有伺服器機密)

輸入驗證:

  • [ ] 所有使用者輸入在發送到 API 前經過驗證
  • [ ] 表單驗證使用適當的驗證模式
  • [ ] 沒有原始 SQL 或使用者輸入的字串插值
  • [ ] 深度連結 URL 在導航前經過驗證和清理

網路安全:

  • [ ] 所有 API 呼叫強制使用 HTTPS
  • [ ] 對高安全性應用程式考慮憑證釘選
  • [ ] 驗證 Token 被適當重新整理和過期
  • [ ] 不記錄或列印敏感資料

10. 套件/依賴審查

評估 pub.dev 套件:

  • [ ] 檢查 pub points 分數(目標 130+/160)
  • [ ] 檢查 讚數人氣 作為社群訊號
  • [ ] 確認發布者在 pub.dev 上 已驗證
  • [ ] 檢查最後發布日期 — 超過 1 年的過時套件有風險
  • [ ] 檢視開放問題與維護者的回應時間
  • [ ] 檢查授權條款與專案的相容性
  • [ ] 確認平台支援涵蓋您的目標

版本約束:

  • [ ] 對依賴使用插入符號語法(^1.2.3)— 允許相容更新
  • [ ] 僅在絕對必要時鎖定確切版本
  • [ ] 定期執行 flutter pub outdated 以追蹤過時依賴
  • [ ] 正式環境的 pubspec.yaml 中沒有依賴覆寫 — 僅用於臨時修正,並附上註解/問題連結
  • [ ] 最小化傳遞依賴數量 — 每個依賴都是一個攻擊面

Monorepo 特定(melos/workspace):

  • [ ] 內部套件僅從公開 API 匯入 — 沒有 package:other/src/internal.dart(破壞 Dart 套件封裝)
  • [ ] 內部套件依賴使用 workspace 解析,而非硬編碼的 path: ../../ 相對字串
  • [ ] 所有子套件共享或繼承根目錄的 analysis_options.yaml

11. 導航與路由

通用原則(適用於任何路由解決方案):

  • [ ] 一致使用一種路由方法 — 不混合命令式 Navigator.push 與宣告式路由器
  • [ ] 路由參數有型別 — 沒有 Map<String, dynamic>Object? 轉型
  • [ ] 路由路徑定義為常數、列舉或產生 — 程式碼中不散佈魔術字串
  • [ ] 認證守衛/重新導向集中管理 — 不在個別畫面中重複
  • [ ] 深度連結同時為 Android 和 iOS 設定
  • [ ] 深度連結 URL 在導航前經過驗證和清理
  • [ ] 導航狀態可測試 — 路由變更可在測試中驗證
  • [ ] 返回行為在所有平台上正確

12. 錯誤處理

框架錯誤處理:

  • [ ] 覆寫 FlutterError.onError 以捕捉框架錯誤(建置、佈局、繪製)
  • [ ] 設定 PlatformDispatcher.instance.onError 以處理 Flutter 未捕捉的非同步錯誤
  • [ ] 自訂 ErrorWidget.builder 以用於發布模式(使用者友善而非紅屏)
  • [ ] 在 runApp 周圍使用全域錯誤捕捉包裝(例如 runZonedGuarded、Sentry/Crashlytics 包裝)

錯誤回報:

  • [ ] 整合錯誤回報服務(Firebase Crashlytics、Sentry 或同等服務)
  • [ ] 非致命錯誤附帶堆疊追蹤回報
  • [ ] 狀態管理錯誤觀察器連接到錯誤回報(例如 BlocObserver、ProviderObserver,或您的解決方案的同等機制)
  • [ ] 使用者可識別資訊(使用者 ID)附加到錯誤報告以利除錯

優雅降級:

  • [ ] API 錯誤導致使用者友善的錯誤 UI,而非崩潰
  • [ ] 對暫時性網路失敗有重試機制
  • [ ] 離線狀態被優雅處理
  • [ ] 狀態管理中的錯誤狀態攜帶錯誤資訊以供顯示
  • [ ] 原始例外(網路、解析)在到達 UI 前被映射為使用者友善、本地化的訊息 — 絕不向使用者顯示原始例外字串

13. 國際化(l10n)

設定:

  • [ ] 設定本地化解決方案(Flutter 內建的 ARB/l10n、easy_localization 或同等方案)
  • [ ] 在應用程式設定中宣告支援的語言環境

內容:

  • [ ] 所有使用者可見字串使用本地化系統 — Widget 中沒有硬編碼字串
  • [ ] 範本檔案包含給翻譯人員的描述/上下文
  • [ ] 對複數、性別、選擇使用 ICU 訊息語法
  • [ ] 佔位符定義型別
  • [ ] 各語言環境之間沒有遺失的鍵

程式碼審查:

  • [ ] 在整個專案中一致使用本地化存取器
  • [ ] 日期、時間、數字和貨幣格式具有語言環境感知
  • [ ] 如果目標語言為阿拉伯語、希伯來語等,支援文字方向(RTL)
  • [ ] 不對本地化文字進行字串串接 — 使用參數化訊息

14. 依賴注入

原則(適用於任何 DI 方法):

  • [ ] 類別依賴抽象(介面),而非在層級邊界依賴具體實作
  • [ ] 依賴透過建構子、DI 框架或 provider 圖從外部提供 — 不在內部建立
  • [ ] 註冊區分生命週期:單例 vs 工廠 vs 延遲單例
  • [ ] 環境特定繫結(開發/暫存/正式)使用設定,而非執行時期的 if 檢查
  • [ ] DI 圖中沒有循環依賴
  • [ ] 服務定位器呼叫(如果使用)不散佈在商業邏輯中

15. 靜態分析

設定:

  • [ ] analysis_options.yaml 存在並啟用嚴格設定
  • [ ] 嚴格分析器設定:strict-casts: truestrict-inference: truestrict-raw-types: true
  • [ ] 包含全面的 lint 規則集(very_good_analysis、flutter_lints 或自訂嚴格規則)
  • [ ] Monorepo 中的所有子套件繼承或共享根目錄的分析選項

強制執行:

  • [ ] 提交的程式碼中沒有未解決的分析器警告
  • [ ] Lint 抑制(// ignore:)附有註解說明原因
  • [ ] flutter analyze 在 CI 中執行,失敗會阻止合併

無論 lint 套件為何都要驗證的關鍵規則:

  • [ ] prefer_const_constructors — Widget 樹中的效能
  • [ ] avoid_print — 使用適當的日誌
  • [ ] unawaited_futures — 防止 fire-and-forget 非同步錯誤
  • [ ] prefer_final_locals — 變數層級的不可變性
  • [ ] always_declare_return_types — 明確的合約
  • [ ] avoid_catches_without_on_clauses — 特定的錯誤處理
  • [ ] always_use_package_imports — 一致的匯入風格

狀態管理快速參考

下表將通用原則映射到熱門解決方案的實作。使用此表將審查規則調整為專案使用的解決方案。

原則 BLoC/Cubit Riverpod Provider GetX MobX Signals 內建
狀態容器 Bloc/Cubit Notifier/AsyncNotifier ChangeNotifier GetxController Store signal() StatefulWidget
UI 消費者 BlocBuilder ConsumerWidget Consumer Obx/GetBuilder Observer Watch setState
選擇器 BlocSelector/buildWhen ref.watch(p.select(...)) Selector N/A computed computed() N/A
副作用 BlocListener ref.listen Consumer 回呼 ever()/once() reaction effect() 回呼
釋放 透過 BlocProvider 自動 .autoDispose 透過 Provider 自動 onClose() ReactionDisposer 手動 dispose()
測試 blocTest() ProviderContainer 直接 ChangeNotifier 測試中 Get.put 直接 store 直接 signal Widget 測試

來源