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

مؤقت Flutter (Flutter Timer)

beginner

في الدليل التالي، سنتناول كيفية بناء تطبيق مؤقت باستخدام مكتبة Bloc. يجب أن يبدو التطبيق النهائي كما يلي:

demo

  • مراقبة تغييرات الحالة باستخدام BlocObserver.
  • BlocProvider، وهي ويدجت (Widget) من Flutter توفر كتلة (Bloc) لأبنائها.
  • BlocBuilder، وهي ويدجت من Flutter تتولى بناء الويدجت استجابةً للحالات الجديدة.
  • منع إعادة البناء غير الضرورية باستخدام Equatable.
  • تعلم كيفية استخدام StreamSubscription في Bloc.
  • منع إعادة البناء غير الضرورية باستخدام buildWhen.

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

Terminal window
flutter create flutter_timer

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

pubspec.yaml
name: flutter_timer
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
equatable: ^2.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
mocktail: ^1.0.0
flutter:
uses-material-design: true

بعد ذلك، قم بتشغيل flutter pub get لتثبيت جميع التبعيات.

├── lib
| ├── timer
│ │ ├── bloc
│ │ │ └── timer_bloc.dart
| | | └── timer_event.dart
| | | └── timer_state.dart
│ │ └── view
│ │ | ├── timer_page.dart
│ │ ├── timer.dart
│ ├── app.dart
│ ├── ticker.dart
│ └── main.dart
├── pubspec.lock
├── pubspec.yaml

سيكون المؤقت (Ticker) هو مصدر البيانات لتطبيق المؤقت. سيكشف عن دفق من النبضات (stream of ticks) يمكننا الاشتراك فيه والتفاعل معه.

ابدأ بإنشاء ticker.dart.

lib/ticker.dart
class Ticker {
const Ticker();
Stream<int> tick({required int ticks}) {
return Stream.periodic(
const Duration(seconds: 1),
(x) => ticks - x - 1,
).take(ticks);
}
}

كل ما يفعله صنف Ticker الخاص بنا هو كشف دالة tick تأخذ عدد النبضات (الثواني) التي نريدها وتُرجع دفقًا (stream) يُصدر الثواني المتبقية كل ثانية.

بعد ذلك، نحتاج إلى إنشاء TimerBloc الخاص بنا والذي سيستهلك Ticker.

سنبدأ بتعريف TimerStates التي يمكن أن يكون عليها TimerBloc الخاص بنا.

يمكن أن تكون حالة TimerBloc الخاصة بنا واحدة مما يلي:

  • TimerInitial: جاهز لبدء العد التنازلي من المدة المحددة.
  • TimerRunInProgress: يعد تنازليًا بنشاط من المدة المحددة.
  • TimerRunPause: متوقف مؤقتًا عند مدة متبقية معينة.
  • TimerRunComplete: اكتمل بمدة متبقية 0.

كل من هذه الحالات سيكون له تأثير على واجهة المستخدم والإجراءات التي يمكن للمستخدم القيام بها. على سبيل المثال:

  • إذا كانت الحالة هي TimerInitial، فسيتمكن المستخدم من بدء المؤقت.
  • إذا كانت الحالة هي TimerRunInProgress، فسيتمكن المستخدم من إيقاف المؤقت مؤقتًا وإعادة تعيينه، بالإضافة إلى رؤية المدة المتبقية.
  • إذا كانت الحالة هي TimerRunPause، فسيتمكن المستخدم من استئناف المؤقت وإعادة تعيينه.
  • إذا كانت الحالة هي TimerRunComplete، فسيتمكن المستخدم من إعادة تعيين المؤقت.

للحفاظ على جميع ملفات الـ bloc الخاصة بنا معًا، دعنا ننشئ دليل bloc مع bloc/timer_state.dart.

lib/timer/bloc/timer_state.dart
part of 'timer_bloc.dart';
sealed class TimerState extends Equatable {
const TimerState(this.duration);
final int duration;
@override
List<Object> get props => [duration];
}
final class TimerInitial extends TimerState {
const TimerInitial(super.duration);
@override
String toString() => 'TimerInitial { duration: $duration }';
}
final class TimerRunPause extends TimerState {
const TimerRunPause(super.duration);
@override
String toString() => 'TimerRunPause { duration: $duration }';
}
final class TimerRunInProgress extends TimerState {
const TimerRunInProgress(super.duration);
@override
String toString() => 'TimerRunInProgress { duration: $duration }';
}
final class TimerRunComplete extends TimerState {
const TimerRunComplete() : super(0);
}

لاحظ أن جميع TimerStates توسع الصنف الأساسي المجرد TimerState الذي يحتوي على خاصية المدة (duration). هذا لأنه بغض النظر عن الحالة التي يوجد بها TimerBloc الخاص بنا، نريد أن نعرف مقدار الوقت المتبقي. بالإضافة إلى ذلك، يوسع TimerState من Equatable لتحسين الكود الخاص بنا من خلال ضمان أن تطبيقنا لا يؤدي إلى إعادة بناء إذا حدثت نفس الحالة.

بعد ذلك، دعنا نحدد وننفذ TimerEvents التي سيعالجها TimerBloc الخاص بنا.

سيحتاج TimerBloc الخاص بنا إلى معرفة كيفية معالجة الأحداث التالية:

  • TimerStarted: يُعلم TimerBloc بضرورة بدء المؤقت.
  • TimerPaused: يُعلم TimerBloc بضرورة إيقاف المؤقت مؤقتًا.
  • TimerResumed: يُعلم TimerBloc بضرورة استئناف المؤقت.
  • TimerReset: يُعلم TimerBloc بضرورة إعادة تعيين المؤقت إلى حالته الأصلية.
  • _TimerTicked: يُعلم TimerBloc بحدوث نبضة (tick) وبضرورة تحديث حالته وفقًا لذلك.

إذا لم تستخدم إضافات IntelliJ أو VSCode، فقم بإنشاء bloc/timer_event.dart ودعنا ننفذ تلك الأحداث.

lib/timer/bloc/timer_event.dart
part of 'timer_bloc.dart';
sealed class TimerEvent {
const TimerEvent();
}
final class TimerStarted extends TimerEvent {
const TimerStarted({required this.duration});
final int duration;
}
final class TimerPaused extends TimerEvent {
const TimerPaused();
}
final class TimerResumed extends TimerEvent {
const TimerResumed();
}
class TimerReset extends TimerEvent {
const TimerReset();
}
class _TimerTicked extends TimerEvent {
const _TimerTicked({required this.duration});
final int duration;
}

بعد ذلك، دعنا ننفذ TimerBloc!

إذا لم تكن قد قمت بذلك بالفعل، فقم بإنشاء bloc/timer_bloc.dart وأنشئ TimerBloc فارغًا.

lib/timer/bloc/timer_bloc.dart
import 'package:bloc/bloc.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
// TODO: set initial state
TimerBloc(): super() {
// TODO: implement event handlers
}
}

أول شيء نحتاج إلى القيام به هو تحديد الحالة الأولية لـ TimerBloc الخاص بنا. في هذه الحالة، نريد أن يبدأ TimerBloc في حالة TimerInitial بمدة محددة مسبقًا تبلغ دقيقة واحدة (60 ثانية).

lib/timer/bloc/timer_bloc.dart
import 'package:bloc/bloc.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
static const int _duration = 60;
TimerBloc() : super(TimerInitial(_duration)) {
// TODO: implement event handlers
}
}

بعد ذلك، نحتاج إلى تحديد التبعية على Ticker الخاص بنا.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
// TODO: implement event handlers
}
}

نقوم أيضًا بتعريف StreamSubscription لـ Ticker الخاص بنا والذي سنتطرق إليه بعد قليل.

في هذه المرحلة، كل ما تبقى هو تنفيذ معالجات الأحداث. لتحسين قابلية القراءة، أحب تقسيم كل معالج حدث إلى دالة مساعدة خاصة به. سنبدأ بحدث TimerStarted.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
}

إذا تلقى TimerBloc حدث TimerStarted، فإنه يدفع حالة TimerRunInProgress بمدة البدء. بالإضافة إلى ذلك، إذا كان هناك بالفعل _tickerSubscription مفتوح، فنحن بحاجة إلى إلغائه لتحرير الذاكرة. نحتاج أيضًا إلى تجاوز طريقة close في TimerBloc الخاص بنا حتى نتمكن من إلغاء _tickerSubscription عند إغلاق TimerBloc. أخيرًا، نستمع إلى دفق _ticker.tick وفي كل نبضة، نضيف حدث _TimerTicked بالمدة المتبقية.

بعد ذلك، دعنا ننفذ معالج حدث _TimerTicked.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

في كل مرة يتم فيها تلقي حدث _TimerTicked، إذا كانت مدة النبضة أكبر من 0، فنحن بحاجة إلى دفع حالة TimerRunInProgress محدثة بالمدة الجديدة. بخلاف ذلك، إذا كانت مدة النبضة هي 0، فقد انتهى المؤقت الخاص بنا ونحتاج إلى دفع حالة TimerRunComplete.

الآن دعنا ننفذ معالج حدث TimerPaused.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

في _onPaused إذا كانت state الخاصة بـ TimerBloc هي TimerRunInProgress، فيمكننا إيقاف _tickerSubscription مؤقتًا ودفع حالة TimerRunPause بالمدة الحالية للمؤقت.

بعد ذلك، دعنا ننفذ معالج حدث TimerResumed حتى نتمكن من استئناف المؤقت.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<TimerResumed>(_onResumed);
on<_TimerTicked>(_onTicked);
}
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onResumed(TimerResumed resume, Emitter<TimerState> emit) {
if (state is TimerRunPause) {
_tickerSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: TimerRunComplete(),
);
}
}

معالج حدث TimerResumed مشابه جدًا لمعالج حدث TimerPaused. إذا كان TimerBloc في state من نوع TimerRunPause وتلقى حدث TimerResumed، فإنه يستأنف _tickerSubscription ويدفع حالة TimerRunInProgress بالمدة الحالية.

أخيرًا، نحتاج إلى تنفيذ معالج حدث TimerReset.

lib/timer/bloc/timer_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter_timer/ticker.dart';
part 'timer_event.dart';
part 'timer_state.dart';
class TimerBloc extends Bloc<TimerEvent, TimerState> {
TimerBloc({required Ticker ticker})
: _ticker = ticker,
super(const TimerInitial(_duration)) {
on<TimerStarted>(_onStarted);
on<TimerPaused>(_onPaused);
on<TimerResumed>(_onResumed);
on<TimerReset>(_onReset);
on<_TimerTicked>(_onTicked);
}
final Ticker _ticker;
static const int _duration = 60;
StreamSubscription<int>? _tickerSubscription;
@override
Future<void> close() {
_tickerSubscription?.cancel();
return super.close();
}
void _onStarted(TimerStarted event, Emitter<TimerState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerSubscription?.cancel();
_tickerSubscription = _ticker
.tick(ticks: event.duration)
.listen((duration) => add(_TimerTicked(duration: duration)));
}
void _onPaused(TimerPaused event, Emitter<TimerState> emit) {
if (state is TimerRunInProgress) {
_tickerSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
void _onResumed(TimerResumed resume, Emitter<TimerState> emit) {
if (state is TimerRunPause) {
_tickerSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
void _onReset(TimerReset event, Emitter<TimerState> emit) {
_tickerSubscription?.cancel();
emit(const TimerInitial(_duration));
}
void _onTicked(_TimerTicked event, Emitter<TimerState> emit) {
emit(
event.duration > 0
? TimerRunInProgress(event.duration)
: const TimerRunComplete(),
);
}
}

إذا تلقى TimerBloc حدث TimerReset، فإنه يحتاج إلى إلغاء _tickerSubscription الحالي حتى لا يتم إخطاره بأي نبضات إضافية ويدفع حالة TimerInitial بالمدة الأصلية.

هذا كل ما يتعلق بـ TimerBloc. الآن كل ما تبقى هو تنفيذ واجهة المستخدم لتطبيق المؤقت الخاص بنا.

واجهة مستخدم التطبيق (Application UI)

Section titled “واجهة مستخدم التطبيق (Application UI)”

يمكننا أن نبدأ بحذف محتويات main.dart واستبدالها بما يلي.

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_timer/app.dart';
void main() => runApp(const App());

بعد ذلك، دعنا ننشئ ويدجت ‘App’ الخاص بنا في app.dart، والذي سيكون جذر تطبيقنا.

lib/app.dart
import 'package:flutter/material.dart';
import 'package:flutter_timer/timer/timer.dart';
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Timer',
theme: ThemeData(
colorScheme: const ColorScheme.light(
primary: Color.fromRGBO(72, 74, 126, 1),
),
),
home: const TimerPage(),
);
}
}

بعد ذلك، نحتاج إلى تنفيذ ويدجت Timer الخاص بنا.

سيكون ويدجت Timer الخاص بنا (lib/timer/view/timer_page.dart) مسؤولاً عن عرض الوقت المتبقي جنبًا إلى جنب مع الأزرار المناسبة التي ستمكن المستخدمين من بدء المؤقت وإيقافه مؤقتًا وإعادة تعيينه.

lib/timer/view/timer_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_timer/ticker.dart';
import 'package:flutter_timer/timer/timer.dart';
class TimerPage extends StatelessWidget {
const TimerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => TimerBloc(ticker: Ticker()),
child: const TimerView(),
);
}
}
class TimerView extends StatelessWidget {
const TimerView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter Timer')),
body: Stack(
children: [
const Background(),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 100.0),
child: Center(child: TimerText()),
),
Actions(),
],
),
],
),
);
}
}
class TimerText extends StatelessWidget {
const TimerText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final duration = context.select((TimerBloc bloc) => bloc.state.duration);
final minutesStr =
((duration / 60) % 60).floor().toString().padLeft(2, '0');
final secondsStr = (duration % 60).floor().toString().padLeft(2, '0');
return Text(
'$minutesStr:$secondsStr',
style: Theme.of(
context,
).textTheme.displayLarge?.copyWith(fontWeight: FontWeight.w500),
);
}
}

حتى الآن، نحن نستخدم BlocProvider فقط للوصول إلى مثيل TimerBloc الخاص بنا.

بعد ذلك، سنقوم بتنفيذ ويدجت Actions الخاص بنا والذي سيحتوي على الإجراءات المناسبة (بدء، إيقاف مؤقت، وإعادة تعيين).

لتنظيف عمليات الاستيراد الخاصة بنا من قسم Timer، نحتاج إلى إنشاء ملف تجميع (barrel file) باسم timer/timer.dart.

lib/timer/timer.dart
export 'bloc/timer_bloc.dart';
export 'view/timer_page.dart';
lib/timer/view/timer_page.dart
class Actions extends StatelessWidget {
const Actions({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<TimerBloc, TimerState>(
buildWhen: (prev, state) => prev.runtimeType != state.runtimeType,
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
...switch (state) {
TimerInitial() => [
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () => context
.read<TimerBloc>()
.add(TimerStarted(duration: state.duration)),
),
],
TimerRunInProgress() => [
FloatingActionButton(
child: const Icon(Icons.pause),
onPressed: () =>
context.read<TimerBloc>().add(const TimerPaused()),
),
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
],
TimerRunPause() => [
FloatingActionButton(
child: const Icon(Icons.play_arrow),
onPressed: () =>
context.read<TimerBloc>().add(const TimerResumed()),
),
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
],
TimerRunComplete() => [
FloatingActionButton(
child: const Icon(Icons.replay),
onPressed: () =>
context.read<TimerBloc>().add(const TimerReset()),
),
]
}
],
);
},
);
}
}

ويدجت Actions هو مجرد StatelessWidget آخر يستخدم BlocBuilder لإعادة بناء واجهة المستخدم في كل مرة نحصل فيها على TimerState جديدة. يستخدم Actions الدالة context.read<TimerBloc>() للوصول إلى مثيل TimerBloc ويُرجع أزرار FloatingActionButton مختلفة بناءً على الحالة الحالية لـ TimerBloc. يضيف كل زر من أزرار FloatingActionButton حدثًا في دالة رد الاتصال onPressed لإخطار TimerBloc.

إذا كنت تريد تحكمًا دقيقًا في وقت استدعاء دالة builder، يمكنك توفير buildWhen اختياريًا لـ BlocBuilder. يأخذ buildWhen حالة الـ bloc السابقة وحالة الـ bloc الحالية ويُرجع قيمة منطقية (boolean). إذا أرجع buildWhen القيمة true، فسيتم استدعاء builder بالحالة وسيتم إعادة بناء الويدجت. إذا أرجع buildWhen القيمة false، فلن يتم استدعاء builder بالحالة ولن تحدث إعادة بناء.

في هذه الحالة، لا نريد إعادة بناء ويدجت Actions في كل نبضة لأن ذلك سيكون غير فعال. بدلاً من ذلك، نريد فقط إعادة بناء Actions إذا تغير runtimeType لـ TimerState (على سبيل المثال: TimerInitial => TimerRunInProgress، TimerRunInProgress => TimerRunPause، إلخ…).

نتيجة لذلك، إذا قمنا بتلوين الويدجت عشوائيًا في كل عملية إعادة بناء، فسيبدو الأمر كما يلي:

BlocBuilder buildWhen demo

أخيرًا، أضف ويدجت الخلفية كما يلي:

lib/timer/view/timer_page.dart
class Background extends StatelessWidget {
const Background({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.blue.shade50,
Colors.blue.shade500,
],
),
),
);
}
}

هذا كل ما في الأمر! في هذه المرحلة، لدينا تطبيق مؤقت قوي إلى حد ما يعيد بناء الويدجت التي تحتاج إلى إعادة بناء فقط بكفاءة.

يمكن العثور على المصدر الكامل لهذا المثال هنا.