Flutter Counter
في هذا الدليل التعليمي، سنبني تطبيق عداد (Counter) في Flutter باستخدام مكتبة Bloc.

المواضيع الرئيسية (Key Topics)
Section titled “المواضيع الرئيسية (Key Topics)”- مراقبة تغييرات الحالة باستخدام
BlocObserver. BlocProvider، وهي Widget في Flutter توفّر Bloc للأبناء.BlocBuilder، وهي Widget في Flutter تتولّى إعادة البناء استجابةً للحالات الجديدة.- استخدام Cubit بدلاً من Bloc. ما هو الفرق؟
- إضافة الأحداث باستخدام
context.read.
الإعداد (Setup)
Section titled “الإعداد (Setup)”سنبدأ بإنشاء مشروع Flutter جديد بالكامل:
flutter create flutter_counterبعد ذلك، يمكننا استبدال محتويات ملف pubspec.yaml بما يلي:
name: flutter_counterdescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ^3.12.0
dependencies: bloc: ^9.0.0 flutter: sdk: flutter flutter_bloc: ^9.1.0
dev_dependencies: bloc_lint: ^0.3.0 bloc_test: ^10.0.0 flutter_test: sdk: flutter integration_test: sdk: flutter mocktail: ^1.0.0
flutter: uses-material-design: trueثم نثبّت جميع التبعيات (dependencies):
flutter pub getهيكل المشروع (Project Structure)
Section titled “هيكل المشروع (Project Structure)”├── lib│ ├── app.dart│ ├── counter│ │ ├── counter.dart│ │ ├── cubit│ │ │ └── counter_cubit.dart│ │ └── view│ │ ├── counter_page.dart│ │ ├── counter_view.dart│ │ └── view.dart│ ├── counter_observer.dart│ └── main.dart├── pubspec.lock├── pubspec.yamlيستخدم التطبيق هيكل مجلدات يعتمد على الميزات (feature-driven directory structure). هذا النمط يساعدنا على توسيع المشروع عبر ميزات مستقلة بذاتها. في هذا المثال لدينا ميزة واحدة فقط (العداد نفسه)، لكن في التطبيقات الأكثر تعقيدًا قد نمتلك مئات الميزات المختلفة.
BlocObserver
Section titled “BlocObserver”أول ما سنراجعه هو إنشاء BlocObserver الذي يساعدنا على مراقبة جميع تغييرات
الحالة في التطبيق.
لننشئ الملف lib/counter_observer.dart:
import 'package:bloc/bloc.dart';
/// {@template counter_observer}/// [BlocObserver] for the counter application which/// observes all state changes./// {@endtemplate}class CounterObserver extends BlocObserver { /// {@macro counter_observer} const CounterObserver();
@override void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) { super.onChange(bloc, change); // ignore: avoid_print print('${bloc.runtimeType} $change'); }}في هذه الحالة، نقوم فقط بعمل override للدالة onChange لمتابعة جميع تغييرات
الحالة.
main.dart
Section titled “main.dart”بعد ذلك، استبدل محتويات الملف lib/main.dart بما يلي:
import 'package:bloc/bloc.dart';import 'package:flutter/widgets.dart';import 'package:flutter_counter/app.dart';import 'package:flutter_counter/counter_observer.dart';
void main() { Bloc.observer = const CounterObserver(); runApp(const CounterApp());}هنا نهيّئ CounterObserver الذي أنشأناه للتو، ثم نستدعي runApp باستخدام
Widget CounterApp التي سنراجعها الآن.
تطبيق العداد (Counter App)
Section titled “تطبيق العداد (Counter App)”لننشئ الملف lib/app.dart:
CounterApp هو MaterialApp ويحدد home على أنه CounterPage.
import 'package:flutter/material.dart';import 'package:flutter_counter/counter/counter.dart';
/// {@template counter_app}/// A [MaterialApp] which sets the `home` to [CounterPage]./// {@endtemplate}class CounterApp extends MaterialApp { /// {@macro counter_app} const CounterApp({super.key}) : super(home: const CounterPage());}لننتقل الآن إلى CounterPage.
صفحة العداد (Counter Page)
Section titled “صفحة العداد (Counter Page)”لننشئ الملف lib/counter/view/counter_page.dart:
تتولى Widget CounterPage إنشاء CounterCubit (الذي سنراجعه بعد قليل) وتوفيره
إلى CounterView.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_counter/counter/counter.dart';
/// {@template counter_page}/// A [StatelessWidget] which is responsible for providing a/// [CounterCubit] instance to the [CounterView]./// {@endtemplate}class CounterPage extends StatelessWidget { /// {@macro counter_page} const CounterPage({super.key});
@override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterCubit(), child: const CounterView(), ); }}Counter Cubit
Section titled “Counter Cubit”لننشئ الملف lib/counter/cubit/counter_cubit.dart:
تعرض class CounterCubit طريقتين (methods):
increment: تضيف 1 إلى الحالة الحاليةdecrement: تطرح 1 من الحالة الحالية
نوع الحالة الذي يديره CounterCubit هو int فقط، والحالة الأولية هي 0.
import 'package:bloc/bloc.dart';
/// {@template counter_cubit}/// A [Cubit] which manages an [int] as its state./// {@endtemplate}class CounterCubit extends Cubit<int> { /// {@macro counter_cubit} CounterCubit() : super(0);
/// Add 1 to the current state. void increment() => emit(state + 1);
/// Subtract 1 from the current state. void decrement() => emit(state - 1);}بعد ذلك، لنراجع CounterView، وهي المسؤولة عن استهلاك الحالة والتفاعل مع
CounterCubit.
عرض العداد (Counter View)
Section titled “عرض العداد (Counter View)”لننشئ الملف lib/counter/view/counter_view.dart:
CounterView مسؤولة عن عرض قيمة العداد الحالية، وإظهار زري
FloatingActionButton لزيادة/إنقاص العداد.
import 'package:flutter/material.dart';import 'package:flutter_bloc/flutter_bloc.dart';import 'package:flutter_counter/counter/counter.dart';
/// {@template counter_view}/// A [StatelessWidget] which reacts to the provided/// [CounterCubit] state and notifies it in response to user input./// {@endtemplate}class CounterView extends StatelessWidget { /// {@macro counter_view} const CounterView({super.key});
@override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Scaffold( body: Center( child: BlocBuilder<CounterCubit, int>( builder: (context, state) { return Text('$state', style: textTheme.displayMedium); }, ), ), floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[ FloatingActionButton( key: const Key('counterView_increment_floatingActionButton'), child: const Icon(Icons.add), onPressed: () => context.read<CounterCubit>().increment(), ), const SizedBox(height: 8), FloatingActionButton( key: const Key('counterView_decrement_floatingActionButton'), child: const Icon(Icons.remove), onPressed: () => context.read<CounterCubit>().decrement(), ), ], ), ); }}نستخدم BlocBuilder لتغليف Widget Text بهدف تحديث النص كلما تغيّرت حالة
CounterCubit. بالإضافة إلى ذلك، نستخدم context.read<CounterCubit>() للعثور
على أقرب instance من CounterCubit.
Barrel (تجميع الصادرات)
Section titled “Barrel (تجميع الصادرات)”لننشئ الملف lib/counter/view/view.dart:
أضف view.dart لتصدير جميع الأجزاء العامة (public) الخاصة بعرض العداد.
export 'counter_page.dart';export 'counter_view.dart';لننشئ الملف lib/counter/counter.dart:
أضف counter.dart لتصدير جميع الأجزاء العامة (public) الخاصة بميزة العداد.
export 'cubit/counter_cubit.dart';export 'view/view.dart';انتهينا! قمنا بفصل طبقة العرض (presentation layer) عن طبقة منطق الأعمال
(business logic layer). لا تعرف CounterView ماذا يحدث عند ضغط المستخدم على
الزر؛ هي فقط تُخطر CounterCubit. وفي المقابل، لا يعرف CounterCubit شيئًا عن
طريقة عرض الحالة (قيمة العداد)، بل يصدر حالات جديدة استجابةً لاستدعاء methods.
يمكننا تشغيل التطبيق بالأمر flutter run وعرضه على الجهاز أو
simulator/emulator.
يمكن العثور على المصدر الكامل (بما في ذلك اختبارات الوحدة واختبارات Widgets) لهذا المثال هنا.