Flutter学习 - Bloc - 05 倒计时
本文主要使用bloc 模式创建一个倒计时,包括暂停重置等状态
1. 分析
首先就是如下图所示,是一个倒计时,提供了开始,暂停,继续,重置几种状态,同时还有一个完成状态当,倒计时结束后达到重置效果。
同时有一个计时器,我们可以订阅它,之后发送我们的秒数。这个时候可以使用stream
流就行执行。对于UI方面,我们会根据状态展示不同按钮。
2. periodic
我们使用Stream
初始化一个流,每秒执行1次,执行次数通过方法传进来,之后转化下剩余秒数。
class TimeTicker{
const TimeTicker();
Stream<int> tick({required int ticks}){
return Stream.periodic(const Duration(seconds: 1),(count) => ticks - count -1).take(ticks);
}
}
这里对于必填的参数我们使用required
修饰,这样明确语意
3. State
我们之前分析了计时器的几种状态,因此我们可以设置几种状态
CalculateInitial
:准备从指定的时间开始倒计时,用户可以进行倒计时。TimerRunInProgress
:正在倒计时中,暂停和重置计时器并且可以看到剩余的时间。TimerRunPause
:暂停,恢复倒计时和重置计时器。TimerRunComplete
:结束,重置计时器。
part of 'calculate_bloc.dart';
abstract class CalculateState extends Equatable {
final int duration;
const CalculateState(this.duration);
@override
List<Object> get props => [duration];
}
class CalculateInitial extends CalculateState {
const CalculateInitial(super.duration);
}
class TimerRunPause extends CalculateState {
const TimerRunPause(super.duration);
}
class TimerRunInProgress extends CalculateState {
const TimerRunInProgress(super.duration);
}
class TimerRunComplete extends CalculateState {
const TimerRunComplete() : super(0);
}
注意所有的states
都继承自抽象基类CalculateState
,它有一个duration属性。这是因为不管TimerBloc
在哪里,我们都想知道还剩余多少时间。另外CalculateState
还继承了Equatable
用于确保如果有相同状态不会再次触发重建。
4. Event
对于我们Event事件驱动,我们对应state也有相对应的event
TimerStarted
:通知Bloc开始计时。TimerPaused
:通知Bloc暂停。TimerResumed
:通知Bloc恢复计时。TimerReset
:通知Bloc重置计时器到原来的状态。TimerTicked
:通知Bloc一个tick已经发生,需要更新它对应的状态。
part of 'calculate_bloc.dart';
abstract class CalculateEvent extends Equatable {
const CalculateEvent();
@override
List<Object> get props => [];
}
class TimerStarted extends CalculateEvent {
final int duration;
const TimerStarted({required this.duration });
}
class TimerTicked extends CalculateEvent {
const TimerTicked({required this.duration});
final int duration;
@override
List<Object> get props => [duration];
}
class TimerPaused extends CalculateEvent {
const TimerPaused();
}
class TimerResumed extends CalculateEvent {
const TimerResumed();
}
class TimerReset extends CalculateEvent {
const TimerReset();
5. Bloc
我们这里实现逻辑,针对event,处理逻辑发送对应的state
5.1 初始化
part 'calculate_event.dart';
part 'calculate_state.dart';
class CalculateBloc extends Bloc<CalculateEvent, CalculateState> {
static const int _duration = 60;
final TimeTicker _ticker;
StreamSubscription<int>? _tickerStreamSubscription;
CalculateBloc({required TimeTicker ticker}) :_ticker = ticker , super(const CalculateInitial(_duration)) {
我们定义倒计时和计时器,同时定义一个订阅者用于订阅Stream,从而控制我们的Stream是否暂停,继续以及销毁。
我们关闭的时候需要,关闭订阅
/// 关闭的时候 关闭订阅
@override
Future<void> close() async {
_tickerStreamSubscription?.cancel();
return super.close();
}
5.2 TimerStarted
如果CalculateBloc
收到TimerStarted
事件,它会发送一个带有开始时间的TimerRunInProgress
状态。此外,如果已经打开了_tickerSubscription
我们需要取消它释放内存。我们也需要在TimerBloc
中重载close
方法,当TimerBloc
被关闭的时候能取消_tickerSubscription
。最后我们监听_ticker.tick
流并且在每个触发时间我们添加一个包含剩余时间的TimerTicked
事件。
/// 开始:1。发送一个带有开始时间的状态
void _onStarted(TimerStarted event, Emitter<CalculateState> emit) {
emit(TimerRunInProgress(event.duration));
_tickerStreamSubscription?.cancel();
_tickerStreamSubscription = _ticker
.tick(ticks: event.duration)
.listen((event) => add(TimerTicked(duration: event)));
}
5.3 TimerTicked
每次接收到TimerTicked
事件,如果剩余时间大于0,我们需要发送一个带有新的剩余时间的TimerRunInProgress
事件来更新状态。否则,如果剩余时间等于0,那么倒计时已经结束,我们需要发送TimerRunComplete
状态。
/// 计时中,判断是否剩余时间
void _onTicked(TimerTicked event, Emitter<CalculateState> emit){
emit(event.duration>0 ? TimerRunInProgress(event.duration): const TimerRunComplete());
}
5.4 TimerPaused
在_onPaused
中如果我们TimerBloc
中的状态是TimerRunInProgress
,我们可以暂停_tickerSubscription
并且发送一个带有当前时间的TimerRunPause
状态。
/// 暂停,判断当时是否state为TimerRunInprogress类型,之后暂停订阅,发送暂停state
void _onPaused(TimerPaused event, Emitter<CalculateState> emit) {
if( state is TimerRunInProgress) {
_tickerStreamSubscription?.pause();
emit(TimerRunPause(state.duration));
}
}
5.5 TimerResumed
TimerResumed
事件处理和TimerPaused
事件的处理非常相似。如果CalculateBloc
的state
是TimerRunPause
并且它接收到一个TimerResumed
事件,它恢复_tickerSubscription
并且发送一个带有当前时间的TimerRunInProgress
状态。
/// 恢复,我们判断当前state为暂停状态后,进行恢复jiant
void _onResumed(TimerResumed event, Emitter<CalculateState> emit){
if( state is TimerRunPause) {
_tickerStreamSubscription?.resume();
emit(TimerRunInProgress(state.duration));
}
}
5.6 TimerReset
如果CalculateBloc
接收到一个TimerReset
事件,它需要取消当前的_tickerSubscription
这样它就不会被计时器通知,并且发送一个带有初始时间的CalculateInitial
状态。
/// 重置,初始化
void _onReset(TimerReset event ,Emitter<CalculateState> emit) {
_tickerStreamSubscription?.cancel();
emit(const CalculateInitial(_duration));
}
6. UI
我们通过BlocProvider关联我们的页面和bloc
class TimerPage extends StatelessWidget {
const TimerPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CalculateBloc(ticker: const TimeTicker()),
child: const TimerView(),
);
}
}
6.1 TimerPage
根据布局我们使用Stack,方便我们添加背景什么的。
class TimerView extends StatelessWidget {
const TimerView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('倒计时')),
body: Stack(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: const <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 100.0),
child: Center(child: TimerText()),
),
Actions(),
],
),
],
),
);
}
}
6.2 TimerText
通过context获取Bloc,之后展示在Text中
class TimerText extends StatelessWidget {
const TimerText({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final duration = context.select((CalculateBloc 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.headline1,
);
}
}
6.3 Actions
class Actions extends StatelessWidget {
const Actions({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocBuilder<CalculateBloc, CalculateState>(
buildWhen: (prev, state) => prev.runtimeType != state.runtimeType,
builder: (context, state) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
if (state is CalculateInitial) ...[
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.play_arrow),
onPressed: () => context
.read<CalculateBloc>()
.add(TimerStarted(duration: state.duration)),
),
],
if (state is TimerRunInProgress) ...[
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.pause),
onPressed: () => context.read<CalculateBloc>().add(const TimerPaused()),
),
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.replay),
onPressed: () => context.read<CalculateBloc>().add(const TimerReset()),
),
],
if (state is TimerRunPause) ...[
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.play_arrow),
onPressed: () => context.read<CalculateBloc>().add(const TimerResumed()),
),
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.replay),
onPressed: () => context.read<CalculateBloc>().add(const TimerReset()),
),
],
if (state is TimerRunComplete) ...[
FloatingActionButton(
heroTag: null,
child: const Icon(Icons.replay),
onPressed: () => context.read<CalculateBloc>().add(const TimerReset()),
),
]
],
);
},
);
}
}
Actions
小部件只是另一个StatelessWidget
,每当我们获取到一个新的TimerState时,它使用BlocBuilder
来重建UI。Actions
使用context.read<TimerBloc>()
访问TimerBloc
实例并且基于当前TimerBloc
状态返回不同的FloatingActionButtons
。每个FloatingActionButtons
的onPressed
回调中都添加一个事件通知CalculateBloc
。
如果你想细微的控制,当builder
方法被调用的时候你可以提供一个可选的buildWhen
到BlocBuilder
。buildWhen
携带前一个bloc状态和当前的bloc状态,并且返回一个boolean
值。如果buildWhen
返回true
,将调用带有state
的builder
并且重建组件。如果buildWhen
返回false
,带有state
的builder
将不会被调用并且不会被重建。
这种情况下,我们不想每次都重新构建Actions
组件,这样效率很低。我们只想在TimeState
的runtimeType
改变的时候(TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, 等…)重建 Actions
。
7. 小结
对于一些多状态的场景,我们可以继承抽象类
,每种状态代表一种情况,都持有抽象类参数duration
。与之对应的是event
,通过event
发送不同state刷新界面。同时我们通过stream
可以监听,根据传递状态做出对应操作,最后做到了 event-state
一一对应。
转载自:https://juejin.cn/post/7133154194344640543