Flutter: إدارة الحالة — Riverpod

الحالة هي دم التطبيق — إذا أُسيئت إدارتها، تُسبب "نزيفًا داخليًا": تناقض بيانات، عواصف إعادة بناء، تسرب ذاكرة.

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

1. ما ستتعلمه


2. قصة حقيقية عن كارثة حالة

(1) المشكلة: مأزق الحالة العامة مع setState

يستخدم بوب setState لإدارة حالة سلة التسوق في ShopApp. المشكلة: بيانات السلة مطلوبة في صفحات الرئيسية والتفاصيل والسلة، مما يتطلب استدعاءات عكسية لتمريرها عبر كل طبقة من شجرة الويدجت. بعد تغيير CartPage للكمية، لا يتحدث شارة AppBar في الرئيسية — لأن حالة الرئيسية لا تعرف ما غيّرته CartPage. الأسوأ، 10 صفحات تحتفظ بـ 5 نسخ من بيانات السلة، كلها غير متزامنة.

(2) حل Riverpod

يرفع Riverpod الحالة إلى مزوّدين عامين. أي صفحة يمكنها ref.watch نفس حالة السلة، والتغيير في مكان واحد يُحدّث تلقائيًا جميع المستمعين.

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ تبعية تطوير: flutter pub add --dev riverpod_generator build_runner

// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - CartItem: see CartNotifier example below

// Global cart state - any page can access
@riverpod
class CartNotifier extends _$CartNotifier {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    state = [...state, CartItem(product: product, quantity: 1)];
  }

  void removeItem(int productId) {
    state = state.where((item) => item.product.id != productId).toList();
  }
}

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

(3) الفائدة: تغيير واحد، مزامنة عامة

بعد استخدام Riverpod، تعيش حالة سلة بوب في مكان واحد — تغييرات أي صفحة تُحدّث تلقائيًا جميع المستمعين. جحيم الاستدعاءات يختفي، والبيانات تبقى متسقة.


3. المفاهيم الأساسية لـ Riverpod

100%
graph TD
    PS[ProviderScope] --> CN[CartNotifier]
    PS --> PN[ProductAsyncNotifier]
    PS --> UN[UserNotifier]
    CN --> |ref.watch| CV[CartView]
    PN --> |ref.watch| PV[ProductListView]
    UN --> |ref.watch| UV[UserProfileView]
    CN --> |totalItems| Badge[BottomNav Badge]
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

(1) أنواع المزوّدين

المزوّد نوع الحالة حالة الاستخدام
Provider قيمة ثابتة حقن التبعيات، الإعدادات
StateProvider قيمة متغيرة بسيطة عداد، تبديل
FutureProvider نتيجة Future بيانات غير متزامنة لمرة واحدة
StreamProvider نتيجة Stream تدفق بيانات فوري
NotifierProvider نسخة Notifier حالة معقدة + منطق أعمال
AsyncNotifierProvider AsyncNotifier تهيئة غير متزامنة + منطق أعمال

(2) مقارنة طرق ref

الطريقة الغرض تُشغّل إعادة البناء أين تُستخدم
ref.watch الاستماع لتغييرات الحالة نعم (عند تغير الحالة) داخل دالة build
ref.read قراءة لمرة واحدة لا الاستدعاءات / معالجات الأحداث
ref.listen استماع + تنفيذ آثار جانبية لا داخل build (تنقل/SnackBar)
⚠️ ملاحظة: لا تستخدم ref.watch خارج build، ولا تستخدم ref.read للاستماع للحالة داخل build.


4. المزوّدات المكتوبة يدويًا

▶ مثال

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

: StateProvider حالة بسيطة

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod

// Simple counter with StateProvider
final counterProvider = StateProvider<int>((ref) => 0);

// Theme mode provider
final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);

// Usage in widget
class CounterWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Column(
      children: [
        Text('Count: $count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).state++,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}
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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

: FutureProvider بيانات غير متزامنة

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod

// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - ProductTile: custom Widget for displaying a single product
// - productRepositoryProvider: see Lesson 14 ProductRepository

// Load products once
final productsProvider = FutureProvider<List<Product>>((ref) async {
  final repo = ref.read(productRepositoryProvider);
  return repo.getProducts();
});

// Usage
class ProductList extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productsProvider);
    return productsAsync.when(
      loading: () => const CircularProgressIndicator(),
      error: (err, _) => Text('Error: $err'),
      data: (products) => ListView.builder(
        itemCount: products.length,
        itemBuilder: (_, i) => ProductTile(product: products[i]),
      ),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

5. Notifier وAsyncNotifier

▶ مثال

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

: CartNotifier (منطق سلة تسوق كامل)

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod

// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model (simplified version below)
/*
class Product {
  final int id;
  final String name;
  final double price;
  const Product({required this.id, required this.name, required this.price});
}
*/

class CartItem {
  final Product product;
  final int quantity;
  const CartItem({required this.product, required this.quantity});
  double get total => product.price * quantity;
  CartItem copyWith({int? quantity}) => CartItem(product: product, quantity: quantity ?? this.quantity);
}

class CartNotifier extends Notifier<List<CartItem>> {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    final idx = state.indexWhere((i) => i.product.id == product.id);
    if (idx >= 0) {
      state = [...state]..[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
    } else {
      state = [...state, CartItem(product: product, quantity: 1)];
    }
  }

  void updateQuantity(int productId, int quantity) {
    if (quantity <= 0) {
      removeItem(productId);
      return;
    }
    state = [
      for (final item in state)
        if (item.product.id == productId) item.copyWith(quantity: quantity) else item,
    ];
  }

  void removeItem(int productId) {
    state = state.where((i) => i.product.id != productId).toList();
  }

  void clear() => state = [];
}

final cartProvider = NotifierProvider<CartNotifier, List<CartItem>>(CartNotifier.new);

// Derived state: total price
final cartTotalProvider = Provider<double>((ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0.0, (sum, item) => sum + item.total);
});

// Derived state: item count
final cartItemCountProvider = Provider<int>((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` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

▶ مثال

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

: AsyncNotifier (تهيئة منتجات غير متزامنة)

DART
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod

// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - productRepositoryProvider: see Lesson 14 ProductRepository

class ProductAsyncNotifier extends AsyncNotifier<List<Product>> {
  @override
  Future<List<Product>> build() async {
    final repo = ref.read(productRepositoryProvider);
    return repo.getProducts();
  }

  Future<void> loadMore() async {
    final current = state.valueOrNull ?? [];
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      final more = await ref.read(productRepositoryProvider).getProducts(page: (current.length ~/ 20) + 1);
      return [...current, ...more];
    });
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => ref.read(productRepositoryProvider).getProducts());
  }
}

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

6. توليد أكواد @riverpod

▶ مثال

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

: استخدام riverpod_generator

DART
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod riverpod_annotation
// ⚙️ تبعية تطوير: flutter pub add --dev riverpod_generator build_runner

// Custom class definition sources:
// - CartItem: see Lesson 5 CartNotifier example in this lesson
// - User/AuthService: see Lesson 14 Auth Notifier

part 'providers.g.dart';

// Generated notifier
@riverpod
class Cart extends _$Cart {
  @override
  List<CartItem> build() => [];

  void addItem(Product product) {
    final idx = state.indexWhere((i) => i.product.id == product.id);
    if (idx >= 0) {
      state[idx] = state[idx].copyWith(quantity: state[idx].quantity + 1);
      state = [...state]; // Trigger rebuild
    } else {
      state = [...state, CartItem(product: product, quantity: 1)];
    }
  }
}

// Generated provider (derived)
@riverpod
double cartTotal(CartTotalRef ref) {
  final items = ref.watch(cartProvider);
  return items.fold(0.0, (sum, item) => sum + item.total);
}

// Keep alive across widget lifecycle
@Riverpod(keepAlive: true)
class Auth extends _$Auth {
  @override
  AsyncValue<User?> build() => const AsyncData(null);

  Future<void> login(String email, String password) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => AuthService.login(email, password));
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

7. مثال كامل: تكامل Riverpod في ShopApp

DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

// ⚙️ تثبيت التبعية: flutter pub add flutter_riverpod go_router

// Custom class definition sources:
// - Product: see Lesson 11 json_serializable model
// - cartProvider/cartTotalProvider/cartItemCountProvider: see Lesson 5 in this lesson
// - themeModeProvider: see Lesson 4 StateProvider in this lesson

// main.dart
void main() {
  runApp(ProviderScope(child: ShopApp()));
}

class ShopApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);
    return MaterialApp.router(
      title: 'ShopApp',
      theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
      darkTheme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true, brightness: Brightness.dark),
      themeMode: themeMode,
      routerConfig: router,
    );
  }
}

// Cart badge in AppBar - auto-updates
class CartBadge extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(cartItemCountProvider);
    return Stack(
      alignment: Alignment.center,
      children: [
        const Icon(Icons.shopping_cart),
        if (count > 0)
          Positioned(right: 0, top: 0,
            child: CircleAvatar(radius: 8, backgroundColor: Colors.red,
              child: Text('$count', style: const TextStyle(fontSize: 9, color: Colors.white)))),
      ],
    );
  }
}

// Add to cart action
class AddToCartButton extends ConsumerWidget {
  final Product product;
  const AddToCartButton({super.key, required this.product});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return FilledButton.icon(
      onPressed: () {
        ref.read(cartProvider.notifier).addItem(product);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('${product.name} added to cart')),
        );
      },
      icon: const Icon(Icons.shopping_cart),
      label: const Text('Add to Cart'),
    );
  }
}

// Cart page with Riverpod
class CartPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final items = ref.watch(cartProvider);
    final total = ref.watch(cartTotalProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Cart (${items.length})')),
      body: items.isEmpty
          ? const Center(child: Text('Cart is empty'))
          : Column(children: [
              Expanded(child: ListView.separated(
                itemCount: items.length,
                separatorBuilder: (_, __) => const Divider(),
                itemBuilder: (_, i) {
                  final item = items[i];
                  return ListTile(
                    title: Text(item.product.name),
                    subtitle: Text('\$${item.total.toStringAsFixed(2)}'),
                    trailing: Row(mainAxisSize: MainAxisSize.min, children: [
                      IconButton(icon: const Icon(Icons.remove), onPressed: () =>
                        ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity - 1)),
                      Text('${item.quantity}'),
                      IconButton(icon: const Icon(Icons.add), onPressed: () =>
                        ref.read(cartProvider.notifier).updateQuantity(item.product.id, item.quantity + 1)),
                    ]),
                  );
                },
              )),
              Padding(padding: const EdgeInsets.all(16),
                child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
                  Text('\$${total.toStringAsFixed(2)}', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
                  FilledButton(onPressed: () => context.push('/checkout'), child: const Text('Checkout')),
                ])),
            ]),
    );
  }
}
TEXT
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.

❓ أسئلة شائعة

س ما الفرق بين Riverpod وحزمة Provider؟
ج Riverpod هو تطوير لـ Provider، يحل مشاكل حقن التبعيات غير الآمن والاختبار في Provider. استخدم Riverpod للمشاريع الجديدة.
س كيف أختار بين ref.watch وref.read؟
ج استخدم ref.watch داخل build (عندما تحتاج UI للاستجابة لتغييرات الحالة)؛ استخدم ref.read في الاستدعاءات/معالجات الأحداث (لا حاجة لإعادة البناء).
س هل state = [...state] ضروري؟
ج نعم. Riverpod يستخدم فحصًا متطابقًا لكشف تغييرات الحالة. تعديل محتوى List لا يُنشئ كائنًا جديدًا، لذا Riverpod لن يُبلغ بالتحديثات.
س كيف أتعامل مع AsyncLoading/AsyncData/AsyncError في AsyncNotifier؟
ج استخدم نمط المطابقة .when(loading:, error:, data:) لعرض مؤشرات التحميل وصفحات الخطأ ومحتوى البيانات.
س ما التبعيات التي يحتاجها توضيح @riverpod؟
ج أضف riverpod_annotation + riverpod_generator + build_runner إلى pubspec.yaml، ثم شغّل dart run build_runner watch.
س ما الفرق بين keepAlive وautoDispose؟
ج autoDispose (الافتراضي) يُدمر المزوّد عندما لا يستمع أحد؛ keepAlive لا يُدمره أبدًا. استخدم keepAlive للحالة العامة (مثل Auth).

📖 ملخص


📝 تمارين

  1. أساسي (⭐): استخدم StateProvider لتنفيذ تبديل الوضع الداكن، واعرض الوضع الحالي في AppBar باستخدام ConsumerWidget.
  2. متوسط (⭐⭐): استخدم Notifier لتنفيذ CartNotifier مع إضافة/إزالة/تحديث وحساب السعر الإجمالي، مع عرض حالة السلة على صفحتين مختلفتين.
  3. متقدم (⭐⭐⭐): استخدم توليد أكواد @riverpod لتنفيذ مزوّدي Auth + Product + Cart: بعد تسجيل الدخول، تحميل المنتجات تلقائيًا؛ بعد الإضافة للسلة، تحديث الشارة تلقائيًا.

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

Web-Tutorial.com

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

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

100%