Flutter Zamanlayıcı
Bu eğitimde, bloc kütüphanesini kullanarak nasıl bir zamanlayıcı uygulaması oluşturacağımızı ele alacağız. Tamamlanan uygulama şöyle görünmelidir:

Önemli Konular
Section titled “Önemli Konular”- BlocObserver ile durum değişikliklerini gözlemleme.
- BlocProvider, alt widget'larına bir bloc sağlayan Flutter widget'ı.
- BlocBuilder, yeni durumlara karşılık widget'ın yeniden oluşturulmasını yöneten Flutter widget'ı.
- Equatable ile gereksiz yeniden oluşturmaları önleme.
- Bir Bloc içinde
StreamSubscriptionkullanmayı öğrenme. buildWhenile gereksiz yeniden oluşturmaları önleme.
Kurulum
Section titled “Kurulum”Yepyeni bir Flutter projesi oluşturarak başlayacağız:
flutter create flutter_timerArdından pubspec.yaml dosyasının içeriğini şununla değiştirebiliriz:
name: flutter_timerdescription: A new Flutter project.version: 1.0.0+1publish_to: none
environment: sdk: ^3.12.0
dependencies: bloc: ^9.0.0 equatable: ^2.1.0-dev.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: trueSonrasında, tüm bağımlılıkları kurmak için flutter pub get komutunu
çalıştırın.
Proje Yapısı
Section titled “Proje Yapısı”├── 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.yamlTicker
Section titled “Ticker”Ticker, zamanlayıcı uygulamamız için veri kaynağımız olacak. Abone olup tepki verebileceğimiz bir tik stream'i sunacaktır.
ticker.dart dosyasını oluşturarak başlayalım.
class Ticker { const Ticker(); Stream<int> tick({required int ticks}) { return Stream.periodic( const Duration(seconds: 1), (x) => ticks - x - 1, ).take(ticks); }}Ticker sınıfımızın yaptığı tek şey, istediğimiz tik (saniye) sayısını alıp her
saniye kalan saniyeleri yayan bir stream döndüren bir tick fonksiyonu sunmaktır.
Sırada, Ticker'ı tüketecek olan TimerBloc'umuzu oluşturmamız gerekiyor.
Timer Bloc
Section titled “Timer Bloc”TimerState
Section titled “TimerState”TimerBloc'umuzun bulunabileceği TimerStates'leri tanımlayarak başlayalım.
TimerBloc durumu aşağıdakilerden biri olabilir:
TimerInitial: belirtilen süreden geriye saymaya hazır.TimerRunInProgress: belirtilen süreden aktif olarak geri sayıyor.TimerRunPause: belirli bir kalan sürede duraklatılmış.TimerRunComplete: kalan süre 0 ile tamamlanmış.
Bu durumların her biri, kullanıcı arayüzü ve kullanıcının yapabileceği eylemler üzerinde bir etkiye sahip olacaktır. Örneğin:
- Durum
TimerInitialise kullanıcı zamanlayıcıyı başlatabilecek. - Durum
TimerRunInProgressise kullanıcı zamanlayıcıyı duraklatabilecek ve sıfırlayabilecek, ayrıca kalan süreyi görebilecek. - Durum
TimerRunPauseise kullanıcı zamanlayıcıyı devam ettirebilecek ve sıfırlayabilecek. - Durum
TimerRunCompleteise kullanıcı zamanlayıcıyı sıfırlayabilecek.
Tüm bloc dosyalarımızı bir arada tutmak için bloc/timer_state.dart ile bir
bloc dizini oluşturalım.
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);}Tüm TimerStates'lerin, bir duration özelliğine sahip soyut temel sınıf
TimerState'i genişlettiğine dikkat edin. Bunun nedeni, TimerBloc'umuzun
hangi durumda olduğundan bağımsız olarak, ne kadar süre kaldığını bilmek
istememizdir. Ek olarak, TimerState, aynı durum oluştuğunda uygulamamızın
yeniden oluşturmaları tetiklememesini sağlayarak kodumuzu optimize etmek için
Equatable'ı genişletir.
Sırada, TimerBloc'umuzun işleyeceği TimerEvents'leri tanımlayalım ve
uygulayalım.
TimerEvent
Section titled “TimerEvent”TimerBloc'umuzun aşağıdaki olayları nasıl işleyeceğini bilmesi gerekecek:
TimerStarted: TimerBloc'a zamanlayıcının başlatılması gerektiğini bildirir.TimerPaused: TimerBloc'a zamanlayıcının duraklatılması gerektiğini bildirir.TimerResumed: TimerBloc'a zamanlayıcının devam ettirilmesi gerektiğini bildirir.TimerReset: TimerBloc'a zamanlayıcının orijinal duruma sıfırlanması gerektiğini bildirir._TimerTicked: TimerBloc'a bir tikin gerçekleştiğini ve durumunu buna göre güncellemesi gerektiğini bildirir.
IntelliJ veya
VSCode
eklentilerini kullanmadıysanız, bloc/timer_event.dart dosyasını oluşturun ve
bu olayları uygulayalım.
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;}Sırada, TimerBloc'u uygulayalım!
TimerBloc
Section titled “TimerBloc”Henüz oluşturmadıysanız, bloc/timer_bloc.dart dosyasını oluşturun ve boş bir
TimerBloc oluşturun.
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 }}İlk yapmamız gereken şey, TimerBloc'umuzun başlangıç durumunu tanımlamaktır.
Bu durumda, TimerBloc'un 1 dakikalık (60 saniye) önceden ayarlanmış bir
süreyle TimerInitial durumunda başlamasını istiyoruz.
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 }}Sırada, Ticker'a olan bağımlılığımızı tanımlamamız gerekiyor.
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 }}Ayrıca biraz sonra inceleyeceğimiz Ticker'ımız için bir StreamSubscription
da tanımlıyoruz.
Bu noktada yapmamız gereken tek şey olay işleyicilerini uygulamaktır. Daha iyi
okunabilirlik için, her olay işleyicisini kendi yardımcı fonksiyonuna ayırmayı
tercih ediyorum. TimerStarted olayıyla başlayalım.
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, bir TimerStarted olayı alırsa, başlangıç süresiyle bir
TimerRunInProgress durumunu yaymaktadır. Ek olarak, halihazırda açık bir
_tickerSubscription varsa, belleği serbest bırakmak için onu iptal etmemiz
gerekir. Ayrıca TimerBloc kapatıldığında _tickerSubscription'ı iptal
edebilmek için TimerBloc'umuzdaki close metodunu da geçersiz kılmamız
gerekir. Son olarak, _ticker.tick stream'ini dinliyor ve her tikte kalan
süreyle bir _TimerTicked olayı ekliyoruz.
Sırada, _TimerTicked olay işleyicisini uygulayalım.
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(), ); }}Her _TimerTicked olayı alındığında, eğer tikin süresi 0'dan büyükse, yeni
süreyle güncellenmiş bir TimerRunInProgress durumu yaymaktayız. Aksi takdirde,
tikin süresi 0 ise, zamanlayıcımız sona ermiştir ve bir TimerRunComplete
durumu yaymamız gerekir.
Şimdi TimerPaused olay işleyicisini uygulayalım.
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 içinde, TimerBloc'umuzun state'i TimerRunInProgress ise,
_tickerSubscription'ı duraklatabilir ve mevcut zamanlayıcı süresiyle bir
TimerRunPause durumu yayabiliriz.
Sırada, zamanlayıcının duraklatılmasını kaldırabilmek için TimerResumed olay
işleyicisini uygulayalım.
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 olay işleyicisi, TimerPaused olay işleyicisine çok benzer.
TimerBloc TimerRunPause state'inde ise ve bir TimerResumed olayı alırsa,
_tickerSubscription'ı devam ettirir ve mevcut süreyle bir TimerRunInProgress
durumu yayar.
Son olarak, TimerReset olay işleyicisini uygulamamız gerekir.
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 this._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, bir TimerReset olayı alırsa, ek tikler hakkında bildirim almaması
için mevcut _tickerSubscription'ı iptal etmesi ve orijinal süreyle bir
TimerInitial durumu yayması gerekir.
TimerBloc ile ilgili her şey bu kadar. Şimdi geriye kalan tek şey Zamanlayıcı
Uygulamamız için kullanıcı arayüzünü uygulamaktır.
Uygulama Arayüzü
Section titled “Uygulama Arayüzü”main.dart'ın içeriğini silip aşağıdakiyle değiştirerek başlayabiliriz.
import 'package:flutter/material.dart';import 'package:flutter_timer/app.dart';
void main() => runApp(const App());Sırada, uygulamamızın kök widget'ı olacak app.dart içinde 'App' widget'ımızı
oluşturalım.
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(), ); }}Sırada, Timer widget'ımızı uygulamamız gerekiyor.
Timer widget'ımız (lib/timer/view/timer_page.dart), kalan süreyi ve
kullanıcıların zamanlayıcıyı başlatmasını, duraklatmasını ve sıfırlamasını
sağlayan uygun düğmelerle birlikte görüntülemekten sorumlu olacaktır.
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), ); }}Şimdiye kadar, sadece TimerBloc'umuzun örneğine erişmek için BlocProvider
kullanıyoruz.
Sırada, uygun eylemlere (başlat, duraklat ve sıfırla) sahip olacak Actions
widget'ımızı uygulayacağız.
Barrel
Section titled “Barrel”Timer bölümündeki içe aktarmalarımızı temizlemek için bir barrel dosyası
timer/timer.dart oluşturmamız gerekir.
export 'bloc/timer_bloc.dart';export 'view/timer_page.dart';Actions
Section titled “Actions”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 widget'ı, her yeni TimerState aldığımızda kullanıcı arayüzünü
yeniden oluşturmak için bir BlocBuilder kullanan başka bir
StatelessWidget'tan başka bir şey değildir. Actions, TimerBloc örneğine
erişmek için context.read<TimerBloc>() kullanır ve TimerBloc'un mevcut
durumuna göre farklı FloatingActionButtons döndürür.
FloatingActionButtons'ların her biri, TimerBloc'u bilgilendirmek için
onPressed geri çağrısında bir olay ekler.
builder fonksiyonunun ne zaman çağrılacağı konusunda ince ayarlı bir kontrol
istiyorsanız, BlocBuilder'a opsiyonel bir buildWhen sağlayabilirsiniz.
buildWhen, önceki bloc durumunu ve mevcut bloc durumunu alır ve bir boolean
döndürür. buildWhen true döndürürse, builder state ile çağrılır ve
widget yeniden oluşturulur. buildWhen false döndürürse, builder state
ile çağrılmaz ve yeniden oluşturma gerçekleşmez.
Bu durumda, verimsiz olacağı için Actions widget'ının her tikte yeniden
oluşturulmasını istemiyoruz. Bunun yerine, yalnızca TimerState'in
runtimeType'ı değiştiğinde (TimerInitial => TimerRunInProgress,
TimerRunInProgress => TimerRunPause, vb.) Actions'ın yeniden oluşturulmasını
istiyoruz.
Sonuç olarak, her yeniden oluşturmada widget'ları rastgele renklendirseydik, şuna benzeyecekti:

Background
Section titled “Background”Son olarak, arka plan widget'ını aşağıdaki gibi ekleyin:
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, ], ), ), ); }}Hepsini bir araya getirme
Section titled “Hepsini bir araya getirme”İşte hepsi bu kadar! Bu noktada, yalnızca yeniden oluşturulması gereken widget'ları verimli bir şekilde yeniden oluşturan oldukça sağlam bir zamanlayıcı uygulamamız var.
Bu örneğin tam kaynak kodu buradan ulaşılabilir.