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:developer的log()或日誌套件 - [ ] 產生的檔案(
.g.dart、.freezed.dart、.gr.dart)為最新版本或已加入.gitignore - [ ] 平台特定程式碼隔離在抽象層之後
2. Dart 語言陷阱
- [ ] 隱含的 dynamic:缺少型別註解導致
dynamic— 啟用strict-casts、strict-inference、strict-raw-types - [ ] 空安全誤用:過度使用
!(強制運算子)而非適當的空值檢查或 Dart 3 模式匹配(if (value case var v?)) - [ ] 型別提升失敗:使用
this.field而本機變數提升即可達成 - [ ] 捕捉範圍過廣:
catch (e)未加on子句;務必指定例外型別 - [ ] 捕捉
Error:Error子型別代表程式錯誤,不應被捕捉 - [ ] 未使用的
async:標記為async但從未await的函式 — 不必要的開銷 - [ ] 過度使用
late:在可為空或建構子初始化更安全的地方使用late,將錯誤延遲到執行時期 - [ ] 迴圈中的字串串接:使用
StringBuffer而非+進行迭代字串建構 - [ ]
const上下文中的可變狀態:const建構子類別中的欄位不應是可變的 - [ ] 忽略
Future回傳值:使用await或明確呼叫unawaited()來表達意圖 - [ ] 使用
var而final即可:偏好對區域變數使用final,對編譯期常數使用const - [ ] 相對匯入:使用
package:匯入以保持一致性 - [ ] 暴露可變集合:公開 API 應回傳不可修改的視圖,而非原始
List/Map - [ ] 缺少 Dart 3 模式匹配:偏好 switch 表達式和
if-case而非冗長的is檢查與手動轉型 - [ ] 為多重回傳建立一次性類別:使用 Dart 3 記錄
(String, int)而非一次性 DTO - [ ] 正式環境程式碼中的
print():使用dart:developer的log()或專案的日誌套件;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 — 偏好共享儲存庫或呈現層協調
- 在其他解決方案中:遵循文件記載的跨元件通訊慣例
- 在 Riverpod 中:provider 依賴其他 provider(透過
不可變性與值相等性(適用於不可變狀態解決方案:BLoC、Riverpod、Redux):
- [ ] 狀態物件是不可變的 — 透過
copyWith()或建構子建立新實例,絕不原地修改 - [ ] 狀態類別正確實作
==和hashCode(所有欄位都納入比較) - [ ] 機制在整個專案中保持一致 — 手動覆寫、
Equatable、freezed、Dart 記錄或其他 - [ ] 狀態物件內的集合不應以原始可變
List/Map形式暴露
反應式紀律(適用於反應式變異解決方案:MobX、GetX、Signals):
- [ ] 狀態僅透過該解決方案的反應式 API 進行變異(MobX 的
@action、signal 的.value、GetX 的.obs)— 直接欄位變異會繞過變更追蹤 - [ ] 衍生值使用該解決方案的計算機制,而非冗餘儲存
- [ ] 反應與釋放器被妥善清理(MobX 的
ReactionDisposer、Signals 的 effect 清理)
狀態形狀設計:
- [ ] 互斥狀態使用密封型別、聯合變體或該解決方案的內建非同步狀態型別(例如 Riverpod 的
AsyncValue)— 而非布林旗標(isLoading、isError、hasData) - [ ] 每個非同步操作將載入、成功和錯誤建模為不同的狀態
- [ ] 所有狀態變體在 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 等)範圍盡可能窄
- [ ] 使用選擇器僅在特定欄位變更時重建 — 而非每次狀態發射時
- [ ] 使用
constWidget 阻止重建在樹中傳播 - [ ] 計算/衍生狀態以反應式方式計算,而非冗餘儲存
訂閱與釋放:
- [ ] 所有手動訂閱(
.listen())在dispose()/close()中取消 - [ ] StreamController 在不再需要時關閉
- [ ] 計時器在釋放生命週期中取消
- [ ] 偏好框架管理的生命週期而非手動訂閱(宣告式 builder 優於
.listen()) - [ ] 在非同步回呼中呼叫
setState前檢查mounted - [ ] 在
await後不使用BuildContext,除非檢查context.mounted(Flutter 3.7+)— 過時的 context 會導致崩潰 - [ ] 在非同步間隔後,未確認 Widget 仍掛載時,不進行導航、對話框或 Scaffold 訊息
- [ ]
BuildContext絕不儲存在單例、狀態管理器或靜態欄位中
區域狀態 vs 全域狀態:
- [ ] 短暫 UI 狀態(核取方塊、滑桿、動畫)使用區域狀態(
setState、ValueNotifier) - [ ] 共享狀態僅提升到必要的高度 — 不過度全域化
- [ ] 功能範圍的狀態在功能不再活躍時被妥善釋放
5. 效能
不必要的重建:
- [ ]
setState()不在根 Widget 層級呼叫 — 將狀態變更局部化 - [ ] 使用
constWidget 阻止重建傳播 - [ ] 在獨立重繪的複雜子樹周圍使用
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)
其他:
- [ ] 避免在動畫中使用
OpacityWidget — 使用AnimatedOpacity或FadeTransition - [ ] 避免在動畫中裁剪 — 預先裁剪圖片
- [ ] 不在 Widget 上覆寫
operator ==— 改用const建構子 - [ ] 謹慎使用內在尺寸 Widget(
IntrinsicHeight、IntrinsicWidth)(額外佈局傳遞)
6. 測試
測試類型與期望:
- [ ] 單元測試:涵蓋所有商業邏輯(狀態管理器、儲存庫、工具函式)
- [ ] Widget 測試:涵蓋個別 Widget 行為、互動與視覺輸出
- [ ] 整合測試:端到端涵蓋關鍵使用者流程
- [ ] 黃金測試:對設計關鍵的 UI 元件進行像素完美比較
涵蓋率目標:
- [ ] 目標商業邏輯行涵蓋率 80% 以上
- [ ] 所有狀態轉換都有對應的測試(載入→成功、載入→錯誤、重試等)
- [ ] 測試邊界情況:空狀態、錯誤狀態、載入狀態、邊界值
測試隔離:
- [ ] 外部依賴(API 客戶端、資料庫、服務)被模擬或偽造
- [ ] 每個測試檔案僅測試一個類別/單元
- [ ] 測試驗證行為,而非實作細節
- [ ] Stub 僅定義每個測試所需的行為(最小 stub)
- [ ] 測試案例之間不共享可變狀態
Widget 測試品質:
- [ ] 正確使用
pumpWidget和pump處理非同步操作 - [ ] 適當使用
find.byType、find.text、find.byKey - [ ] 沒有依賴時間的不穩定測試 — 使用
pumpAndSettle或明確的pump(Duration) - [ ] 測試在 CI 中執行,失敗會阻止合併
7. 無障礙
語意 Widget:
- [ ] 在自動標籤不足時,使用
SemanticsWidget 提供螢幕閱讀器標籤 - [ ] 對純裝飾性元素使用
ExcludeSemantics - [ ] 使用
MergeSemantics將相關 Widget 合併為單一可存取元素 - [ ] 圖片設定
semanticLabel屬性
螢幕閱讀器支援:
- [ ] 所有互動元素都可聚焦並具有有意義的描述
- [ ] 焦點順序符合邏輯(遵循視覺閱讀順序)
視覺無障礙:
- [ ] 文字與背景的對比度 >= 4.5:1
- [ ] 可點擊目標至少 48x48 像素
- [ ] 顏色不是狀態的唯一指示器(同時使用圖示/文字)
- [ ] 文字隨系統字型大小設定縮放
互動無障礙:
- [ ] 沒有無作用的
onPressed回呼 — 每個按鈕都有功能或被停用 - [ ] 錯誤欄位建議修正
- [ ] 使用者輸入資料時,上下文不會意外變更
8. 平台特定考量
iOS/Android 差異:
- [ ] 在適當情況下使用平台適應性 Widget
- [ ] 正確處理返回導航(Android 返回按鈕、iOS 滑動返回)
- [ ] 透過
SafeAreaWidget 處理狀態列和安全區域 - [ ] 在
AndroidManifest.xml和Info.plist中宣告平台特定權限
響應式設計:
- [ ] 使用
LayoutBuilder或MediaQuery進行響應式佈局 - [ ] 一致地定義斷點(手機、平板、桌面)
- [ ] 文字在小螢幕上不溢出 — 使用
Flexible、Expanded、FittedBox - [ ] 測試橫向模式或明確鎖定
- [ ] 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: true、strict-inference: true、strict-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 測試 |






