السلسلة: هذا الدرس جزء من سلسلة Flutter. اقرأ المقال الرئيسي.
Riverpod 3 حوّل إدارة الحالة في Flutter. لا حاجة للتلاعب بـ BuildContext، لا تكرار بين StateNotifier وChangeNotifier، والآن mutations أصلية لإدارة أفعال المستخدم (loading/error/success) وpersistance offline مدمجة. هذا الدرس يبني تطبيقاً كاملاً from scratch — قائمة مهام persisted.
المتطلبات
- Flutter SDK 3.41+ وDart 3.11+ مثبَّتان
- معرفة أساسية بـ Dart وwidgets (StatelessWidget، MaterialApp، Scaffold)
- VS Code أو Android Studio مع امتداد Flutter
- emulator Android أو هاتف موصول
- 60 دقيقة
الخطوة 1 — إنشاء المشروع وإضافة Riverpod
flutter create todo_riverpod
cd todo_riverpod
flutter pub add flutter_riverpod
flutter pub add sqflite path riverpod_sqflite
عند الكتابة، flutter_riverpod في سلسلة 3.3 المستقرّة. حزم sqflite وpath للـ persistance لاحقاً. pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^3.3.0
sqflite: ^2.4.2
path: ^1.9.1
riverpod_sqflite: ^0.4.2
الخطوة 2 — تفعيل ProviderScope
كل نظام Riverpod يدور حول ProviderContainer. في Flutter، يُحقن في شجرة widgets عبر ProviderScope.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(const ProviderScope(child: TodoApp()));
}
class TodoApp extends StatelessWidget {
const TodoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo Riverpod',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
home: const TodoHomePage(),
);
}
}
class TodoHomePage extends StatelessWidget {
const TodoHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Mes taches')),
body: const Center(child: Text('A venir')),
);
}
}
الخطوة 3 — تعريف أول provider واستهلاكه
provider في Riverpod مصنع قيمة بطيء. القيمة تُحسَب فقط عند أول read أو watch وتبقى في cache.
// lib/todo_model.dart
class Todo {
const Todo({required this.id, required this.title, this.done = false});
final String id;
final String title;
final bool done;
Todo copyWith({String? id, String? title, bool? done}) => Todo(
id: id ?? this.id,
title: title ?? this.title,
done: done ?? this.done,
);
Map<String, Object?> toJson() =>
{'id': id, 'title': title, 'done': done};
factory Todo.fromJson(Map<String, Object?> json) => Todo(
id: json['id'] as String,
title: json['title'] as String,
done: (json['done'] as bool?) ?? false,
);
}
// lib/todos_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_model.dart';
class TodosNotifier extends Notifier<List<Todo>> {
@override
List<Todo> build() => const [];
void add(String title) {
final newTodo = Todo(
id: DateTime.now().microsecondsSinceEpoch.toString(),
title: title,
);
state = [...state, newTodo];
}
void toggle(String id) {
state = [
for (final t in state)
if (t.id == id) t.copyWith(done: !t.done) else t,
];
}
void remove(String id) {
state = state.where((t) => t.id != id).toList();
}
}
final todosProvider =
NotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
نمط Notifier تطوّر StateNotifier. أبسط، أكثر قابلية للاختبار، وكلياً immuable.
الخطوة 4 — توصيل UI بـ ConsumerWidget
class TodoHomePage extends ConsumerWidget {
const TodoHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todosProvider);
return Scaffold(
appBar: AppBar(title: const Text('Mes taches')),
body: todos.isEmpty
? const Center(child: Text('Aucune tache'))
: ListView.separated(
itemCount: todos.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final todo = todos[index];
return CheckboxListTile(
value: todo.done,
title: Text(todo.title),
onChanged: (_) =>
ref.read(todosProvider.notifier).toggle(todo.id),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
}
اصطلاحان للحفظ: ref.watch(provider) يشترك في التغييرات ويُطلق rebuild؛ ref.read(provider.notifier) يستردّ الـ controller بلا اشتراك، مثالي في callback.
الخطوة 5 — Mutation للأفعال غير المتزامنة
Future<void> add(String title) async {
await Future.delayed(const Duration(milliseconds: 800));
if (title.toLowerCase().contains('erreur')) {
throw Exception('Mot interdit');
}
final newTodo = Todo(
id: DateTime.now().microsecondsSinceEpoch.toString(),
title: title,
);
state = [...state, newTodo];
}
final addTodoMutation = Mutation<void>();
// في _showAddDialog:
addTodoMutation.run(ref, () async {
await ref.read(todosProvider.notifier).add(result);
});
// في build:
final addState = ref.watch(addTodoMutation);
final isAdding = addState is MutationPending;
Mutation provider مستقلّ. تستطيع مراقبتها (ref.watch) للرّد على انتقالات MutationIdle ← MutationPending ← MutationSuccess أو MutationError.
الخطوة 6 — حفظ القائمة بـ offline persistence
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_sqflite/riverpod_sqflite.dart';
import 'package:path/path.dart' as p;
import 'package:sqflite/sqflite.dart';
import 'todo_model.dart';
final storageProvider = FutureProvider<JsonSqFliteStorage>((ref) async {
final path = p.join(await getDatabasesPath(), 'todos.db');
return JsonSqFliteStorage.open(path);
});
class TodosNotifier extends AsyncNotifier<List<Todo>> {
@override
Future<List<Todo>> build() async {
persist(
ref.watch(storageProvider.future),
key: 'todos',
encode: (todos) => jsonEncode(todos.map((t) => t.toJson()).toList()),
decode: (raw) {
final list = jsonDecode(raw) as List;
return list
.map((e) => Todo.fromJson(e as Map<String, Object?>))
.toList();
},
);
return const [];
}
Future<void> add(String title) async {
await Future.delayed(const Duration(milliseconds: 300));
final current = await future;
state = AsyncData([
...current,
Todo(
id: DateTime.now().microsecondsSinceEpoch.toString(),
title: title),
]);
}
}
final todosProvider =
AsyncNotifierProvider<TodosNotifier, List<Todo>>(TodosNotifier.new);
persist() في build() يربط notifier بالـ storage. أول إطلاق يحاول قراءة المفتاح todos؛ عند كل تحديث، يُسلسل ويكتب.
// UI:
final todosAsync = ref.watch(todosProvider);
return todosAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(child: Text('Erreur : $err')),
data: (todos) => todos.isEmpty
? const Center(child: Text('Aucune tache'))
: ListView.separated(/* ... */),
);
أعد flutter run، أضف ثلاث مهام، أغلق التطبيق، أعد الإطلاق. المهام لا تزال موجودة.
الخطوة 7 — التحقق والممارسات الجيدة
- نظّم الكود حسب feature — كل مجلد
features/<اسم>/يحوي نموذجه، provider، widgets — بدل طبقات تقنية. - لا تضع أبداً آثاراً جانبية في
build(). ما يجب تنفيذه مرة، أطلقه من UI أو عبرref.listen. ref.invalidate(todosProvider)لفرض rebuild كامل — مفيد في pull-to-refresh.ref.read(provider.notifier)في callback زر، لاref.watch.
فهم أنواع providers Riverpod
Provider الأساسي: قيمة immuable محسوبة مرة في cache — مفضّل لخدمة singleton (HTTP، إعدادات، parsers). FutureProvider: يُرجع Future ويعرضه كـ AsyncValue — لاستدعاء API عند الإقلاع. StreamProvider: لـ Stream، يُعاد الاشتراك آلياً — لـ Firestore أو WebSocket.
لحالة قابلة للتعديل: NotifierProvider (حالة متزامنة، الأغلبية)، AsyncNotifierProvider (حالة أولية async — تحميل من API أو قاعدة محلية).
اللاحقة .family تسمح بتمرير معامل: todoByIdProvider(String id). اللاحقة .autoDispose تحرّر آلياً حين لا widget يستمع.
توليد كود اختياري
Riverpod يقترح متغيّراً بـ code generation. تعنون دالة أو class بـ @riverpod وbuild_runner يولّد provider. أكثر اختصاراً والمصرّف يضمن تناسق الأنواع. العيب: build runner دائم. للمشاريع تحت 20 provider، API يدوي يكفي.
اختبارات للـ notifiers
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_riverpod/todos_provider.dart';
void main() {
test('ajoute une tache au debut vide', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
await container.read(todosProvider.future);
await container.read(todosProvider.notifier).add('Acheter du pain');
final state = await container.read(todosProvider.future);
expect(state.length, 1);
expect(state.first.title, 'Acheter du pain');
});
}
لتجاوز الـ persistance في اختبار، أعد تعريف storageProvider بـ override يُرجع تخزيناً في الذاكرة أو يُعطّل الكتابة.
تنظيم المشروع على نطاق واسع
lib/
main.dart
core/
router.dart
theme.dart
storage.dart
features/
todos/
todo_model.dart
todos_provider.dart
todos_screen.dart
widgets/
todo_tile.dart
add_todo_dialog.dart
auth/
auth_provider.dart
auth_screen.dart
shared/
widgets/
empty_state.dart
أخطاء شائعة
| الخطأ | السبب | الحل |
|---|---|---|
| «The provider … has no scope» | Widget خارج ProviderScope | تحقّق أن ProviderScope يغلّف MaterialApp في main() |
| الحالة لا تتحدّث | تعديل في مكان بلا إعادة إسناد | أنشئ دائماً قائمة/كائن جديد: state = [...state, item]، لا state.add(item) |
| «setState() called after dispose» | mutation أُطلقت بعد اختفاء widget | تحقّق ref.mounted قبل استدعاء state = في تتمة async |
| provider يُعاد إطلاقه في حلقة | ref.watch في provider يعتمد قيمة تتغيّر كل rebuild |
استخدم ref.read للقيم الثابتة، ref.listen للآثار |
| Build runner يفشل | cache قديم | dart run build_runner clean && dart run build_runner build --delete-conflicting-outputs |
أسئلة شائعة
Riverpod أم Provider كلاسيكي؟ لمشروع جديد في 2026، Riverpod بلا تردّد.
هل أستخدم توليد الكود؟ اختياري. للمشاريع الكبيرة أكثر اختصاراً. للمتوسطة، API يدوي شفّاف.
كيف نختبر Notifier؟ أنشئ ProviderContainer، استدع container.read(provider.notifier).method()، ثم تحقّق container.read(provider).
كيف ندير التنقل مع Riverpod؟ Riverpod لا يعتني بالتنقل. اجمعه مع go_router الذي يعرض refreshListenable.
هل يعمل Riverpod للتطبيقات المعقّدة؟ نعم. محرّرات كـ Reflectly، Wonderous وعدة néobanques تشتغل بـ Riverpod في الإنتاج.