Dart コードレビューのベストプラクティス は何をしますか?
Flutter/Dart アプリケーションをレビューするための、ライブラリに依存しない包括的なチェックリストです。これらの原則は、使用される状態管理ソリューション、ルーティングライブラリ、DIフレームワークに関係なく適用されます。
1. プロジェクト全体の健全性
- プロジェクトは一貫したフォルダ構造(機能優先またはレイヤー優先)に従っている
- 関心事の適切な分離:UI、ビジネスロジック、データ層
- ウィジェット内にビジネスロジックがない;ウィジェットは純粋にプレゼンテーション用である
-
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を有効にする - Null安全の誤用:適切なnullチェックやDart 3のパターンマッチング(
if (value case var v?))の代わりに過剰な!(bang演算子)を使用している - 型昇格の失敗:ローカル変数の昇格が機能する場所で
this.fieldを使用している - 過度に広範なキャッチ:
on句なしのcatch (e)— 常に例外の種類を指定する -
Errorのキャッチ:Errorのサブタイプはバグを示しており、キャッチすべきではない - 未使用の
async:awaitを決して行わないasyncとマークされた関数 — 不必要なオーバーヘッド -
lateの過剰使用:null許容型やコンストラクター初期化の方が安全な場所でlateを使用している;エラーを実行時まで遅延させる - ループ内での文字列連結:反復的な文字列構築には
+の代わりにStringBufferを使用する -
constコンテキストでの可変状態:constコンストラクタークラスのフィールドは可変であってはならない -
Futureの戻り値の無視:awaitを使用するか、明示的にunawaited()を呼び出して意図を示す -
finalで十分な場所でのvar:ローカル変数にはfinalを、コンパイル時定数にはconstを優先する - 相対インポート:一貫性のために
package:インポートを使用する - 可変コレクションの公開:公開APIは生の
List/Mapではなく、変更不可能なビューを返すべき - Dart 3のパターンマッチングの欠如:冗長な
isチェックや手動キャストよりも、switch式とif-caseを優先する - 複数の戻り値のための使い捨てクラス:単一用途のDTOの代わりにDart 3のレコード
(String, int)を使用する - プロダクションコードでの
print():dart:developerのlog()またはプロジェクトのロギングパッケージを使用する;print()にはログレベルがなく、フィルタリングできない
3. ウィジェットのベストプラクティス
ウィジェットの分解:
- 単一のウィジェットで
build()メソッドが約80〜100行を超えていない - ウィジェットはカプセル化と、それらがどのように変化するか(リビルド境界)の両方で分割されている
- ウィジェットを返すプライベートな
_build*()ヘルパーメソッドは、個別のウィジェットクラスに抽出されている(要素の再利用、const伝播、およびフレームワークの最適化を可能にする) - 可変なローカル状態が必要ない場合は、StatefulウィジェットよりもStatelessウィジェットが優先されている
- 抽出されたウィジェットは、再利用可能な場合は別のファイルにある
constの使用法:
- 可能な限り
constコンストラクターが使用されている — 不必要なリビルドを防ぐ - 変更されないコレクションには
constリテラル(const []、const {}) - すべてのフィールドがfinalの場合、コンストラクターは
constと宣言されている
Keyの使用法:
- リスト/グリッドで再並び替え全体にわたって状態を保持するために
ValueKeyが使用されている -
GlobalKeyは控えめに使用されている — ツリー全体で状態にアクセスする必要がある場合にのみ -
build()内でのUniqueKeyの使用は避けられている — 毎フレームのリビルドを強制する - 同一性が単一の値ではなくデータオブジェクトに基づく場合に
ObjectKeyが使用されている
テーマとデザインシステム:
- 色は
Theme.of(context).colorSchemeから取得 — ハードコードされたColors.redや16進数値は使用しない - テキストスタイルは
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など)に適用されます。
アーキテクチャ:
- ビジネスロジックはウィジェット層の外側 — 状態管理コンポーネント(BLoC、Notifier、Controller、Store、ViewModelなど)にある
- 状態マネージャーは依存関係をインジェクションで受け取り、内部で構築しない
- サービスまたはリポジトリ層がデータソースを抽象化する — ウィジェットと状態マネージャーはAPIやデータベースを直接呼び出すべきではない
- 状態マネージャーは単一責任を持つ — 無関係な関心事を扱う「神」マネージャーは存在しない
- クロスコンポーネントの依存関係は、そのソリューションの規約に従っている:
- Riverpodの場合:
ref.watchを介したプロバイダーの依存は想定内 — 循環的または過度に絡み合ったチェーンのみフラグを立てる - BLoCの場合:blocは他のblocに直接依存すべきではない — 共有リポジトリまたはプレゼンテーション層の調整を優先する
- その他のソリューションの場合:コンポーネント間通信のための文書化された規約に従う
- Riverpodの場合:
イミュータビリティと値の等価性(不変状態ソリューションの場合:BLoC、Riverpod、Redux):
- 状態オブジェクトは不変である —
copyWith()またはコンストラクターを介して新しいインスタンスが作成され、インプレースで変更されることはない - 状態クラスは
==とhashCodeを適切に実装している(比較にすべてのフィールドが含まれている) - メカニズムはプロジェクト全体で一貫している — 手動オーバーライド、
Equatable、freezed、Dartレコード、またはその他 - 状態オブジェクト内のコレクションは、生の可変
List/Mapとして公開されていない
リアクティビティの規律(リアクティブミューテーションソリューションの場合:MobX、GetX、Signals):
- 状態はそのソリューションのリアクティブAPI(MobXの
@action、Signalsの.value、GetXの.obs)を介してのみ変更される — 直接のフィールド変更は変更追跡をバイパスする - 派生値は、冗長に保存するのではなく、そのソリューションの計算メカニズムを使用する
- リアクションとディスポーザーは適切にクリーンアップされている(MobXの
ReactionDisposer、Signalsのエフェクトクリーンアップ)
状態形状の設計:
- 相互に排他的な状態は、シールド型、共用体バリアント、またはそのソリューションの組み込みの非同期状態型(例:Riverpodの
AsyncValue)を使用する — ブールフラグ(isLoading、isError、hasData)は使用しない - すべての非同期操作は、読み込み、成功、エラーを明確な状態としてモデル化する
- すべての状態バリアントはUIで網羅的に処理されている — 黙って無視されるケースはない
- エラー状態には表示用のエラー情報が含まれている;読み込み状態には古いデータが含まれていない
- null許容データはローディングインジケーターとして使用されていない — 状態は明示的である
// BAD — ブール値フラグの混在により不可能な状態が表現可能になる
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError が表現可能!
User? user;
}
// GOOD(不変アプローチ) — シールド型により不可能な状態が表現不可能になる
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);
}
// GOOD(リアクティブアプローチ) — 観測可能なenum + データ、リアクティビティAPIを介したミューテーション
// enum UserStatus { initial, loading, loaded, error }
// ソリューションのobservable/signalを使用して、ステータスとデータを個別にラップする
リビルドの最適化:
- 状態コンシューマーウィジェット(Builder、Consumer、Observer、Obx、Watchなど)は可能な限り狭いスコープにする
- すべての状態出力ではなく、特定のフィールドが変更された場合にのみリビルドするためにセレクターを使用する
-
constウィジェットを使用して、ツリーを介したリビルドの伝播を停止する - 計算/派生状態はリアクティブに計算され、冗長に保存されない
サブスクリプションと破棄:
- すべての手動サブスクリプション(
.listen())はdispose()/close()でキャンセルされている - ストリームコントローラーは不要になったらクローズされている
- タイマーは破棄ライフサイクルでキャンセルされている
- フレームワーク管理のライフサイクルが手動サブスクリプションよりも優先されている(
.listen()よりも宣言的ビルダー) - 非同期コールバック内の
setStateの前にmountedチェックが行われている -
context.mounted(Flutter 3.7+)をチェックせずにawaitの後にBuildContextを使用していない — 古いコンテキストはクラッシュを引き起こす - 非同期ギャップの後、ウィジェットがまだマウントされていることを確認せずに、ナビゲーション、ダイアログ、またはスキャフォールドメッセージがない
-
BuildContextはシングルトン、状態マネージャー、または静的フィールドに保存されない
ローカル状態とグローバル状態:
- 一時的なUI状態(チェックボックス、スライダー、アニメーション)はローカル状態(
setState、ValueNotifier)を使用する - 共有状態は必要な範囲でのみ持ち上げられる — 過度にグローバル化されない
- 機能スコープの状態は、その機能がアクティブでなくなったら適切に破棄される
5. パフォーマンス
不必要なリビルド:
-
setState()がルートウィジェットレベルで呼び出されていない — 状態変更をローカライズする -
constウィジェットを使用してリビルドの伝播を停止する - 独立して再描画する複雑なサブツリーの周りで
RepaintBoundaryが使用されている - アニメーションから独立したサブツリーに
AnimatedBuilderのchildパラメーターが使用されている
build() 内の高価な操作:
-
build()内で大きなコレクションのソート、フィルタリング、マッピングがない — 状態管理層で計算する -
build()内での正規表現のコンパイルがない -
MediaQuery.of(context)の使用は具体的である(例:MediaQuery.sizeOf(context))
画像最適化:
- ネットワーク画像はキャッシュを使用している(プロジェクトに適した任意のキャッシュソリューション)
- ターゲットデバイスに適切な画像解像度(サムネイルに4K画像をロードしない)
-
Image.assetをcacheWidth/cacheHeightと共に使用して表示サイズでデコードする - ネットワーク画像にプレースホルダーとエラーウィジェットが提供されている
遅延読み込み:
- 大きなリストや動的なリストには
ListView(children: [...])の代わりにListView.builder/GridView.builderが使用されている(具体的なコンストラクターは小さく静的なリストには問題ない) - 大規模なデータセットに対してページネーションが実装されている
- Webビルドで重いライブラリに対して遅延読み込み(
deferred as)が使用されている
その他:
- アニメーションでの
Opacityウィジェットの使用は避ける —AnimatedOpacityまたはFadeTransitionを使用する - アニメーションでのクリッピングは避ける — 画像を事前にクリップする
- ウィジェットでの
operator ==のオーバーライドは行わない — 代わりにconstコンストラクターを使用する - 内在的な寸法ウィジェット(
IntrinsicHeight、IntrinsicWidth)は控えめに使用する(余分なレイアウトパス)
6. テスト
テストの種類と期待事項:
- 単体テスト:すべてのビジネスロジック(状態マネージャー、リポジトリ、ユーティリティ関数)をカバーする
- ウィジェットテスト:個々のウィジェットの動作、インタラクション、および視覚的出力をカバーする
- 統合テスト:重要なユーザーフローをエンドツーエンドでカバーする
- ゴールデンテスト:デザイン上重要なUIコンポーネントのピクセルパーフェクトな比較
カバレッジ目標:
- ビジネスロジックの行カバレッジ80%以上を目指す
- すべての状態遷移に対応するテストがある(読み込み → 成功、読み込み → エラー、リトライなど)
- エッジケースのテスト:空の状態、エラー状態、読み込み状態、境界値
テストの分離:
- 外部依存関係(APIクライアント、データベース、サービス)はモックまたはフェイクされている
- 各テストファイルは正確に1つのクラス/ユニットをテストする
- テストは実装の詳細ではなく、動作を検証する
- スタブは各テストに必要な動作のみを定義する(最小限のスタブ)
- テストケース間で可変な共有状態がない
ウィジェットテストの品質:
-
pumpWidgetとpumpが非同期操作に正しく使用されている -
find.byType、find.text、find.byKeyが適切に使用されている - タイミングに依存する不安定なテストがない —
pumpAndSettleまたは明示的なpump(Duration)を使用する - テストはCIで実行され、失敗はマージをブロックする
7. アクセシビリティ
セマンティックウィジェット:
- 自動ラベルが不十分な場合にスクリーンリーダーラベルを提供するために
Semanticsウィジェットが使用されている - 純粋に装飾的な要素には
ExcludeSemanticsが使用されている - 関連するウィジェットを単一のアクセス可能な要素に結合するために
MergeSemanticsが使用されている - 画像には
semanticLabelプロパティが設定されている
スクリーンリーダーサポート:
- すべてのインタラクティブ要素はフォーカス可能で、意味のある説明がある
- フォーカス順序は論理的である(視覚的な読み取り順序に従う)
視覚的なアクセシビリティ:
- テキストと背景のコントラスト比が4.5:1以上である
- タップ可能なターゲットは最低48x48ピクセルである
- 色は状態の唯一のインジケーターではない(アイコン/テキストも併用する)
- テキストはシステムのフォントサイズ設定に応じて拡大縮小する
インタラクションのアクセシビリティ:
- 何もしない
onPressedコールバックがない — すべてのボタンは何かを行うか、無効化されている - エラーフィールドは修正を提案する
- ユーザーがデータを入力中にコンテキストが予期せず変わらない
8. プラットフォーム固有の懸念事項
iOS/Androidの違い:
- 必要に応じてプラットフォームアダプティブウィジェットが使用されている
- 戻るナビゲーションが正しく処理されている(Androidの戻るボタン、iOSのスワイプで戻る)
- ステータスバーとセーフエリアは
SafeAreaウィジェットを介して処理されている - プラットフォーム固有のパーミッションが
AndroidManifest.xmlとInfo.plistで宣言されている
レスポンシブデザイン:
- レスポンシブレイアウトに
LayoutBuilderまたはMediaQueryが使用されている - ブレークポイントが一貫して定義されている(電話、タブレット、デスクトップ)
- 小さな画面でテキストがオーバーフローしない —
Flexible、Expanded、FittedBoxを使用する - 横向きがテストされているか、明示的にロックされている
- Web固有:マウス/キーボードのインタラクションがサポートされ、ホバー状態が存在する
9. セキュリティ
セキュアストレージ:
- センシティブなデータは
flutter_secure_storageまたはキーチェーンに保存され、共有プリファレンスには保存されない - 認証トークンはメモリ内にのみ保持され、ディスクに永続化されるのは適切に暗号化されている場合のみ
- ディープリンクは検証され、認証チェックをバイパスするために使用されない
API通信:
- すべてのAPI呼び出しはHTTPSを使用する
- 証明書のピン留めが高セキュリティ環境で検討されている
- ログに機密データ(パスワード、トークン)が含まれていない
入力検証:
- すべてのユーザー入力はバックエンドに送信される前にクライアント側で検証される
- 出力はXSS脆弱性を防ぐためにエスケープされる
10. クリーンアーキテクチャと依存関係
レイヤーと依存関係のルール:
- データ層はドメイン層にのみ依存し、プレゼンテーション層には依存しない
- ドメイン層は外部ライブラリやフレームワークに依存しない — 純粋なDart
- 依存関係の逆転:上位レベルのモジュールは下位レベルのモジュールに依存せず、抽象化に依存する
- 具象実装はコンパイル時または実行時の依存性注入を介して提供される
依存性注入:
- サービスロケーターパターン(静的ゲッター)の代わりに、適切な依存性注入(
get_it、Riverpod、Provider、手動コンストラクターDI)が使用されている - DIコンテナの登録はアプリケーションの起動時に1回行われる
- テストでは、実装の代わりにモックでDIコンテナをオーバーライドする機能が確認されている
このチェックリストをFlutter/Dartコードレビューのプロセスに統合してください。常にコンテキストと判断を適用し、これらのチェック項目はすべての状況に適用されるわけではありません。