close
تخطَّ إلى المحتوى

Flutter Counter

beginner

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

demo

المواضيع الرئيسية (Key Topics)

Section titled “المواضيع الرئيسية (Key Topics)”
  • مراقبة تغييرات الحالة باستخدام BlocObserver.
  • BlocProvider، وهي Widget في Flutter توفّر Bloc للأبناء.
  • BlocBuilder، وهي Widget في Flutter تتولّى إعادة البناء استجابةً للحالات الجديدة.
  • استخدام Cubit بدلاً من Bloc. ما هو الفرق؟
  • إضافة الأحداث باستخدام context.read.

سنبدأ بإنشاء مشروع Flutter جديد بالكامل:

Terminal window
flutter create flutter_counter

بعد ذلك، يمكننا استبدال محتويات ملف pubspec.yaml بما يلي:

pubspec.yaml
name: flutter_counter
description: A new Flutter project.
version: 1.0.0+1
publish_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):

Terminal window
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 الذي يساعدنا على مراقبة جميع تغييرات الحالة في التطبيق.

لننشئ الملف lib/counter_observer.dart:

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 لمتابعة جميع تغييرات الحالة.

بعد ذلك، استبدل محتويات الملف lib/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 التي سنراجعها الآن.

لننشئ الملف lib/app.dart:

CounterApp هو MaterialApp ويحدد home على أنه CounterPage.

lib/app.dart
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.

لننشئ الملف lib/counter/view/counter_page.dart:

تتولى Widget CounterPage إنشاء CounterCubit (الذي سنراجعه بعد قليل) وتوفيره إلى CounterView.

lib/counter/view/counter_page.dart
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(),
);
}
}

لننشئ الملف lib/counter/cubit/counter_cubit.dart:

تعرض class CounterCubit طريقتين (methods):

  • increment: تضيف 1 إلى الحالة الحالية
  • decrement: تطرح 1 من الحالة الحالية

نوع الحالة الذي يديره CounterCubit هو int فقط، والحالة الأولية هي 0.

lib/counter/cubit/counter_cubit.dart
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.

لننشئ الملف lib/counter/view/counter_view.dart:

CounterView مسؤولة عن عرض قيمة العداد الحالية، وإظهار زري FloatingActionButton لزيادة/إنقاص العداد.

lib/counter/view/counter_view.dart
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.

لننشئ الملف lib/counter/view/view.dart:

أضف view.dart لتصدير جميع الأجزاء العامة (public) الخاصة بعرض العداد.

lib/counter/view/view.dart
export 'counter_page.dart';
export 'counter_view.dart';

لننشئ الملف lib/counter/counter.dart:

أضف counter.dart لتصدير جميع الأجزاء العامة (public) الخاصة بميزة العداد.

lib/counter/counter.dart
export 'cubit/counter_cubit.dart';
export 'view/view.dart';

انتهينا! قمنا بفصل طبقة العرض (presentation layer) عن طبقة منطق الأعمال (business logic layer). لا تعرف CounterView ماذا يحدث عند ضغط المستخدم على الزر؛ هي فقط تُخطر CounterCubit. وفي المقابل، لا يعرف CounterCubit شيئًا عن طريقة عرض الحالة (قيمة العداد)، بل يصدر حالات جديدة استجابةً لاستدعاء methods.

يمكننا تشغيل التطبيق بالأمر flutter run وعرضه على الجهاز أو simulator/emulator.

يمكن العثور على المصدر الكامل (بما في ذلك اختبارات الوحدة واختبارات Widgets) لهذا المثال هنا.