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

Cubit: نسخة مبسطة من BLoC بدون أحداث

15 دقيقة الدرس 3 من 14

Cubit: نسخة مبسطة من BLoC بدون أحداث

في الدرس السابق رأيت كيف يفصل BLoC الكامل النية (الأحداث) عن الاستجابة (الحالة). Cubit هو نوع أخف من النمط ذاته يزيل طبقة الأحداث كلياً. بدلاً من إرسال الأحداث، تستدعي طرقاً عامة مباشرةً على Cubit، التي تُصدر بعدها حالة جديدة. النتيجة هي كود أقل حجماً مع الاحتفاظ بجميع مزايا آلة الحالة القابلة للتنبؤ والاختبار.

ملاحظة: Cubit جزء من حزمة flutter_bloc (الإصدار 8 فما فوق). لا تحتاج إلى اعتماد منفصل — يأتي كل من Bloc وCubit معاً.

Cubit مقابل BLoC الكامل بنظرة سريعة

فهم الفرق يساعدك على اختيار الأداة المناسبة لكل ميزة:

  • BLoC — تُرسل الأحداث من واجهة المستخدم؛ يحوّل الـ bloc كل حدث إلى تغيير في الحالة. الأفضل عندما تحتاج مسار تدقيق، تحويلات أحداث، أو التحكم في التزامن عبر EventTransformer.
  • Cubit — تستدعي واجهة المستخدم طريقة مسماة مباشرةً؛ يُصدر الـ cubit حالة جديدة. الأفضل للمنطق البسيط على غرار CRUD حيث لا تضيف كائنات الأحداث قيمة.
  • كلاهما يكشف نفس واجهة برمجة التطبيقات BlocBuilder / BlocListener / BlocProvider في شجرة الودجات، لذا التنقل بينهما سلس.
نصيحة: قاعدة جيدة — إذا وجدت نفسك تنشئ فئة حدث بحقل واحد ومعالج واحد، استبدلها بطريقة Cubit. إذا احتجت لاحقاً إلى EventTransformer (مثل debounce أو restartable)، رقِّ الـ Cubit إلى BLoC كامل.

تعريف فئات الحالة لـ Cubit

كل Cubit عام (generic) على نوع حالته. يمكن أن تكون الحالة أي شيء — نوع بدائي، أو enum، أو فئة غنية غير قابلة للتغيير. للميزات غير التافهة، تُوصى فئة حالة مخصصة لأنها تجعل الانتقالات صريحة وسهلة الاختبار.

هناك نمطان شائعان:

  • تسلسل فئات sealed — قاعدة مجردة واحدة بالإضافة إلى فئات فرعية محددة لكل حالة (Loading, Loaded, Error). تتيح فئات Dart 3 sealed المطابقة الشاملة للأنماط.
  • فئة بيانات واحدة مع حقل status — فئة واحدة مع حقل enum للحالة وحمولة قابلة للنقل. أبسط لكن كل خاصية موجودة دائماً حتى عند عدم استخدامها.

مثال 1 — Counter Cubit (حالة بدائية)

import 'package:flutter_bloc/flutter_bloc.dart';

// الحالة هي int فقط — لا حاجة لفئة مُغلِّفة
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0); // الحالة الأولية = 0

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
  void reset()     => emit(0);
}

طريقة emit() هي الطريقة الوحيدة لدفع حالة جديدة. استدعاء emit بقيمة مساوية للحالة الحالية لا يفعل شيئاً — لا يُطلَق التدفق ولا تُعاد بناء واجهة المستخدم.

مثال 2 — Auth Cubit مع تسلسل فئات sealed

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';

// --- تعريفات الحالة ---
sealed class AuthState extends Equatable {
  const AuthState();
  @override
  List<Object?> get props => [];
}

final class AuthInitial extends AuthState {
  const AuthInitial();
}

final class AuthLoading extends AuthState {
  const AuthLoading();
}

final class AuthAuthenticated extends AuthState {
  const AuthAuthenticated({required this.userId, required this.email});
  final String userId;
  final String email;
  @override
  List<Object?> get props => [userId, email];
}

final class AuthFailure extends AuthState {
  const AuthFailure({required this.message});
  final String message;
  @override
  List<Object?> get props => [message];
}

// --- Cubit ---
class AuthCubit extends Cubit<AuthState> {
  AuthCubit({required this.authRepository}) : super(const AuthInitial());

  final AuthRepository authRepository;

  Future<void> login(String email, String password) async {
    emit(const AuthLoading());
    try {
      final user = await authRepository.login(email, password);
      emit(AuthAuthenticated(userId: user.id, email: user.email));
    } catch (e) {
      emit(AuthFailure(message: e.toString()));
    }
  }

  void logout() => emit(const AuthInitial());
}

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

توفير Cubit واستهلاكه في واجهة المستخدم يستخدم نفس واجهة BlocProvider / BlocBuilder التي تعرفها من BLoC الكامل. التبديل غير مرئي للودجات.

  • لف شجرة فرعية بـ BlocProvider(create: (_) => CounterCubit()) لجعل الـ cubit متاحاً.
  • استخدم BlocBuilder<CounterCubit, int> لإعادة بناء الودجات عند تغير الحالة.
  • أطلق تغييرات الحالة باستدعاء context.read<CounterCubit>().increment() — لا حاجة لبناء كائن حدث.

متى تختار Cubit على BLoC الكامل

استخدم Cubit عندما:

  • تتوافق انتقالات الحالة واحداً لواحد مع استدعاءات الطرق — بلا منطق تفريع لكل حدث.
  • الميزة مكتفية بذاتها ومن غير المرجح أن تحتاج تحويلات التدفق.
  • تريد حلقة ردود فعل سريعة: كود أقل للكتابة وملفات أقل لكل ميزة.
  • تقوم بالنمذجة الأولية وقد تُرقيه لـ BLoC كامل لاحقاً.

فضّل BLoC عندما:

  • تحتاج إلى إبطاء (debounce)، أو تقييد (throttle)، أو إلغاء العمل غير المتزامن الجاري عبر EventTransformer.
  • تشترك عدة أنواع أحداث في نفس نتيجة الحالة وتريد تسجيلاً صريحاً للتدقيق.
  • يفرض فريقك الفصل الصارم بين النية والتنفيذ لقواعد الكود الكبيرة.
تحذير: لا تستدعِ emit() أبداً بعد إغلاق الـ Cubit. يحدث هذا عادةً عند اكتمال عملية غير متزامنة بعد التخلص من الودجت. احمِ الاستدعاء بـ if (!isClosed) emit(newState); أو ألغِ العملية في close().

ملخص

يزيل Cubit طبقة الأحداث من نمط BLoC، مما يتيح لك إطلاق انتقالات الحالة باستدعاء الطرق مباشرةً. يأتي في نفس حزمة flutter_bloc ويتكامل مع نفس واجهة الودجات. عرّف الحالة كنوع بدائي للعدادات والأعلام البسيطة، وكـ تسلسل فئات sealed مع Equatable للميزات ذات حالات التحميل والخطأ والنجاح. استخدم Cubit للمنطق النظيف الموجز وانتقل إلى BLoC الكامل فقط عندما تحتاج التحكم المتقدم في تزامن التدفق.