Flutter: إدارة الحالة — Riverpod
الحالة هي دم التطبيق — إذا أُسيئت إدارتها، تُسبب "نزيفًا داخليًا": تناقض بيانات، عواصف إعادة بناء، تسرب ذاكرة.
📋 المتطلبات السابقة: يجب أن تتقن ما يلي أولًا
- الدرس 5: StatefulWidget والتفاعل
- الدرس 11: الشبكات وREST API
1. ما ستتعلمه
- نظام المزوّد: Provider / StateProvider / FutureProvider / StreamProvider / NotifierProvider
- Riverpod 2.x: Notifier / AsyncNotifier / توضيح @riverpod لتوليد الأكواد
- سيناريوهات استخدام ref.watch / ref.read / ref.listen والمفاضلات في الأداء
- ProviderScope وConsumerWidget / ConsumerStatefulWidget
- ShopApp: CartNotifier لسلة التسوق + AsyncProductNotifier لتحميل المنتجات غير المتزامن
2. قصة حقيقية عن كارثة حالة
(1) المشكلة: مأزق الحالة العامة مع setState
يستخدم بوب setState لإدارة حالة سلة التسوق في ShopApp. المشكلة: بيانات السلة مطلوبة في صفحات الرئيسية والتفاصيل والسلة، مما يتطلب استدعاءات عكسية لتمريرها عبر كل طبقة من شجرة الويدجت. بعد تغيير CartPage للكمية، لا يتحدث شارة AppBar في الرئيسية — لأن حالة الرئيسية لا تعرف ما غيّرته CartPage. الأسوأ، 10 صفحات تحتفظ بـ 5 نسخ من بيانات السلة، كلها غير متزامنة.
(2) حل Riverpod
يرفع Riverpod الحالة إلى مزوّدين عامين. أي صفحة يمكنها ref.watch نفس حالة السلة، والتغيير في مكان واحد يُحدّث تلقائيًا جميع المستمعين.
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);
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) الفائدة: تغيير واحد، مزامنة عامة
بعد استخدام Riverpod، تعيش حالة سلة بوب في مكان واحد — تغييرات أي صفحة تُحدّث تلقائيًا جميع المستمعين. جحيم الاستدعاءات يختفي، والبيانات تبقى متسقة.
3. المفاهيم الأساسية لـ Riverpod
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]
> الإخراج: شغّل محليًا باستخدام 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) |
4. المزوّدات المكتوبة يدويًا
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: StateProvider حالة بسيطة
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'),
),
],
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: FutureProvider بيانات غير متزامنة
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]),
),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
5. Notifier وAsyncNotifier
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: CartNotifier (منطق سلة تسوق كامل)
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);
});
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: AsyncNotifier (تهيئة منتجات غير متزامنة)
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,
);
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. توليد أكواد @riverpod
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: استخدام riverpod_generator
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));
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
7. مثال كامل: تكامل Riverpod في ShopApp
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')),
])),
]),
);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
.when(loading:, error:, data:) لعرض مؤشرات التحميل وصفحات الخطأ ومحتوى البيانات.dart run build_runner watch.📖 ملخص
- Riverpod يدير الحالة العامة بشكل موحد — تغيير واحد يُحدّث جميع المستمعين تلقائيًا
- Notifier مناسب للحالة المعقدة + منطق الأعمال؛ AsyncNotifier للتهيئة غير المتزامنة
- ref.watch يستمع في build، ref.read يعمل في الاستدعاءات، ref.listen يعالج الآثار الجانبية
- المزوّدات المشتقة (cartTotalProvider) تحسب تلقائيًا الحالة التابعة
- توليد أكواد @riverpod يقلل الكود المتكرر — موصى به للمشاريع الجديدة
📝 تمارين
- أساسي (⭐): استخدم StateProvider لتنفيذ تبديل الوضع الداكن، واعرض الوضع الحالي في AppBar باستخدام ConsumerWidget.
- متوسط (⭐⭐): استخدم Notifier لتنفيذ CartNotifier مع إضافة/إزالة/تحديث وحساب السعر الإجمالي، مع عرض حالة السلة على صفحتين مختلفتين.
- متقدم (⭐⭐⭐): استخدم توليد أكواد @riverpod لتنفيذ مزوّدي Auth + Product + Cart: بعد تسجيل الدخول، تحميل المنتجات تلقائيًا؛ بعد الإضافة للسلة، تحديث الشارة تلقائيًا.