Flutter: تطوير المشروع — تنفيذ ميزات ShopApp
التصميم هو المخطط، التطوير هو البناء — هذا الدرس يحول المخطط إلى تطبيق قابل للتشغيل.
📋 المتطلبات السابقة: يجب أن تكون قد أتقنت ما يلي أولًا
- الدرس 26: تصميم المشروع — تخطيط بنية ShopApp
1. ما ستتعلمه
- وحدة المنتجات: تصفح الفئات / تصفية البحث / تفاصيل المنتج / نظام المراجعات (Firestore فوري)
- وحدة السلة: CartNotifier بـ Riverpod + استمرارية محلية + إدارة الكمية/المواصفات
- وحدة الدفع: تكامل Stripe SDK / تسوية بعملات USD متعددة / آلة حالة الطلب
- وحدة المستخدم: تسجيل دخول/تسجيل Firebase Auth / ملف المستخدم / إدارة عناوين الشحن
- وحدة العمليات: تتبع أحداث Firebase Analytics / إشعارات ترويجية Cloud Messaging
2. قصة تكامل ميزات مفقود
(1) المشكلة: الميزات مكتملة لكنها غير متصلة
أكمل بوب جميع الوحدات بشكل مستقل، لكن أثناء التكامل اكتشف: بعد الإضافة للسلة، حالة "أُضيف للسلة" في صفحة التفاصيل لم تُحدَّث (setStates مستقلة)؛ مخزون المنتجات بيع زيادة (قراءة وكتابة Firestore بلا معاملات)؛ بعد نجاح الدفع، حالة الطلب لم تُحدَّث تلقائيًا. كل إصلاح يحتاج تغييرات في 3-4 ملفات.
(2) حل تنسيق الحالة العامة بـ Riverpod
Riverpod يتيح لجميع الوحدات مشاركة نفس مصدر الحالة: عندما تتغير حالة السلة، صفحة التفاصيل والشارة والإجمالي تتحدث تلقائيًا؛ معاملات Firestore تضمن عدم بيع المخزون زيادة.
(3) النتيجة: تكامل وحدات سلس
بعد ربط جميع الوحدات بـ Riverpod، التفاعلات عبر الوحدات تعمل تلقائيًا — لا مزامنة حالة يدوية بعد الآن.
3. تنفيذ وحدة المنتجات
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: Product Notifier (تحميل غير متزامن + بحث + تصفية بالفئة)
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() ?? [];
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: مراجعات Firestore الفورية
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),
)),
]),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
4. تنفيذ وحدة السلة
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: CartNotifier (نسخة كاملة + استمرارية)
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);
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
5. تنفيذ وحدة الدفع
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تكامل دفع Stripe
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;
}
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: آلة حالة الطلب
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;
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. تنفيذ وحدة المستخدم
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: Auth Notifier + Firestore User
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();
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
7. تنفيذ وحدة العمليات
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: تتبع أحداث Analytics
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);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: معالجة الإشعارات الترويجية FCM
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');
}
});
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
8. مثال كامل: تكامل وحدات ShopApp main.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!,
);
},
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
📖 ملخص
- وحدة المنتجات: Riverpod AsyncNotifier + مراجعات Firestore الفورية + تخزين Hive المؤقت دون اتصال
- وحدة السلة: CartNotifier يدير الحالة العامة + استمرارية محلية + اختيار المواصفات
- وحدة الدفع: دفع Stripe متعدد العملات + معاملات Firestore تمنع البيع الزائد + آلة حالة الطلب
- وحدة المستخدم: تسجيل دخول Firebase Auth متعدد الطرق + ملف مستخدم Firestore
- وحدة العمليات: تتبع أحداث Analytics + إشعارات FCM الفورية
📝 تمارين
- أساسي (الصعوبة ⭐): نفذ Product Notifier مع تحميل، تحديث، وتصفية بالفئة، مع Hive للتخزين المؤقت دون اتصال.
- متوسط (الصعوبة ⭐⭐): نفذ تدفق السلة + الدفع الكامل: CartNotifier + اختيار المواصفات + دفع محاكي + آلة حالة الطلب.
- متقدم (الصعوبة ⭐⭐⭐): نفذ جميع وحدات ShopApp الخمس: المنتجات (Firestore فوري) + السلة (استمرارية) + الدفع (Stripe) + المستخدم (Auth) + العمليات (Analytics + FCM)، متصلة عبر Riverpod.