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

أساسيات Riverpod: المزودون ونطاق المزود

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

أساسيات Riverpod: المزودون ونطاق المزود

Riverpod إطار عمل لإدارة الحالة في Flutter و Dart يتميز بالأمان في وقت الترجمة والقابلية للاختبار والتفاعلية. على عكس سلفه Provider، يُزيل Riverpod الحاجة إلى BuildContext عند قراءة الحالة، ويكتشف الأخطاء في وقت الترجمة بدلاً من وقت التشغيل، ويُسهّل دمج المزودين وتجاوزهم في الاختبارات. كل جزء من الحالة في تطبيق Riverpod مُغلَّف في مزوِّد (provider) — وصف تصريحي لكيفية إنشاء قيمة ما.

ملاحظة: يتوفر Riverpod بنكهتين: flutter_riverpod (لتطبيقات Flutter) وriverpod (لـ Dart النقي). يستخدم هذا الدرس flutter_riverpod. حزمة توليد الكود riverpod_generator اختيارية وتُغطَّى في درس لاحق — نستخدم هنا الواجهة البرمجية الكلاسيكية بدون تعليقات توضيحية لتعلم الأساسيات.

إعداد ProviderScope

قبل استخدام أي مزوِّد، يجب تغليف شجرة الودجات بالكامل داخل ProviderScope. هو الحاوية التي تخزّن حالة جميع المزودين. ضعه في أعلى الشجرة داخل main():

تغليف التطبيق بـ ProviderScope

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

void main() {
  runApp(
    const ProviderScope(   // يجب أن يغلف التطبيق بالكامل
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Demo',
      home: const HomeScreen(),
    );
  }
}
نصيحة: يمكنك تداخل عدة نطاقات ProviderScope لتجاوز المزودين في شجرة فرعية — نمط قوي للاختبار وعزل الميزات. ومع ذلك، في التطبيقات العادية تحتاج فقط إلى نطاق واحد في الجذر.

ConsumerWidget: الودجت المدرك لـ Riverpod

لقراءة المزودين داخل شجرة الودجات، استبدل StatelessWidget بـ ConsumerWidget، وStatefulWidget بـ ConsumerStatefulWidget. كلاهما يمنحانك كائن WidgetRef ref — البوابة لكل مزوِّد:

  • ref.watch(provider) — يشترك في المزوِّد ويعيد بناء الودجت كلما تغيرت القيمة.
  • ref.read(provider) — يقرأ القيمة الحالية مرة واحدة دون اشتراك؛ استخدمه داخل الاستدعاءات الراجعة وinitState.
  • ref.listen(provider, callback) — يتفاعل مع التغييرات بشكل أمري (مثل عرض SnackBar) دون إعادة بناء الودجت.

Provider: القيم المحسوبة للقراءة فقط

أبسط أنواع المزودين. استخدمه للـثوابت والقيم المحسوبة والخدمات التي لا تتغير من تلقاء نفسها. تُنشأ القيمة بشكل كسول عند أول وصول وتُخزَّن مؤقتاً طوال عمر ProviderScope.

Provider لسلسلة ترحيب محسوبة

import 'package:flutter_riverpod/flutter_riverpod.dart';

// أعلن المزودين على المستوى الأعلى (خارج أي صنف)
final greetingProvider = Provider<String>((ref) {
  return 'Hello, Riverpod!';
});

// خدمة بسيطة مكشوفة كمزوِّد
final appVersionProvider = Provider<String>((ref) => '2.4.1');

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch يُبقي الودجت متزامناً؛ مناسب حتى للقيم الثابتة
    final greeting = ref.watch(greetingProvider);
    final version = ref.watch(appVersionProvider);

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Text(greeting, style: const TextStyle(fontSize: 24)),
        Text('v$version', style: const TextStyle(color: Colors.grey)),
      ],
    );
  }
}

StateProvider: الحالة القابلة للتغيير البسيطة

يحتفظ StateProvider بقيمة واحدة قابلة للتغيير ويكشف عن StateController عبر ref.read(provider.notifier). إنه مثالي للـعدادات البسيطة والتبديلات ومؤشرات التبويب المحددة وقيم التصفية — أي شيء يناسب نوعاً أولياً واحداً.

عداد باستخدام StateProvider

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

// المزوِّد يحتفظ بعدد صحيح يبدأ من 0
final counterProvider = StateProvider<int>((ref) => 0);

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watch يعيد بناء هذا الودجت عند تغير العداد
    final count = ref.watch(counterProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Riverpod Counter')),
      body: Center(
        child: Text(
          'Count: $count',
          style: Theme.of(context).textTheme.headlineLarge,
        ),
      ),
      floatingActionButton: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          FloatingActionButton(
            heroTag: 'inc',
            // ref.read داخل الاستدعاءات — لا نحتاج للمشاهدة هنا
            onPressed: () => ref.read(counterProvider.notifier).state++,
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'dec',
            onPressed: () => ref.read(counterProvider.notifier).state--,
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}
تحذير: لا تستدعِ أبداً ref.watch داخل onPressed لزر أو أي استدعاء راجع آخر. المشاهدة داخل الاستدعاءات تتسبب في قراءة المزودين في أوقات غير متوقعة وتُسرِّب الاشتراكات. استخدم ref.read في الاستدعاءات وref.watch في طريقة build فقط.

FutureProvider: تحميل البيانات غير المتزامن

صُمِّم FutureProvider للـعمليات غير المتزامنة الأحادية مثل طلبات HTTP أو قراءات قواعد البيانات. يُعيد AsyncValue<T> وهو اتحاد مختوم من ثلاث حالات: AsyncData وAsyncLoading وAsyncError. استخدم .when() للتعامل مع الحالات الثلاث بأمان.

FutureProvider للبيانات البعيدة

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

// يحاكي استدعاء شبكة يُعيد قائمة بأسماء المستخدمين
final usersProvider = FutureProvider<List<String>>((ref) async {
  // في تطبيق حقيقي: final response = await http.get(Uri.parse('...'));
  await Future.delayed(const Duration(seconds: 1)); // محاكاة زمن الاستجابة
  return ['Alice', 'Bob', 'Carol'];
});

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncUsers = ref.watch(usersProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Users')),
      body: asyncUsers.when(
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (err, stack) => Center(child: Text('Error: $err')),
        data: (users) => ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) => ListTile(
            leading: const Icon(Icons.person),
            title: Text(users[index]),
          ),
        ),
      ),
    );
  }
}
نصيحة: لتحديث FutureProvider يدوياً (مثلاً عند سحب للتحديث)، استدعِ ref.invalidate(usersProvider). سيُعيد Riverpod تشغيل الدالة غير المتزامنة وسينتقل الودجت مجدداً عبر حالة التحميل.

تركيب المزودين

يمكن للمزودين أن يعتمدوا على مزودين آخرين عبر كائن ref الممرَّر لدالة الإنشاء. هكذا تبني رسماً بيانياً نظيفاً للتبعيات دون محدد خدمة:

تركيب المزودين

// المزودون الأساسيون
final baseUrlProvider = Provider<String>((ref) => 'https://api.example.com');

final httpClientProvider = Provider<String>((ref) {
  final base = ref.watch(baseUrlProvider); // يقرأ مزوِّداً آخر
  return 'HttpClient(baseUrl: $base)';     // يُعيد عميلاً مُهيَّأً
});

// FutureProvider يستخدم عميل HTTP
final profileProvider = FutureProvider<String>((ref) async {
  final client = ref.watch(httpClientProvider);
  // نظرياً: return await client.get('/profile');
  return 'Profile loaded via $client';
});

ملخص

يحل نموذج المزودين الآمن من حيث الترجمة في Riverpod مشكلات رئيسية في الحلول المبنية على InheritedWidget. الأنواع الثلاثة للمزودين المُقدَّمة هنا تغطي غالبية حالات الاستخدام الحقيقية:

  • Provider — الثوابت والخدمات والقيم المحسوبة للقراءة فقط.
  • StateProvider — الحالة البسيطة القابلة للتغيير (الأنواع الأولية والتعدادات وقيم التصفية).
  • FutureProvider — العمليات غير المتزامنة مع معالجة مدمجة للتحميل والأخطاء عبر AsyncValue.

في الدرس التالي ستتعلم StateNotifierProvider وNotifierProvider لإدارة الكائنات المعقدة، وكيفية هيكلة ميزة حقيقية من البداية إلى النهاية باستخدام Riverpod.

النقطة الرئيسية: غلِّف دائماً تطبيقك بـ ProviderScope، وأعلن المزودين على المستوى الأعلى، واستخدم ref.watch في build لواجهة مستخدم تفاعلية، وref.read في معالجات الأحداث. لا تشاهد أبداً داخل الاستدعاءات الراجعة.