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

Flutter Counter

beginner

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

demo

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

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

سنبدأ بإنشاء مشروع فلاتر جديد تمامًا:

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.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) الخاصة بنا:

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

يستخدم التطبيق هيكل دليل (Directory Structure) يعتمد على الميزات (Feature-driven). يتيح لنا هيكل المشروع هذا توسيع نطاق المشروع من خلال وجود ميزات مكتفية ذاتيًا. في هذا المثال، سيكون لدينا ميزة واحدة فقط (العداد نفسه)، ولكن في التطبيقات الأكثر تعقيدًا، يمكن أن يكون لدينا المئات من الميزات المختلفة.

أول شيء سننظر إليه هو كيفية إنشاء 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');
}
}

في هذه الحالة، نحن نقوم فقط بتجاوز الدالة 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 باستخدام ويدجت 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:

تتحمل ويدجت 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:

ستكشف فئة 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 مسؤول عن عرض العدد الحالي وعرض زرين عائمين للإجراءات (FloatingActionButtons) لزيادة/إنقاص العداد.

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 لتغليف ويدجت Text من أجل تحديث النص في أي وقت تتغير فيه حالة CounterCubit. بالإضافة إلى ذلك، يتم استخدام context.read<CounterCubit>() للبحث عن أقرب نسخة من 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 لتصدير جميع الأجزاء العامة لميزة العداد.

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

هذا كل شيء! لقد قمنا بفصل طبقة العرض (Presentation Layer) عن طبقة منطق الأعمال (Business Logic Layer). لا تملك CounterView أي فكرة عما يحدث عندما يضغط المستخدم على زر؛ إنها فقط تخطر CounterCubit. علاوة على ذلك، لا يملك CounterCubit أي فكرة عما يحدث مع الحالة (قيمة العداد)؛ إنه ببساطة يصدر حالات جديدة استجابةً لاستدعاء الطرق (Methods).

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

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