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-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()以表明意圖 - 在
final可行的情況下使用var:偏好對區域變數使用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 傳播和框架最佳化) - 在不需要可變區域狀態的情況下,優先使用無狀態 widget 而非有狀態 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 記錄或其他 - 狀態物件內的集合不會暴露為原始的可變
List/Map
反應式紀律(適用於反應式變異解決方案:MobX、GetX、Signals):
- 狀態僅透過解決方案的反應式 API(MobX 中的
@action、信號上的.value、GetX 中的.obs)進行變更 — 直接欄位變更會繞過變更追蹤 - 衍生值使用解決方案的計算機制,而不是冗餘地儲存
- 反應和處置器已正確清理(MobX 中的
ReactionDisposer,Signals 中的 effect cleanup)
狀態形狀設計:
- 互斥的狀態使用密封類型、聯合變體或解決方案內建的異步狀態類型(例如 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 }
// 使用解決方案的可觀察物件/信號來分別包裝 status 和 data
重建最佳化:
- 狀態消費者 widget(Builder、Consumer、Observer、Obx、Watch 等)的範圍應盡可能縮小
- 使用選擇器在特定欄位變更時才重建 — 而不是在每次狀態發射時都重建
- 使用
constwidget 來阻止重建傳播到樹中 - 計算/衍生狀態應以反應式方式計算,而不是冗餘地儲存
訂閱與處置:
- 所有手動訂閱(
.listen())都在dispose()/close()中取消 - Stream controller 在不再需要時關閉
- 計時器在處置生命週期中取消
- 偏好使用框架管理的生命週期而非手動訂閱(宣告式建構器優於
.listen()) - 在異步回呼中使用
setState之前檢查mounted - 在未檢查
context.mounted(Flutter 3.7+)的情況下,不要在await之後使用BuildContext— 過期的 context 會導致崩潰 - 在未確認 widget 仍掛載的情況下,不要在異步間隔後執行導航、對話框或 Scaffold 訊息
-
BuildContext絕不儲存在單例、狀態管理器或靜態欄位中
局部狀態與全域狀態:
- 短暫的 UI 狀態(核取方塊、滑桿、動畫)使用局部狀態(
setState、ValueNotifier) - 共享狀態僅在必要時才提升 — 不要過度全域化
- 功能範圍的狀態在功能不再活躍時應妥善處置
5. 效能
不必要的重建:
- 不要在根 Widget 層級呼叫
setState()— 將狀態變更侷限化 - 使用
constwidget 來阻止重建傳播 - 在獨立重繪的複雜子樹周圍使用
RepaintBoundary - 對於獨立於動畫的子樹,使用
AnimatedBuilderchild 參數
Build 中的昂貴運算:
- 不要在
build()中對大型集合進行排序、過濾或映射 — 在狀態管理層中計算 - 不要在
build()中編譯正規表達式 - 特定使用
MediaQuery.of(context)(例如MediaQuery.sizeOf(context))
圖片最佳化:
- 網路圖片使用快取(任何適合專案的快取解決方案)
- 為目標裝置使用適當的圖片解析度(不要為縮圖載入 4K 圖片)
- 使用帶有
cacheWidth/cacheHeight的Image.asset以顯示尺寸解碼 - 為網路圖片提供佔位符和錯誤 widget
懶載入:
- 對於大型或動態清單,使用
ListView.builder/GridView.builder而不是ListView(children: [...])(具體建構子適用於小型靜態清單) - 為大型資料集實作分頁
- 在 Web 建置中,對大型函式庫使用延遲載入(
deferred as)
其他:
- 在動畫中避免使用
Opacitywidget — 使用AnimatedOpacity或FadeTransition - 在動畫中避免裁剪 — 預先裁剪圖片
- 不要在 widget 上覆寫
operator ==— 改用const建構子 - 謹慎使用內在維度 widget(
IntrinsicHeight、IntrinsicWidth)(會增加額外的佈局傳遞)
6. 測試
測試類型與期望:
- 單元測試:涵蓋所有商業邏輯(狀態管理器、儲存庫、工具函式)
- Widget 測試:涵蓋個別 widget 的行為、互動和視覺輸出
- 整合測試:端到端涵蓋關鍵使用者流程
- Golden 測試:對設計關鍵的 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. 安全性
安全儲存:
- 敏感資料使用安全儲存解決方案(
flutter_secure_storage、Keychain、KeyStore)進行儲存 — 絕不使用SharedPreferences或純文字檔案 - API 金鑰、令牌和機密資料不使用原始碼進行版本控制 — 使用環境變數或安全配置服務
- 認證令牌在登出或令牌到期時從安全儲存中清除
- 生物辨識驗證在支援的裝置上用於存取高風險操作
網路安全:
- 所有 API 通訊都使用 HTTPS — 正式環境中不使用 HTTP
- SSL/TLS 憑證在正式環境中經過驗證(不忽略憑證錯誤)
- API 請求中的敏感資料(密碼、個人識別資訊)已加密
- 敏感 API 請求使用 CSRF 令牌
- 正確設定 SSL 固定(用於防止中間人攻擊的高安全性應用程式)
資料保護:
- 所有敏感資料(密碼、金融資訊、個人資料)在儲存前進行加密
- 不再需要的敏感資料會從記憶體和儲存中清除
- 符合資料保護法規(GDPR、CCPA 等)— 使用者可以請求資料刪除
- 日誌或錯誤報告中不記錄敏感資料
輸入驗證與清理:
- 所有使用者輸入都經過驗證(類型、長度、格式、範圍)和清理(去除危險字元)
- 使用者輸入中的 HTML/JavaScript 在動態顯示時已轉義(防止 XSS)
- 檔案上傳經過驗證(類型、大小、內容掃描)
- SQL 注入預防:使用參數化查詢或 ORM(如果將 SQL 與 Dart 搭配使用)
- 路徑遍歷預防:使用者提供的路徑經過清理,無法存取預期目錄之外的路徑
10. 依賴項與套件
套件選擇:
- 套件已根據專案需求進行評估(功能、大小、授權條款、維護活躍度、相容性)
- 偏好支援空安全的套件
- 避免使用低品質或未維護的套件
- 直接相依性數量保持合理
版本管理:
- 依賴項版本受到適當限制(使用插入符號
^表示向後相容的更新,對於不相容的變更進行鎖定) -
pubspec.lock已提交到版本控制,以確保在所有環境中都能重複建置 - 定期檢查依賴項是否有安全性更新和重大變更
- 過時的套件已更新或替換
平台特定相依性:
- 平台特定套件正確實作,並在必要時在
pubspec.yaml中正確聲明平台支援 - 平台頻道(Android 的 Java/Kotlin、iOS 的 Swift/ObjC)之間的實作沒有不必要的重複
- 平台通道在不再需要時正確清理
11. 架構
乾淨架構:
- 專案遵循乾淨的架構層次(資料、領域、呈現)或類似的關注點分離方法
- 領域層不依賴外部框架或函式庫 — 它是純 Dart
- 資料層完全由領域層抽象化 — 資料來源(API、資料庫、檔案)的變更不會影響領域層
- 呈現層僅依賴領域層進行資料和業務邏輯 — 不直接使用資料層
- 依賴方向指向內層:呈現 → 領域 ← 資料(領域層定義儲存庫介面,資料層實作它們)
SOLID 原則:
- 單一職責:每個類別、函式或模組都有一個明確且明確定義的職責
- 開放封閉:類別應對擴展開放,但對修改封閉 — 使用策略模式、介面或回呼
- 里氏替換:子類型應可替換其基礎類型 — 確保繼承層次結構健全
- 介面隔離:用戶端不應被迫依賴他們不使用的介面 — 使用精確、特定領域的介面
- 依賴反轉:高層模組不應依賴低層模組 — 兩者都應依賴抽象(介面、抽象類別)
設計模式:
- 避免反模式(上帝物件、程式碼重複、循環依賴、過早最佳化)
- 設計模式的選擇適合問題 — 不強迫使用模式
- 遇到的常見模式已標準化並在整個專案中一致使用
- 不當的繼承已被組合取代(偏好組合而非繼承)
12. 錯誤處理與邊界案例
錯誤處理策略:
- 所有錯誤在 UI 層被捕獲並優雅處理 — 沒有未處理的例外會導致應用程式崩潰
- 為使用者提供有意義的錯誤訊息和復原動作
- 商業邏輯錯誤(來自 API 的 4xx 錯誤、驗證錯誤)與系統錯誤(網路中斷、伺服器錯誤)區分開來
- 錯誤已記錄,但敏感的使用者資料不會出現在日誌中
- 對於可重試的錯誤已實作重試策略(有上限和退避)
邊界案例:
- 測試並處理空資料狀態(空清單、null 值、缺少的欄位)
- 測試並處理極端和邊界值(最大/最小數字、非常長的字串、特殊字元)
- 測試並處理非同步操作的超時
- 測試並處理競爭條件(快速點擊、多個快速的狀態變更)
- 測試並處理離線模式和網路恢復
- 測試並處理裝置旋轉、背景/前景轉換和多視窗模式
13. 國際化(i18n)與本地化(l10n)
字串管理:
- 所有使用者可見的字串都透過本地化系統管理(
flutter_localizations、intl或類似的函式庫) - 硬編碼的字串僅用於測試或內部用途
- 字串使用參數化(
$name、{count}),可進行正確的翻譯和複數處理
本地化實作:
- 支援區域設置的變更(語言切換不需要重新啟動應用程式)
- 本地化遵循目標市場的慣例(日期格式、數字格式、貨幣、排序規則)
- 從右到左的語言受到適當支援(文字方向、佈局鏡像)
- 複數規則使用
intl的Intl.plural()或flutter_localizations的plural()正確實作
區域特定考量:
- 不同區域設置正確處理時區
- 不同區域設置正確處理貨幣和貨幣格式
- 不同區域設置正確處理測量單位
- 圖片和媒體針對不同區域進行本地化(不僅是文字)
14. 記錄與監控
記錄策略:
- 使用結構化記錄(記錄框架將上下文資訊新增到每條記錄中)
- 記錄層級適當地設定以區分除錯、資訊、警告、錯誤和嚴重訊息
- 記錄不會記錄個人身分識別資訊或密碼
- 記錄不會對效能產生負面影響(非同步記錄、對生產環境中的除錯記錄進行取樣)
監控與可觀測性:
- 關鍵操作(API 呼叫、頁面載入、表單提交)已檢測效能指標
- 實作的監控可在問題發生時發出警報
- 監控設定為報告應用程式的「數位健康狀態」:崩潰率、載入時間、錯誤率
- 使用者可以透過監控報告問題,而無需離開應用程式
15. 檔案與專案結構
組織:
- 檔案遵循一致的命名慣例(
snake_case用於 Dart 檔案、PascalCase用於類別) - 測試檔案透過反映原始檔案的路徑和命名來與實作保持一致
- 包含大量檔案的資料夾使用子資料夾進行邏輯分組
- 不應該由開發人員手動編輯的產生檔案(
.g.dart、.freezed.dart)被適當地識別
版本控制:
-
.gitignore正確設定以排除產生檔案、IDE 檔案和建置工件 - 不應提交的檔案(私密金鑰、API 金鑰、大型二進位檔案)已被忽略
- 提交訊息遵循一致的格式(語意化提交或類似格式)
- 分支策略遵循一致的模式(Git Flow、GitHub Flow、Trunk-Based Development)
16. 建構與部署
CI/CD:
- 建置在 CI/CD 管道中自動化(GitHub Actions、GitLab CI、Bitrise、Codemagic 等)
- 管道包含 linter、測試和分析器步驟,步驟失敗時會導致管道停止
- 環境特定的配置(開發、預備、正式)與程式碼分離,並透過 CI/CD 環境變數注入
- 部署自動化,並有回滾計劃
版本號碼與持續交付:
- 版本號碼語意化(語意化版本控制)並與建置號碼一致
- 變更日誌在版本之間維護
- 發布版本經過測試並通過 QA 檢查
檢查清單使用說明
- 不需要為每個專案檢查每個項目 — 根據專案的特定需求、技術堆疊和成熟度調整此檢查清單。
- 將此檢查清單直接整合到您的 PR 審查範本中,或將其用作審查過程中的參考。
- 標記為清單完成度的項目,並附上支援審查決策的註解或引用。
- 對於偏離檢查清單的情況,請確保有充分的理由,並記錄權衡取捨。
本文件透過涵蓋從 Dart 語言慣例到架構模式、安全性、無障礙性、效能和部署的最常見問題,提供了一種結構化的 Flutter/Dart 程式碼審查方法。根據您的專案需求進行調整,並將其作為團隊的活文件使用。