إعداد حزمة Provider
ما هو Provider؟
Provider هو حل إدارة الحالة الموصى به من فريق Flutter للتطبيقات ذات التعقيد البسيط إلى المتوسط. هو غلاف حول InheritedWidget يسهّل إنشاء كائنات الحالة وتوفيرها واستهلاكها عبر شجرة الودجات. بدلاً من بناء أصناف فرعية من InheritedWidget أو InheritedNotifier يدويًا، يتعامل Provider مع كل الكود المتكرر نيابةً عنك.
InheritedWidget، ويتعامل تلقائيًا مع التخلص، ويدعم التهيئة الكسولة، ويتكامل بسلاسة مع ChangeNotifier. هو الجسر بين إدارة الحالة اليدوية والحلول الأكثر تقدمًا مثل Riverpod أو Bloc.
تثبيت حزمة Provider
أضف حزمة provider إلى مشروعك باستخدام واجهة سطر أوامر Flutter أو بتعديل pubspec.yaml مباشرةً.
إضافة Provider إلى مشروعك
# باستخدام واجهة سطر أوامر Flutter (موصى به):
flutter pub add provider
# أو أضف يدويًا إلى pubspec.yaml:
# dependencies:
# flutter:
# sdk: flutter
# provider: ^6.1.2
# ثم شغّل:
flutter pub get
استيراد Provider
import 'package:provider/provider.dart';
ChangeNotifierProvider
ChangeNotifierProvider هو أكثر أنواع الموفرين استخدامًا. ينشئ نسخة ChangeNotifier، يوفرها للأحفاد، يستمع للتغييرات، ويتخلص من المُبلِّغ تلقائيًا عند إزالة الموفر من الشجرة.
إنشاء نموذج لـ Provider
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (_count > 0) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
توفير النموذج مع ChangeNotifierProvider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'عرض Provider',
home: const CounterPage(),
);
}
}
create تُستدعى بشكل كسول افتراضيًا — يتم إنشاء CounterModel فقط عندما يحاول حفيد الوصول إليه لأول مرة. يتعامل Provider أيضًا مع التخلص تلقائيًا: عند إزالة ChangeNotifierProvider من الشجرة، يستدعي dispose() على CounterModel نيابةً عنك.
ودجة Consumer
ودجة Consumer هي الطريقة الأكثر وضوحًا لقراءة قيمة موفَّرة. تعيد بناء builder كلما استدعى النموذج notifyListeners(). مثل ValueListenableBuilder، تقبل معامل child للتحسين.
استخدام Consumer لقراءة الحالة
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('عداد Provider')),
body: Center(
child: Consumer<CounterModel>(
builder: (context, counter, child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
child!, // التسمية الثابتة — لا تُعاد بناؤها أبدًا
Text(
'\${counter.count}',
style: const TextStyle(fontSize: 48),
),
],
);
},
child: const Text(
'العدد الحالي:',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
),
floatingActionButton: Consumer<CounterModel>(
builder: (context, counter, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
heroTag: 'decrement',
onPressed: counter.decrement,
child: const Icon(Icons.remove),
),
const SizedBox(width: 8),
FloatingActionButton(
heroTag: 'increment',
onPressed: counter.increment,
child: const Icon(Icons.add),
),
],
);
},
),
);
}
}
context.watch مقابل context.read
يوسّع Provider BuildContext بدالتين أساسيتين: watch و read. فهم متى تستخدم كلًا منهما أمر حاسم للسلوك الصحيح والأداء.
context.watch — تفاعلي (يعيد البناء عند التغيير)
// استخدم watch داخل build() لإعادة البناء عند تغيّر الحالة
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
// هذه الودجة تُعاد بناؤها كلما أشعر CounterModel
final counter = context.watch<CounterModel>();
return Text(
'العدد: \${counter.count}',
style: const TextStyle(fontSize: 32),
);
}
}
context.read — لمرة واحدة (بلا إعادة بناء)
// استخدم read في الاستدعاءات المرجعية للوصول إلى النموذج بدون اشتراك
class IncrementButton extends StatelessWidget {
const IncrementButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
// read لا تسبب إعادة بناء هذه الودجة
context.read<CounterModel>().increment();
},
child: const Text('زيادة'),
);
}
}
context.read داخل دالة build لعرض البيانات. الودجة لن تُعاد بناؤها عند تغيّر البيانات، مما يؤدي إلى واجهة مستخدم قديمة. وبالعكس، لا تستخدم أبدًا context.watch داخل معالجات الأحداث (مثل onPressed) — فهي صالحة فقط أثناء مرحلة البناء.
Provider.of
Provider.of<T>(context) هي واجهة البرمجة الأصلية للوصول إلى القيم الموفَّرة. افتراضيًا تتصرف مثل context.watch (تستمع للتغييرات). مرر listen: false لجعلها تتصرف مثل context.read.
استخدام Provider.of
// مكافئ لـ context.watch<CounterModel>()
final counter = Provider.of<CounterModel>(context);
// مكافئ لـ context.read<CounterModel>()
final counter = Provider.of<CounterModel>(context, listen: false);
context.watch و context.read على Provider.of. فهما أكثر إيجازًا وأقل عرضة للخطأ ويوصلان النية بوضوح. استخدم Provider.of فقط عند الحاجة للتوافق العكسي أو حالات حافة محددة.
متى تستخدم كل طريقة وصول
إليك دليل عملي لاختيار طريقة الوصول الصحيحة:
دليل قرار طريقة الوصول
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
// 1. WATCH: عرض بيانات يجب أن تتحدث تفاعليًا
final counter = context.watch<CounterModel>();
return Column(
children: [
// عرض تفاعلي
Text('العدد: \${counter.count}'),
// 2. READ: استدعاء دوال في الاستدعاءات المرجعية
ElevatedButton(
onPressed: () => context.read<CounterModel>().increment(),
child: const Text('إضافة'),
),
// 3. CONSUMER: تحديد نطاق إعادة البناء لشجرة فرعية صغيرة
Consumer<CounterModel>(
builder: (context, model, child) {
return Text('المجموع: \${model.count}');
},
),
],
);
}
}
// ملخص:
// context.watch<T>() → داخل build()، تحتاج تحديثات تفاعلية
// context.read<T>() → داخل الاستدعاءات المرجعية، وصول لمرة واحدة
// Consumer<T> → تحديد نطاق إعادة البناء لشجرة فرعية محددة
// Provider.of<T>() → واجهة برمجة قديمة، استخدم watch/read بدلاً منها
مثال عملي: تطبيق عداد مع Provider
إليك تطبيق عداد كامل وجاهز للإنتاج باستخدام Provider، يوضح جميع المفاهيم المغطاة في هذا الدرس.
تطبيق العداد الكامل مع Provider
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// النموذج
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
bool get canDecrement => _count > 0;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (canDecrement) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
// نقطة الدخول
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'عداد Provider',
theme: ThemeData(useMaterial3: true),
home: const CounterHomePage(),
);
}
}
class CounterHomePage extends StatelessWidget {
const CounterHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('عداد Provider'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () => context.read<CounterModel>().reset(),
),
],
),
body: const Center(child: CounterDisplay()),
floatingActionButton: const CounterFABs(),
);
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final count = context.watch<CounterModel>().count;
return Text(
'\$count',
style: Theme.of(context).textTheme.displayLarge,
);
}
}
class CounterFABs extends StatelessWidget {
const CounterFABs({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.small(
heroTag: 'inc',
onPressed: () => context.read<CounterModel>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
Consumer<CounterModel>(
builder: (context, model, _) {
return FloatingActionButton.small(
heroTag: 'dec',
onPressed: model.canDecrement ? model.decrement : null,
backgroundColor: model.canDecrement ? null : Colors.grey,
child: const Icon(Icons.remove),
);
},
),
],
);
}
}
مثال عملي: موفر ملف المستخدم
مثال أكثر واقعية لإدارة حالة ملف المستخدم عبر عدة شاشات.
ملف المستخدم مع Provider
class UserProfile extends ChangeNotifier {
String _name = '';
String _email = '';
String? _avatarUrl;
bool _isLoading = false;
String get name => _name;
String get email => _email;
String? get avatarUrl => _avatarUrl;
bool get isLoading => _isLoading;
bool get isLoggedIn => _email.isNotEmpty;
Future<void> loadProfile(String userId) async {
_isLoading = true;
notifyListeners();
try {
// محاكاة استدعاء API
await Future.delayed(const Duration(seconds: 1));
_name = 'إدريس صالح';
_email = 'edrees@example.com';
_avatarUrl = 'https://example.com/avatar.jpg';
} catch (e) {
// معالجة الخطأ
} finally {
_isLoading = false;
notifyListeners();
}
}
void updateName(String newName) {
_name = newName;
notifyListeners();
}
void logout() {
_name = '';
_email = '';
_avatarUrl = null;
notifyListeners();
}
}
// التوفير على مستوى التطبيق:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterModel()),
ChangeNotifierProvider(create: (_) => UserProfile()),
],
child: const MyApp(),
),
);
}
// الاستخدام في شاشة الملف الشخصي:
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
final profile = context.watch<UserProfile>();
if (profile.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return Column(
children: [
if (profile.avatarUrl != null)
CircleAvatar(
backgroundImage: NetworkImage(profile.avatarUrl!),
radius: 40,
),
Text(profile.name, style: const TextStyle(fontSize: 24)),
Text(profile.email, style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => context.read<UserProfile>().logout(),
child: const Text('تسجيل الخروج'),
),
],
);
}
}
MultiProvider هي ودجة مساعدة تتيح لك توفير عدة نماذج بدون تداخل عميق. كل موفر في القائمة مستقل ويمكن الوصول إليه بشكل فردي من قبل الأحفاد.
الملخص
- Provider هو غلاف حول
InheritedWidgetيبسّط إدارة الحالة. - ChangeNotifierProvider ينشئ ويوفر ويتخلص تلقائيًا من
ChangeNotifier. - Consumer يعيد بناء بانيه عندما يُشعر النموذج؛ يدعم معامل
childللتحسين. - context.watch يشترك في التغييرات ويُشغّل إعادة البناء — استخدمه داخل
build(). - context.read يصل إلى النموذج بدون اشتراك — استخدمه داخل الاستدعاءات المرجعية ومعالجات الأحداث.
- Provider.of هي واجهة البرمجة القديمة؛ فضّل
watchوreadللوضوح. - MultiProvider يتيح لك توفير عدة نماذج على نفس المستوى بدون تداخل.