likes
comments
collection
share

Flutter学习 - Bloc - 05 倒计时

作者站长头像
站长
· 阅读数 45

本文主要使用bloc 模式创建一个倒计时,包括暂停重置等状态

1. 分析

首先就是如下图所示,是一个倒计时,提供了开始,暂停,继续,重置几种状态,同时还有一个完成状态当,倒计时结束后达到重置效果。

Flutter学习 - Bloc - 05 倒计时

同时有一个计时器,我们可以订阅它,之后发送我们的秒数。这个时候可以使用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事件的处理非常相似。如果CalculateBlocstateTimerRunPause并且它接收到一个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。每个FloatingActionButtonsonPressed回调中都添加一个事件通知CalculateBloc

如果你想细微的控制,当builder方法被调用的时候你可以提供一个可选的buildWhenBlocBuilderbuildWhen携带前一个bloc状态和当前的bloc状态,并且返回一个boolean值。如果buildWhen返回true,将调用带有statebuilder并且重建组件。如果buildWhen返回false,带有statebuilder将不会被调用并且不会被重建。

这种情况下,我们不想每次都重新构建Actions组件,这样效率很低。我们只想在TimeStateruntimeType改变的时候(TimerInitial => TimerRunInProgress, TimerRunInProgress => TimerRunPause, 等…)重建 Actions

7. 小结

对于一些多状态的场景,我们可以继承抽象类,每种状态代表一种情况,都持有抽象类参数duration。与之对应的是event,通过event发送不同state刷新界面。同时我们通过stream可以监听,根据传递状态做出对应操作,最后做到了 event-state 一一对应。

转载自:https://juejin.cn/post/7133154194344640543
评论
请登录