الأسئلة الشائعة
إليك بعض الأسئلة الشائعة من المجتمع:
ما هي الاختلاف بين ref.refresh و ref.invalidate؟
ربما لاحظت أن ref يوفر طريقتين لإجبار الـ provider على إعادة الحساب، وتتساءل عن الفرق بينهما.
الأمر أبسط مما تعتقد: ref.refresh ليست سوى صياغة مختصرة (syntax sugar) لـ invalidate + read:
T refresh<T>(provider) {
invalidate(provider);
return read(provider);
}
إذا لم تكن مهتماً بالقيمة الجديدة للـ provider بعد إعادة حسابه، فإن invalidate هو الخيار الأنسب.
أما إذا كنت مهتماً بها، فاستخدم refresh بدلاً من ذلك.
يتم تطبيق هذا المنطق تلقائياً من خلال قواعد التدقيق (lint rules).
فإذا حاولت استخدام ref.refresh دون استخدام القيمة المُرجعة، سيظهر لك تحذير.
يكمن الفرق الرئيسي في السلوك في أن قراءة الـ provider مباشرة بعد إبطال صلاحيته (invalidating) تجعله يعيد الحساب فوراً.
بينما لو قمنا باستدعاء invalidate ولم نقم بقراءته مباشرة بعدها، فإن التحديث سيتم تفعيله لاحقاً.
ذلك التحديث "اللاحق" يحدث عادةً عند بداية الإطار (frame) التالي. ومع ذلك، إذا تم إبطال صلاحية provider لا يتم الاستماع إليه حالياً، فلن يتم تحديثه إلا عند الاستماع إليه مجدداً.
لماذا لا توجد واجهة مشتركة بين Ref و WidgetRef؟
تقوم Riverpod بفصل Ref عن WidgetRef بشكل متعمد.
ويتم ذلك عن قصد لتجنب كتابة كود يعتمد بشكل شرطي على أحدهما أو الآخر.
إحدى المشكلات هي أن Ref و WidgetRef، رغم تشابههما الظاهري، إلا أنهما يحتويان على اختلافات دقيقة.
فالكود الذي يعتمد على كليهما سيكون غير موثوق بطرق يصعب رصدها.
في الوقت نفسه، فإن الاعتماد على WidgetRef يعادل الاعتماد على BuildContext.
وهذا يعني عملياً وضع المنطق البرمجي (logic) الخاص بك في طبقة واجهة المستخدم (UI layer)، وهو أمر غير مستحسن.
يجب إعادة هيكلة مثل هذا الكود ليستخدم Ref دائماً.
الحل لهذه المشكلة يكون عادةً بنقل المنطق الخاص بك إلى Notifier (راجع مفاهيم Providers)، ومن ثم جعل هذا المنطق عبارة عن دالة (method) داخل ذلك الـ Notifier.
بهذه الطريقة، عندما تريد الـ widgets لديك استدعاء هذا المنطق، يمكنها كتابة شيء مشابه لما يلي:
ref.read(yourNotifierProvider.notifier).yourMethod();
ستقوم yourMethod باستخدام الـ Ref الخاص بالـ Notifier للتفاعل مع الـ providers الأخرى.
لماذا نحتاج إلى تمديد ConsumerWidget بدلاً من استخدام StatelessWidget العادي؟
يرجع هذا إلى قيد مؤسف في الـ API الخاص بـ InheritedWidget.
هناك عدة مشاكل:
- ليس من الممكن تنفيذ مستمع "عند التغيير" (on change listener) باستخدام
InheritedWidget. هذا يعني أن شيئاً مثلref.listenلا يمكن استخدامه معBuildContext. تعتبرState.didChangeDependenciesأقرب شيء لذلك، لكنها غير موثوقة. إحدى المشكلات هي أن دورة الحياة (life-cycle) قد يتم تفعيلها حتى لو لم تتغير أي تبعية (dependency)، خاصة إذا كانت شجرة الودجت لديك تستخدم GlobalKeys (وبعض ودجات Flutter تفعل ذلك داخلياً بالفعل). - الودجات التي تستمع إلى
InheritedWidgetلا تتوقف أبداً عن الاستماع إليه. عادة ما يكون هذا مقبولاً للبيانات الوصفية البحتة (pure metadata)، مثل "السمة" (theme) أو "استعلام الوسائط" (media query). لكن بالنسبة لمنطق الأعمال (business logic)، هذه مشكلة. لنفترض أنك تستخدم provider لتمثيل API مقسم إلى صفحات (paginated API). عندما تتغير الصفحة الحالية (page offset)، لن ترغب في أن يستمر الودجت الخاص بك في الاستماع إلى الصفحات التي كانت ظاهرة سابقاً. - لا يمتلك
InheritedWidgetطريقة لتتبع متى تتوقف الودجات عن الاستماع إليه. تعتمد Riverpod أحياناً على تتبع ما إذا كان الـ provider مُستمعاً إليه أم لا.
هذه الوظيفة حاسمة لكل من آلية الإلغاء التلقائي (auto dispose) والقدرة على تمرير الوسائط (arguments) إلى الـ providers. هذه الميزات هي ما تجعل Riverpod قوياً للغاية.
ربما في مستقبل بعيد، سيتم إصلاح هذه المشاكل. في تلك الحالة، قد تنتقل Riverpod لاستخدام BuildContext بدلاً من Ref.
وهذا سيمكننا من استخدام StatelessWidget بدلاً من ConsumerWidget.
لكن هذا حديث لوقت آخر!
لماذا لا تقوم hooks_riverpod بتصدير flutter_hooks؟
يتم ذلك مراعاةً لممارسات إدارة الإصدارات الجيدة.
فعلى الرغم من أنك لا تستطيع استخدام hooks_riverpod بدون flutter_hooks، إلا أن إدارة إصدارات الحزمتين تتم بشكل مستقل. فقد يحدث تغيير جذري (breaking change) في إحداهما دون الأخرى.
هل توجد طريقة لإعادة تعيين جميع الـ providers في آن واحد؟
لا، لا توجد طريقة لإعادة تعيين جميع الـ providers في آن واحد.
هذا الأمر مقصود، حيث يُعتبر ذلك نمطاً سلبياً (anti-pattern). فإعادة تعيين جميع الـ providers دفعة واحدة غالباً ما تؤدي إلى إعادة تعيين providers لم تكن تنوي إعادة تعيينها.
يشيع هذا السؤال بين المستخدمين الذين يرغبون في إعادة تعيين حالة تطبيقهم عند تسجيل خروج المستخدم.
إذا كان هذا هو هدفك، فيجب عليك بدلاً من ذلك جعل كل ما يعتمد على حالة المستخدم يقوم بمراقبة (ref.watch) الـ provider الخاص بالمستخدم (user provider).
عندها، وبمجرد تسجيل خروج المستخدم، ستتم إعادة تعيين جميع الـ providers التي تعتمد عليه تلقائياً، بينما سيبقى كل شيء آخر على حاله.
يظهر لي الخطأ "Using "ref" when a widget is about to or has been unmounted is unsafe." بعد التخلص من الـ widget (disposed)، ما المشكلة؟
قد تظهر لك أيضاً الرسالة "Bad state: No ProviderScope found"، وهي رسالة خطأ قديمة لنفس المشكلة.
يحدث هذا الخطأ عند محاولة استخدام ref داخل widget لم يعد مثبتاً (no longer mounted). ويحدث هذا عادةً بعد استخدام await:
ElevatedButton(
onPressed: () async {
await future;
// قد يرمي الاستثناء
// "استخدام 'ref' عندما يكون الـ widget
//على وشك الإزالة أو تمت إزالته بالفعل هو أمر غير آمن."
ref.read(...);
}
)
الحل هو أن تتحقق من mounted قبل استخدام ref، تماماً كما تفعل مع BuildContext:
ElevatedButton(
onPressed: () async {
await future;
if (!context.mounted) return;
ref.read(...); //لن يرمي الاستثناء بعد الآن.
}
)