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

مكونات flutter_bloc: BlocProvider و BlocBuilder

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

مكونات flutter_bloc: BlocProvider و BlocBuilder

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

BlocProvider: حقن التبعيات عبر شجرة الودجات

BlocProvider ودجت مبنية على InheritedWidget تنشئ BLoC (أو Cubit) وتتيحه لجميع الودجات التابعة لها في الشجرة. تمتلك هذه الودجت مثيل BLoC وتستدعي تلقائياً close() عليه حين إزالة المزوّد من الشجرة، مما يمنع تسرب الذاكرة.

  • create — مصنع يستقبل BuildContext ويعيد مثيلاً جديداً من BLoC.
  • child — الشجرة الفرعية التي ستتمكن من الوصول إلى BLoC.
  • lazy — قيمته الافتراضية true؛ يُنشأ BLoC فقط عند أول وصول إليه.

توفير CounterCubit لشاشة

// main.dart — لفّ المسار أو الشاشة في BlocProvider
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart';
import 'counter_page.dart';

class CounterRoute extends StatelessWidget {
  const CounterRoute({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterCubit(),   // يُنشأ هنا ويُعاد هنا
      child: const CounterPage(),
    );
  }
}

// counter_cubit.dart
class CounterCubit extends Cubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);
  void decrement() => emit(state - 1);
}
ملاحظة: أنشئ BLoC دائماً داخل BlocProvider.create، ولا تمرر مثيلاً منشأً مسبقاً تملكه في مكان آخر — فذلك يكسر عقد الإتلاف التلقائي. إن احتجت مشاركة مثيل موجود (مثل تمريره بين المسارات)، استخدم BlocProvider.value وأدر دورة حياته بنفسك.

BlocBuilder: إعادة بناء UI تفاعلية ومحددة الهدف

BlocBuilder<B, S> يستمع إلى BLoC من النوع B الذي يبعث حالات من النوع S. في كل مرة تُبعث حالة جديدة، يُستدعى callback المسمى builder مع BuildContext الحالي وأحدث حالة، ويُعاد بناء الشجرة الفرعية التي تعيدها فقط — دون المساس بأي شيء فوقها.

  • builder — مطلوب؛ يستقبل (context, state) ويعيد Widget.
  • buildWhen — تسلسل اختياري (previous, current) => bool؛ أعد false لتخطي إعادة البناء حين لم تتغير أجزاء الحالة ذات الصلة.
  • bloc — اختياري؛ إن حُذف بحث BlocBuilder عن أقرب BlocProvider مطابق للنوع عبر context.read.

BlocBuilder يعيد بناء شاشة العداد فقط

// counter_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_cubit.dart';

class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('عداد BloC')),
      body: Center(
        // هذه الشجرة الفرعية فقط تُعاد بناؤها عند انبعاث CounterCubit
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) {
            return Text(
              '$count',
              style: Theme.of(context).textTheme.displayLarge,
            );
          },
        ),
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            // context.read لا يشترك — آمن في callbacks
            onPressed: () => context.read<CounterCubit>().increment(),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            onPressed: () => context.read<CounterCubit>().decrement(),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

context.read مقابل context.watch

تُتيح حزمة flutter_bloc امتدادين مريحين على BuildContext يكمّلان الودجات:

  • context.read<T>() — يبحث عن BLoC أو Cubit من النوع T في شجرة الودجات دون الاشتراك. استخدمه داخل معالجات الأحداث ودوال callback للأزرار وinitState — في أي مكان تستدعي فيه طريقة على BLoC دون الحاجة إلى إعادة بناء.
  • context.watch<T>() — يبحث عن BLoC ويشترك في تدفقه، مما يجعل الودجت الحالية تُعاد بناؤها عند كل حالة جديدة. يعادل لفّ الودجت في BlocBuilder. فضّل BlocBuilder لإعادة البناء الدقيقة، واستخدم context.watch حين يكون إعادة بناء الودجت بالكامل مقبولاً.
تحذير: لا تستدعِ أبداً context.watch داخل callback أو initState أو أي طريقة ليست build. إنها تسجّل اشتراكاً على BuildContext وستطرح استثناءً إن استُدعيت خارج مرحلة البناء. استخدم context.read في تلك الحالات.

buildWhen: تحكم دقيق في إعادة البناء

حين يكبر كائن حالتك، يمكنك تفادي إعادة البناء غير الضرورية بتقديم تسلسل buildWhen. يُستدعى builder فقط حين يعيد التسلسل true.

تخطي إعادات البناء باستخدام buildWhen

// لنفترض أن الحالة كلاس أكثر تعقيداً
class ShopState {
  final int cartCount;
  final bool isLoading;
  final List<String> items;

  const ShopState({
    required this.cartCount,
    required this.isLoading,
    required this.items,
  });
}

// شارة السلة تهتم فقط بتغييرات cartCount
BlocBuilder<ShopBloc, ShopState>(
  buildWhen: (previous, current) =>
      previous.cartCount != current.cartCount,
  builder: (context, state) {
    return Badge(
      label: Text('${state.cartCount}'),
      child: const Icon(Icons.shopping_cart),
    );
  },
)
نصيحة: ضع BlocBuilder في أعمق نقطة ممكنة في شجرة الودجات، مالفّاً فقط الودجات التي تعتمد فعلياً على الحالة. إذا وُضع BlocBuilder في أعلى الشجرة فسيُعيد بناء شجرة فرعية ضخمة عند كل انبعاث حالة — وهو نفس خطأ الأداء الناتج عن إساءة استخدام setState على ودجت أصل.

الخلاصة

استخدم BlocProvider لحقن BLoC في الشجرة الفرعية التي تحتاجه، مستفيداً من إدارة دورة الحياة التلقائية. استخدم BlocBuilder لإعادة بناء الودجت المعتمدة على حالة BLoC فحسب، وأمدّه بـbuildWhen لتخطي إعادات البناء حين لا تتغير شريحة الحالة ذات الصلة. الجأ إلى context.read في callbacks الأحداث وإلى context.watch (أو BlocBuilder) في طريقة البناء. معاً توفر هاتان الودجتان جسراً واضحاً وقابلاً للاختبار وعالي الأداء بين منطق عملك وواجهة المستخدم.