Skills Development Flutter Dart Code Review Best Practices

Flutter Dart Code Review Best Practices

v20260517
flutter-dart-code-review
A comprehensive, library-agnostic checklist covering best practices for Flutter and Dart applications. It guides developers on architectural principles, state management patterns (BLoC, Riverpod, etc.), Dart idioms, performance optimization, and clean code principles, making it invaluable for code reviews.
Get Skill
176 downloads
Overview

Flutter/Dartコードレビューベストプラクティス

Flutter/Dartアプリケーションをレビューするための包括的なライブラリに依存しないチェックリスト。これらの原則は、どの状態管理ソリューション、ルーティングライブラリ、またはDIフレームワークを使用していても適用されます。


1. 全般的なプロジェクトの健全性

  • プロジェクトは一貫したフォルダー構造に従っている(フィーチャーファーストまたはレイヤーファースト)
  • 適切な関心の分離: UI、ビジネスロジック、データレイヤー
  • ウィジェットにビジネスロジックがない; ウィジェットは純粋にプレゼンテーション
  • pubspec.yamlが整理されている — 未使用の依存関係がなく、バージョンが適切に固定されている
  • analysis_options.yamlに厳格なリントセットと厳格なアナライザー設定が含まれている
  • 本番コードにprint()文がない — dart:developerlog()またはロギングパッケージを使用
  • 生成されたファイル(.g.dart.freezed.dart.gr.dart)が最新か.gitignoreに含まれている
  • プラットフォーム固有のコードが抽象化の背後に分離されている

2. Dart言語の落とし穴

  • 暗黙的なdynamic: 型アノテーションの欠如がdynamicにつながる — strict-castsstrict-inferencestrict-raw-typesを有効にする
  • Null安全の誤用: 適切なnullチェックやDart 3のパターンマッチング(if (value case var v?))の代わりに過度な!(bang演算子)
  • 型プロモーションの失敗: ローカル変数プロモーションが機能する場所でthis.fieldを使用
  • 過度に広い例外のキャッチ: on句なしのcatch (e); 常に例外型を指定する
  • Errorのキャッチ: Errorのサブタイプはバグを示し、キャッチすべきでない
  • 未使用のasync: awaitしないasyncマークされた関数 — 不要なオーバーヘッド
  • lateの過剰使用: nullable型やコンストラクターの初期化がより安全な場所での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:developerlog()またはプロジェクトのロギングパッケージを使用; print()はログレベルがなくフィルタリングできない

3. ウィジェットのベストプラクティス

ウィジェットの分解:

  • build()メソッドが約80-100行を超える単一ウィジェットがない
  • ウィジェットがカプセル化と変化の仕方(再構築の境界)によって分割されている
  • ウィジェットを返すプライベートな_build*()ヘルパーメソッドが別のウィジェットクラスに抽出されている(要素の再利用、const伝播、フレームワーク最適化を可能にする)
  • 可変のローカル状態が必要でない場合、Statelessウィジェットが優先される
  • 抽出されたウィジェットが再利用可能な場合、別のファイルに存在する

Constの使用:

  • constコンストラクターを可能な限り使用 — 不要な再構築を防ぐ
  • 変化しないコレクションにconstリテラルを使用(const []const {}
  • すべてのフィールドがfinalの場合、コンストラクターがconstとして宣言されている

Keyの使用:

  • 並べ替え時に状態を保持するためにValueKeyをリスト/グリッドで使用
  • GlobalKeyは控えめに使用 — ツリー全体の状態アクセスが本当に必要な場合のみ
  • UniqueKeybuild()内で使用しない — フレームごとに再構築を強制する
  • 単一の値ではなくデータオブジェクトのアイデンティティに基づく場合はObjectKeyを使用

テーマとデザインシステム:

  • 色はTheme.of(context).colorSchemeから取得 — Colors.redやhex値のハードコードなし
  • テキストスタイルは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やデータベースを直接呼び出すべきでない
  • 状態マネージャーが単一の責務を持つ — 無関係な懸念を処理する「god」マネージャーなし
  • コンポーネント間の依存関係がソリューションの規約に従う:
    • Riverpodでは: プロバイダーが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クリーンアップ)

状態の形状設計:

  • 相互に排他的な状態がsealed型、ユニオン変体、またはソリューションの組み込み非同期状態型(例: RiverpodのAsyncValue)を使用 — ブールフラグ(isLoadingisErrorhasData)は使わない
  • すべての非同期操作がローディング、成功、エラーを異なる状態としてモデル化
  • すべての状態変体がUIで網羅的に処理 — サイレントに無視されるケースなし
  • エラー状態が表示のためのエラー情報を持つ; ローディング状態は古いデータを持たない
  • 可変のデータがローディングインジケーターとして使用されない — 状態は明示的
// 悪い例 — ブールフラグの混乱が不可能な状態を許可する
class UserState {
  bool isLoading = false;
  bool hasError = false; // isLoading && hasErrorが表現可能!
  User? user;
}

// 良い例(イミュータブルアプローチ) — sealed型が不可能な状態を表現不可能にする
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);
}

// 良い例(リアクティブアプローチ) — observableのenum + データ、リアクティビティAPIを通じた変異
// enum UserStatus { initial, loading, loaded, error }
// ソリューションのobservable/signalを使用してstatusとdataを別々にラップする

再構築の最適化:

  • 状態コンシューマーウィジェット(Builder、Consumer、Observer、Obx、Watchなど)をできるだけ狭くスコープする
  • 特定のフィールドが変化した場合のみ再構築するためにセレクターを使用 — すべての状態エミッションで再構築しない
  • ツリーを通じた再構築の伝播を止めるためにconstウィジェットを使用
  • 計算/派生状態がリアクティブに計算され、冗長に保存されない

サブスクリプションと廃棄:

  • すべての手動サブスクリプション(.listen())がdispose() / close()でキャンセルされる
  • ストリームコントローラーが不要になったら閉じられる
  • タイマーが廃棄ライフサイクルでキャンセルされる
  • フレームワーク管理のライフサイクルが手動サブスクリプションより優先される(.listen()よりも宣言的ビルダー)
  • 非同期コールバックでのsetState前にmountedチェック
  • await後にBuildContextcontext.mountedをチェックせずに使用しない(Flutter 3.7+) — 古いコンテキストはクラッシュを引き起こす
  • 非同期ギャップの後にウィジェットがまだマウントされていることを確認せずにナビゲーション、ダイアログ、またはscaffoldメッセージを使用しない
  • BuildContextをシングルトン、状態マネージャー、または静的フィールドに保存しない

ローカル対グローバル状態:

  • 一時的なUI状態(チェックボックス、スライダー、アニメーション)がローカル状態(setStateValueNotifier)を使用
  • 共有状態が必要な分だけリフトされる — 過度にグローバル化されない
  • フィーチャースコープの状態がフィーチャーがアクティブでなくなったときに適切に廃棄される

5. パフォーマンス

不要な再構築:

  • setState()がルートウィジェットレベルで呼び出されない — 状態変更をローカル化する
  • constウィジェットが再構築の伝播を止めるために使用される
  • RepaintBoundaryが独立して再描画する複雑なサブツリーの周りに使用される
  • AnimatedBuilderのchildパラメーターがアニメーションから独立したサブツリーに使用される

build()内の高コスト操作:

  • build()内で大きなコレクションのソート、フィルタリング、マッピングがない — 状態管理レイヤーで計算する
  • build()内でregexのコンパイルがない
  • MediaQuery.of(context)の使用が具体的(例: MediaQuery.sizeOf(context)

画像の最適化:

  • ネットワーク画像がキャッシングを使用(プロジェクトに適したキャッシングソリューション)
  • ターゲットデバイスに適した画像解像度(サムネイルに4K画像をロードしない)
  • Image.assetcacheWidth/cacheHeightを使用して表示サイズでデコードする
  • ネットワーク画像にプレースホルダーとエラーウィジェットが提供されている

遅延ローディング:

  • 大きなまたは動的なリストにはListView(children: [...])の代わりにListView.builder / GridView.builderを使用(小さくて静的なリストにはコンクリートコンストラクターが適切)
  • 大きなデータセットにページネーションが実装されている
  • Webビルドで重いライブラリに遅延ローディング(deferred as)を使用

その他:

  • アニメーションでOpacityウィジェットを避ける — AnimatedOpacityまたはFadeTransitionを使用
  • アニメーションでクリッピングを避ける — 画像を事前にクリップする
  • ウィジェットでoperator ==をオーバーライドしない — 代わりにconstコンストラクターを使用
  • 組み込み次元ウィジェット(IntrinsicHeightIntrinsicWidth)を控えめに使用(追加のレイアウトパス)

6. テスト

テストの種類と期待値:

  • ユニットテスト: すべてのビジネスロジック(状態マネージャー、リポジトリ、ユーティリティ関数)をカバー
  • ウィジェットテスト: 個々のウィジェットの動作、インタラクション、視覚的出力をカバー
  • 統合テスト: 重要なユーザーフローをエンドツーエンドでカバー
  • ゴールデンテスト: デザインクリティカルなUIコンポーネントのピクセル単位の比較

カバレッジの目標:

  • ビジネスロジックで80%以上のライン カバレッジを目指す
  • すべての状態遷移が対応するテストを持つ(ローディング→成功、ローディング→エラー、リトライなど)
  • エッジケースのテスト: 空の状態、エラー状態、ローディング状態、境界値

テストの分離:

  • 外部依存関係(APIクライアント、データベース、サービス)がモック化またはフェイク化されている
  • 各テストファイルが正確に1つのクラス/ユニットをテストする
  • テストが実装の詳細ではなく動作を検証する
  • スタブが各テストに必要な動作のみを定義する(最小限のスタッビング)
  • テストケース間で共有された可変状態がない

ウィジェットテストの品質:

  • pumpWidgetpumpが非同期操作に対して正しく使用されている
  • find.byTypefind.textfind.byKeyが適切に使用されている
  • タイミングに依存する不安定なテストがない — pumpAndSettleまたは明示的なpump(Duration)を使用
  • テストがCIで実行され、失敗がマージをブロックする

7. アクセシビリティ

セマンティックウィジェット:

  • 自動ラベルが不十分な場所でスクリーンリーダーラベルを提供するためにSemanticsウィジェットを使用
  • 純粋に装飾的な要素にExcludeSemanticsを使用
  • 関連するウィジェットを単一のアクセシブルな要素に結合するためにMergeSemanticsを使用
  • 画像にsemanticLabelプロパティが設定されている

スクリーンリーダーのサポート:

  • すべてのインタラクティブ要素がフォーカス可能で意味のある説明を持つ
  • フォーカス順序が論理的(視覚的な読み取り順序に従う)

視覚的アクセシビリティ:

  • テキストと背景のコントラスト比が4.5:1以上
  • タップ可能なターゲットが少なくとも48x48ピクセル
  • 色だけが状態の指標でない(アイコン/テキストと共に使用)
  • テキストがシステムフォントサイズ設定に合わせてスケールする

インタラクションのアクセシビリティ:

  • 何もしないonPressedコールバックがない — すべてのボタンが何かをするか無効化されている
  • エラーフィールドが修正を提案する
  • ユーザーがデータを入力している間にコンテキストが予期せず変わらない

8. プラットフォーム固有の考慮事項

iOS/Androidの違い:

  • 適切な場所でプラットフォーム適応型ウィジェットを使用
  • バック ナビゲーションが正しく処理されている(Androidのバックボタン、iOSのスワイプバック)
  • ステータスバーとセーフエリアがSafeAreaウィジェットで処理されている
  • プラットフォーム固有の権限がAndroidManifest.xmlInfo.plistで宣言されている

レスポンシブデザイン:

  • レスポンシブレイアウトにLayoutBuilderまたはMediaQueryを使用
  • ブレークポイントが一貫して定義されている(電話、タブレット、デスクトップ)
  • テキストが小さい画面でオーバーフローしない — FlexibleExpandedFittedBoxを使用
  • 横向きが テストされているか明示的にロックされている
  • Web固有: マウス/キーボードインタラクションがサポートされ、ホバー状態が存在する

9. セキュリティ

安全なストレージ:

  • 機密データ(トークン、資格情報)がプラットフォームセキュアなストレージを使用(iOSのKeychain、AndroidのEncryptedSharedPreferences)
  • 平文ストレージにシークレットを保存しない
  • 機密操作に生体認証ゲーティングを検討

APIキーの処理:

  • APIキーがDartソースにハードコードされていない — --dart-define、VCSから除外された.envファイル、またはコンパイル時設定を使用
  • シークレットがgitにコミットされていない — .gitignoreを確認
  • 本当にシークレットなキーにはバックエンドプロキシを使用(クライアントはサーバーシークレットを保持すべきでない)

入力バリデーション:

  • すべてのユーザー入力がAPIに送信する前にバリデートされる
  • フォームバリデーションが適切なバリデーションパターンを使用
  • ユーザー入力の生のSQLや文字列補間がない
  • ナビゲーション前にディープリンクURLがバリデートおよびサニタイズされる

ネットワークセキュリティ:

  • すべてのAPI呼び出しにHTTPSが強制されている
  • 高セキュリティアプリには証明書のピン留めを検討
  • 認証トークンが適切にリフレッシュおよび期限切れになる
  • 機密データがログや出力に記録されない

10. パッケージ/依存関係のレビュー

pub.devパッケージの評価:

  • pubポイントスコアを確認(130+/160を目指す)
  • コミュニティシグナルとしていいね人気度を確認
  • pub.devでパブリッシャーが認証済みであることを確認
  • 最終公開日を確認 — 古いパッケージ(1年以上)はリスク
  • オープンな問題とメンテナーからの応答時間を確認
  • ライセンスがプロジェクトと互換性があることを確認
  • プラットフォームサポートがターゲットをカバーすることを確認

バージョン制約:

  • 依存関係にキャレット構文(^1.2.3)を使用 — 互換性のある更新を許可
  • 絶対に必要な場合のみ正確なバージョンを固定
  • 古い依存関係を追跡するために定期的にflutter pub outdatedを実行
  • 本番pubspec.yamlでは依存関係のオーバーライドなし — コメント/問題リンク付きの一時的な修正のみ
  • 一時的な依存関係の数を最小化 — 各依存関係は攻撃面

モノリポ固有(melos/workspace):

  • 内部パッケージがパブリックAPIからのみインポートする — package:other/src/internal.dartなし(Dartパッケージのカプセル化を壊す)
  • 内部パッケージの依存関係がワークスペース解決を使用し、ハードコードされたpath: ../../相対文字列でない
  • すべてのサブパッケージがルートのanalysis_options.yamlを共有または継承する

11. ナビゲーションとルーティング

一般原則(任意のルーティングソリューションに適用):

  • 一つのルーティングアプローチが一貫して使用されている — 宣言的ルーターと命令的Navigator.pushの混在なし
  • ルート引数が型付き — Map<String, dynamic>Object?キャストなし
  • ルートパスが定数、enum、または生成として定義されている — コード全体に散らばったマジック文字列なし
  • 認証ガード/リダイレクトが集中管理されている — 個々の画面で重複していない
  • ディープリンクが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、または同等のもの)
  • サポートされているロケールがアプリの設定で宣言されている

コンテンツ:

  • すべてのユーザー向け文字列がローカリゼーションシステムを使用 — ウィジェット内のハードコードされた文字列なし
  • テンプレートファイルが翻訳者向けの説明/コンテキストを含む
  • 複数形、性別、選択にICUメッセージ構文を使用
  • プレースホルダーが型で定義されている
  • ロケール間でキーが欠けていない

コードレビュー:

  • ローカリゼーションアクセサーがプロジェクト全体で一貫して使用されている
  • 日付、時刻、数値、通貨のフォーマットがロケール対応
  • アラビア語、ヘブライ語などをターゲットにする場合、テキストの方向性(RTL)がサポートされている
  • ローカライズされたテキストに文字列連結がない — パラメーター化されたメッセージを使用

14. 依存性注入

原則(任意のDIアプローチに適用):

  • クラスがレイヤー境界で具体的な実装ではなく抽象(インターフェース)に依存する
  • 依存関係がコンストラクター、DIフレームワーク、またはプロバイダーグラフを通じて外部から提供される — 内部で作成されない
  • 登録がライフタイムを区別する: シングルトン対ファクトリー対レイジーシングルトン
  • 環境固有のバインディング(dev/staging/prod)が設定を使用し、ランタイムのifチェックではない
  • DIグラフに循環依存がない
  • サービスロケーターの呼び出し(使用する場合)がビジネスロジック全体に散らばっていない

15. 静的解析

設定:

  • analysis_options.yamlが厳格な設定を有効にして存在する
  • 厳格なアナライザー設定: strict-casts: truestrict-inference: truestrict-raw-types: true
  • 包括的なリントルールセットが含まれている(very_good_analysis、flutter_lints、またはカスタム厳格ルール)
  • モノリポ内のすべてのサブパッケージがルートの解析オプションを継承または共有する

適用:

  • コミットされたコードにアナライザーの未解決の警告がない
  • リントの抑制(// ignore:)が理由を説明するコメントで正当化されている
  • flutter analyzeがCIで実行され、失敗がマージをブロックする

リントパッケージに関わらず確認すべき主要なルール:

  • prefer_const_constructors — ウィジェットツリーのパフォーマンス
  • 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 ストアを直接 signalを直接 ウィジェットテスト

ソース

Info
Category Development
Name flutter-dart-code-review
Version v20260517
Size 30.77KB
Updated At 2026-05-18
Language