مفاهيم Providers
تُعد الـ Providers الميزة الجوهرية في Riverpod. إذا كنت تستخدم Riverpod، فأنت تستخدمها من أجل الـ providers الخاصة بها.
ما هو الـ Provider؟
الـ Providers هي في جوهرها "دوال memoized" (دوال تحفظ نتائجها)، مع بعض الإضافات التجميلية (syntactic sugar). ما يعنيه هذا هو أن الـ providers عبارة عن دوال تُعيد قيمة مخزنة مؤقتاً (cached value) عند استدعائها بنفس المعاملات (parameters).
حالة الاستخدام الأكثر شيوعاً للـ providers هي إجراء طلب عبر الشبكة (network request). لنفترض وجود دالة تقوم بجلب بيانات مستخدم من الـ API:
Future<User> fetchUser() async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
تكمن إحدى مشكلات هذه الدالة في أننا إذا حاولنا استخدامها داخل الـ widgets، فسنضطر إلى تخزين النتيجة مؤقتاً (cache) بأنفسنا؛ ثم إيجاد طريقة لمشاركة هذه القيمة بين جميع الـ widgets التي تحتاجها.
وهنا يأتي دور الـ providers. فالـ Providers هي عبارة عن أغلفة (wrappers) حول الدوال. فهي تقوم بتخزين نتيجة تلك الدالة مؤقتاً وتتيح لعدة widgets الوصول إلى نفس القيمة:
- riverpod
- riverpod_generator
// المعادل لدالة fetchUser الخاصة بنا، ولكن النتيجة مخزنة مؤقتاً (cached).
// استخدام userProvider عدة مرات سيعيد نفس القيمة.
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
});
// المعادل لدالة fetchUser الخاصة بنا، ولكن النتيجة مخزنة مؤقتاً (cached).
// سيولد هذا "userProvider". استخدامه عدة مرات سيؤدي إلى
// إرجاع نفس القيمة.
Future<User> user(Ref ref) async {
final response = await http.get('https://api.example.com/user/123');
return User.fromJson(response.body);
}
بالإضافة إلى التخزين المؤقت الأساسي، تضيف الـ providers ميزات متنوعة لجعلها أكثر قوة:
- آليات مدمجة لإبطال التخزين المؤقت (Cache invalidation) على وجه الخصوص، يسمح لك Ref.watch بدمج عدة caches معاً، مع إبطال ما يلزم تلقائياً.
- Automatic disposal
يمكن للـ providers تحرير الموارد تلقائياً عندما لا تعود هناك حاجة إليها.
- ربط البيانات (Data-binding) تغني الـ Providers عن الحاجة لاستخدام FutureBuilder أو StreamBuilder.
- المعالجة التلقائية للأخطاء يمكن للـ Providers التقاط الأخطاء تلقائياً وعرضها لواجهة المستخدم.
- دعم المحاكاة (Mocking) لتحسين الاختبارات ولأغراض أخرى، يمكن محاكاة (mock) جميع الـ providers. راجع Provider overrides.
- Offline persistence (experimental)
يمكن حفظ نتيجة الـ provider على القرص وإعادة تحميلها تلقائياً عند إعادة تشغيل التطبيق.
توفر الـ Providers طريقة مدمجة لواجهات المستخدم لعرض مؤشر تحميل (spinner) أو خطأ للآثار الجانبية (side effects) (مثل إرسال النماذج).
تأتي الـ Providers في 6 أنواع:
| متزامن (Synchronous) | Future | Stream | |
|---|---|---|---|
| غير قابل للتعديل (Unmodifiable) | Provider | FutureProvider | StreamProvider |
| قابل للتعديل (Modifiable) | NotifierProvider | AsyncNotifierProvider | StreamNotifierProvider |
قد يبدو هذا مربكاً في البداية. دعونا نفصل الأمر ونبسطه.
Sync مقابل Future مقابل Stream: تمثل أعمدة هذا الجدول أنواع Dart المدمجة (built-in) للدوال.
int synchronous() => 0;
Future<int> future() async => 0;
Stream<int> stream() => Stream.value(0);
غير قابل للتعديل (Unmodifiable) مقابل قابل للتعديل (Modifiable): بشكل افتراضي، لا يمكن تعديل الـ providers من قبل الـ widgets. تقوم متغيرات "Notifier" من الـ providers بجعلها قابلة للتعديل خارجياً. يشبه هذا وجود setter خاص (private setter) (الـ providers "غير القابلة للتعديل")
// _state قابل للتعديل داخلياً
// لكن لا يمكن تعديله خارجياً
var _state = 0;
int get state => _state;
مقابل setter عام (الـ providers "القابلة للتعديل")
// يمكن لأي شيء تعديل "state"
var state = 0;
يمكنك أيضاً اعتبار الفرق بين غير القابلة للتعديل (unmodifiable) و القابلة للتعديل (modifiable) كالفرق بين StatelessWidget و StatefulWidget على التوالي، من حيث المبدأ.
هذا التشبيه ليس دقيقاً تماماً، لأن الـ providers ليست widgets، وكلا النوعين يخزنان "حالة" (state). ولكن المبدأ مشابه: "كائن واحد، غير قابل للتغيير (immutable)" مقابل "كائنين، أحدهما قابل للتغيير (mutable)".
إنشاء Provider
يجب إنشاء الـ Providers كتصريحات على مستوى الملف (top-level declarations). وهذا يعني أنه يجب تعريفها خارج أي كلاس (class) أو دالة.
تعتمد الصيغة (syntax) لإنشاء الـ provider على ما إذا كان "قابلاً للتعديل" أو "غير قابل للتعديل"، كما هو موضح في الجدول أعلاه.
- غير قابل للتعديل (وظيفي)
- قابل للتعديل (Notifier)
- riverpod
- riverpod_generator
final name = SomeProvider.someModifier<Result>((ref) { //ضع المنطق الخاص بك هنا });
| متغير الـ provider | هذا المتغير هو ما سيتم استخدامه للتفاعل مع الـ provider الخاص بنا.
يجب أن يكون المتغير ملاحظة لا تدع كون الـ providers عامة (global) يخيفك. فالـ Providers غير قابلة للتغيير (immutable) تماماً. وتعريف الـ provider لا يختلف عن تعريف أي دالة، كما أن الـ providers قابلة للاختبار والصيانة. |
| نوع الـ provider | بشكل عام، يكون إما Provider، أو FutureProvider، أو StreamProvider.
يعتمد نوع الـ provider المُستخدم على القيمة المُرجعة (return value) من الدالة الخاصة بك.
على سبيل المثال، لإنشاء يُعد نصيحة لا تفكر من منطلق "أي provider يجب أن أختار". بدلاً من ذلك، فكر من منطلق "ماذا أريد أن أُرجع (return)". وسيتحدد نوع الـ provider بشكل طبيعي تبعاً لذلك. |
| المُعدِّلات (اختياري) | غالباً، قد ترى "مُعدِّلاً" (modifier) بعد نوع الـ provider. هذه المُعدِّلات اختيارية، وتُستخدم لضبط سلوك الـ provider بطريقة آمنة من حيث النوع (type-safe). يتوفر حالياً مُعدِّلان (modifiers):
|
| Ref | كائن يُستخدم للتفاعل مع الـ providers الأخرى. جميع الـ providers لديها واحد؛ إما كمعامل (parameter) لدالة الـ provider، أو كخاصية (property) تابعة لـ Notifier. |
| دالة الـ provider | هنا نضع المنطق الخاص بالـ providers. سيتم استدعاء هذه الدالة عند قراءة الـ provider لأول مرة. أما القراءات اللاحقة فلن تستدعي الدالة مرة أخرى، بل ستقوم بإرجاع القيمة المخزنة مؤقتاً بدلاً من ذلك. |
@riverpod Result myFunction(Ref ref) { //ضع المنطق الخاص بك هنا }
| التعليق التوضيحي (Annotation) | يجب أن تكون جميع الـ providers المُوَلَّدة مسبوقة بـ |
| الدالة التي تحمل التعليق التوضيحي (Annotated function) | يحدد اسم الدالة التي تحمل التعليق التوضيحي (annotated function) كيفية التفاعل مع الـ provider.
فبالنسبة لدالة معينة باسم يجب أن تحدد الدوال التي تحمل التعليق التوضيحي Ref كأول معامل (parameter).
بالإضافة إلى ذلك، يمكن أن تحتوي الدالة على أي عدد من المعاملات، بما في ذلك الـ generics.
كما أن للدالة الحرية في إرجاع سيتم استدعاء هذه الدالة عند قراءة الـ provider لأول مرة. أما القراءات اللاحقة فلن تستدعي الدالة مرة أخرى، بل ستقوم بإرجاع القيمة المخزنة مؤقتاً (cached value) بدلاً من ذلك. |
| Ref | كائن يُستخدم للتفاعل مع الـ providers الأخرى. تمتلك جميع الـ providers واحداً؛ إما كمعامل (parameter) لدالة الـ provider، أو كخاصية (property) تابعة لـ Notifier. يتم تحديد نوع هذا الكائن بناءً على اسم الدالة/الفئة (function/class). |
- riverpod
- riverpod_generator
final name = SomeNotifierProvider.someModifier<MyNotifier, Result>(MyNotifier.new); class MyNotifier extends SomeNotifier<Result> { @override Result build() { //ضع المنطق الخاص بك هنا } //الدوال البقية هنا }
| متغير الـ provider | هذا المتغير هو ما سيتم استخدامه للتفاعل مع الـ provider.
يجب أن يكون المتغير ملاحظة لا تقلق من كون الـ providers عامة (global). فالـ Providers غير قابلة للتغيير (immutable) تماماً. وتعريف الـ provider لا يختلف عن تعريف أي دالة، كما أن الـ providers قابلة للاختبار والصيانة. |
| نوع الـ provider | بشكل عام، يكون إما NotifierProvider، أو AsyncNotifierProvider، أو StreamNotifierProvider.
يعتمد نوع الـ provider المُستخدم على القيمة المُرجعة (return value) من الدالة الخاصة بك.
على سبيل المثال، لإنشاء يُعد AsyncNotifierProvider هو النوع الذي سترغب في استخدامه في الغالب. نصيحة كما هو الحال مع الـ providers الوظيفية (functional providers)، لا تفكر من منطلق "أي provider يجب أن أختار". قم بإنشاء أي حالة (state) تريد إنشاءها، وسيتحدد نوع الـ provider بشكل طبيعي تبعاً لذلك. |
| المُعدِّلات (اختياري) | غالباً، قد ترى "مُعدِّلاً" (modifier) بعد نوع الـ provider. هذه المُعدِّلات اختيارية، وتُستخدم لضبط سلوك الـ provider بطريقة آمنة من حيث النوع (type-safe). يتوفر حالياً مُعدِّلان (modifiers):
|
| باني الـ Notifier | المعامل (parameter) الخاص بـ "notifier providers" هو دالة يُتوقع منها إنشاء (instantiate) الـ "notifier". وبشكل عام، يجب أن تكون عبارة عن "constructor tear-off". |
| The Notifier | إذا كان هذه الفئة مسؤولة عن إتاحة طرق لتعديل حالة الـ provider.
يمكن للمستهلكين الوصول إلى الدوال العامة (public methods) في هذه الفئة باستخدام تحذير لا تضع أي منطق برمجي (logic) داخل باني (constructor) الـ notifier الخاص بك.
يجب ألا تحتوي الـ Notifiers على باني، لأن |
| نوع الـ Notifier | يجب أن تتطابق الفئة الأساسية (base class) التي يمددها (extends) الـ notifier الخاص بك مع نوع الـ provider + "family"، إذا كان مُستخدماً. فيما يلي بعض الأمثلة:
|
| دالة build | يجب على جميع الـ notifiers إعادة تعريف (override) دالة ينبغي عدم استدعاء هذه الدالة بشكل مباشر. |
@riverpod class MyNotifier extends _$MyNotifier { @override Result build() { //ضع المنطق الخاص بك هنا } <your methods here> }
| التعليق التوضيحي (Annotation) | يجب أن تكون جميع الـ providers المُوَلَّدة مسبوقة بـ |
| الNotifier | عند وضع التعليق التوضيحي (annotation) `@riverpod` على فئة (class)، تُسمى تلك الفئة "Notifier". يجب أن تمدد (extend) الفئة `_$NotifierName`، حيث `NotifierName` هو اسم الفئة. الـ Notifiers مسؤولة عن إتاحة طرق لتعديل حالة الـ provider.
يمكن للمستهلكين الوصول إلى الدوال العامة (public methods) في هذه الفئة باستخدام تحذير لا تضع أي منطق برمجي داخل باني (constructor) الـ notifier الخاص بك.
يجب ألا تحتوي الـ Notifiers على باني، لأن |
| دالة build | يجب على جميع الـ notifiers إعادة تعريف (override) دالة ينبغي عدم استدعاء هذه الدالة بشكل مباشر. |
يمكنك تعريف أي عدد تريده من الـ providers دون قيود.
على عكس استخدام package:provider، تتيح Riverpod إنشاء عدة providers تعرض حالة (state) من نفس "النوع" (type):
- riverpod
- riverpod_generator
final cityProvider = Provider((ref) => 'بغداد');
final countryProvider = Provider((ref) => 'العراق');
String city(Ref ref) => 'بغداد';
String country(Ref ref) => 'العراق';
حقيقة أن كلا الـ providers ينشئان String لا تسبب أي مشكلة.
استخدام الـ providers
تشبه الـ Providers الـ widgets في أنها، بحد ذاتها، لا تفعل شيئاً.
وتماما كما أن الـ widgets هي وصف لواجهة المستخدم (UI)، فإن الـ providers هي وصف للحالة (state).
والمثير للدهشة أن الـ provider بحد ذاته عديم الحالة (stateless) تماماً، وكان من الممكن إنشاؤه كـ const لولا أن ذلك سيجعل بناء الجملة (syntax) أكثر إطالة وتعقيداً بعض الشيء.
لاستخدام الـ provider، تحتاج إلى كائن منفصل: ProviderContainer. راجع ProviderContainers/ProviderScopes لمزيد من المعلومات.
باختصار، قبل أن تتمكن من استخدام الـ provider، قم بتغليف تطبيق Flutter الخاص بك بـ ProviderScope:
void main() {
runApp(ProviderScope(child: MyApp()));
}
بمجرد الانتهاء من ذلك، ستحتاج إلى الحصول على Ref للتفاعل مع الـ providers الخاصة بك. راجع Refs للحصول على معلومات حول ذلك.
باختصار، هناك طريقتان للحصول على Ref:
- تحصل الـ Providers بشكل طبيعي على إمكانية الوصول إليه.
هذا هو المعامل (parameter) الأول لدالة الـ provider، أو خاصية
refالتابعة لـ Notifier. وهذا يُمكن الـ providers من التواصل مع بعضها البعض. - ستحتاج شجرة الـ Widget إلى نوع خاص من الـ widgets، يُسمى Consumers. تقوم هذه الـ widgets بسد الفجوة بين شجرة الـ widget وشجرة الـ provider، عن طريق منحك WidgetRef.
كمثال، لنفترض وجود helloWorldProvider يُرجع سلسلة نصية (string) بسيطة.
يمكنك استخدامه داخل الـ widgets بهذا الشكل:
class Example extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, _) {
// الحصول على قيمة الـ provider
final helloWorld = ref.watch(helloWorldProvider);
// استخدام القيمة في واجهة المستخدم (UI)
return Text(helloWorld);
},
);
}
}