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

المواضيع الرئيسية (Key Topics)
Section titled “المواضيع الرئيسية (Key Topics)”- مراقبة تغييرات الحالة باستخدام
BlocObserver. BlocProvider، وهي ويدجت (Widget) من فلاتر توفر Bloc لأبنائها.BlocBuilder، وهي ويدجت من فلاتر تتولى بناء الويدجت استجابةً للحالات الجديدة.- استخدام Cubit بدلاً من Bloc. ما هو الفرق؟
- إضافة الأحداث باستخدام
context.read.
الإعداد (Setup)
Section titled “الإعداد (Setup)”سنبدأ بإنشاء مشروع فلاتر جديد تمامًا:
flutter create flutter_counterبعد ذلك، يمكننا استبدال محتويات ملف pubspec.yaml بما يلي:
name: flutter_counterdescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ">=3.10.0 <4.0.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يستخدم التطبيق هيكل دليل (Directory Structure) يعتمد على الميزات (Feature-driven). يتيح لنا هيكل المشروع هذا توسيع نطاق المشروع من خلال وجود ميزات مكتفية ذاتيًا. في هذا المثال، سيكون لدينا ميزة واحدة فقط (العداد نفسه)، ولكن في التطبيقات الأكثر تعقيدًا، يمكن أن يكون لدينا المئات من الميزات المختلفة.
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'); }}في هذه الحالة، نحن نقوم فقط بتجاوز الدالة 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 باستخدام ويدجت
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:
تتحمل ويدجت 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:
ستكشف فئة 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 مسؤول عن عرض العدد الحالي وعرض زرين عائمين للإجراءات
(FloatingActionButtons) لزيادة/إنقاص العداد.
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 لتغليف ويدجت Text من أجل تحديث النص في أي وقت تتغير
فيه حالة CounterCubit. بالإضافة إلى ذلك، يتم استخدام
context.read<CounterCubit>() للبحث عن أقرب نسخة من 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 لتصدير جميع الأجزاء العامة لميزة العداد.
export 'cubit/counter_cubit.dart';export 'view/view.dart';هذا كل شيء! لقد قمنا بفصل طبقة العرض (Presentation Layer) عن طبقة منطق الأعمال
(Business Logic Layer). لا تملك CounterView أي فكرة عما يحدث عندما يضغط
المستخدم على زر؛ إنها فقط تخطر CounterCubit. علاوة على ذلك، لا يملك
CounterCubit أي فكرة عما يحدث مع الحالة (قيمة العداد)؛ إنه ببساطة يصدر حالات
جديدة استجابةً لاستدعاء الطرق (Methods).
يمكننا تشغيل تطبيقنا باستخدام الأمر flutter run وعرضه على جهازنا أو
المحاكي/المحاكي.
يمكن العثور على المصدر الكامل (بما في ذلك اختبارات الوحدة والويدجت) لهذا المثال هنا.