تحديد نطاق المزود والتجاوزات ومعدِّلات Family
تحديد نطاق المزود والتجاوزات ومعدِّلات Family
يمنحك Riverpod ثلاثة آليات قوية للتحكم في مكان وجود المزود، وما يُرجعه في سياق معين، وكيفية قبوله للمعاملات. إن فهم تحديد النطاق والتجاوزات ومعدِّل .family يتيح لك بناء تطبيقات Flutter معزولة الميزات وقابلة للاختبار وعالية التركيب.
1. تحديد نطاق المزود باستخدام ProviderScope
تلفّ كل تطبيقات Riverpod شجرة الودجات في ProviderScope على المستوى الأعلى. جميع المزودون متاحون افتراضياً للشجرة بأكملها. تحديد النطاق يعني إدراج ProviderScope متداخل في مستوى أدنى من الشجرة وتجاوز مزودين محددين داخل تلك الشجرة الفرعية فقط. الودجات داخل النطاق المتداخل ترى القيمة المُجاوَزة؛ أما الودجات الخارجية فتستمر في رؤية القيمة الأصلية.
- مفيد عندما تحتاج ميزة فرعية إلى نسختها المعزولة الخاصة من مزود.
- يُتخلص من النطاق المتداخل عند إزالة الشجرة الفرعية — لا حاجة لتنظيف يدوي.
- يُستخدم عادةً في القوائم حيث يحتاج كل عنصر إلى حالته الخاصة (مثل حالة تحرير عنصر مهمة).
ProviderScope المتداخل — عزل لكل عنصر
// مزود يحمل حالة "المحدد" لعنصر واحد
final selectedItemProvider = StateProvider<bool>((ref) => false);
class ItemList extends StatelessWidget {
final List<String> items;
const ItemList({super.key, required this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
// كل عنصر يحصل على ProviderScope الخاص به، لذا فإن selectedItemProvider
// مستقل تماماً عن كل عنصر آخر.
return ProviderScope(
overrides: [
selectedItemProvider.overrideWith((ref) => false),
],
child: ItemTile(label: items[index]),
);
},
);
}
}
class ItemTile extends ConsumerWidget {
final String label;
const ItemTile({super.key, required this.label});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isSelected = ref.watch(selectedItemProvider);
return ListTile(
title: Text(label),
trailing: Icon(
isSelected ? Icons.check_circle : Icons.circle_outlined,
color: isSelected ? Colors.green : Colors.grey,
),
onTap: () => ref.read(selectedItemProvider.notifier).state = !isSelected,
);
}
}
ProviderScope متداخلاً، يكون التجاوز مرئياً فقط داخل الشجرة الفرعية لذلك النطاق. أي ودجت فوق أو بجانب النطاق المتداخل يستمر في قراءة المزود الأصلي دون تأثر.2. تجاوزات المزود
تتيح لك التجاوزات استبدال تنفيذ مزود في وقت التشغيل — في معظم الأحيان في سيناريوهين:
- الاختبار: استبدال مستودع حقيقي بمزيف حتى لا تصل اختبارات الوحدة/الودجات أبداً إلى الشبكة أو قاعدة البيانات.
- أعلام الميزات / البيئات: حقن تنفيذ مختلف بناءً على نكهة أو متغير بيئي.
تُقدم التجاوزات عبر معامل overrides في ProviderScope. استخدم overrideWithValue() للاستبدال بنسخة ملموسة، أو overrideWith() للاستبدال بمصنع مزود جديد.
تجاوز مستودع لاختبارات الودجات
// مزود الإنتاج
final productRepositoryProvider = Provider<ProductRepository>(
(ref) => HttpProductRepository(),
);
final productListProvider = FutureProvider<List<Product>>((ref) async {
final repo = ref.watch(productRepositoryProvider);
return repo.fetchAll();
});
// في اختبار ودجت — حقن مزيف دون لمس كود الإنتاج
testWidgets('shows product list', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
productRepositoryProvider.overrideWithValue(FakeProductRepository()),
],
child: const MaterialApp(home: ProductListPage()),
),
);
await tester.pumpAndSettle();
expect(find.text('Widget A'), findsOneWidget);
});
3. معدِّل .family
المزودون عادةً وحيدون (singleton) — نسخة واحدة لكل نطاق. معدِّل .family يحوّل مزوداً إلى مصنع: كل معامل فريد ينتج نسخته المخبأة الخاصة. هذه هي الطريقة الاصطلاحية في Riverpod لتحميل البيانات بالمعرف.
- يعمل مع
ProviderوFutureProviderوStateNotifierProviderوNotifierProviderوغيرها. - يصبح المعامل جزءاً من هوية المزود —
productProvider(1)وproductProvider(2)هما مزودان مختلفان. - يجب أن تكون المعاملات قابلة للمقارنة (تجاوز
==وhashCode)، لذا فإن الأنواع الأولية والتعدادات أو كائنات القيمة@freezedهي الأنسب. - يتم تخبئة النسخ طوال عمر النطاق؛ يُتخلص منها عندما لا يستمع إليها أي ودجت.
FutureProvider.family — جلب منتج بالمعرف
// تعريف مزود family مُعامَل بمعرف المنتج (int)
final productByIdProvider = FutureProvider.family<Product, int>(
(ref, productId) async {
final repo = ref.watch(productRepositoryProvider);
return repo.fetchById(productId);
},
);
// استهلاكه في ودجت — تمرير المعرف كمعامل
class ProductDetailPage extends ConsumerWidget {
final int productId;
const ProductDetailPage({super.key, required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
// كل productId يحصل على AsyncValue مخبأ خاص به
final productAsync = ref.watch(productByIdProvider(productId));
return productAsync.when(
loading: () => const CircularProgressIndicator(),
error: (err, _) => Text('Error: $err'),
data: (product) => Column(
children: [
Text(product.name, style: const TextStyle(fontSize: 24)),
Text(product.description),
Text('\$${product.price.toStringAsFixed(2)}'),
],
),
);
}
}
4. الجمع بين تحديد النطاق والتجاوزات وFamily
هذه الميزات الثلاث تتركب بشكل طبيعي. يمكنك مثلاً إنشاء ProviderScope متداخل يتجاوز مزود .family لحقن بيانات اختبار لمعرف محدد دون التأثير على بقية الشجرة.
== صحيح) كمعاملات .family. يستخدم Riverpod المعامل كمفتاح لخريطة التجزئة؛ إذا لم يُعتبر معاملان يجب أن يكونا متساويين متساويين، ستحصل على نسخ مزود مكررة وأخطاء خفية.5. autoDispose مع family
إقران .family مع .autoDispose (أو استخدام keepAlive في Riverpod 2) يضمن التخلص التلقائي من نسخ المزود للمعرفات التي لم تعد على الشاشة، مما يمنع تسرب الذاكرة في التطبيقات طويلة الأمد مع معرفات فريدة كثيرة.
autoDispose + family — مزودات آمنة للذاكرة لكل معرف
// autoDispose يتخلص من نسخة المزود عندما لا يستمع إليها أي ودجت
final userProfileProvider = FutureProvider.autoDispose.family<UserProfile, String>(
(ref, userId) async {
// اختياري: الإبقاء حياً لمدة 30 ثانية بعد انفصال آخر مستمع
final link = ref.keepAlive();
Timer(const Duration(seconds: 30), link.close);
final api = ref.watch(apiClientProvider);
return api.getUserProfile(userId);
},
);
ملخص
تحديد نطاق المزود يعزل الحالة إلى شجرة ودجات فرعية دون أي آثار جانبية عالمية. التجاوزات تفصل التنفيذات الإنتاجية عن التنفيذات الخاصة بالاختبار أو البيئة. معدِّل .family يُعامل المزودين بحيث يحصل كل معامل فريد على نسخته المخبأة المدارة دورة حياتها — مما يجعل أنماطاً مثل "الجلب بالمعرف" موجزة وصحيحة في آنٍ واحد. معاً، تمنحك هذه الأدوات سيطرة دقيقة وتصريحية على طولوجيا حالة تطبيقك.