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 傳播及框架最佳化) - 在不需要可變區域狀態時,偏好 Stateless widget 而非 Stateful widget
- 可重用的提取 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 - 已驗證深色模式相容性 — 不要假設淺色背景
- 間距和尺寸使用一致的設計記號或常數,而非魔術數字
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 透過
ref.watch依賴其他 provider 是預期的 — 僅標記循環或過度糾纏的鏈 - 在 BLoC 中:bloc 不應直接依賴其他 bloc — 偏好共享的儲存庫或呈現層協調
- 在其他解決方案中:遵循文件記載的跨元件通訊慣例
- 在 Riverpod 中:provider 透過
不可變性與值相等性(適用於不可變狀態的解決方案:BLoC、Riverpod、Redux):
- 狀態物件是不可變的 — 透過
copyWith()或建構子建立新實例,絕不原地修改 - 狀態類別正確實作
==和hashCode(比較中包含所有欄位) - 機制在整個專案中保持一致 — 手動覆寫、
Equatable、freezed、Dart records 或其他 - 狀態物件內的集合不會以原始可變的
List/Map形式暴露
反應式原則(適用於反應式變動的解決方案:MobX、GetX、Signals):
- 狀態僅透過解決方案的反應式 API(MobX 的
@action、Signals 的.value、GetX 的.obs)進行變動 — 直接欄位賦值會繞過變更追蹤 - 衍生值使用解決方案的計算機制,而非冗餘儲存
- 反應和清除器已正確清理(MobX 的
ReactionDisposer、Signals 的效果清理)
狀態形狀設計:
- 互斥的狀態使用密封類型、聯合變體或解決方案內建的異步狀態類型(例如 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);
}
// 好的做法(反應式方法) — 可觀察的 enum + 資料,透過反應式 API 進行變動
// enum UserStatus { initial, loading, loaded, error }
// 使用解決方案的 observable/signal 分別包裝狀態和資料
重建最佳化:
- 狀態消費者 widget(Builder、Consumer、Observer、Obx、Watch 等)的範圍盡可能窄
- 使用選擇器僅在特定欄位變更時重建 — 不是在每個狀態發射時
- 使用
constwidget 來阻止重建在樹中傳播 - 計算/衍生狀態以反應式方式計算,而非冗餘儲存
訂閱與釋放:
- 所有手動訂閱(
.listen())在dispose()/close()中取消 - Stream controller 在不再需要時關閉
- 定時器在釋放生命週期中取消
- 偏好使用框架管理的生命週期而非手動訂閱(宣告式 builder 勝過
.listen()) - 在異步回呼中呼叫
setState之前檢查mounted - 在未檢查
context.mounted(Flutter 3.7+)的情況下,不要在await之後使用BuildContext— 過時的 context 會導致崩潰 - 在未驗證 widget 仍然掛載的情況下,不要在異步間隔後進行導航、顯示對話框或訊息
-
BuildContext絕不儲存在單例、狀態管理器或靜態欄位中
區域與全域狀態:
- 短暫的 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. 安全性
安全儲存:
- 敏感資料