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

مكونات flutter_bloc: BlocListener و MultiBlocProvider

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

مكونات flutter_bloc: BlocListener و MultiBlocProvider

تأتي حزمة flutter_bloc مع مجموعة من الودجات المبنية لأغراض محددة تغطي كل نمط تفاعل تحتاجه عند العمل مع BLoCs. في هذا الدرس نركز على الثلاثة الأكثر إساءةً للفهم: BlocListener، وBlocConsumer، وMultiBlocProvider. فهم سبب وجود كل ودجت لا يقل أهمية عن معرفة كيفية استخدامه.

BlocListener — التفاعلات ذات الآثار الجانبية فقط

صُمِّم BlocListener للـآثار الجانبية ذات المرة الواحدة التي يجب ألا تطلق إعادة بناء لواجهة المستخدم أبدًا: الانتقال بين الصفحات، وعرض ScaffoldMessenger.showSnackBar، ونوافذ الحوار، وأحداث التحليلات. يُطلَق رد النداء listener في كل مرة يُصدر فيها الـ bloc حالة جديدة، لكنه لا يُصيِّر أي ودجت بذاته — فهو ببساطة يلف عنصراً child.

القاعدة الأساسية: إذا كان التفاعل مع الحالة يجب أن يسبب إعادة بناء مرئية، استخدم BlocBuilder. إذا كان يجب أن يطلق أثرًا جانبيًا بدون إعادة بناء، استخدم BlocListener. خلط الآثار الجانبية داخل رد نداء builder في BlocBuilder هو نمط مضاد شائع يسبب استدعاءات مكررة.

BlocListener — مثال على التنقل وعرض SnackBar

BlocListener<AuthBloc, AuthState>(
  // اختياري: يُطلق فقط عندما يكون هذا الشرط صحيحًا
  listenWhen: (previous, current) =>
      previous.status != current.status,
  listener: (context, state) {
    if (state.status == AuthStatus.authenticated) {
      // انتقل بدون إعادة بناء الودجت الحالي
      Navigator.of(context).pushReplacementNamed('/home');
    } else if (state.status == AuthStatus.error) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.errorMessage ?? 'Login failed')),
      );
    }
  },
  child: const LoginFormWidget(),
)

المحدد الاختياري listenWhen يعكس buildWhen في BlocBuilder: يستقبل الحالة السابقة والحالية ويجب أن يُرجع true فقط عندما يجب إطلاق المستمع. استخدمه لتجنب التفاعل مع كل إصدار عندما تهتم فقط بانتقال حقل محدد.

BlocConsumer — البناء والاستماع معًا

BlocConsumer هو ودجت مريح يجمع BlocBuilder وBlocListener في ودجت واحد عندما تحتاج كليهما — إعادة بناء لواجهة المستخدم وأثرًا جانبيًا — استجابةً لنفس تغيير الحالة. يقبل builder، وlistener، وbuildWhen، وlistenWhen.

متى تختار BlocConsumer: استخدمه عندما يجب أن يُصدر نفس الإصدار (1) تحديث جزء من واجهة المستخدم و(2) إطلاق أثر جانبي. مثال كلاسيكي: إرسال نموذج يعرض مؤشر تحميل في واجهة المستخدم بينما يعرض أيضًا شريط snackbar عند الخطأ.

BlocConsumer — إرسال نموذج مع مؤشر تحميل وSnackBar

BlocConsumer<RegistrationBloc, RegistrationState>(
  listenWhen: (prev, curr) => curr.isFailure || curr.isSuccess,
  listener: (context, state) {
    if (state.isSuccess) {
      Navigator.of(context).pushReplacementNamed('/dashboard');
    }
    if (state.isFailure) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(state.errorMessage),
          backgroundColor: Colors.red,
        ),
      );
    }
  },
  buildWhen: (prev, curr) => prev.isSubmitting != curr.isSubmitting,
  builder: (context, state) {
    return ElevatedButton(
      onPressed: state.isSubmitting
          ? null
          : () => context
              .read<RegistrationBloc>()
              .add(const FormSubmitted()),
      child: state.isSubmitting
          ? const SizedBox(
              width: 20,
              height: 20,
              child: CircularProgressIndicator(strokeWidth: 2),
            )
          : const Text('Register'),
    );
  },
)

MultiBlocProvider — تركيب BLoCs عند جذر التطبيق

التطبيقات الحقيقية لديها BLoCs كثيرة. تداخل عدة BlocProviders يخلق شجرة ودجات ذات مسافة بادئة عميقة يصعب قراءتها. يُسطِّح MultiBlocProvider تلك البنية بقبوله قائمة providers. كل إدخال هو BlocProvider، وتُنشأ بترتيب القائمة. النتيجة مطابقة لتداخلها يدويًا، لكنها أكثر قابلية للقراءة بكثير.

MultiBlocProvider عند جذر التطبيق

// main.dart
void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MultiBlocProvider(
      providers: [
        BlocProvider<AuthBloc>(
          create: (context) => AuthBloc(
            authRepository: context.read<AuthRepository>(),
          )..add(const AppStarted()),
        ),
        BlocProvider<ThemeBloc>(
          create: (context) => ThemeBloc(
            settingsRepository: context.read<SettingsRepository>(),
          ),
        ),
        BlocProvider<CartBloc>(
          create: (context) => CartBloc(
            cartRepository: context.read<CartRepository>(),
          ),
        ),
      ],
      child: BlocBuilder<ThemeBloc, ThemeState>(
        builder: (context, themeState) => MaterialApp(
          themeMode: themeState.mode,
          theme: AppTheme.light,
          darkTheme: AppTheme.dark,
          home: const AppRouter(),
        ),
      ),
    );
  }
}
تحذير: تُعدّ BLoCs المقدَّمة عبر MultiBlocProvider بالقرب من الجذر كائنات مفردة (singletons) للشجرة الفرعية من الودجات الموجودة تحتها. تجنب إنشاء BLoCs ثقيلة الحمل في أعلى الشجرة إذا كانت مطلوبة فقط في شاشة واحدة. يُفضَّل استخدام أغلفة BlocProvider محددة النطاق على مستوى المسار للـ BLoCs الخاصة بالشاشة.

MultiBlocListener و MultiRepositoryProvider

نفس نمط "Multi" يمتد إلى المستمعين وحقن المستودعات. يحل MultiBlocListener محل ودجات BlocListener المتداخلة على مستوى الموجِّه. أما MultiRepositoryProvider فيحقن كائنات Dart العادية للخدمات/المستودعات (بدون BLoC) في الشجرة بحيث يمكن قراءتها باستخدام context.read<T>().

الملخص

  • BlocListener — للآثار الجانبية فقط (التنقل، الـ snackbars، الحوارات)؛ لا يعيد البناء أبدًا.
  • BlocConsumer — إعادة بناء مدمجة مع أثر جانبي؛ استخدمه عندما تحتاج كليهما لنفس الإصدار.
  • MultiBlocProvider — تركيب مسطّح وقابل للقراءة لعدة BlocProviders؛ مثالي عند جذر التطبيق أو المسار.
  • استخدم listenWhen / buildWhen لتصفية الإصدارات ومنع العمل غير الضروري.
النقطة الرئيسية: اختيار الودجت المناسب للمهمة يُبقي كودك نظيفًا وقابلًا للتنبؤ. BlocListener يعالج الآثار، و BlocBuilder يعالج واجهة المستخدم، و BlocConsumer يعالجهما معًا — لكن الجوء إلى BlocConsumer فقط عندما تحتاج كليهما في نفس الوقت فعلًا.