إدارة الحالة (BLoC & Cubit) State Management (BLoC & Cubit)
التحكم في تدفق البيانات وتحويل الأحداث إلى شاشات تفاعلية باستخدام مكتبة Bloc. Controlling data flow and transforming events into interactive screens using the Bloc library.
الـ BLoC (Business Logic Component) هو المعيار الذهبي في تطبيقات Flutter الكبيرة. فايدته إنه بيفصل "إيه اللي بيحصل" عن "إزاي بيتعرض".
BLoC is the gold standard in large Flutter apps. Its main benefit is separating "what happens" from "how it's displayed".
| Concept | Function (EN) | Benefit (AR) |
|---|---|---|
| Events | What happened in UI | اليوزر ضغط على زرار، أو الصفحة حملت. |
| States | What UI should show | عرض دائرة التحميل، عرض لستة البيانات، أو رسالة خطأ. |
| Streams | Data flow pipeline | توصيل التغييرات من الداتابيز للشاشة لحظياً. |
في MRE CashBook، إحنا بنتبع قاعدة صارمة: كل ميزة لازم تتقسم لـ 3 ملفات منفصلة. ده بيخلي الكود منظم جداً وسهل يتجرب (Testable).
بيحتوي على كل الـ Actions اللي ممكن تحصل (مثلاً: LoadBooks, AddBook).
بيوصف شكل الشاشة في كل حالة (مثلاً: BooksLoading, BooksLoaded).
ده المخ، اللي بياخد الـ Event ويحولها لـ State.
أقوى حاجة في الـ BLoC بتاعنا إنه "مشاكس"، دايماً مراقب الداتابيز. بص على الكود ده:
void _onLoadBooks(LoadBooks event, Emitter emit) {
emit(const BooksLoading());
_booksSubscription?.cancel();
_booksSubscription = _repository.watchBooks().listen(
(books) => add(BooksUpdated(books)),
);
}
البلوك هنا بيعمل "اشتراك" في ماسورة البيانات. أول ما الداتابيز تتغير، البلوك بيبعت لنفسه Event اسمه BooksUpdated وتلقائياً الشاشة بتحدث نفسها.
لو فتحت اشتراك (Subscription) ونسيت تقفله، الأبلكيشن هيحرق رامات الموبايل لغاية ما يقفل. عشان كدة لازم نستخدم close().
@override
Future close() {
_booksSubscription?.cancel(); // لازم تقفل الحنفية!
return super.close();
}
في المشروع ده، إحنا بنستخدم النوعين. كل واحد ليه وقته ومكانه:
- مناسب للعمليات المعقدة اللي فيها Events كتير.
- بيستخدم نظام الـ Streams داخلياً.
- مثال:
BooksBlocلأنه بيراقب الداتابيز وبيتفاعل مع أكتر من Event.
- مناسب للعمليات البسيطة (CRUD).
- بيعتمد على الـ Methods بدل الـ Events.
- مثال:
AddEntryCubitلأنه مجرد فورم بتتملي وبتتبعت.
نصيحة: ابدأ دايماً بـ Cubit. لو لقيت الكود بقى معقد ومحتاج تتحكم في الـ Events بشكل أدق، حوله لـ BLoC.
في MRE CashBook، بنستخدم الـ sealed classes عشان نضمن إننا مغطيين كل الاحتمالات الممكنة للشاشة.
sealed class BooksState extends Equatable {
const BooksState();
@override
List
عشان Flutter "ذكي". لو طلعنا نفس الـ State مرتين ورا بعض بنفس الداتا بالضبط، Equatable هتقول لـ Flutter: "متحركش الـ UI، مفيش حاجة جديدة حصلت". ده بيوفر "كهرباء" (Performance) كتير!
إزاي الشاشة بتعرف إن فيه BLoC أصلاً؟ عن طريق الـ BlocProvider و BlocBuilder.
BlocBuilder(
builder: (context, state) {
return switch (state) {
BooksLoading() => const AppLoadingWidget(),
BooksLoaded(books: var items) => BooksList(items: items),
BooksError() => ErrorWidget(state.message),
_ => const SizedBox.shrink(),
};
},
)
BooksLoaded مش بنرجع لنقطة الصفر لو الداتا اتحدثت. اليوزر بيفضل شايف اللستة القديمة لغاية ما اللستة الجديدة تجهز وتتبدل في أجزاء من الثانية. ده اللي بنسميه تجربة مستخدم "سلسة".تعالوا نتتبع اللي بيحصل لما اليوزر يضغط "إضافة كتاب":
اليوزر يضغط الزرار، الشاشة تنفذ: context.read<BooksBloc>().add(AddBookEvent(name: 'Work')).
البلوك يستقبل الـ Event وينفذ الـ Handler: _onAddBook.
البلوك يكلم الـ Repository: _repository.addBook(event.name).
الداتابيز تنفذ الـ SQL وتغير الملف.
الداتابيز تبعت "إشارة" للـ Stream اللي البلوك مشترك فيه، البلوك يبعت BooksUpdated، والشاشة تتحدث أوتوماتيك.
إيه الفرق بين إنك "تبني شاشة" (Build) وبين إنك "تطلع رسالة" (Action)؟
| Widget | Purpose (AR) | Purpose (EN) |
|---|---|---|
BlocBuilder |
بناء UI (مربعات، نصوص، ألوان). | Building UI components. |
BlocListener |
تنفيذ أوامر (Snackbar، التنقل، رسائل تنبيه). | Side effects (Navigation, Modals). |
BlocConsumer |
الاتنين مع بعض في نفس الوقت. | Both at once. |
BlocListener(
listener: (context, state) {
if (state is AddBookSuccess) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Success!')));
Navigator.pop(context); // قفل الصفحة بعد النجاح
}
},
child: const AddBookForm(),
)
عشان نتطمن إن البلوك شغال صح، بنستخدم مكتبة bloc_test. ده شكل الاختبار:
blocTest(
'يجب أن يخرج BooksLoaded عند استلام BooksUpdated',
build: () => BooksBloc(repository: mockRepo),
act: (bloc) => bloc.add(const BooksUpdated([mockBook])),
expect: () => [
isA().having((s) => s.books.length, 'length', 1),
],
);
الاختبار ده بيضمن لنا إن لو أي حد عدل في الكود وبوظ المنطق، التست هيفشل ونكتشف المشكلة فوراً.
تخيل يوزر ضغط على زرار "إضافة" 10 مرات في ثانية واحدة. لو معملناش حسابنا، الداتابيز هتتملي داتا مكررة. الـ BLoC بيحل ده بالـ Transformers.
import 'package:bloc_concurrency/bloc_concurrency.dart';
// في الـ Constructor بتاع البلوك
on(
_onAddBook,
transformer: droppable(), // "طنش" أي Event جديد لغاية ما القديم يخلص
);
| Transformer | Behavior (EN) | Use Case (AR) |
|---|---|---|
droppable() | Ignore new events while busy | منع تكرار الإضافة عند ضغط الزرار بسرعة. |
restartable() | Cancel old, start new | في البحث (Search) عشان نأخد أخر كلمة بس. |
sequential() | One after another | عمليات الدفع أو الحذف بالترتيب. |
لو عندك شاشة فيها 100 معلومة، والـ BLoC طلع State جديدة فيها معلومة واحدة بس اللي اتغيرت، ليه نعيد بناء الشاشة كلها؟ هنا بييجي دور الـ BlocSelector.
BlocSelector(
selector: (state) {
if (state is BooksLoaded) return state.isPrivacyModeOn;
return false;
},
builder: (context, isPrivacyModeOn) {
// الـ Widget ده مش هيعمل rebuild إلا لو الـ isPrivacyModeOn اتغيرت بس!
return PrivacyIcon(isOn: isPrivacyModeOn);
},
)
ده بيخلي الأبلكيشن "خفيف" جداً حتى على الموبايلات القديمة.
حاول تلاقي الغلطة في الكود ده:
الكود: emit(state);
الحل: الـ BLoC مش هيحدث الشاشة لو بعت نفس الـ Object. لازم تستخدم state.copyWith(...) عشان تطلع نسخة جديدة ببيانات مختلفة.
السيناريو: البلوك بيبعت Loading بس مش بيبعت Success أو Error.
الحل: دايماً استخدم try-catch وابعث ErrorState في الـ catch عشان الدائرة متفضلش تلف للأبد.
تخيل اليوز غير الـ Theme أو قفل الـ Privacy Mode، وقفل الأبلكيشن فتحه تاني. إزاي بنضمن إن الاختيارات دي متتمسحش؟ الحل هو Hydrated BLoC.
class SettingsBloc extends HydratedBloc {
// ...
@override
SettingsState? fromJson(Map json) => SettingsState.fromMap(json);
@override
Map? toJson(SettingsState state) => state.toMap();
}
دريفت بيخزن الداتا المهمة، بس Hydrated BLoC بيخزن "حالة الشاشة" في ملف JSON صغير. أول ما الأبلكيشن يفتح، البلوك بيرجع لآخر حالة كان عليها أوتوماتيك.
عشان نعرف إيه اللي بيحصل في كل الـ Blocs بتاعة الأبلكيشن في نفس الوقت، بنستخدم الـ BlocObserver. ده زي "كاميرا مراقبة" لكل الشاشات.
class AppBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
log('Change in ${bloc.runtimeType}: $change');
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
log('Error in ${bloc.runtimeType}: $error');
super.onError(bloc, error, stackTrace);
}
}
بمجرد ما نربطه في الـ main.dart، أي Error هيحصل في أي مكان في البرنامج هيظهر لنا في الـ Console فوراً مع اسم البلوك اللي حصل فيه المشكلة.
مش كل حاجة محتاجة BLoC. عشان تكون مهندس شاطر، لازم تختار الأداة الصح للمكان الصح:
| Level | Tool | Use Case (AR) |
|---|---|---|
| Local State | StatefulWidget | فورم بسيطة، أو زرار بيغير لونه. |
| Transient State | Cubit | صفحة واحدة فيها شوية Logic بسيط. |
| Feature State | BLoC | ميزة كاملة (زي الكتب) محتاجة تفاعل وتعقيد. |
| Global State | Provided Bloc | الثيم، اللغة، بيانات اليوزر الأساسية. |
إزاي بنستخرج الداتا من الـ BLoC جوه الـ Widgets؟ إحنا عندنا 3 طرق سحرية:
| Method | Utility (AR) | When to use (EN) |
|---|---|---|
context.read<T>() |
أمر "اقرأ مرة واحدة". | Inside button callbacks (onPressed). |
context.watch<T>() |
أمر "راقب التغيير". | Inside build method when UI depends on full state. |
context.select<T, R>() |
أمر "راقب حتة معينة". | When you only care about one field (Optimization). |
// ❌ غلط: متستخدمش watch جوه onPressed
onPressed: () => context.watch().add(LoadBooks()),
// ✅ صح: استخدم read جوه الـ callbacks
onPressed: () => context.read().add(LoadBooks()),
ليه في CashBook اختارنا BLoC بالذات؟ بص على المقارنة دي:
| Feature | BLoC | Provider | Riverpod |
|---|---|---|---|
| Learning Curve | Steep (صعب شوية) | Easy (سهل) | Medium (متوسط) |
| Safety | High (عالي جداً) | Low (قليل) | High (عالي) |
| Standardization | Very High (معياري) | Low (حر) | Medium (ناشئ) |
| Testing | Built-in (ممتاز) | Manual (يدوي) | Excellent (ممتاز) |
الـ BLoC بيكريه "نظام" (System) إجباري. وده اللي بنحتاجه في المشاريع اللي فيها مطورين كتير أو داتا مالية حساسة.
عشان نختبر الـ BLoC من غير ما نلمس الداتابيز الحقيقية، بنستخدم الـ Mocking. بنعمل "نسخة مزيفة" من الـ Repository ونقولها تتصرف إزاي.
import 'package:mocktail/mocktail.dart';
class MockBookRepository extends Mock implements IBookRepository {}
// جوه التست
final mockRepo = MockBookRepository();
when(() => mockRepo.watchBooks()).thenAnswer((_) => Stream.value([mockBook]));
ليه ده مهم؟ عشان التست يكون سريع جداً (بيخلص في أجزاء من الثانية) وميعتمدش على إن فيه ملفات موجودة على الهارد ولا لا.
أحياناً بنحتاج "نفلتر" الـ Events قبل ما تروح للـ Handler. مثلاً مش عاوزين نعمل LoadBooks لو هي أصلاً بتحمل حالياً.
void _onLoadBooks(LoadBooks event, Emitter emit) {
if (state is BooksLoading) return; // فلترة: لو بنحمل، طنش الـ Event ده
emit(const BooksLoading());
// ... بقية المنطق
}
ده بيمنع "الرعشة" (UI Flickering) وبيخلي الأبلكيشن يحس بالاستقرار والقوة.
خلاصة الخبرة في التعامل مع الـ BLoC في المشاريع الضخمة:
- أبداً متعملش
navigationجوه الـ BLoCHandler. استخدمBlocListener. - أبداً متسميش الـ States بأسماء أفعال (مثلاً
LoadingDataغلط،BooksLoadingصح). - دائماً ابدأ الـ State بـ
Initialعشان تفرق بين أول مرة والتحديثات. - دائماً استخدم الـ
exportفي ملف الـ Bloc عشان تسهل الـ Import على بقية الـ Widgets.
قبل ما تقفل الفصل ده، اتأكد إنك استوعبت النقط دي:
| Topic | Key Takeaway (AR) | Key Takeaway (EN) |
|---|---|---|
| 3-File Rule | التنظيم هو أساس النجاح. | Separation of concerns is key. |
| Reactive Streams | الداتا بتتحرك لوحدها من الـ DB للـ UI. | Automatic UI updates via streams. |
| Sealed States | تغطية كل الاحتمالات (Loading, Success, Error). | Guarding against unhandled states. |
| BlocListener | استخدمه للـ Navigation والـ Snackbars. | Handling side effects efficiently. |
| Equatable | تحسين الأداء عن طريق منع الـ Rebuilds الزيادة. | Preventing redundant UI rebuilds. |
إدارة الحالة هي "الجهاز العصبي" للتطبيق. إحنا كدة بنينا "الذاكرة" (Chapter 4) و"المخ" (Chapter 5). فاضل نعرف إزاي بنربطهم ببعض من غير ما نكتب كود كتير مكرر.
في الفصل الجاي، هنتعلم عن حقن التبعيات (Dependency Injection) باستخدام GetIt، وده اللي هيخلينا نقدر نستخدم الـ BLoCs والـ Repositories في أي مكان بسهولة.
إدارة الحالة هي الروح اللي بتخلي الصور الثابتة تطبيق شغال. BLoC هو اختيارنا الاستراتيجي للاستقرار.