Cloud Functionsまで含めてDartに統一するFlutterアプリのアーキテクチャーについて、AIに相談した結果メモ(現時点では未検証):
Dart 3.5の Dart Workspaces をインフラの主軸とし、バックエンド(Cloud Functions)には公式の firebase_admin_sdk を採用。フロントエンドの「Feature-first(機能駆動)」と、バックエンドの「データグラフの集約」を完璧に両立させる構成です。
無駄なパッケージ分割による pubspec.yaml の乱造を防ぎ、開発者が迷わずコードを読み書きできる究極に身軽な構成です。
my_project_workspace/
├── pubspec.yaml # ワークスペース定義 (resolution: workspace)
│
├── apps/
│ ├── main/ # 📱 メインアプリ (Flutter)
│ │ ├── pubspec.yaml # 依存: shared_schema, shared_ui, riverpod 等
│ │ └── lib/
│ │ ├── core/ # アプリ固有の基盤 (go_router, DI設定など)
│ │ └── features/ # 【機能分割】 auth, feed, user 等 (UI/状態管理)
│ │
│ └── admin/ # 💻 管理アプリ (Flutter Web等)
│ ├── pubspec.yaml # 依存: shared_schema, shared_ui, riverpod 等
│ └── lib/
│ ├── core/
│ └── features/ # 【機能分割】 auth, user_management 等
│
├── functions/ # ☁️ Cloud Functions (Pure Dart)
│ ├── pubspec.yaml # 依存: shared_schema, firebase_admin_sdk
│ └── lib/
│ └── ... # エントリーポイントとトリガーロジック
│
└── packages/
├── shared_schema/ # 🟢 【Pure Dart】 全モデル・定数・ドメインロジック
│ ├── pubspec.yaml # 依存: freezed, json_serializable のみ
│ └── lib/
│ ├── core/ # 静的レゾルバ (TimestampResolver 等)
│ └── features/ # auth, feed 等のデータモデル・バリデーション定義
│
└── shared_ui/ # 🔵 【Flutter】 真に共通なデザインシステム
├── pubspec.yaml # 依存: flutter
└── lib/ # 共通テーマ、汎用ボタン、ダイアログ等
サードパーティのMelosによる依存関係のリンクを廃止し、Dart標準の機能でモノレポを構築します。親の pubspec.yaml で対象ディレクトリを宣言し、各子パッケージで resolution: workspace を指定することで、バージョン衝突を根本から防ぎます。
※ build_runner の並列実行などのタスクランナー用途としてのみ、限定的にMelosやスクリプトを併用します。
- データ(Domain)は集約: Functionsが複数機能をまたぐトランザクション処理を行う際、依存地獄に陥らないよう、すべてのデータモデルは
shared_schema1つに集約します。これにより Functions へのdart:ui誤混入も物理的に防げます。 - UI/状態は分散:
riverpodやUIを含むコードは、パッケージ分割の手間を省くためapps/mainなどの内部でfeatures/としてフォルダ分割します。コンテキストスイッチを最小限に抑えます。
cloud_firestore と firebase_admin_sdk の Timestamp 型の衝突を、共通モデルを汚染せずに解決します。
モデル側はSDKに依存せず、アノテーションのみを使用します。
// packages/shared_schema/lib/core/timestamp_resolver.dart
import 'package:json_annotation/json_annotation.dart';
class TimestampResolver {
// 初期値は安全なDateTime変換
static DateTime Function(dynamic json) fromJson = (json) => DateTime.parse(json as String);
static dynamic Function(DateTime date) toJson = (date) => date.toIso8601String();
}
class SharedTimestampConverter implements JsonConverter<DateTime, dynamic> {
const SharedTimestampConverter();
@override DateTime fromJson(dynamic json) => TimestampResolver.fromJson(json);
@override dynamic toJson(DateTime date) => TimestampResolver.toJson(date);
}
// packages/shared_schema/lib/features/user/user_profile.dart
@freezed
class UserProfile with _$UserProfile {
const factory UserProfile({
required String name,
@SharedTimestampConverter() required DateTime createdAt,
}) = _UserProfile;
factory UserProfile.fromJson(Map<String, dynamic> json) => _$UserProfileFromJson(json);
}アプリ起動時に cloud_firestore のロジックを注入します。
// apps/main/lib/main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:shared_schema/shared_schema.dart';
void main() {
TimestampResolver.fromJson = (json) => (json as Timestamp).toDate();
TimestampResolver.toJson = (date) => Timestamp.fromDate(date);
runApp(const ProviderScope(child: MyApp()));
}サーバー起動時に firebase_admin_sdk のロジックを注入します。
// functions/lib/main.dart
import 'package:firebase_admin_sdk/firebase_admin_sdk.dart';
import 'package:shared_schema/shared_schema.dart';
void main() {
TimestampResolver.fromJson = (json) => (json as Timestamp).toDate();
TimestampResolver.toJson = (date) => Timestamp.fromDate(date);
final admin = FirebaseAdminApp.initializeApp(projectId: 'your-project-id');
// ... routing to specific functions
}