Flutter: プロジェクト設計 — ShopAppアーキテクチャ計画

建てる前に青写真を描く — アーキテクチャ設計がコードの「耐力壁」の位置を決めます。

📋 前提条件: 以下を先にマスターしている必要があります

1. このレッスンで学ぶこと


2. アーキテクチャなしプロジェクトのストーリー

(1) 悩み:曖昧な要件 + 混乱するアーキテクチャ

BobがShopAppプロジェクトを受けた時、唯一の要件は「越境ECアプリを作る」だけ。すぐにコーディングを始め、3ヶ月後に判明:ユーザーストーリーが不明確(商人管理機能が欠落)、データモデルが頻繁に変更(Orderは3回書き直し)、アーキテクチャが密結合(決済の変更が商品一覧に影響)。プロジェクトは2ヶ月遅延。

(2) 先に設計、後にコーディング

プロジェクト設計フェーズで要件、アーキテクチャ、データモデル、UI計画を事前に明確にすれば — コーディングは「青写真に従って建てる」ことになります。

(3) 成果:コーディング時間半減 + 変更コスト80%削減

2週間の設計に費やした後、Bobは4週間でコーディングを完了(以前の3+2ヶ月から)、要件変更は該当レイヤーの修正のみで他モジュールに影響なし。


3. 要件分析

(1) ユーザーストーリー

100%
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レイヤリング

100%
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) 依存ルール


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テストで各レイヤー境界をカバー。

📖 まとめ


📝 練習問題

  1. 基本 (⭐):ShopAppのユーザーストーリーを10個作成し、P0/P1/P2優先度で分類してください。
  2. 中級 (⭐⭐):Clean Architecture 4レイヤーのディレクトリ構造を設計し、Product/Cart/Orderエンティティと対応するRepositoryインターフェースを定義してください。
  3. チャレンジ (⭐⭐⭐):完全なプロジェクト設計ドキュメントを作成してください:ユーザーストーリー + 技術選定比較表 + 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
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%