Flutter: プロジェクト設計 — ShopAppアーキテクチャ計画
建てる前に青写真を描く — アーキテクチャ設計がコードの「耐力壁」の位置を決めます。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン1:Flutter入門と環境構築
- レッスン2:Dart言語クラッシュコース
- レッスン3:ウィジェットの基礎
- レッスン4:レイアウトシステム
- レッスン5:StatefulWidgetとインタラクション
- レッスン6:Material Designと主要コンポーネント
- レッスン7:フェーズ1実習 — ShopAppスターター
- レッスン8:ナビゲーションとルーティング
- レッスン9:フォームと入力
- レッスン10:リストとスクロール
- レッスン11:ネットワークとREST API
- レッスン12:状態管理 — Riverpod
- レッスン13:ローカルストレージ
- レッスン14:フェーズ2実習 — ShopAppコア
- レッスン15:アニメーションシステム
- レッスン16:テーマとスタイリング
- レッスン17:プラットフォームチャンネルとネイティブ相互運用
- レッスン18:Firebase連携
- レッスン19:国際化とローカライゼーション
- レッスン20:カスタム描画とCustomPainter
- レッスン21:テスト — Unit/Widget/Integration
- レッスン22:パフォーマンス最適化
- レッスン23:WebとDesktopマルチプラットフォームデプロイ
- レッスン24:CI/CD自動化
- レッスン25:マルチプラットフォーム公開とストア審査
1. このレッスンで学ぶこと
- 要件分析:ユーザーストーリー(Aliceの閲覧・注文 / Bobの商人管理 / Charlieの越境決済)
- 技術選定:Flutter 3.x + Riverpod + GoRouter + Dio + Firebase + Hive
- アーキテクチャ設計:Clean Architectureレイヤリング(Domain / Data / Presentation)
- データモデリング:Product / Cart / Order / UserエンティティとFirestore Schema
- UI/UX設計:ワイヤーフレーム + コンポーネントライブラリ計画 + デザイントークンシステム
2. アーキテクチャなしプロジェクトのストーリー
(1) 悩み:曖昧な要件 + 混乱するアーキテクチャ
BobがShopAppプロジェクトを受けた時、唯一の要件は「越境ECアプリを作る」だけ。すぐにコーディングを始め、3ヶ月後に判明:ユーザーストーリーが不明確(商人管理機能が欠落)、データモデルが頻繁に変更(Orderは3回書き直し)、アーキテクチャが密結合(決済の変更が商品一覧に影響)。プロジェクトは2ヶ月遅延。
(2) 先に設計、後にコーディング
プロジェクト設計フェーズで要件、アーキテクチャ、データモデル、UI計画を事前に明確にすれば — コーディングは「青写真に従って建てる」ことになります。
(3) 成果:コーディング時間半減 + 変更コスト80%削減
2週間の設計に費やした後、Bobは4週間でコーディングを完了(以前の3+2ヶ月から)、要件変更は該当レイヤーの修正のみで他モジュールに影響なし。
3. 要件分析
(1) ユーザーストーリー
graph TD
subgraph User Stories
ALICE[Alice: Browse → Order → Pay $299.99]
BOB[Bob: Manage Products → View Orders]
CHARLIE[Charlie: Cross-border → JPY → International Payment]
end
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
| 役割 | ユーザーストーリー | 優先度 |
|---|---|---|
| Alice(消費者) | 100万商品リストの閲覧 | P0 |
| Alice | 検索/フィルター/カテゴリ別閲覧 | P0 |
| Alice | 商品詳細 + レビューの閲覧 | P0 |
| Alice | カート追加 + 数量管理 | P0 |
| Alice | USD/CNY/JPYマルチ通貨決済 | P1 |
| Alice | 注文ステータス + 物流追跡の閲覧 | P1 |
| Bob(商人) | 商品出品/非出品 + 在庫管理 | P1 |
| Bob | 売上データレポートの閲覧 | P2 |
| Charlie(越境ユーザー) | 越境ショッピング + 国際決済 | P2 |
| Charlie | 多言語切替(EN/ZH/JA) | P1 |
4. 技術選定
(1) 技術スタック決定
| 領域 | 選定 | 理由 |
|---|---|---|
| UIフレームワーク | Flutter 3.x(Material 3) | クロスプラットフォーム + カスタム描画エンジン |
| 状態管理 | Riverpod 2.x(@riverpod) | 型安全 + コード生成 |
| ルーティング | GoRouter | 宣言型 + Web URL対応 |
| ネットワーク | Dio + Interceptors | インターセプター + トークンリフレッシュ |
| バックエンド | Firebase(Auth+FS+Storage) | 運用ゼロ + リアルタイム同期 |
| キャッシュ | Hive | 軽量NoSQL + オフライン |
| 暗号化ストレージ | flutter_secure_storage | 安全なトークン保存 |
| シリアライゼーション | json_serializable + freezed | 型安全 + イミュータブル |
| 国際化 | flutter_localizations + intl | ARBワークフロー |
| テスト | flutter_test + mocktail | Unit/Widget/Integration |
| CI/CD | GitHub Actions | 無料枠 + マルチマトリックス |
(2) Riverpod vs BLoC比較
| 項目 | Riverpod | BLoC |
|---|---|---|
| 学習曲線 | 中程度 | 高い |
| ボイラープレート | 少ない(@riverpod) | 多い(Event/State/Bloc) |
| 型安全性 | 強い | 強い |
| テスト | シンプル | シンプル |
| コード生成 | ✅ | ❌ |
| 最適な用途 | 中小規模プロジェクト | 大規模チームプロジェクト |
💡 ヒント: ShopAppはRiverpodを選択 — コード量が少なく、コード生成でボイラープレートを削減、学習曲線は中程度。
5. Clean Architectureレイヤリング
graph TD
subgraph Presentation
PAGE[Pages/Widgets]
NOTI[Notifiers/Providers]
end
subgraph Domain
ENT[Entities]
REPO_I[Repository Interfaces]
USECASE[Use Cases]
end
subgraph Data
REPO_IMPL[Repository Impl]
DS[Data Sources]
DTO2[DTOs / Models]
end
PAGE --> NOTI
NOTI --> USECASE
USECASE --> REPO_I
REPO_I -.-> REPO_IMPL
REPO_IMPL --> DS
DS --> DTO2
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(1) レイヤー責務
| レイヤー | ディレクトリ | 責務 | 依存方向 |
|---|---|---|---|
| Presentation | screens/, widgets/ |
UI + ユーザーインタラクション | → Application |
| Application | notifiers/, usecases/ |
ビジネスロジック + 状態 | → Domain |
| Domain | entities/, repositories/ |
コアビジネスルール | 外部依存なし |
| Data | repositories/impl/, datasources/ |
データ取得 + 永続化 | → Domain |
(2) 依存ルール
- Domainレイヤー:純粋Dart、Flutter/サードパーティ依存なし
- Dataレイヤー:DomainのRepositoryインターフェースを実装
- Applicationレイヤー:DomainとPresentationを調整
- Presentationレイヤー:Applicationレイヤーのみに依存
6. データモデリング
(1) コアエンティティ
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Domainエンティティ定義
DART
// domain/entities/product.dart
// Pure Dart class, no external dependencies
class Product {
final int id;
final String name;
final double price;
final String imageUrl;
final String category;
final double rating;
final int reviewCount;
final int stock;
final String currency;
const Product({
required this.id,
required this.name,
required this.price,
required this.imageUrl,
this.category = 'General',
this.rating = 0.0,
this.reviewCount = 0,
this.stock = 0,
this.currency = 'USD',
});
bool get inStock => stock > 0;
bool get hasDiscount => false; // Extended in sale scenario
}
// domain/entities/cart_item.dart
class CartItem {
final Product product;
final int quantity;
final String? selectedSize;
final String? selectedColor;
const CartItem({
required this.product,
required this.quantity,
this.selectedSize,
this.selectedColor,
});
double get lineTotal => product.price * quantity;
CartItem copyWith({int? quantity, String? selectedSize, String? selectedColor}) =>
CartItem(product: product, quantity: quantity ?? this.quantity,
selectedSize: selectedSize ?? this.selectedSize,
selectedColor: selectedColor ?? this.selectedColor);
}
// domain/entities/address.dart
class Address {
final String name;
final String street;
final String city;
final String state;
final String zip;
final String country;
const Address({
required this.name,
required this.street,
required this.city,
required this.state,
required this.zip,
this.country = 'US',
});
String get fullAddress => '$street, $city, $state $zip, $country';
Map<String, dynamic> toJson() => {
'name': name, 'street': street, 'city': city,
'state': state, 'zip': zip, 'country': country,
};
}
// domain/entities/order.dart
enum OrderStatus { pending, confirmed, shipped, delivered, cancelled }
class Order {
final String id;
final String userId;
final List<CartItem> items;
final double total;
final String currency;
final OrderStatus status;
final DateTime createdAt;
final Address shippingAddress;
const Order({
required this.id,
required this.userId,
required this.items,
required this.total,
this.currency = 'USD',
this.status = OrderStatus.pending,
required this.createdAt,
required this.shippingAddress,
});
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
(2) Firestore Schema設計
TEXT
firestore/
├── products/{productId}
│ ├── name: string
│ ├── price: number
│ ├── stock: number
│ ├── category: string
│ ├── rating: number
│ ├── imageUrl: string
│ └── currency: string
├── users/{userId}
│ ├── email: string
│ ├── name: string
│ ├── preferences: map
│ │ ├── theme: string
│ │ ├── currency: string
│ │ └── locale: string
│ └── addresses: array
├── orders/{orderId}
│ ├── userId: string
│ ├── items: array<{productId, quantity, price}>
│ ├── total: number
│ ├── currency: string
│ ├── status: string
│ └── createdAt: timestamp
└── reviews/{reviewId}
├── productId: string
├── userId: string
├── rating: number
└── comment: string
```text
```text
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. UI/UX計画
(1) ページ一覧
| ページ | ルート | コアコンポーネント |
|---|---|---|
| ホーム | / |
SliverAppBar + GridView + CategoryChips |
| 検索 | /search |
SearchBar + FilterSheet + ProductList |
| 商品詳細 | /product/:id |
Hero + SliverAppBar + BottomSheet |
| カート | /cart |
CartList + QuantitySelector + TotalBar |
| チェックアウト | /checkout |
AddressForm + PaymentSelector + OrderSummary |
| 注文確認 | /order/:id |
OrderTimeline + TrackingMap |
| プロフィール | /profile |
UserCard + OrderHistory + Settings |
| ログイン | /login |
EmailForm + GoogleButton + AppleButton |
(2) デザイントークン
DART
import 'package:flutter/material.dart';
// Design tokens: unified color/spacing/radius/font constants, carried by ThemeExtension
class ShopDesignTokens {
// Colors
static const primary = Color(0xFF0066CC);
static const secondary = Color(0xFFFF6B35);
static const sale = Color(0xFF4CAF50);
static const discount = Color(0xFFEF5350);
// Spacing
static const xs = 4.0;
static const sm = 8.0;
static const md = 16.0;
static const lg = 24.0;
static const xl = 32.0;
// Radius
static const cardRadius = 12.0;
static const buttonRadius = 8.0;
static const inputRadius = 8.0;
// Typography
static const headlineSize = 24.0;
static const titleSize = 18.0;
static const bodySize = 14.0;
static const captionSize = 12.0;
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:プロジェクトディレクトリ構造
TEXT
shop_app/
├── lib/
│ ├── main.dart
│ ├── app.dart
│ ├── core/
│ │ ├── router/
│ │ │ └── app_router.dart
│ │ ├── network/
│ │ │ ├── dio_client.dart
│ │ │ └── api_exception.dart
│ │ ├── storage/
│ │ │ ├── secure_storage.dart
│ │ │ ├── hive_cache.dart
│ │ │ └── shared_prefs.dart
│ │ ├── theme/
│ │ │ ├── app_theme.dart
│ │ │ └── brand_tokens.dart
│ │ └── platform/
│ │ └── platform_helper.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── product.dart
│ │ │ ├── cart_item.dart
│ │ │ ├── order.dart
│ │ │ └── user.dart
│ │ └── repositories/
│ │ ├── product_repository.dart
│ │ ├── auth_repository.dart
│ │ └── order_repository.dart
│ ├── data/
│ │ ├── repositories/
│ │ │ ├── product_repository_impl.dart
│ │ │ ├── auth_repository_impl.dart
│ │ │ └── order_repository_impl.dart
│ │ ├── datasources/
│ │ │ ├── firestore_datasource.dart
│ │ │ └── hive_datasource.dart
│ │ └── models/
│ │ ├── product_dto.dart
│ │ └── order_dto.dart
│ ├── application/
│ │ ├── notifiers/
│ │ │ ├── auth_notifier.dart
│ │ │ ├── product_notifier.dart
│ │ │ └── cart_notifier.dart
│ │ └── providers/
│ │ └── infrastructure_providers.dart
│ ├── presentation/
│ │ ├── screens/
│ │ │ ├── home_page.dart
│ │ │ ├── detail_page.dart
│ │ │ ├── cart_page.dart
│ │ │ ├── checkout_page.dart
│ │ │ └── login_page.dart
│ │ └── widgets/
│ │ ├── product_card.dart
│ │ ├── quantity_selector.dart
│ │ ├── cart_badge.dart
│ │ └── price_tag.dart
│ └── l10n/
│ ├── app_en.arb
│ ├── app_zh.arb
│ └── app_ja.arb
├── test/
│ ├── domain/
│ ├── application/
│ └── presentation/
├── integration_test/
├── android/
├── ios/
├── web/
├── windows/
├── macos/
├── pubspec.yaml
├── l10n.yaml
└── .github/workflows/
```text
```text
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q Clean Architectureの全レイヤーが必要ですか?
A 中小規模プロジェクトではApplicationレイヤーとDomainレイヤーを1つのApplicationレイヤーに統合可能。Dataレイヤーは常に必要(データソース抽象化)。
Q EntityとDTOの違いは何ですか?
A Entityはドメイン概念(ビジネスロジックで使用)、DTOはデータ転送オブジェクト(API/DB用)。Entityは外部に依存せず、DTOはJSONシリアライゼーションアノテーションを含む場合があります。
Q RepositoryインターフェースをDomainレイヤーに配置する意味はありますか?
A はい。これは依存関係逆転の原則:Domainがインターフェースを定義し、Dataレイヤーが実装。これによりDomainレイヤーはFirebase/Dioなどの外部ライブラリに依存しません。
Q プロジェクト設計フェーズはどのくらいかけるべきですか?
A 中小規模プロジェクト:1-2週間。大規模プロジェクト:2-4週間。時間の20%を設計に投資すれば、開発時間の50%を節約できます。
Q Firestore Schemaの最適な設計方法は?
A 1) 1:1または1:Nのリレーションでドキュメントを設計、2) 深いネストを避ける、3) 頻繁に読むデータは非正規化(名前を冗長保存)、4) 頻繁に書くデータは正規化。
Q アーキテクチャ設計が実際に守られるようにするには?
A 1) lintルールで依存方向を強制、2) Code Reviewでレイヤリング準拠を確認、3) Unitテストで各レイヤー境界をカバー。
📖 まとめ
- ユーザーストーリーが要件分析を推進、役割と優先度で整理
- 技術選定はチーム能力、プロジェクト規模、エコシステム成熟度のバランスが必要
- Clean Architecture 4レイヤー:Presentation → Application → Domain → Data
- Domainレイヤーは純粋Dartで外部依存なし、RepositoryインターフェースはDomainで定義
- デザイントークンが色/間隔/角丸/フォントを統一、ThemeExtensionで運用
📝 練習問題
- 基本 (⭐):ShopAppのユーザーストーリーを10個作成し、P0/P1/P2優先度で分類してください。
- 中級 (⭐⭐):Clean Architecture 4レイヤーのディレクトリ構造を設計し、Product/Cart/Orderエンティティと対応するRepositoryインターフェースを定義してください。
- チャレンジ (⭐⭐⭐):完全なプロジェクト設計ドキュメントを作成してください:ユーザーストーリー + 技術選定比較表 + Clean Architectureレイヤリング図 + Firestore Schema + ページ一覧 + デザイントークン。
(2) ▶ サンプル:ShopAppモジュール依存グラフ
DART
final Map<String, List<String>> moduleDeps = {
'core': [],
'cart': ['core'],
'checkout': ['core', 'cart'],
'profile': ['core'],
'catalog': ['core'],
};
出力:
TEXT
core: 0 direct dependencies
cart: depends on core
checkout: depends on core, cart
profile: depends on core
catalog: depends on core
(3) ▶ サンプル:ルートテーブル
DART
final Map<String, String> routes = {
'/': 'CatalogPage',
'/cart': 'CartPage',
'/checkout': 'CheckoutPage',
'/profile': 'ProfilePage',
};
出力:
TEXT
/ -> CatalogPage
/cart -> CartPage
/checkout -> CheckoutPage
/profile -> ProfilePage