الأسئلة الشائعة (FAQs)
عدم تحديث الحالة (State Not Updating)
Section titled “عدم تحديث الحالة (State Not Updating)”❔ سؤال: أقوم بإصدار حالة (emitting a state) في الـ bloc الخاص بي ولكن واجهة المستخدم (UI) لا تتحدث. ما الخطأ الذي أرتكبه؟
💡 إجابة: إذا كنت تستخدم Equatable، فتأكد من تمرير جميع الخصائص إلى مُحصّل
الخصائص (props getter).
✅ صحيح (GOOD)
sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => [property]; // pass all properties to props}❌ خاطئ (BAD)
sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => [];}sealed class MyState extends Equatable { const MyState();}
final class StateA extends MyState { final String property;
const StateA(this.property);
@override List<Object> get props => null;}بالإضافة إلى ذلك، تأكد من أنك تصدر مثيلاً جديدًا للحالة (new instance of the state) في الـ bloc الخاص بك.
✅ صحيح (GOOD)
MyBloc() { on<MyEvent>((event, emit) { // always create a new instance of the state you are going to yield emit(state.copyWith(property: event.property)); });}MyBloc() { on<MyEvent>((event, emit) { final data = _getData(event.info); // always create a new instance of the state you are going to yield emit(MyState(data: data)); });}❌ خاطئ (BAD)
MyBloc() { on<MyEvent>((event, emit) { // never modify/mutate state state.property = event.property; // never emit the same instance of state emit(state); });}متى يجب استخدام Equatable
Section titled “متى يجب استخدام Equatable”❔سؤال: متى يجب علي استخدام Equatable؟
💡إجابة:
MyBloc() { on<MyEvent>((event, emit) { emit(StateA('hi')); emit(StateA('hi')); });}في السيناريو أعلاه، إذا كانت StateA ترث من Equatable، فسيحدث تغيير واحد فقط
في الحالة (سيتم تجاهل الإصدار الثاني). بشكل عام، يجب عليك استخدام Equatable
إذا كنت ترغب في تحسين الكود الخاص بك لتقليل عدد عمليات إعادة البناء. يجب ألا
تستخدم Equatable إذا كنت تريد أن تؤدي نفس الحالة المتتالية إلى تشغيل انتقالات
متعددة.
بالإضافة إلى ذلك، فإن استخدام Equatable يجعل اختبار الـ blocs أسهل بكثير حيث
يمكننا توقع مثيلات محددة لحالات الـ bloc بدلاً من استخدام Matchers أو
Predicates.
blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ MyStateA(), MyStateB(), ],);بدون Equatable، سيفشل الاختبار أعلاه وسيحتاج إلى إعادة كتابته على النحو
التالي:
blocTest( '...', build: () => MyBloc(), act: (bloc) => bloc.add(MyEvent()), expect: [ isA<MyStateA>(), isA<MyStateB>(), ],);التعامل مع الأخطاء (Handling Errors)
Section titled “التعامل مع الأخطاء (Handling Errors)”❔ سؤال: كيف يمكنني التعامل مع خطأ مع الاستمرار في عرض البيانات السابقة؟
💡 إجابة:
يعتمد هذا بشكل كبير على كيفية نمذجة حالة الـ bloc. في الحالات التي يجب فيها الاحتفاظ بالبيانات حتى في حالة وجود خطأ، فكر في استخدام فئة حالة واحدة.
enum Status { initial, loading, success, failure }
class MyState { const MyState({ this.data = Data.empty, this.error = '', this.status = Status.initial, });
final Data data; final String error; final Status status;
MyState copyWith({Data data, String error, Status status}) { return MyState( data: data ?? this.data, error: error ?? this.error, status: status ?? this.status, ); }}سيسمح هذا للويدجتات بالوصول إلى خصائص data و error في وقت واحد، ويمكن للـ
bloc استخدام state.copyWith للاحتفاظ بالبيانات القديمة حتى عند حدوث خطأ.
on<DataRequested>((event, emit) { try { final data = await _repository.getData(); emit(state.copyWith(status: Status.success, data: data)); } catch(error) { emit(state.copyWith(status: Status.failure, error: 'Something went wrong!')); }});Bloc مقابل Redux
Section titled “Bloc مقابل Redux”❔ سؤال: ما الفرق بين Bloc و Redux؟
💡 إجابة:
BLoC هو نمط تصميم (design pattern) يتم تعريفه بالقواعد التالية:
- مدخلات ومخرجات الـ BLoC هي مجرد تدفقات (Streams) ومصارف (Sinks).
- يجب أن تكون التبعيات قابلة للحقن (injectable) ومستقلة عن المنصة (Platform agnostic).
- لا يُسمح بالتفرع الخاص بالمنصة (platform branching).
- يمكن أن يكون التنفيذ أي شيء تريده طالما أنك تتبع القواعد المذكورة أعلاه.
إرشادات واجهة المستخدم (UI) هي:
- كل مكون “معقد بما فيه الكفاية” لديه BLoC مقابل.
- يجب أن ترسل المكونات المدخلات “كما هي”.
- يجب أن تعرض المكونات المخرجات أقرب ما يمكن إلى “كما هي”.
- يجب أن يعتمد كل التفرع على مخرجات BLoC المنطقية (boolean) البسيطة.
تنفذ مكتبة Bloc نمط تصميم BLoC وتهدف إلى تجريد RxDart لتبسيط تجربة المطور.
المبادئ الثلاثة لـ Redux هي:
- مصدر واحد للحقيقة (Single source of truth).
- الحالة للقراءة فقط (State is read-only).
- يتم إجراء التغييرات باستخدام دوال نقية (pure functions).
تنتهك مكتبة bloc المبدأ الأول؛ ففي bloc، يتم توزيع الحالة عبر عدة blocs. علاوة على ذلك، لا يوجد مفهوم للـ middleware في bloc، وتم تصميم bloc لجعل تغييرات الحالة غير المتزامنة (async state changes) سهلة للغاية، مما يسمح لك بإصدار حالات متعددة لحدث واحد.
Bloc مقابل Provider
Section titled “Bloc مقابل Provider”❔ سؤال: ما الفرق بين Bloc و Provider؟
💡 إجابة: تم تصميم provider لحقن التبعية (Dependency Injection) (فهو يغلف
InheritedWidget). لا يزال يتعين عليك معرفة كيفية إدارة حالتك (عبر
ChangeNotifier، Bloc، Mobx، وما إلى ذلك…). تستخدم مكتبة Bloc حزمة
provider داخليًا لتسهيل توفير الـ blocs والوصول إليها في جميع أنحاء شجرة الـ
Widgets.
فشل BlocProvider.of() في العثور على Bloc
Section titled “فشل BlocProvider.of() في العثور على Bloc”❔ سؤال: عند استخدام BlocProvider.of(context)، لا يمكنه العثور على الـ
bloc. كيف يمكنني إصلاح ذلك؟
💡 إجابة: لا يمكنك الوصول إلى bloc من نفس السياق الذي تم توفيره فيه، لذا يجب
عليك التأكد من استدعاء BlocProvider.of() ضمن سياق بناء فرعي (BuildContext
child).
✅ صحيح (GOOD)
@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: MyChild(); );}
class MyChild extends StatelessWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... }, ) ... }}@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: Builder( builder: (context) => ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... }, ), ), );}❌ خاطئ (BAD)
@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => BlocA(), child: ElevatedButton( onPressed: () { final blocA = BlocProvider.of<BlocA>(context); ... } ) );}هيكلة المشروع (Project Structure)
Section titled “هيكلة المشروع (Project Structure)”❔ سؤال: كيف يجب أن أقوم بهيكلة مشروعي؟
💡 إجابة: على الرغم من عدم وجود إجابة صحيحة/خاطئة حقًا لهذا السؤال، فإن بعض المراجع الموصى بها هي:
الشيء الأكثر أهمية هو أن يكون لديك هيكل مشروع متسق و مقصود.
إضافة الأحداث داخل Bloc
Section titled “إضافة الأحداث داخل Bloc”❔ سؤال: هل من المقبول إضافة الأحداث داخل bloc؟
💡 إجابة: في معظم الحالات، يجب إضافة الأحداث خارجيًا، ولكن في بعض الحالات المختارة قد يكون من المنطقي إضافة الأحداث داخليًا.
الحالة الأكثر شيوعًا التي تُستخدم فيها الأحداث الداخلية هي عندما يجب أن تحدث تغييرات الحالة استجابةً للتحديثات في الوقت الفعلي من مستودع (repository). في هذه الحالات، يكون المستودع هو المحفز لتغيير الحالة بدلاً من حدث خارجي مثل النقر على زر.
في المثال التالي، تعتمد حالة MyBloc على المستخدم الحالي الذي يتم كشفه عبر
Stream<User> من UserRepository. يستمع MyBloc للتغييرات في المستخدم الحالي
ويضيف حدثًا داخليًا _UserChanged كلما تم إصدار مستخدم من تدفق المستخدم.
class MyBloc extends Bloc<MyEvent, MyState> { MyBloc({required UserRepository userRepository}) : super(...) { on<_UserChanged>(_onUserChanged); _userSubscription = userRepository.user.listen( (user) => add(_UserChanged(user)), ); }}من خلال إضافة حدث داخلي، يمكننا أيضًا تحديد transformer مخصص للحدث لتحديد
كيفية معالجة أحداث _UserChanged المتعددة - سيتم معالجتها بالتوازي افتراضيًا.
يوصى بشدة بأن تكون الأحداث الداخلية خاصة (private). هذه طريقة واضحة للإشارة إلى أن حدثًا معينًا يُستخدم فقط داخل الـ bloc نفسه ويمنع المكونات الخارجية من معرفة هذا الحدث.
sealed class MyEvent {}
// `EventA` is an external event.final class EventA extends MyEvent {}
// `EventB` is an internal event.// We are explicitly making `EventB` private so that it can only be used// within the bloc.final class _EventB extends MyEvent {}يمكننا بدلاً من ذلك تعريف حدث خارجي Started واستخدام واجهة برمجة التطبيقات
emit.forEach للتعامل مع التفاعل مع تحديثات المستخدم في الوقت الفعلي:
class MyBloc extends Bloc<MyEvent, MyState> { MyBloc({required UserRepository userRepository}) : _userRepository = userRepository, super(...) { on<Started>(_onStarted); }
Future<void> _onStarted(Started event, Emitter<MyState> emit) { return emit.forEach( _userRepository.user, onData: (user) => MyState(...) ); }}فوائد هذا النهج هي:
- لا نحتاج إلى حدث داخلي
_UserChanged. - لا نحتاج إلى إدارة
StreamSubscriptionيدويًا. - لدينا سيطرة كاملة على متى يشترك الـ bloc في تدفق تحديثات المستخدم.
عيوب هذا النهج هي:
- لا يمكننا بسهولة
pauseأوresumeالاشتراك. - نحتاج إلى كشف حدث عام
Startedيجب إضافته خارجيًا. - لا يمكننا استخدام
transformerمخصص لضبط كيفية تفاعلنا مع تحديثات المستخدم.
كشف الدوال العامة (Exposing Public Methods)
Section titled “كشف الدوال العامة (Exposing Public Methods)”❔ سؤال: هل من المقبول كشف دوال عامة (public methods) على مثيلات bloc و cubit الخاصة بي؟
💡 إجابة
عند إنشاء cubit، يوصى بكشف الدوال العامة فقط لغرض تشغيل تغييرات الحالة. ونتيجة
لذلك، يجب أن تعيد جميع الدوال العامة على مثيل cubit بشكل عام void أو
Future<void>.
عند إنشاء bloc، يوصى بتجنب كشف أي دوال عامة مخصصة وبدلاً من ذلك إخطار الـ bloc
بالأحداث عن طريق استدعاء add.