Flutter: تطوير المشروع — تنفيذ ميزات ShopApp

التصميم هو المخطط، التطوير هو البناء — هذا الدرس يحول المخطط إلى تطبيق قابل للتشغيل.

📋 المتطلبات السابقة: يجب أن تكون قد أتقنت ما يلي أولًا

1. ما ستتعلمه


2. قصة تكامل ميزات مفقود

(1) المشكلة: الميزات مكتملة لكنها غير متصلة

أكمل بوب جميع الوحدات بشكل مستقل، لكن أثناء التكامل اكتشف: بعد الإضافة للسلة، حالة "أُضيف للسلة" في صفحة التفاصيل لم تُحدَّث (setStates مستقلة)؛ مخزون المنتجات بيع زيادة (قراءة وكتابة Firestore بلا معاملات)؛ بعد نجاح الدفع، حالة الطلب لم تُحدَّث تلقائيًا. كل إصلاح يحتاج تغييرات في 3-4 ملفات.

(2) حل تنسيق الحالة العامة بـ Riverpod

Riverpod يتيح لجميع الوحدات مشاركة نفس مصدر الحالة: عندما تتغير حالة السلة، صفحة التفاصيل والشارة والإجمالي تتحدث تلقائيًا؛ معاملات Firestore تضمن عدم بيع المخزون زيادة.

(3) النتيجة: تكامل وحدات سلس

بعد ربط جميع الوحدات بـ Riverpod، التفاعلات عبر الوحدات تعمل تلقائيًا — لا مزامنة حالة يدوية بعد الآن.


3. تنفيذ وحدة المنتجات

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: مراجعات 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

4. تنفيذ وحدة السلة

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

5. تنفيذ وحدة الدفع

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تكامل دفع 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: آلة حالة الطلب

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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

6. تنفيذ وحدة المستخدم

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. تنفيذ وحدة العمليات

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: تتبع أحداث 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: معالجة الإشعارات الترويجية 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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س هل معاملات Firestore تضمن عدم بيع المخزون زيادة؟
ج نعم. معاملات Firestore تقفل المستندات بين القراءة والكتابة، مما يضمن أن الكتابة المتزامنة لن تسبب بيعًا زائدًا.
س كيف أختار بين Stripe والدفع الأصلي؟
ج للدفع عبر الحدود/متعدد العملات، اختر Stripe (عالمي)؛ للأسواق الفردية، اختر Apple Pay/Google Pay (تجربة أفضل). يمكنك دعم كليهما.
س هل يجب تخزين بيانات السلة محليًا أو في Firestore؟
ج للمستخدمين غير المصادق عليهم، خزّن محليًا (Hive)؛ بعد تسجيل الدخول،زامن مع Firestore. هكذا يحصل المستخدمون غير المصادق عليهم أيضًا على تجربة سلة.
س هل مستمعات Firestore الفورية تستهلك حصة قراءة كبيرة؟
ج كل اتصال مستخدم يُحسب كقراءة واحدة + قراءات إضافية عند تغيير المستندات. قيّد نطاق المستمع (فلاتر where) والكمية (limit 50) للتحكم في الاستهلاك.
س كيف تتواصل Notifiers المتعددة مع بعضها؟
ج عبر ref.read للوصول لمزودات أخرى. مثلًا، Cart Notifier يستدعي Auth Notifier للتحقق من حالة تسجيل الدخول في clear(). تجنب التبعيات الدائرية.
س هل حجم كبير من أحداث Analytics يؤثر على الأداء؟
ج Firebase Analytics يرفع دفعات على العميل، لذا لا يؤثر على أداء واجهة المستخدم. لكن انتبه للحصة المجانية (1 مليون حدث شهريًا).

📖 ملخص


📝 تمارين

  1. أساسي (الصعوبة ⭐): نفذ Product Notifier مع تحميل، تحديث، وتصفية بالفئة، مع Hive للتخزين المؤقت دون اتصال.
  2. متوسط (الصعوبة ⭐⭐): نفذ تدفق السلة + الدفع الكامل: CartNotifier + اختيار المواصفات + دفع محاكي + آلة حالة الطلب.
  3. متقدم (الصعوبة ⭐⭐⭐): نفذ جميع وحدات ShopApp الخمس: المنتجات (Firestore فوري) + السلة (استمرارية) + الدفع (Stripe) + المستخدم (Auth) + العمليات (Analytics + FCM)، متصلة عبر Riverpod.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%