携手创造,一起成长!这是我参与「日新计划 8 月更文挑战」的第19天,点击查看活动详情

本文首要运用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 一一对应。