إدارة الحالة المتقدمة (Bloc و Riverpod)

نمط BLoC: المفاهيم الأساسية

16 دقيقة الدرس 2 من 14

نمط BLoC: المفاهيم الأساسية

نمط مكوّن منطق الأعمال (BLoC) هو نمط معماري لإدارة الحالة طورته Google لإطار Flutter. فلسفته المحورية بسيطة: تدخل الأحداث، وتخرج الحالات. يجلس BLoC بين طبقة واجهة المستخدم وطبقة البيانات، فيستقبل الأحداث المدفوعة من المستخدم أو النظام، ويعالج منطق الأعمال، ويصدر حالات جديدة تستجيب لها واجهة المستخدم. ينشئ هذا حدًّا صارمًا وقابلًا للاختبار بين طبقة العرض والمنطق.

ملاحظة: قدّم Paolo Soares النمط في Google I/O 2018 بوصفه نمطًا قابلًا للتوسع في Flutter. الحزمة الرسمية flutter_bloc (من تطوير Felix Angelov) تُبنى فوق هذا المفهوم الأساسي وهي الأكثر انتشارًا في المجتمع.

تدفق البيانات الأساسي

فهم BLoC يبدأ بتدفق بياناته أحادي الاتجاه. يوجد ثلاثة مشاركين رئيسيين:

  • الحدث (Event) — كائن غير قابل للتعديل يمثل شيئًا حدث (مثل ضغطة زر، تحميل صفحة، إرسال استعلام بحث). الأحداث هي مُدخلات BLoC.
  • BLoC — المكوّن الذي يستقبل الأحداث، ويُصدر حالات جديدة بعد معالجة منطق الأعمال (التحقق، واجهات API، التحويلات).
  • الحالة (State) — كائن غير قابل للتعديل يصف الوضع الراهن لميزة ما (مثل جارٍ التحميل، تحميل البيانات، خطأ). الحالات هي مخرجات BLoC.

تُرسل واجهة المستخدم الأحداث إلى BLoC وتستمع لتغييرات الحالة. المهم أن واجهة المستخدم لا تحتوي أبدًا على منطق أعمال، وأن BLoC لا يحتفظ بأي مرجع لودجات واجهة المستخدم.

تعريف الأحداث والحالات

// --- الأحداث (مُدخلات BLoC) ---
abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
class CounterResetPressed extends CounterEvent {}

// --- الحالات (مخرجات BLoC) ---
class CounterState {
  final int count;
  const CounterState(this.count);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is CounterState && other.count == count);

  @override
  int get hashCode => count.hashCode;
}

تنفيذ BLoC

مع حزمة flutter_bloc، يرث BLoC من Bloc<Event, State>. تُعرّف حالة ابتدائية في المُنشئ وتُسجّل معالجات الأحداث باستخدام on<EventType>(handler). يستقبل كل معالج الحدث ومُصدِّرًا (Emitter) تستدعيه لدفع الحالات الجديدة للمستمعين.

مكوّن BLoC كامل للعداد

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<CounterIncrementPressed>(_onIncrement);
    on<CounterDecrementPressed>(_onDecrement);
    on<CounterResetPressed>(_onReset);
  }

  void _onIncrement(
    CounterIncrementPressed event,
    Emitter<CounterState> emit,
  ) {
    emit(CounterState(state.count + 1));
  }

  void _onDecrement(
    CounterDecrementPressed event,
    Emitter<CounterState> emit,
  ) {
    // منطق أعمال: منع النزول دون الصفر
    if (state.count > 0) {
      emit(CounterState(state.count - 1));
    }
  }

  void _onReset(
    CounterResetPressed event,
    Emitter<CounterState> emit,
  ) {
    emit(const CounterState(0));
  }
}
نصيحة: احرص على أن تكون دوال معالجة الأحداث صغيرة ومُركّزة على مسؤولية واحدة. إذا تجاوز المعالج 10–15 سطرًا، فكر في استخلاص دالة مساعدة خاصة أو فئة خدمة مخصصة. مهمة BLoC هي تنسيق المنطق لا تنفيذه من الصفر بشكل مباشر.

ربط BLoC بواجهة المستخدم

توفر حزمة flutter_bloc ودجتَين رئيسيتَين لربط BLoC بشجرة الودجات:

  • BlocProvider — يُنشئ BLoC ويُوفّره لشجرته الفرعية عبر شجرة الودجات (باستخدام InheritedWidget داخليًا). كما يتولى إغلاق BLoC عند إزالة الودجت.
  • BlocBuilder — يُعيد بناء شجرته الفرعية كلما أصدر BLoC حالة جديدة. يعيد البناء فقط عند تغيُّر الحالة فعليًا (بناءً على المساواة).

ربط BLoC بشجرة الودجات

// توفير BLoC فوق الودجت الذي يحتاجه
BlocProvider(
  create: (context) => CounterBloc(),
  child: CounterPage(),
)

// داخل CounterPage — قراءة الحالة والاستجابة لها
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('عداد BLoC')),
      body: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Center(
            child: Text(
              'العدد: ${state.count}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'inc',
            onPressed: () =>
                context.read<CounterBloc>().add(CounterIncrementPressed()),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'dec',
            onPressed: () =>
                context.read<CounterBloc>().add(CounterDecrementPressed()),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

لماذا يُطبّق BLoC حدودًا بين واجهة المستخدم والمنطق

يُطبّق نمط BLoC الفصل بين المخاوف عبر قيوده التصميمية:

  • يستقبل BLoC كائنات Dart نقية (أحداث) فحسب ويُصدر كائنات Dart نقية (حالات). لا يعتمد بتاتًا على ودجات Flutter أو BuildContext.
  • لكون BLoC كود Dart نقيًا، يمكن اختباره وحدويًا دون الحاجة لتشغيل شجرة ودجات. تُدخل الأحداث وتتحقق من الحالات المُصدَرة.
  • طبقة واجهة المستخدم غلاف رفيع — تُحوّل إيماءات المستخدم إلى أحداث وتُحوّل الحالات إلى ودجات. لا ينبغي أن تتضمن دوال build() أي منطق أعمال.
  • يمكن لودجات متعددة مراقبة حالة BLoC ذاته بصورة مستقلة، مما يُتيح إعادة بناء دقيقة دون الحاجة لتمرير الخصائص عبر الشجرة.
تحذير: من الأخطاء الشائعة في BLoC إصدار نفس كائن الحالة على التوالي. افتراضيًا تستخدم flutter_bloc فحوصات المساواة — إذا كانت حالتان متتاليتان متساويتَين، لن يُصدر BLoC الثانية ولن يُعيد BlocBuilder البناء. تأكد دومًا من أن فئات الحالة تُنفّذ == وhashCode بشكل صحيح، أو استخدم حزمة equatable.

ملخص

يُنظّم نمط BLoC إدارة الحالة حول ثلاثة مفاهيم: الأحداث (مدخلات)، وBLoC (معالج)، والحالات (مخرجات). يتدفق البيانات في اتجاه واحد — ترسل واجهة المستخدم الأحداث، يعالجها BLoC ويُصدر الحالات، وتُعيد واجهة المستخدم البناء استجابةً لذلك. يجعل هذا الفصل تطبيقات Flutter الكبيرة أسهل فهمًا واختبارًا وصيانةً. في الدرس التالي ستستكشف Cubit، المتغيّر المُبسَّط من BLoC الذي يستبدل الأحداث باستدعاءات مباشرة للدوال.