إدارة الحالة باستخدام Riverpod عبر الميزات
إدارة الحالة باستخدام Riverpod عبر الميزات
في تطبيق Flutter الحقيقي، نادراً ما تنتمي الحالة إلى شاشة واحدة. حدث مصادقة المستخدم ينعكس على مجموعة التنقل وصفحة الملف الشخصي والتغذية الرئيسية وشريط التطبيق في آنٍ واحد. يحل Riverpod هذه المشكلة بالسماح لك بتعريف المزودين (providers) مرة واحدة وقراءتهم من أي مكان في شجرة الودجات — دون تمرير الخصائص (prop drilling)، دون تعقيدات InheritedWidget، ودون كائن عالمي قابل للتغيير.
يربط هذا الدرس مزودي Riverpod مباشرةً بـطبقة النطاق (domain layer) في تطبيق المشروع النهائي، ويمثّل البيانات غير المتزامنة باستخدام AsyncValue، ويوضح كيف تشترك شاشات متعددة في نفس جزء الحالة عبر مرجع مزود واحد.
مراجعة: تشريح المزود في Riverpod
كل مزود في Riverpod يتبع نفس العقد: يعرض قيمة ويُخطر المستمعين عند تغييرها. أربعة أنواع من المزودين ستستخدمها أكثر في تطبيق غني بالميزات:
- Provider — قيمة محسوبة للقراءة فقط ومتزامنة (مثل: نسخة مستودع)
- StateNotifierProvider — حالة قابلة للتغيير تُدار بفئة فرعية من
StateNotifier - FutureProvider — يلتف حول
FutureويعرضAsyncValue<T> - StreamProvider — يلتف حول
StreamويعرضAsyncValue<T>
keepAlive: true أو ref.keepAlive()).ربط المزودين بطبقة النطاق
تفصل البنية الجيدة واجهة المستخدم عن منطق الأعمال. تحتوي طبقة النطاق على حالات الاستخدام وواجهات المستودعات؛ تعمل مزودات Riverpod كجسر بين النطاق وواجهة المستخدم. النمط القياسي هو:
- أعلن مزود المستودع الذي يُعيد المستودع المحدد.
- أعلن مزود المُخطر (notifier) الذي يستدعي طرق المستودع.
- تقرأ الودجات مزود المُخطر وتستجيب لحالات
AsyncValue.
مزود المستودع ← StateNotifier ← واجهة المستخدم
// ── domain/repositories/task_repository.dart ──────────────────
abstract class TaskRepository {
Future<List<Task>> fetchAll();
Future<void> create(Task task);
Future<void> delete(String id);
}
// ── data/repositories/remote_task_repository.dart ─────────────
class RemoteTaskRepository implements TaskRepository {
final Dio _dio;
RemoteTaskRepository(this._dio);
@override
Future<List<Task>> fetchAll() async {
final response = await _dio.get('/tasks');
return (response.data as List)
.map((json) => Task.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<void> create(Task task) async {
await _dio.post('/tasks', data: task.toJson());
}
@override
Future<void> delete(String id) async {
await _dio.delete('/tasks/$id');
}
}
// ── providers/task_providers.dart ─────────────────────────────
final dioProvider = Provider<Dio>((ref) => Dio());
final taskRepositoryProvider = Provider<TaskRepository>((ref) {
return RemoteTaskRepository(ref.watch(dioProvider));
});
// StateNotifier يقود ميزة قائمة المهام
class TaskListNotifier extends StateNotifier<AsyncValue<List<Task>>> {
final TaskRepository _repository;
TaskListNotifier(this._repository) : super(const AsyncValue.loading()) {
load();
}
Future<void> load() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _repository.fetchAll());
}
Future<void> delete(String id) async {
await _repository.delete(id);
await load(); // إعادة التحميل بعد التعديل
}
}
final taskListProvider =
StateNotifierProvider<TaskListNotifier, AsyncValue<List<Task>>>((ref) {
return TaskListNotifier(ref.watch(taskRepositoryProvider));
});
AsyncValue: نمذجة الحالة غير المتزامنة بأمان
AsyncValue<T> هو نوع مغلق (sealed union) بثلاثة متغيرات: loading وdata وerror. بدلاً من الاحتفاظ بثلاثة قيم منطقية منفصلة (isLoading، hasError، data)، تستدعي when() وتعالج كل حالة في تعبير واحد. هذا يجعل من المستحيل نسيان حالة التحميل أو الخطأ في موقع الاستدعاء.
استهلاك AsyncValue بـ .when() في ودجت
class TaskListScreen extends ConsumerWidget {
const TaskListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tasksAsync = ref.watch(taskListProvider);
return Scaffold(
appBar: AppBar(
title: const Text('مهامي'),
actions: [
// شارة مشتركة عبر الشاشات — نفس المزود، دون تمرير خصائص
Consumer(
builder: (context, ref, _) {
final count = ref.watch(
taskListProvider.select(
(value) => value.asData?.value.length ?? 0,
),
);
return Badge(label: Text('$count'));
},
),
],
),
body: tasksAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('خطأ: $err'),
ElevatedButton(
onPressed: () =>
ref.read(taskListProvider.notifier).load(),
child: const Text('إعادة المحاولة'),
),
],
),
),
data: (tasks) => ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return ListTile(
title: Text(task.title),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () =>
ref.read(taskListProvider.notifier).delete(task.id),
),
);
},
),
),
);
}
}
مشاركة الحالة بين الشاشات دون تمرير الخصائص
إحدى أقوى ميزات Riverpod أن أي ودجت في الشجرة يمكنه قراءة أي مزود دون تلقيه كمعامل في المُنشئ. افترض شاشة تفاصيل يجب أن تعكس بيانات المهمة ذاتها التي تعرضها شاشة القائمة، وودجت إحصائيات في لوحة التحكم يعرض إجمالي عدد المهام. تقرأ الثلاثة نفس taskListProvider — يضمن Riverpod مشاركتها لنسخة واحدة.
ref.watch(provider.select(...)) للاشتراك في جزء فقط من الحالة. هذا يمنع إعادة البناء غير الضرورية عند تغيير أجزاء غير ذات صلة من الحالة — أمر بالغ الأهمية للحفاظ على أداء 60 إطاراً في الثانية في القوائم الطويلة.مزودو Family: حالة خاصة بكل ميزة
عندما تحتاج كل ميزة لجزء معزول خاص بها من نفس نوع المُخطر، استخدم معدّل .family. مثال شائع هو قوائم المهام الخاصة بكل مشروع في تطبيق إدارة مشاريع:
مزود family لحالة خاصة بكل مشروع
final projectTasksProvider = StateNotifierProvider.family<
TaskListNotifier,
AsyncValue<List<Task>>,
String /* projectId */>((ref, projectId) {
return TaskListNotifier(
ref.watch(taskRepositoryProvider),
projectId: projectId,
);
});
// في ProjectDetailScreen:
// ref.watch(projectTasksProvider('proj_42')) → حالة معزولة لكل مشروع
ref.read() داخل طريقة build() للحالة التفاعلية — استخدم دائماً ref.watch(). ref.read() مخصص للإجراءات الأمرية ذات الاستدعاء الواحد (مثل: داخل ردود أفعال الأزرار). استخدام ref.read() بشكل تفاعلي سيتخطى إعادة البناء بصمت وينتج واجهة مستخدم قديمة.الخلاصة
يتيح لك رسم بياني مزودات Riverpod نمذجة الدورة الحياتية الكاملة لميزة غير متزامنة — التحميل والخطأ والبيانات — عبر AsyncValue، مع الإبقاء على منطق النطاق في فئات Dart البسيطة التي يسهل اختبارها. إعلان المزودين على المستوى الأعلى، وربطهم بتجريدات المستودعات، وقراءتهم من أي ودجت يُلغي تمرير الخصائص ويُبقي حالة كل ميزة معزولة ولكن يمكن الوصول إليها عالمياً عند الحاجة.
AsyncValue وعالج المتغيرات الثلاثة بـ.when(). استخدم ref.watch(provider.select(...)) للاشتراكات الدقيقة. استفد من .family للحالة الخاصة بكل كيان. هذه الممارسات الأربع معاً تُنتج حالة ميزات قابلة للصيانة وقابلة للاختبار وعالية الأداء في أي تطبيق Flutter.