Flutter: プロジェクト開発 — ShopApp機能実装
設計は青写真、開発は建設 — このレッスンで青写真を実動アプリに変えます。
📋 前提条件: 以下を先にマスターしている必要があります
- レッスン26:プロジェクト設計 — ShopAppアーキテクチャ計画
1. このレッスンで学ぶこと
- 商品モジュール:カテゴリ閲覧 / 検索フィルタリング / 商品詳細 / レビューシステム(Firestoreリアルタイム)
- カートモジュール:Riverpod CartNotifier + ローカル永続化 + 数量/仕様管理
- 決済モジュール:Stripe SDK連携 / USDマルチ通貨決済 / 注文状態マシン
- ユーザーモジュール:Firebase Authログイン/登録 / ユーザープロフィール / 配送先住所管理
- 運用モジュール:Firebase Analyticsイベントトラッキング / Cloud Messagingプロモーションプッシュ
2. 機能統合欠落のストーリー
(1) 悩み:機能は完成したが繋がっていない
Bobは全モジュールを個別に完成させたが、統合時に判明:カート追加後、詳細ページの「カートに追加済み」状態が更新されない(2つの独立したsetState)、商品在庫が過剰販売(Firestoreの読み書きにトランザクションなし)、決済成功後に注文ステータスが自動更新されない。各修正で3-4ファイルに変更が必要。
(2) Riverpodグローバル状態調整ソリューション
Riverpodは全モジュールが同じ状態ソースを共有:カート状態が変化すると、詳細ページ、バッジ、合計が自動更新、Firestoreトランザクションが在庫の過剰販売を防止。
(3) 成果:シームレスなモジュール統合
Riverpodで全モジュールを接続後、クロスモジュールインタラクションが自動動作 — 手動状態同期が不要に。
3. 商品モジュール実装
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Product Notifier(非同期読み込み + 検索 + カテゴリフィルタリング)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Dependencies: flutter_riverpod: ^2.0.0, riverpod_annotation: ^2.0.0
// Product / ProductRepository / productCacheProvider / productRepositoryProvider
// selectedCategoryProvider come from the ShopApp project, full definitions in Lessons 26/27
@riverpod
class Products extends _$Products {
@override
Future<List<Product>> build() async {
return _loadFromCacheOrApi();
}
Future<List<Product>> _loadFromCacheOrApi() async {
final cache = ref.read(productCacheProvider);
final cached = await cache.getProducts();
if (cached.isNotEmpty) return cached;
return _fetchFromApi();
}
Future<List<Product>> _fetchFromApi() async {
final repo = ref.read(productRepositoryProvider);
final products = await repo.getProducts();
await ref.read(productCacheProvider).saveProducts(products);
return products;
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() => _fetchFromApi());
}
Future<void> search(String query) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repo = ref.read(productRepositoryProvider);
return repo.searchProducts(query);
});
}
Future<void> filterByCategory(String category) async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final repo = ref.read(productRepositoryProvider);
return repo.getProducts(category: category);
});
}
}
// Derived: filtered products
@riverpod
List<Product> filteredProducts(FilteredProductsRef ref) {
final productsAsync = ref.watch(productsProvider);
final category = ref.watch(selectedCategoryProvider);
return productsAsync.valueOrNull?.where((p) =>
category == 'All' || p.category == category).toList() ?? [];
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Firestoreリアルタイムレビュー
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
// Dependencies: flutter_riverpod: ^2.0.0, cloud_firestore: ^4.0.0
// Review comes from the ShopApp project, full definition below
// Simplified Review entity
class Review {
final String id;
final String productId;
final String userName;
final int rating;
final String comment;
final DateTime createdAt;
const Review({
required this.id, required this.productId, required this.userName,
required this.rating, required this.comment, required this.createdAt,
});
factory Review.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return Review(
id: doc.id,
productId: data['productId'] ?? '',
userName: data['userName'] ?? 'Anonymous',
rating: data['rating'] ?? 0,
comment: data['comment'] ?? '',
createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
);
}
}
@riverpod
Stream<List<Review>> productReviews(ProductReviewsRef ref, String productId) {
return FirebaseFirestore.instance
.collection('reviews')
.where('productId', isEqualTo: productId)
.orderBy('createdAt', descending: true)
.limit(50)
.snapshots()
.map((s) => s.docs.map((d) => Review.fromFirestore(d)).toList());
}
// Widget
class ReviewSection extends ConsumerWidget {
final String productId;
const ReviewSection({super.key, required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final reviewsAsync = ref.watch(productReviewsProvider(productId));
return reviewsAsync.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (reviews) => Column(children: [
Text('Reviews (${reviews.length})', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...reviews.map((r) => ListTile(
leading: CircleAvatar(child: Text(r.userName[0])),
title: Row(children: [
Text(r.userName),
const SizedBox(width: 8),
...List.generate(5, (i) => Icon(Icons.star, size: 14,
color: i < r.rating ? Colors.amber : Colors.grey[300])),
]),
subtitle: Text(r.comment),
)),
]),
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
4. カートモジュール実装
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:CartNotifier(フルバージョン + 永続化)
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Dependencies: flutter_riverpod: ^2.0.0, riverpod_annotation: ^2.0.0
// Product / CartItem / cartStorageProvider / ShopAnalytics come from the ShopApp project
// CartItem / Address full definitions in Lesson 26
@riverpod
class Cart extends _$Cart {
@override
List<CartItem> build() {
_loadFromStorage();
return [];
}
Future<void> _loadFromStorage() async {
final storage = ref.read(cartStorageProvider);
final items = await storage.loadCart();
if (items.isNotEmpty) state = items;
}
Future<void> _persist() async {
await ref.read(cartStorageProvider).saveCart(state);
}
void addItem(Product product, {String? size, String? color}) {
final idx = state.indexWhere((i) =>
i.product.id == product.id && i.selectedSize == size && i.selectedColor == color);
if (idx >= 0) {
state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
} else {
state = [...state, CartItem(product: product, quantity: 1, selectedSize: size, selectedColor: color)];
}
_persist();
ShopAnalytics.logAddToCart(product, 1);
}
void updateQuantity(int index, int quantity) {
if (quantity <= 0) { removeAt(index); return; }
state = [for (int i = 0; i < state.length; i++) i == index ? state[i].copyWith(quantity: quantity) : state[i]];
_persist();
}
void removeAt(int index) {
state = [...state]..removeAt(index);
_persist();
}
void clear() { state = []; _persist(); }
}
@riverpod
double cartTotal(CartTotalRef ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.lineTotal);
}
@riverpod
int cartItemCount(CartItemCountRef ref) {
final items = ref.watch(cartProvider);
return items.fold(0, (sum, item) => sum + item.quantity);
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
5. 決済モジュール実装
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Stripe決済連携
DART
import 'package:dio/dio.dart';
import 'package:flutter_stripe/flutter_stripe.dart';
// Dependencies: dio: ^5.0.0, flutter_stripe: ^10.0.0 (add to dependencies)
// Stripe backend must be deployed separately, see https://stripe.com/docs
class PaymentService {
static Future<bool> processPayment({
required double amount,
required String currency,
required String paymentMethodId,
}) async {
try {
// Create payment intent via backend
final response = await Dio().post('https://api.shopapp.com/v1/payments/create-intent', data: {
'amount': (amount * 100).toInt(), // Stripe uses cents
'currency': currency.toLowerCase(),
'payment_method_id': paymentMethodId,
});
final clientSecret = response.data['client_secret'];
// Confirm payment
final paymentIntent = await Stripe.instance.confirmPayment(
paymentIntentClientSecret: clientSecret,
data: PaymentMethodParams.cardFromMethodId(
paymentMethodData: PaymentMethodDataCardFromMethod(id: paymentMethodId),
),
);
return paymentIntent.status == PaymentIntentsStatus.Succeeded;
} catch (e) {
return false;
}
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:注文状態マシン
DART
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
// Dependency: cloud_firestore: ^4.0.0 (add to dependencies)
// CartItem / Address / Product full definitions in Lesson 26
enum OrderStatus { pending, confirmed, processing, shipped, delivered, cancelled }
extension OrderStatusX on OrderStatus {
bool get canCancel => this == OrderStatus.pending || this == OrderStatus.confirmed;
bool get canTrack => this == OrderStatus.shipped;
String get label => switch (this) {
OrderStatus.pending => 'Pending',
OrderStatus.confirmed => 'Confirmed',
OrderStatus.processing => 'Processing',
OrderStatus.shipped => 'Shipped',
OrderStatus.delivered => 'Delivered',
OrderStatus.cancelled => 'Cancelled',
};
Color get color => switch (this) {
OrderStatus.pending => Colors.orange,
OrderStatus.confirmed => Colors.blue,
OrderStatus.processing => Colors.indigo,
OrderStatus.shipped => Colors.teal,
OrderStatus.delivered => Colors.green,
OrderStatus.cancelled => Colors.red,
};
}
// Firestore order creation with transaction
Future<String> createOrder(List<CartItem> items, Address address, String userId) async {
final db = FirebaseFirestore.instance;
final orderId = 'ORD-${DateTime.now().millisecondsSinceEpoch}';
await db.runTransaction((txn) async {
// Check stock for all items
for (final item in items) {
final productDoc = await txn.get(db.collection('products').doc(item.product.id.toString()));
final stock = productDoc.data()?['stock'] ?? 0;
if (stock < item.quantity) throw Exception('Insufficient stock for ${item.product.name}');
txn.update(db.collection('products').doc(item.product.id.toString()), {
'stock': stock - item.quantity,
});
}
// Create order
txn.set(db.collection('orders').doc(orderId), {
'userId': userId,
'items': items.map((i) => {'productId': i.product.id, 'quantity': i.quantity,
'price': i.product.price, 'size': i.selectedSize}).toList(),
'total': items.fold(0.0, (s, i) => s + i.lineTotal),
'currency': 'USD',
'status': 'pending',
'shippingAddress': address.toJson(),
'createdAt': FieldValue.serverTimestamp(),
});
});
return orderId;
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
6. ユーザーモジュール実装
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Auth Notifier + Firestore User
DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:google_sign_in/google_sign_in.dart';
// Dependencies: firebase_auth: ^4.0.0, cloud_firestore: ^4.0.0, google_sign_in: ^6.0.0
// User / cartProvider come from the ShopApp project
// User full definition in Lesson 26 domain/entities/user.dart
// Simplified User (full definition in project domain/entities/user.dart)
class User {
final String uid;
final String email;
final String name;
const User({required this.uid, required this.email, required this.name});
factory User.fromFirestore(DocumentSnapshot doc) {
final data = doc.data() as Map<String, dynamic>;
return User(
uid: doc.id,
email: data['email'] ?? '',
name: data['name'] ?? 'User',
);
}
}
@riverpod
class Auth extends _$Auth {
@override
AsyncValue<User?> build() {
FirebaseAuth.instance.authStateChanges().listen((firebaseUser) {
if (firebaseUser != null) {
_loadUserProfile(firebaseUser.uid);
} else {
state = const AsyncData(null);
}
});
return AsyncData(FirebaseAuth.instance.currentUser != null
? User(uid: FirebaseAuth.instance.currentUser!.uid,
email: FirebaseAuth.instance.currentUser!.email ?? '',
name: FirebaseAuth.instance.currentUser!.displayName ?? 'User')
: null);
}
Future<void> _loadUserProfile(String uid) async {
final doc = await FirebaseFirestore.instance.collection('users').doc(uid).get();
if (doc.exists) {
state = AsyncData(User.fromFirestore(doc));
}
}
Future<void> loginWithGoogle() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final googleUser = await GoogleSignIn().signIn();
final googleAuth = await googleUser?.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken, idToken: googleAuth?.idToken);
final userCred = await FirebaseAuth.instance.signInWithCredential(credential);
await _ensureUserProfile(userCred.user!);
return _mapToUser(userCred.user!);
});
}
Future<void> _ensureUserProfile(User firebaseUser) async {
final doc = FirebaseFirestore.instance.collection('users').doc(firebaseUser.uid);
final snapshot = await doc.get();
if (!snapshot.exists) {
await doc.set({
'email': firebaseUser.email,
'name': firebaseUser.displayName ?? 'User',
'preferences': {'theme': 'system', 'currency': 'USD', 'locale': 'en'},
'createdAt': FieldValue.serverTimestamp(),
});
}
}
Future<void> logout() async {
await FirebaseAuth.instance.signOut();
await GoogleSignIn().signOut();
ref.read(cartProvider.notifier).clear();
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
7. 運用モジュール実装
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:Analyticsイベントトラッキング
DART
import 'package:firebase_analytics/firebase_analytics.dart';
// Dependency: firebase_analytics: ^10.0.0 (add to dependencies)
// Product comes from the ShopApp project, full definition in Lesson 26
class ShopAnalytics {
static final _analytics = FirebaseAnalytics.instance;
static Future<void> logViewProduct(Product p) =>
_analytics.logEvent(name: 'view_product', parameters: {
'product_id': p.id, 'product_name': p.name,
'price': p.price, 'category': p.category, 'currency': p.currency,
});
static Future<void> logAddToCart(Product p, int qty) =>
_analytics.logEvent(name: 'add_to_cart', parameters: {
'product_id': p.id, 'quantity': qty, 'price': p.price, 'currency': 'USD',
});
static Future<void> logBeginCheckout(double total, int items) =>
_analytics.logEvent(name: 'begin_checkout', parameters: {
'value': total, 'currency': 'USD', 'item_count': items,
});
static Future<void> logPurchase(String orderId, double total) =>
_analytics.logEvent(name: 'purchase', parameters: {
'order_id': orderId, 'value': total, 'currency': 'USD',
});
static Future<void> logSearch(String query) =>
_analytics.logEvent(name: 'search', parameters: {'search_term': query});
static Future<void> setUser(String userId, String name) {
_analytics.setUserId(id: userId);
_analytics.setUserProperty(name: 'user_name', value: name);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
:FCMプロモーションプッシュ処理
DART
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:go_router/go_router.dart';
// Dependencies: firebase_messaging: ^14.0.0, go_router: ^13.0.0 (add to dependencies)
// router comes from the ShopApp project app_router.dart
class PushNotificationService {
static Future<void> init() async {
await FirebaseMessaging.instance.requestPermission();
final token = await FirebaseMessaging.instance.getToken();
// Send token to server
FirebaseMessaging.onMessage.listen((message) {
final title = message.notification?.title ?? 'ShopApp';
final body = message.notification?.body ?? '';
// Show local notification or in-app banner
});
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final productId = message.data['product_id'];
if (productId != null) {
// Navigate to product detail
router.push('/product/$productId');
}
});
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
8. 完全な例:ShopAppモジュール統合 main.dart
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Dependencies: flutter_riverpod: ^2.0.0, firebase_core: ^2.0.0,
// firebase_analytics: ^10.0.0, shared_preferences: ^2.0.0
// DefaultFirebaseOptions / initHive / PushNotificationService /
// ShopApp / ShopAppTheme / appRouter / AppLocalizations /
// themeModeNotifierProvider / localeNotifierProvider / sharedPreferencesProvider
// all come from the ShopApp project, see individual module files
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Core initialization
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await initHive();
final prefs = await SharedPreferences.getInstance();
// Analytics
await FirebaseAnalytics.instance.logAppOpen();
// Push notifications
await PushNotificationService.init();
runApp(ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)],
child: const ShopApp(),
));
}
class ShopApp extends ConsumerWidget {
const ShopApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final themeMode = ref.watch(themeModeNotifierProvider).flutterThemeMode;
final locale = ref.watch(localeNotifierProvider);
return MaterialApp.router(
title: 'ShopApp',
debugShowCheckedModeBanner: false,
theme: ShopAppTheme.light(),
darkTheme: ShopAppTheme.dark(),
themeMode: themeMode,
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
routerConfig: appRouter,
builder: (context, child) {
final mq = MediaQuery.of(context);
return MediaQuery(
data: mq.copyWith(textScaleFactor: mq.textScaleFactor.clamp(0.8, 1.5)),
child: child!,
);
},
);
}
}
TEXT
> 出力: ローカルのFlutter SDKで実行してください(Flutter 3.x / Dart 3.x)。PistonサーバーにはFlutterがインストールされていません — 手元のマシンで`flutter run`して動作確認してください。実際のUI/状態はプラットフォームにより多少異なる場合があります。
❓ よくある質問
Q Firestoreトランザクションは過剰販売を防止できますか?
A はい。Firestoreトランザクションは読み取りと書き込み間でドキュメントをロックし、並行書き込みによる過剰販売を防止します。
Q Stripeとネイティブ決済のどちらを選ぶべきですか?
A 越境/マルチ通貨ならStripe(グローバル対応)、単一市場ならApple Pay/Google Pay(体験が良い)。両方サポートも可能。
Q カートデータはローカルとFirestoreのどちらに保存すべきですか?
A 未認証ユーザーはローカル(Hive)に保存、ログイン後にFirestoreに同期。これで未認証ユーザーもカート体験を得られます。
Q Firestoreリアルタイムリスナーは読み取りクオータを大量に消費しますか?
A 各リスナー接続は1回の読み取り + ドキュメント変更時の追加読み取りとしてカウント。リスナーのスコープ(whereフィルター)と数量(limit 50)を制限して消費を制御してください。
Q 複数のNotifier間はどう通信しますか?
A ref.readで他のProviderにアクセス。例えばCart Notifierがclear()でAuth Notifierのログイン状態を確認。循環依存は避けてください。
Q 大量のAnalyticsイベントはパフォーマンスに影響しますか?
A Firebase Analyticsはクライアント側でバッチアップロードするため、UIパフォーマンスには影響しません。ただし無料枠(月100万イベント)に注意してください。
📖 まとめ
- 商品モジュール:Riverpod AsyncNotifier + Firestoreリアルタイムレビュー + Hiveオフラインキャッシュ
- カートモジュール:CartNotifierがグローバル状態を管理 + ローカル永続化 + 仕様選択
- 決済モジュール:Stripeマルチ通貨決済 + Firestoreトランザクションで過剰販売防止 + 注文状態マシン
- ユーザーモジュール:Firebase Authマルチ方式ログイン + Firestoreユーザープロフィール
- 運用モジュール:Analyticsイベントトラッキング + FCMプッシュ通知
📝 練習問題
- 基本 (⭐):読み込み、リフレッシュ、カテゴリフィルタリングを持つProduct Notifierを実装し、Hiveでオフラインキャッシュを実装してください。
- 中級 (⭐⭐):完全なカート + 決済フローを実装してください:CartNotifier + 仕様選択 + 模擬決済 + 注文状態マシン。
- チャレンジ (⭐⭐⭐):ShopAppの5モジュールをすべて実装してください:商品(Firestoreリアルタイム)+ カート(永続化)+ 決済(Stripe)+ ユーザー(Auth)+ 運用(Analytics + FCM)、Riverpodで接続。