انتقل إلى المحتوى الرئيسي

مفاهيم 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 الوصول إلى نفس القيمة:

// المعادل لدالة 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);
});

بالإضافة إلى التخزين المؤقت الأساسي، تضيف الـ 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)FutureStream
غير قابل للتعديل (Unmodifiable)ProviderFutureProviderStreamProvider
قابل للتعديل (Modifiable)NotifierProviderAsyncNotifierProviderStreamNotifierProvider

قد يبدو هذا مربكاً في البداية. دعونا نفصل الأمر ونبسطه.

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 على ما إذا كان "قابلاً للتعديل" أو "غير قابل للتعديل"، كما هو موضح في الجدول أعلاه.

final name = SomeProvider.someModifier<Result>((ref) {
  //ضع المنطق الخاص بك هنا
});
متغير الـ provider

هذا المتغير هو ما سيتم استخدامه للتفاعل مع الـ provider الخاص بنا. يجب أن يكون المتغير final و "top-level" (عاماً).

ملاحظة

لا تدع كون الـ providers عامة (global) يخيفك. فالـ Providers غير قابلة للتغيير (immutable) تماماً. وتعريف الـ provider لا يختلف عن تعريف أي دالة، كما أن الـ providers قابلة للاختبار والصيانة.

نوع الـ provider

بشكل عام، يكون إما Provider، أو FutureProvider، أو StreamProvider. يعتمد نوع الـ provider المُستخدم على القيمة المُرجعة (return value) من الدالة الخاصة بك. على سبيل المثال، لإنشاء Future<Activity>، ستحتاج إلى استخدام FutureProvider<Activity>.

يُعد FutureProvider هو النوع الذي سترغب في استخدامه في الغالب.

نصيحة

لا تفكر من منطلق "أي provider يجب أن أختار". بدلاً من ذلك، فكر من منطلق "ماذا أريد أن أُرجع (return)". وسيتحدد نوع الـ provider بشكل طبيعي تبعاً لذلك.

المُعدِّلات (اختياري)

غالباً، قد ترى "مُعدِّلاً" (modifier) بعد نوع الـ provider. هذه المُعدِّلات اختيارية، وتُستخدم لضبط سلوك الـ provider بطريقة آمنة من حيث النوع (type-safe).

يتوفر حالياً مُعدِّلان (modifiers):

  • autoDispose، والذي سيقوم بمسح التخزين المؤقت (cache) تلقائياً عندما يتوقف استخدام الـ provider. راجع أيضاً Automatic disposal
  • family، والذي يتيح تمرير وسائط (arguments) إلى الـ provider الخاص بك. راجع أيضاً Family.
Ref

كائن يُستخدم للتفاعل مع الـ providers الأخرى. جميع الـ providers لديها واحد؛ إما كمعامل (parameter) لدالة الـ provider، أو كخاصية (property) تابعة لـ Notifier.

دالة الـ provider

هنا نضع المنطق الخاص بالـ providers. سيتم استدعاء هذه الدالة عند قراءة الـ provider لأول مرة. أما القراءات اللاحقة فلن تستدعي الدالة مرة أخرى، بل ستقوم بإرجاع القيمة المخزنة مؤقتاً بدلاً من ذلك.

معلومات

يمكنك تعريف أي عدد تريده من الـ providers دون قيود. على عكس استخدام package:provider، تتيح Riverpod إنشاء عدة providers تعرض حالة (state) من نفس "النوع" (type):

final cityProvider = Provider((ref) => 'بغداد');
final countryProvider = Provider((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);
},
);
}
}