إدارة الحالة المتقدمة (Bloc و Riverpod)

المُخبِرون في Riverpod: StateNotifier و NotifierProvider

16 دقيقة الدرس 8 من 14

المُخبِرون في Riverpod: StateNotifier و NotifierProvider

مع تزايد تعقيد الميزات التي تديرها Riverpod، يصبح StateProvider البسيط غير كافٍ — فهو يكشف الحالة القابلة للتغيير مباشرةً ويخلط منطق الأعمال مع واجهة المستخدم. يحلّ StateNotifier وخلفه الحديث Notifier هذه المشكلة بتغليف كلٍّ من الحالة والعمليات التي قد تغيّرها داخل فئة مخصصة، بينما لا تتلقى الودجات سوى لقطة غير قابلة للتغيير من تلك الحالة.

ملاحظة معمارية: فكّر في المُخبِر (Notifier) كـ ViewModel أو مخزن صغير ومركّز. فهو يمتلك الحالة، ويكشف أساليب تصف عمليات الأعمال، وتستهلك واجهة المستخدم الحالة للقراءة فقط من خلال NotifierProvider (أو StateNotifierProvider). لا تُغيّر الودجات الحالة مباشرةً أبدًا.

StateNotifier — الواجهة البرمجية الكلاسيكية

StateNotifier<S> من حزمة state_notifier (يُعاد تصديرها من Riverpod) هو فئة:

  • تحتفظ بحالة واحدة غير قابلة للتغيير من النوع S.
  • تكشف أساليب لإنتاج حالة جديدة (تستبدل state، ولا تُغيّرها مباشرةً).
  • تُخطر المستمعين تلقائيًا عند إعادة تعيين state.
  • تُسجَّل عبر StateNotifierProvider لتتمكن أي ودجت من مراقبتها.

StateNotifier — مثال سلة التسوق

import 'package:flutter_riverpod/flutter_riverpod.dart';

// 1. تعريف فئة حالة غير قابلة للتغيير
class CartState {
  final List<String> items;
  final bool isLoading;

  const CartState({this.items = const [], this.isLoading = false});

  // نمط copyWith يحافظ على أمان التعديلات
  CartState copyWith({List<String>? items, bool? isLoading}) {
    return CartState(
      items: items ?? this.items,
      isLoading: isLoading ?? this.isLoading,
    );
  }
}

// 2. توسيع StateNotifier بمنطق الأعمال
class CartNotifier extends StateNotifier<CartState> {
  CartNotifier() : super(const CartState());

  void addItem(String product) {
    // أنشئ دائمًا حالة جديدة — لا تُعدّل القائمة الموجودة مباشرةً
    state = state.copyWith(items: [...state.items, product]);
  }

  void removeItem(String product) {
    state = state.copyWith(
      items: state.items.where((i) => i != product).toList(),
    );
  }

  Future<void> checkout() async {
    state = state.copyWith(isLoading: true);
    await Future.delayed(const Duration(seconds: 2)); // محاكاة الشبكة
    state = const CartState(); // إعادة تعيين بعد الدفع
  }
}

// 3. كشف المُخبِر من خلال مزوّد
final cartProvider = StateNotifierProvider<CartNotifier, CartState>(
  (ref) => CartNotifier(),
);

استهلاك StateNotifierProvider في واجهة المستخدم

تستخدم الودجات ref.watch للاشتراك في الحالة وref.read (داخل ردود النداء) لاستدعاء أساليب المُخبِر. يُصل إلى نسخة المُخبِر عبر المُعدِّل .notifier.

ودجت تقرأ StateNotifier وتتحكم فيه

class CartScreen extends ConsumerWidget {
  const CartScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // watch() يشترك — يُعيد البناء عند تغيير CartState
    final cart = ref.watch(cartProvider);

    return Scaffold(
      appBar: AppBar(title: Text('السلة (${cart.items.length})')),
      body: cart.isLoading
          ? const Center(child: CircularProgressIndicator())
          : ListView(
              children: cart.items
                  .map((item) => ListTile(
                        title: Text(item),
                        trailing: IconButton(
                          icon: const Icon(Icons.remove_circle),
                          onPressed: () => ref
                              .read(cartProvider.notifier)
                              .removeItem(item),
                        ),
                      ))
                  .toList(),
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =>
            ref.read(cartProvider.notifier).addItem('منتج جديد'),
        child: const Icon(Icons.add),
      ),
    );
  }
}
نصيحة: استخدم ref.watch(provider) داخل build للحصول على حالة تفاعلية. استخدم ref.read(provider.notifier) داخل ردود النداء ومعالجات الأحداث — القراءة (لا المراقبة) أثناء حدث تتجنب إعادات البناء العرضية التي قد تنجم عن القراءة نفسها.

الواجهة البرمجية الحديثة Notifier (Riverpod 2.x)

قدّمت Riverpod 2 كلًّا من Notifier<S> وNotifierProvider كبديل موصى به لـ StateNotifier. الفوارق الرئيسية:

  • تمتد فئة المُخبِر من Notifier<S> بدلًا من StateNotifier<S>.
  • تُعاد الحالة الأولية من الأسلوب المطلوب build() — مما يُسهّل عمل توليد الكود (riverpod_generator).
  • للمُخبِر وصول مباشر لـ ref عبر this.ref، ليتمكن من قراءة مزوّدين آخرين دون حقن بالمُنشئ.
  • يُسجَّل بـ NotifierProvider<NotifierClass, StateType>.

نفس سلة التسوق — مُعاد كتابتها بواجهة Notifier

import 'package:flutter_riverpod/flutter_riverpod.dart';

// نفس CartState غير القابل للتغيير كما سبق (أعد استخدام نمط copyWith)

class CartNotifier2 extends Notifier<CartState> {
  @override
  CartState build() {
    // build() يحلّ محل استدعاء super() في المُنشئ
    return const CartState();
  }

  void addItem(String product) {
    state = state.copyWith(items: [...state.items, product]);
  }

  void removeItem(String product) {
    state = state.copyWith(
      items: state.items.where((i) => i != product).toList(),
    );
  }

  Future<void> checkout() async {
    state = state.copyWith(isLoading: true);
    // الوصول لمزوّدين آخرين عبر ref دون تمريرهم بالمُنشئ
    // final user = ref.read(currentUserProvider);
    await Future.delayed(const Duration(seconds: 2));
    state = const CartState();
  }
}

// NotifierProvider — الأسلوب الجديد
final cartProvider2 = NotifierProvider<CartNotifier2, CartState>(
  CartNotifier2.new,
);
تحذير مهم: مُعيِّن state في كلا الواجهتين يطلق إعادة بناء فقط إذا لم تكن القيمة الجديدة مطابقة للقديمة (باستخدام ==). إذا لم تُعيد فئة الحالة تعريف ==، تقارن Dart بالمرجع — لذا أنشئ دائمًا نسخة جديدة (عبر copyWith أو استدعاء مُنشئ جديد) بدلًا من تعديل الحقول في المكان.

المُخبِر غير المتزامن (AsyncNotifier)

عندما تتضمن حالتك بيانات غير متزامنة (استدعاءات API، قراءات قاعدة البيانات المحلية)، استخدم AsyncNotifier<S> مع AsyncNotifierProvider. يصبح أسلوب build() من النوع Future<S> build()، وتُلفَّف الحالة تلقائيًا في AsyncValue<S> للتعامل المجاني مع حالات التحميل والخطأ والبيانات.

الاختيار بين StateNotifier و Notifier

  • المشاريع الجديدة: فضّل Notifier / NotifierProvider — فهو الواجهة الموصى بها رسميًا في Riverpod 2.x وتتكامل بسلاسة مع riverpod_generator.
  • قواعد الكود الموجودة: StateNotifierProvider يعمل بشكل كامل؛ لا استعجال للترحيل. كلا الواجهتين مدعومتان بالكامل.
  • العمليات غير المتزامنة: استخدم AsyncNotifier / AsyncNotifierProvider للحصول على تغليف AsyncValue تلقائي.

الخلاصة

يُعدّ StateNotifier والواجهة الأحدث Notifier الأدواتَ الرئيسية لتغليف الحالة القابلة للتغيير ومنطق الأعمال في Riverpod. يتبع كلاهما النمط ذاته: فئة حالة غير قابلة للتغيير (غالبًا مع copyWith)، وفئة مُخبِر تكشف أساليب عمليات الأعمال المُسمّاة، ومزوّد يربطهما بشجرة الودجات. تبقى الودجات نقية: تراقب الحالة للقراءة فقط وتستدعي أساليب المُخبِر — دون أي لمس مباشر للحالة.

النقطة الرئيسية: فضّل كائنات الحالة غير القابلة للتغيير مع copyWith. استخدم StateNotifier للتوافق مع الكود القديم وNotifier لكود Riverpod 2 الجديد. صِل إلى الأساليب عبر ref.read(provider.notifier) واشترك في الحالة عبر ref.watch(provider).