تخطَّ إلى المحتوى

الأسئلة الشائعة (FAQs)

عدم تحديث الحالة (State Not Updating)

Section titled “عدم تحديث الحالة (State Not Updating)”

سؤال: أقوم بإصدار حالة (emitting a state) في الـ bloc الخاص بي ولكن واجهة المستخدم (UI) لا تتحدث. ما الخطأ الذي أرتكبه؟

💡 إجابة: إذا كنت تستخدم Equatable، فتأكد من تمرير جميع الخصائص إلى مُحصّل الخصائص (props getter).

صحيح (GOOD)

my_state.dart
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)

my_state.dart
sealed class MyState extends Equatable {
const MyState();
}
final class StateA extends MyState {
final String property;
const StateA(this.property);
@override
List<Object> get props => [];
}
my_state.dart
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)

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// always create a new instance of the state you are going to yield
emit(state.copyWith(property: event.property));
});
}
my_bloc.dart
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)

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
// never modify/mutate state
state.property = event.property;
// never emit the same instance of state
emit(state);
});
}

سؤال: متى يجب علي استخدام Equatable؟

💡إجابة:

my_bloc.dart
MyBloc() {
on<MyEvent>((event, emit) {
emit(StateA('hi'));
emit(StateA('hi'));
});
}

في السيناريو أعلاه، إذا كانت StateA ترث من Equatable، فسيحدث تغيير واحد فقط في الحالة (سيتم تجاهل الإصدار الثاني). بشكل عام، يجب عليك استخدام Equatable إذا كنت ترغب في تحسين الكود الخاص بك لتقليل عدد عمليات إعادة البناء. يجب ألا تستخدم Equatable إذا كنت تريد أن تؤدي نفس الحالة المتتالية إلى تشغيل انتقالات متعددة.

بالإضافة إلى ذلك، فإن استخدام Equatable يجعل اختبار الـ blocs أسهل بكثير حيث يمكننا توقع مثيلات محددة لحالات الـ bloc بدلاً من استخدام Matchers أو Predicates.

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
MyStateA(),
MyStateB(),
],
);

بدون Equatable، سيفشل الاختبار أعلاه وسيحتاج إلى إعادة كتابته على النحو التالي:

my_bloc_test.dart
blocTest(
'...',
build: () => MyBloc(),
act: (bloc) => bloc.add(MyEvent()),
expect: [
isA<MyStateA>(),
isA<MyStateB>(),
],
);

التعامل مع الأخطاء (Handling Errors)

Section titled “التعامل مع الأخطاء (Handling Errors)”

سؤال: كيف يمكنني التعامل مع خطأ مع الاستمرار في عرض البيانات السابقة؟

💡 إجابة:

يعتمد هذا بشكل كبير على كيفية نمذجة حالة الـ bloc. في الحالات التي يجب فيها الاحتفاظ بالبيانات حتى في حالة وجود خطأ، فكر في استخدام فئة حالة واحدة.

my_state.dart
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 للاحتفاظ بالبيانات القديمة حتى عند حدوث خطأ.

my_bloc.dart
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؟

💡 إجابة:

BLoC هو نمط تصميم (design pattern) يتم تعريفه بالقواعد التالية:

  1. مدخلات ومخرجات الـ BLoC هي مجرد تدفقات (Streams) ومصارف (Sinks).
  2. يجب أن تكون التبعيات قابلة للحقن (injectable) ومستقلة عن المنصة (Platform agnostic).
  3. لا يُسمح بالتفرع الخاص بالمنصة (platform branching).
  4. يمكن أن يكون التنفيذ أي شيء تريده طالما أنك تتبع القواعد المذكورة أعلاه.

إرشادات واجهة المستخدم (UI) هي:

  1. كل مكون “معقد بما فيه الكفاية” لديه BLoC مقابل.
  2. يجب أن ترسل المكونات المدخلات “كما هي”.
  3. يجب أن تعرض المكونات المخرجات أقرب ما يمكن إلى “كما هي”.
  4. يجب أن يعتمد كل التفرع على مخرجات BLoC المنطقية (boolean) البسيطة.

تنفذ مكتبة Bloc نمط تصميم BLoC وتهدف إلى تجريد RxDart لتبسيط تجربة المطور.

المبادئ الثلاثة لـ Redux هي:

  1. مصدر واحد للحقيقة (Single source of truth).
  2. الحالة للقراءة فقط (State is read-only).
  3. يتم إجراء التغييرات باستخدام دوال نقية (pure functions).

تنتهك مكتبة bloc المبدأ الأول؛ ففي bloc، يتم توزيع الحالة عبر عدة blocs. علاوة على ذلك، لا يوجد مفهوم للـ middleware في bloc، وتم تصميم bloc لجعل تغييرات الحالة غير المتزامنة (async state changes) سهلة للغاية، مما يسمح لك بإصدار حالات متعددة لحدث واحد.

سؤال: ما الفرق بين 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)

my_widget.dart
@override
Widget 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);
...
},
)
...
}
}
my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: Builder(
builder: (context) => ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
},
),
),
);
}

خاطئ (BAD)

my_widget.dart
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => BlocA(),
child: ElevatedButton(
onPressed: () {
final blocA = BlocProvider.of<BlocA>(context);
...
}
)
);
}

هيكلة المشروع (Project Structure)

Section titled “هيكلة المشروع (Project Structure)”

سؤال: كيف يجب أن أقوم بهيكلة مشروعي؟

💡 إجابة: على الرغم من عدم وجود إجابة صحيحة/خاطئة حقًا لهذا السؤال، فإن بعض المراجع الموصى بها هي:

الشيء الأكثر أهمية هو أن يكون لديك هيكل مشروع متسق و مقصود.

سؤال: هل من المقبول إضافة الأحداث داخل bloc؟

💡 إجابة: في معظم الحالات، يجب إضافة الأحداث خارجيًا، ولكن في بعض الحالات المختارة قد يكون من المنطقي إضافة الأحداث داخليًا.

الحالة الأكثر شيوعًا التي تُستخدم فيها الأحداث الداخلية هي عندما يجب أن تحدث تغييرات الحالة استجابةً للتحديثات في الوقت الفعلي من مستودع (repository). في هذه الحالات، يكون المستودع هو المحفز لتغيير الحالة بدلاً من حدث خارجي مثل النقر على زر.

في المثال التالي، تعتمد حالة MyBloc على المستخدم الحالي الذي يتم كشفه عبر Stream<User> من UserRepository. يستمع MyBloc للتغييرات في المستخدم الحالي ويضيف حدثًا داخليًا _UserChanged كلما تم إصدار مستخدم من تدفق المستخدم.

my_bloc.dart
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 نفسه ويمنع المكونات الخارجية من معرفة هذا الحدث.

my_event.dart
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 للتعامل مع التفاعل مع تحديثات المستخدم في الوقت الفعلي:

my_bloc.dart
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.