Instruction file imported from YumNumm/EQMonitor (
.cursor/rules/flutter-rules.mdc). Copyright stays with the author.
EQMonitor Flutter Rules
生命に関わる情報・TODO・大前提は project-rule.mdc を参照する。
ツール・コマンド
- Flutter / Dart に関するコマンドは常に
mise exec --経由で実行する - 依存追加は
flutter pub addを使う(pubspec.yaml を直接編集しない) - コード生成:
dart run build_runner build --delete-conflicting-outputs
テスト方針
- TDD は有効な選択肢だが、一律の必須手順にはしない。ユーザーの指示と変更リスクに応じて、テスト先行・テスト追加・既存テストによる回帰確認を選択する。
- 文言や情報配置など表示専用の軽微な変更では、Widget Test の追加を必須としない。
- 緊急情報の判定、データ変換、状態遷移、通知条件、永続化、障害修正など、誤動作の影響や回帰リスクが高いロジックには自動テストを用意する。
- 新しいテストを追加しない場合も、関連する既存テストと静的解析を実行し、追加しない理由を作業結果に記載する。
設計原則
- SOLID 原則を厳格に適用する
- Presentation(Widget)・Domain(ロジック)・Data(リポジトリ / API)のレイヤー構成
- レイヤー内の命令的な処理は、パッケージ側の宣言的な実装で代替できないか検討する(不可能または機能欠損が生じる場合は命令的な実装も可)
ディレクトリ構成
全体構造
lib/
├── core/ # アプリ全体の根幹(共通コンポーネント・テーマ・ルーター・ユーティリティ等)
├── feature/ # 機能単位のモジュール
├── page/ # トップレベルのページ(必要に応じて)
├── app.dart
└── main.dart
feature/${NAME}/ の構造
feature/${NAME}/
├── data/
│ ├── model/
│ ├── repository/
│ ├── data_source/ # (任意) 複数APIや複雑性がある場合のみ
│ ├── notifier/
│ ├── provider/
│ └── flow/
└── ui/
├── page/ # *_page.dart を配置
└── components/ # 再利用可能な Widget を配置
Data 層の各ディレクトリの役割
data/model/
- アプリ固有の型定義を行う
- API パッケージの型は
as apiでエイリアス import し、アプリの型に変換する extension を定義する - Freezed を積極的に利用する
- 新しい型を作成する前に、既存の型に類似・重複するものがないか必ず確認する
- 同じフィールド構成(code + name + intensity 等)を持つ型がないか検索する
- 別 feature に同様の役割を持つ型がないか確認する
- 汎用的なレスポンス型(ページネーション等)が既に存在しないか確認する
- 重複がある場合は、既存の型を拡張・再利用する方針を優先する
import 'package:eqapi_types/eqapi_types.dart' as api;
extension EarthquakeModelConverter on api.EarthquakeResponse {
EarthquakeModel toModel() => EarthquakeModel(...);
}
data/repository/
- API の Fetch と型変換を担う
- API 通信を行うものは
Future<Result<T, ApiException>>を返す(例外あり) - Riverpod で DI する
- 基本的にすべての外部データアクセスは repository を経由する
- SharedPreferences へのアクセスは
SharedPreferencesDataSource経由に限定する(SharedPreferences/sharedPreferencesProviderを Repository・Notifier・Provider・UI から直接呼ばない)
SharedPreferences アクセス規約
- キーは
SharedPreferencesKeyenum で一元管理する(preferences-key-management.mdc参照) - 読み書きは
app/lib/core/data/preferences/shared/shared_preferences_data_source.dartのSharedPreferencesDataSourceを利用する - 例外:
SharedPreferencesDataSource本体、デバッグ用の SharedPreferences 閲覧画面、main.dartでの初期化、App Group 用の別ストレージ
// ❌ 悪い例: Repository が SharedPreferences に直接アクセス
final prefs = ref.watch(sharedPreferencesProvider);
prefs.getBool(SharedPreferencesKey.deviceProvisioned.key);
// ✅ 良い例: SharedPreferencesDataSource 経由
final dataSource = await ref.watch(sharedPreferencesDataSourceProvider.future);
await dataSource.getBool(key: SharedPreferencesKey.deviceProvisioned);
data/data_source/
- 複数の API がある場合や、1 つのレイヤーだと複雑になる場合にのみ使用する
- repository が data_source を利用する形で構成する
data/notifier/
- アプリケーションの状態を保持する
@riverpodアノテーションを使用する- 副作用を持つ関数は Riverpod 3 の Mutation を利用する
data/provider/
- notifier を持たない、派生・加工された状態(computed provider)のみを配置する
- DI や API クライアント初期化は repository 層で行う
data/flow/
- 非同期処理の後にダイアログ表示や画面遷移など UI 操作を伴うユースケースを記述する
- UI 層と UI 処理ロジックを分離して可読性を確保する
- Riverpod 関連で唯一、関数の引数に
WidgetRef refとBuildContext contextを持つことが許される
UI 層の各ディレクトリの役割
ui/page/
- 画面全体を表す Widget を
*_page.dartとして配置する - ファイル名のサフィックスは
_page.dartに統一する(_screen.dartは使わない)
ui/components/
- その feature 内で再利用する Widget を配置する
- feature をまたいで共通利用する Widget は
lib/core/component/に配置する
状態管理
- Riverpod + flutter_hooks を使用する
- StatefulWidget は基本的に利用しない。HookWidget または HookConsumerWidget で状態を管理する
- UI ステート(エフェメラル)とアプリステートを分離する
ルーティング
- go_router を使用する
- すべて go_router_builder を使用
型安全
dynamic、any、Object型はMap<String, dynamic>以外での利用を禁止- Null Safety:
!演算子の使用を禁止する。?とフロー解析(if (x != null))で安全に扱う
Widget 設計
- Widget のコードを不用意に長くしない。再利用しない Widget は private class で作成する
- Widget に関数やゲッターを定義することを禁止する
- 適度に変数・定数に Widget を切り出して可読性を維持する
- build メソッドは純粋かつ高速に保つ。副作用やネットワーク呼び出しを含めない
constコンストラクタを積極的に使用する- リスト表示には
ListView.builderまたはSliverListを使用する- 基本的に
SingleChildScrollViewは利用しない
- 基本的に
- テキストを含む要素に固定の高さ(
height、SizedBox等)を数値指定しない(textScale 拡大時の overflow 防止) Row内にTextを配置する場合等に、overflow を防ぐためにExpandedやFlexibleを適切に使用する
ビジュアルデザイン(Material 3)
ThemeData+ColorScheme.fromSeedでテーマを構築する- Light / Dark モード両対応(
ThemeMode.system) google_fontsで統一的なタイプスケールを定義する- カスタムトークン(色・サイズ)には
ThemeExtensionを使用する - レスポンシブ対応には
LayoutBuilderを使用する
命名規則
- 型名:
PascalCase - メンバー:
camelCase - ファイル名:
snake_case - 画面ファイル:
*_page.dart(*_screen.dartは使わない)
ログ
- 基本的に talker を使用する
dart:developerのlog()も利用可print()の使用を禁止する
コードスタイル
- 2つ以上の引数を持つ関数・クラスは原則として名前付き引数を使用する
// ❌ 悪い例
void doSomething(String name, int age) { ... }
// ✅ 良い例
void doSomething({required String name, required int age}) { ... }
- 不用意に static method にしない(必要な箇所のみ)
- 関数は簡潔に保つ(目安: 20 行以内)
- 内部で非同期処理を
unawaitedするくらいなら、関数自体をasyncにしてawaitすること
// ❌ 悪い例
void doSomething() {
unawaited(someAsyncOperation());
}
// ✅ 良い例
Future<void> doSomething() async {
await someAsyncOperation();
}
- 変数宣言と代入の分離を禁止する。
finalによる即時代入を必ず使用する。switch 式・条件式・三項演算子などを活用して一行で完結させる
// ❌ 悪い例
String? label;
switch (value) {
case Foo(:final x):
label = 'foo $x';
case null:
label = defaultLabel;
}
// ✅ 良い例
final label = switch (value) {
Foo(:final x) => 'foo $x',
null => defaultLabel,
};
- コメントはコード上明らかな部分には不要。複雑な処理や実装意図が読み取りにくい箇所にのみ書く
- 重い処理(JSON パース等)には
compute()で Isolate を活用する - enum等でdartのdot shorthandを利用できる場合は利用する
グローバル関数・プライベート関数の禁止と代替設計
top-level / global 関数の定義は禁止する。プライベートな top-level 関数(例: _buildLayer)も禁止する。
処理は用途に応じて専用 class に切り出し、必要な場所では Riverpod で DI すること。
クラス内でのプライベートメソッドは、テスト可能性を損なうため原則禁止する。 用途に応じて以下のルールに従って代替実装を選ぶこと。
ロジック系プライベートメソッド(例: _buildRegionGeoJson, _buildCityGeoJson)
- 禁止: クラス内にプライベートメソッドとしてロジックを定義する
- 正解: 処理を行う専用クラスを別ファイルに切り出し、Riverpod で DI する
// ❌ 悪い例: Widget や Notifier 内にプライベートメソッドでロジックを隠蔽
String _buildRegionGeoJson({required List<Region> regions}) { ... }
// ✅ 良い例: 別クラスに切り出して Riverpod で注入
// data/repository/region_geojson_builder.dart
@riverpod
RegionGeoJsonBuilder regionGeoJsonBuilder(Ref ref) => RegionGeoJsonBuilder();
class RegionGeoJsonBuilder {
String build({required List<Region> regions}) { ... }
}
イベントハンドラ系プライベートメソッド(例: _handleTap)
- 禁止: Widget 内に
_handleXxxのようなプライベートメソッドを定義する - 正解:
XxxActionクラスとして別ファイルに切り出し、Riverpod で DI する
// ❌ 悪い例: Widget 内にプライベートメソッド
void _handleTap(LatLng point) { ... }
// ❌ 悪い例: コンストラクタに ref を渡す
class EarthquakeHistoryMapAction {
EarthquakeHistoryMapAction(this._ref);
final Ref _ref;
void handleTap(LatLng point) { ... }
}
// ✅ 良い例: ref / context はコンストラクタではなく各メソッドの引数で受け取る
// ui/action/earthquake_history_map_action.dart
@riverpod
EarthquakeHistoryMapAction earthquakeHistoryMapAction(Ref ref) =>
EarthquakeHistoryMapAction();
class EarthquakeHistoryMapAction {
void handleTap(WidgetRef ref, BuildContext context, LatLng point) { ... }
}
ref/contextを Action クラスのコンストラクタに渡すことを禁止する- Action クラスのメソッドに限り、
WidgetRef ref/BuildContext contextを引数として渡すことを許可する - Action 以外のクラス・関数に
refやcontextを渡すことを絶対に禁止する(data/flow/の flow 関数は除く)
Widget 分割で解消できる場合
- Widget が肥大化してプライベートメソッドが必要になっているなら、まず Widget を private class に分割 することを優先する
switch 式 / 単純な変換のための関数
- 一箇所のみで使う場合: 関数に切り出さず 変数・定数として直接定義 する
- build メソッド内の switch 式: 関数を作らずインラインで記述する
// ❌ 悪い例
String _getLabel(DisplayMode mode) => switch (mode) { ... };
// ✅ 良い例
final label = switch (mode) { ... };