الفصل 05Chapter 05

إدارة الحالة (BLoC & Cubit) State Management (BLoC & Cubit)

التحكم في تدفق البيانات وتحويل الأحداث إلى شاشات تفاعلية باستخدام مكتبة Bloc. Controlling data flow and transforming events into interactive screens using the Bloc library.

🚂 ليه الـ BLoC؟ Why BLoC?

الـ 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".

ConceptFunction (EN)Benefit (AR)
EventsWhat happened in UIاليوزر ضغط على زرار، أو الصفحة حملت.
StatesWhat UI should showعرض دائرة التحميل، عرض لستة البيانات، أو رسالة خطأ.
StreamsData flow pipelineتوصيل التغييرات من الداتابيز للشاشة لحظياً.
📂 قاعدة الملفات الثلاثة The 3-File Rule

في MRE CashBook، إحنا بنتبع قاعدة صارمة: كل ميزة لازم تتقسم لـ 3 ملفات منفصلة. ده بيخلي الكود منظم جداً وسهل يتجرب (Testable).

1
name_event.dart

بيحتوي على كل الـ Actions اللي ممكن تحصل (مثلاً: LoadBooks, AddBook).

2
name_state.dart

بيوصف شكل الشاشة في كل حالة (مثلاً: BooksLoading, BooksLoaded).

3
name_bloc.dart

ده المخ، اللي بياخد الـ Event ويحولها لـ State.

📡 النمط التفاعلي (Reactive Implementation) Reactive Implementation

أقوى حاجة في الـ BLoC بتاعنا إنه "مشاكس"، دايماً مراقب الداتابيز. بص على الكود ده:

📁 lib/features/books/presentation/bloc/books_bloc.dart
void _onLoadBooks(LoadBooks event, Emitter emit) {
  emit(const BooksLoading());
  _booksSubscription?.cancel();
  _booksSubscription = _repository.watchBooks().listen(
    (books) => add(BooksUpdated(books)),
  );
}
watchBooks().listen(...)

البلوك هنا بيعمل "اشتراك" في ماسورة البيانات. أول ما الداتابيز تتغير، البلوك بيبعت لنفسه Event اسمه BooksUpdated وتلقائياً الشاشة بتحدث نفسها.

♻️ أمان الذاكرة (Memory Cleanup) Memory Safety & Lifecycle

لو فتحت اشتراك (Subscription) ونسيت تقفله، الأبلكيشن هيحرق رامات الموبايل لغاية ما يقفل. عشان كدة لازم نستخدم close().

@override
Future close() {
  _booksSubscription?.cancel(); // لازم تقفل الحنفية!
  return super.close();
}
⚖️ الفرق بين BLoC و Cubit BLoC vs Cubit: The Decision

في المشروع ده، إحنا بنستخدم النوعين. كل واحد ليه وقته ومكانه:

BLoC
  • مناسب للعمليات المعقدة اللي فيها Events كتير.
  • بيستخدم نظام الـ Streams داخلياً.
  • مثال: BooksBloc لأنه بيراقب الداتابيز وبيتفاعل مع أكتر من Event.
Cubit
  • مناسب للعمليات البسيطة (CRUD).
  • بيعتمد على الـ Methods بدل الـ Events.
  • مثال: AddEntryCubit لأنه مجرد فورم بتتملي وبتتبعت.

نصيحة: ابدأ دايماً بـ Cubit. لو لقيت الكود بقى معقد ومحتاج تتحكم في الـ Events بشكل أدق، حوله لـ BLoC.

🎭 تشريح الحالة (State Anatomy) The Anatomy of a State

في MRE CashBook، بنستخدم الـ sealed classes عشان نضمن إننا مغطيين كل الاحتمالات الممكنة للشاشة.

📁 lib/features/books/presentation/bloc/books_state.dart
sealed class BooksState extends Equatable {
  const BooksState();
  @override
  List get props => [];
}

class BooksInitial extends BooksState { const BooksInitial(); }
class BooksLoading extends BooksState { const BooksLoading(); }
class BooksLoaded extends BooksState {
  final List books;
  final bool isPrivacyModeOn;

  const BooksLoaded({required this.books, required this.isPrivacyModeOn});

  @override
  List get props => [books, isPrivacyModeOn];
}
Equatable
🔍 ليه بنستخدمها؟🔍 Why use it?

عشان Flutter "ذكي". لو طلعنا نفس الـ State مرتين ورا بعض بنفس الداتا بالضبط، Equatable هتقول لـ Flutter: "متحركش الـ UI، مفيش حاجة جديدة حصلت". ده بيوفر "كهرباء" (Performance) كتير!

🔌 ربط الـ BLoC بالواجهة (UI Integration) Bridging the Gap: UI Integration

إزاي الشاشة بتعرف إن فيه 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(),
    };
  },
)
Seamless Refresh: لاحظ إننا في الـ BooksLoaded مش بنرجع لنقطة الصفر لو الداتا اتحدثت. اليوزر بيفضل شايف اللستة القديمة لغاية ما اللستة الجديدة تجهز وتتبدل في أجزاء من الثانية. ده اللي بنسميه تجربة مستخدم "سلسة".
🎢 رحلة الـ Event: من الشاشة للداتابيز The Journey of an Event: From UI to DB

تعالوا نتتبع اللي بيحصل لما اليوزر يضغط "إضافة كتاب":

1
UI (Widget)

اليوزر يضغط الزرار، الشاشة تنفذ: context.read<BooksBloc>().add(AddBookEvent(name: 'Work')).

2
BLoC (Broker)

البلوك يستقبل الـ Event وينفذ الـ Handler: _onAddBook.

3
Repository (Contract)

البلوك يكلم الـ Repository: _repository.addBook(event.name).

4
Database (Drift)

الداتابيز تنفذ الـ SQL وتغير الملف.

5
Reactive Loop

الداتابيز تبعت "إشارة" للـ Stream اللي البلوك مشترك فيه، البلوك يبعت BooksUpdated، والشاشة تتحدث أوتوماتيك.

🔔 الآثار الجانبية: Listener vs Builder Side Effects: BlocListener vs BlocBuilder

إيه الفرق بين إنك "تبني شاشة" (Build) وبين إنك "تطلع رسالة" (Action)؟

WidgetPurpose (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 Testing the Brain: bloc_test Explained

عشان نتطمن إن البلوك شغال صح، بنستخدم مكتبة bloc_test. ده شكل الاختبار:

📁 test/features/books/presentation/bloc/books_bloc_test.dart
blocTest(
  'يجب أن يخرج BooksLoaded عند استلام BooksUpdated',
  build: () => BooksBloc(repository: mockRepo),
  act: (bloc) => bloc.add(const BooksUpdated([mockBook])),
  expect: () => [
    isA().having((s) => s.books.length, 'length', 1),
  ],
);

الاختبار ده بيضمن لنا إن لو أي حد عدل في الكود وبوظ المنطق، التست هيفشل ونكتشف المشكلة فوراً.

🚦 التحكم في الزحام (Concurrency Transformers) Advanced Mastery: Concurrency Transformers

تخيل يوزر ضغط على زرار "إضافة" 10 مرات في ثانية واحدة. لو معملناش حسابنا، الداتابيز هتتملي داتا مكررة. الـ BLoC بيحل ده بالـ Transformers.

import 'package:bloc_concurrency/bloc_concurrency.dart';

// في الـ Constructor بتاع البلوك
on(
  _onAddBook,
  transformer: droppable(), // "طنش" أي Event جديد لغاية ما القديم يخلص
);
TransformerBehavior (EN)Use Case (AR)
droppable()Ignore new events while busyمنع تكرار الإضافة عند ضغط الزرار بسرعة.
restartable()Cancel old, start newفي البحث (Search) عشان نأخد أخر كلمة بس.
sequential()One after anotherعمليات الدفع أو الحذف بالترتيب.
🎯 تحسين الأداء: BlocSelector Performance Tuning: BlocSelector

لو عندك شاشة فيها 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);
  },
)

ده بيخلي الأبلكيشن "خفيف" جداً حتى على الموبايلات القديمة.

🕵️‍♂️ تحدي: المحقق بلوك (The Bloc Detective) Interactive Exercise: The Bloc Detective

حاول تلاقي الغلطة في الكود ده:

1
Problem: UI not updating

الكود: emit(state);

الحل: الـ BLoC مش هيحدث الشاشة لو بعت نفس الـ Object. لازم تستخدم state.copyWith(...) عشان تطلع نسخة جديدة ببيانات مختلفة.

2
Problem: Infinite Loading

السيناريو: البلوك بيبعت Loading بس مش بيبعت Success أو Error.

الحل: دايماً استخدم try-catch وابعث ErrorState في الـ catch عشان الدائرة متفضلش تلف للأبد.

❄️🔥 بقاء الحالة: Hydrated BLoC Survival of the State: Hydrated BLoC

تخيل اليوز غير الـ Theme أو قفل الـ Privacy Mode، وقفل الأبلكيشن فتحه تاني. إزاي بنضمن إن الاختيارات دي متتمسحش؟ الحل هو Hydrated BLoC.

class SettingsBloc extends HydratedBloc {
  // ...
  @override
  SettingsState? fromJson(Map json) => SettingsState.fromMap(json);

  @override
  Map? toJson(SettingsState state) => state.toMap();
}
toJson / fromJson

دريفت بيخزن الداتا المهمة، بس Hydrated BLoC بيخزن "حالة الشاشة" في ملف JSON صغير. أول ما الأبلكيشن يفتح، البلوك بيرجع لآخر حالة كان عليها أوتوماتيك.

👁️ الأخ الأكبر: BlocObserver The Big Brother: BlocObserver

عشان نعرف إيه اللي بيحصل في كل الـ Blocs بتاعة الأبلكيشن في نفس الوقت، بنستخدم الـ BlocObserver. ده زي "كاميرا مراقبة" لكل الشاشات.

📁 lib/core/bloc/app_bloc_observer.dart
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 فوراً مع اسم البلوك اللي حصل فيه المشكلة.

📐 هرم إدارة الحالة (The Pyramid) The State Management Pyramid

مش كل حاجة محتاجة BLoC. عشان تكون مهندس شاطر، لازم تختار الأداة الصح للمكان الصح:

LevelToolUse Case (AR)
Local StateStatefulWidgetفورم بسيطة، أو زرار بيغير لونه.
Transient StateCubitصفحة واحدة فيها شوية Logic بسيط.
Feature StateBLoCميزة كاملة (زي الكتب) محتاجة تفاعل وتعقيد.
Global StateProvided Blocالثيم، اللغة، بيانات اليوزر الأساسية.
🔑 الوصول للمخ: read و watch و select Accessing the Brain: read, watch, and select

إزاي بنستخرج الداتا من الـ BLoC جوه الـ Widgets؟ إحنا عندنا 3 طرق سحرية:

MethodUtility (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()),
🥊 صراع العمالقة: BLoC vs Riverpod vs Provider The State Management Cage Match

ليه في CashBook اختارنا BLoC بالذات؟ بص على المقارنة دي:

FeatureBLoCProviderRiverpod
Learning Curve Steep (صعب شوية) Easy (سهل) Medium (متوسط)
Safety High (عالي جداً) Low (قليل) High (عالي)
Standardization Very High (معياري) Low (حر) Medium (ناشئ)
Testing Built-in (ممتاز) Manual (يدوي) Excellent (ممتاز)

الـ BLoC بيكريه "نظام" (System) إجباري. وده اللي بنحتاجه في المشاريع اللي فيها مطورين كتير أو داتا مالية حساسة.

أسئلة شائعة في إدارة الحالة State Management FAQ
هل ينفع أحط الـ BLoC جوه BLoC تاني؟
Can I put a BLoC inside another BLoC?
ينفع، بس الأفضل تستخدم الـ StreamSubscription عشان تراقب بلوك من بلوك تاني، أو تمرر الـ Data بينهم في الـ UI.
Yes, but it's better to use StreamSubscription or pass data between them in the UI layer.
إمتى أستخدم StatefulWidget بدل BLoC؟
When should I use StatefulWidget instead?
لما تكون الداتا "محلية" تماماً (مثلاً: لون زرار بيتغير، أو TextController). متكبرش الموضوع لو مش محتاج.
When the data is strictly local (e.g., button color, TextController). Don't overengineer.
🎭🧪 إتقان الـ Mocking: استخدام Mocktail Advanced Testing: Mocking with Mocktail

عشان نختبر الـ BLoC من غير ما نلمس الداتابيز الحقيقية، بنستخدم الـ Mocking. بنعمل "نسخة مزيفة" من الـ Repository ونقولها تتصرف إزاي.

📁 test/helpers/mocks.dart
import 'package:mocktail/mocktail.dart';

class MockBookRepository extends Mock implements IBookRepository {}

// جوه التست
final mockRepo = MockBookRepository();
when(() => mockRepo.watchBooks()).thenAnswer((_) => Stream.value([mockBook]));

ليه ده مهم؟ عشان التست يكون سريع جداً (بيخلص في أجزاء من الثانية) وميعتمدش على إن فيه ملفات موجودة على الهارد ولا لا.

🛡️ فلترة المنطق: التحكم في الأحداث Logic Filtering: Controlling the Stream

أحياناً بنحتاج "نفلتر" الـ Events قبل ما تروح للـ Handler. مثلاً مش عاوزين نعمل LoadBooks لو هي أصلاً بتحمل حالياً.

void _onLoadBooks(LoadBooks event, Emitter emit) {
  if (state is BooksLoading) return; // فلترة: لو بنحمل، طنش الـ Event ده
  
  emit(const BooksLoading());
  // ... بقية المنطق
}

ده بيمنع "الرعشة" (UI Flickering) وبيخلي الأبلكيشن يحس بالاستقرار والقوة.

📜 دليل النجاة في إدارة الحالة The State Management Survival Guide

خلاصة الخبرة في التعامل مع الـ BLoC في المشاريع الضخمة:

  • أبداً متعملش navigation جوه الـ BLoCHandler. استخدم BlocListener.
  • أبداً متسميش الـ States بأسماء أفعال (مثلاً LoadingData غلط، BooksLoading صح).
  • دائماً ابدأ الـ State بـ Initial عشان تفرق بين أول مرة والتحديثات.
  • دائماً استخدم الـ export في ملف الـ Bloc عشان تسهل الـ Import على بقية الـ Widgets.
🏁 ملخص الفصل: قائمة المراجعة Chapter 5 Recap: The Checklist

قبل ما تقفل الفصل ده، اتأكد إنك استوعبت النقط دي:

TopicKey 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.
🚀 خاتمة وطريق الفصل القادم Final Thoughts & Next Steps

إدارة الحالة هي "الجهاز العصبي" للتطبيق. إحنا كدة بنينا "الذاكرة" (Chapter 4) و"المخ" (Chapter 5). فاضل نعرف إزاي بنربطهم ببعض من غير ما نكتب كود كتير مكرر.

في الفصل الجاي، هنتعلم عن حقن التبعيات (Dependency Injection) باستخدام GetIt، وده اللي هيخلينا نقدر نستخدم الـ BLoCs والـ Repositories في أي مكان بسهولة.

📝 الملخص Summary

إدارة الحالة هي الروح اللي بتخلي الصور الثابتة تطبيق شغال. BLoC هو اختيارنا الاستراتيجي للاستقرار.

الفصل السابقPrevious الفصل التاليNext