Flutter 状态管理 | 业务逻辑与构建逻辑分离

2022-09-20 10:36:05 浏览数 (1)

目前我的状态管理相关文章有:

《Flutter 状态管理 | 第一论 - 对状态管理的看法与理解》

《Flutter 桌面探索 | 自定义可拖拽导航栏》

《Flutter 状态管理 | 第二论 - 业务逻辑与界面构建分离》

本文秒表的界面基础详见这两篇文章

《Flutter 绘制集录 | 秒表盘的绘制》

《Flutter 绘制集录 | 秒表运动与Ticker》


1. 业务逻辑和构建逻辑

对界面呈现来说,最重要的逻辑有两个部分:业务数据的维护逻辑界面布局的构建逻辑 。其中应用运行中相关数据的获取、修改、删除、存储等操作,就是业务逻辑。比如下面是秒表的三个界面,核心 数据 是秒表的时刻。在秒表应用执行功能时,数据的变化体现在秒数的变化、记录、重置等。

默认情况

暂停

记录


界面的构建逻辑主要体现在界面如何布局,维持界面的出现效果。另外,在界面构建过程中,除了业务数据,还有一些数据会影响界面呈现。比如打开秒表时,只有一个启动按钮;在运行中,显示暂停按钮和记录按钮;在暂停时,记录按钮不可用,重置按钮可用。这样在不同的交互场景中,有不同的界面表现,也是构建逻辑处理的一部分。


2. 数据的维护

所以的逻辑本身都是对 数据 的维护,界面能够显示出什么内容,都依赖于数据进行表现。理解需要哪些数据、数据存储在哪里,从哪里来,要传到哪里去,是编程过程中非常重要的一个环节。由于数据需要在构建界面时使用,所以很自然的:在布局写哪里,数据就在哪里维护。

比如默认的计数器项目,其中只有一个核心数据 _counter ,用于表示当前点击的次数。


代码实现时, _counter 数据定义在 _MyHomePageState 中,改数据的维护也在状态类中:

对于一些简单的场景,这样的处理无可厚非。但在复杂的交互场景中,业务逻辑和构建逻辑杂糅在 State 派生类中,会导致代码复杂,逻辑混乱,不便于阅读和维护。


3.秒表状态数据对布局的影响

现在先通过代码来实现如下交互,首先通过 StopWatchType 枚举来标识秒表运行状态。在初始状态 none 时,只有一个开始按钮;点击开始,秒表在运行中,此时显示三个按钮,重置按钮是灰色,不可点击,点击旗子按钮,可以记录当前秒表值;暂停时,旗子按钮不可点击,点击重置按钮时,回到初始态。

代码语言:javascript复制
enum StopWatchType{
  none, // 初始态
  stopped, // 已停止
  running, // 运行中
}

如下所示,通过 _buildBtnByState 方法根据 StopWatchState 状态值构建底部按钮。根据不同的 state 情况处理不同的显示效果,这就是构建逻辑的体检。而此时的关键数据就是 StopWatchState 对象。

代码语言:javascript复制
Widget _buildBtnByState(StopWatchType state) {
  bool running = state == StopWatchType.running;
  bool stopped = state == StopWatchType.stopped;
  Color activeColor  = Theme.of(context).primaryColor;
  return Wrap(
    spacing: 20,
    children: [
      if(state!=StopWatchType.none)
        FloatingActionButton(
        child: const Icon(Icons.refresh),
        backgroundColor: stopped?activeColor:Colors.grey,
        onPressed: stopped?reset:null,
      ),
      FloatingActionButton(
        child: running?const Icon(Icons.stop):const Icon(Icons.play_arrow_outlined),
        onPressed: onTapIcon,
      ),
      if(state!=StopWatchType.none)
        FloatingActionButton(
          backgroundColor: running?activeColor:Colors.grey,
          child: const Icon(Icons.flag),
        onPressed: running?onTapFlag:null,
      ),
    ],
  );
}

这样按照常理,应该在 _HomePageState 中定义 StopWatchType 对象,并在相关逻辑中维护 state 数据的值,如下 tag1,2,3 处:

代码语言:javascript复制
StopWatchType state = StopWatchState.none;

void reset(){
  duration.value = Duration.zero;
  setState(() {
    state = StopWatchState.none; // tag1
  });
}

void onTapIcon() {
  if (_ticker.isTicking) {
    _ticker.stop();
    lastDuration = Duration.zero;
    setState(() {
      state = StopWatchType.stopped; // tag2
    });
  } else {
    _ticker.start();
    setState(() {
      state = StopWatchType.running; // tag3
    });
  }
}

4.秒表记录值的维护

如下所示,在秒表运行时点击旗子,可以记录当前的时刻并显示在右侧:

由于布局界面在 _HomePageState 中,事件的触发也在该类中定义。按照常理,又需要在其中维护 durationRecord 列表数据,进行界面的展现。

代码语言:javascript复制
List<Duration> durationRecord = [];
final TextStyle recordTextStyle = const TextStyle(color: Colors.grey);

Widget buildRecordeList(){
  return ListView.builder(
      itemCount: durationRecord.length,
      itemBuilder: (_,index)=>Center(child:
      Padding(
        padding: const EdgeInsets.all(4.0),
        child: Text(
            durationRecord[index].toString(),style: recordTextStyle,
        ),
      )
      ));
}

void onTapFlag() {
  setState(() {
    durationRecord.add(duration.value);
  });
}

void reset(){
  duration.value = Duration.zero;
  durationRecord.clear();
  setState(() {
    state = StopWatchState.none;
  });
}

其实到这里可以发现,随着功能的增加,需要维护的数据会越来越多。虽然全部塞在 _HomePageState 类型访问和修改比较方便,但随着代码的增加,状态类会越来越臃肿。所以分离逻辑在复杂的场景中是非常必要的。


5. 基于 flutter_bloc 的状态管理

状态类的核心逻辑应该在于界面的 构建逻辑,而业务数据的维护,我们可以提取出来。这里通过 flutter_bloc 来将秒表中数据的维护逻辑进行分离,由 bloc 承担。

我们的目的是为 _HomePageState 状态类 "瘦身" ,如下,其中对于数据的处理逻辑都交由 StopWatchBloc 通过 add 相关事件来触发。_HomePageState 自身就无须书写维护业务数据的逻辑,可以在很大程度上减少 _HomePageState 的代码量,从而让状态类专注于界面构建逻辑。

代码语言:javascript复制
class _HomePageState extends State<HomePage> {
  StopWatchBloc get stopWatchBloc => BlocProvider.of<StopWatchBloc>(context);

  void onTapIcon() {
    stopWatchBloc.add(const ToggleStopWatch());
  }

  void onTapFlag() {
    stopWatchBloc.add(const RecordeStopWatch());
  }

  void reset() {
    stopWatchBloc.add(const ResetStopWatch());
  }

首先创建状态类 StopWatchState 来维护这三个数据:

代码语言:javascript复制
part of 'bloc.dart';

enum StopWatchType {
  none, // 初始态
  stopped, // 已停止
  running, // 运行中
}

class StopWatchState {
  final StopWatchType type;
  final List<Duration> durationRecord;
  final Duration duration;

  const StopWatchState({
    this.type = StopWatchType.none,
    this.durationRecord = const [],
    this.duration = Duration.zero,
  });

  StopWatchState copyWith({
    StopWatchType? type,
    List<Duration>? durationRecord,
    Duration? duration,
  }) {
    return StopWatchState(
      type: type ?? this.type,
      durationRecord: durationRecord??this.durationRecord,
      duration: duration??this.duration,
    );
  }
}

然后定义先关的行为事件,比如 ToggleStopWatch 用于开启或暂停秒表;ResetStopWatch 用于重置秒表;RecordeStopWatch 用于记录值。这就是最核心的三个功能:

代码语言:javascript复制
abstract class StopWatchEvent {
  const StopWatchEvent();
}

class ResetStopWatch extends StopWatchEvent{
  const ResetStopWatch();
}

class ToggleStopWatch extends StopWatchEvent {
  const ToggleStopWatch();
}

class _UpdateDuration extends StopWatchEvent {
  final Duration duration;

  _UpdateDuration(this.duration);
}

class RecordeStopWatch extends StopWatchEvent {
  const RecordeStopWatch();
}

最后在 StopWatchBloc 中监听相关的事件,进行逻辑处理,产出正确的 StopWatchState 状态量。这样就将数据的维护逻辑封装到了 StopWatchBloc 中。

代码语言:javascript复制
part 'event.dart';
part 'state.dart';

class StopWatchBloc extends Bloc<StopWatchEvent,StopWatchState>{
  Ticker? _ticker;

  StopWatchBloc():super(const StopWatchState()){
    on<ToggleStopWatch>(_onToggleStopWatch);
    on<ResetStopWatch>(_onResetStopWatch);
    on<RecordeStopWatch>(_onRecordeStopWatch);
    on<_UpdateDuration>(_onUpdateDuration);
  }

  void _initTickerWhenNull() {
    if(_ticker!=null) return;
    _ticker = Ticker(_onTick);
  }

  Duration _dt = Duration.zero;
  Duration _lastDuration = Duration.zero;


  void _onTick(Duration elapsed) {
    _dt = elapsed - _lastDuration;
    add(_UpdateDuration(state.duration _dt));
    _lastDuration = elapsed;
  }

  @override
  Future<void> close() async{
    _ticker?.dispose();
    _ticker = null;
    return super.close();
  }

  void _onToggleStopWatch(ToggleStopWatch event, Emitter<StopWatchState> emit) {
    _initTickerWhenNull();
    if (_ticker!.isTicking) {
      _ticker!.stop();
      _lastDuration = Duration.zero;
      emit(state.copyWith(type:StopWatchType.stopped));
    } else {
      _ticker!.start();
      emit(state.copyWith(type:StopWatchType.running));
    }
  }

  void _onUpdateDuration(_UpdateDuration event, Emitter<StopWatchState> emit) {
    emit(state.copyWith(
      duration: event.duration
    ));
  }

  void _onResetStopWatch(ResetStopWatch event, Emitter<StopWatchState> emit) {
    _lastDuration = Duration.zero;
    emit(const StopWatchState());
  }

  void _onRecordeStopWatch(RecordeStopWatch event, Emitter<StopWatchState> emit) {
    List<Duration> currentList = state.durationRecord.map((e) => e).toList();
    currentList.add(state.duration);
    emit(state.copyWith(durationRecord: currentList));
  }
}

6. 组件状态类对状态的访问

这样 StopWatchBloc 封装了状态的变化逻辑,那如何在构建时让 组件状态类 访问到 StopWatchState 呢?实现需要在 HomePage 的上层包裹 BlocProvider 来为子节点能访问 StopWatchBloc 对象。

代码语言:javascript复制
BlocProvider(
  create: (_) => StopWatchBloc(),
  child: const HomePage(),
),

比如构建表盘是通过 BlocBuilder 替代 ValueListenableBuilder ,这样当状态量 StopWatchState 发生变化是,且满足 buildWhen 条件时,就会 局部构建 来更新 StopWatchWidget 组件 。其他两个部分同理。这样在保证功能的实现下,就对逻辑进行了分离:

代码语言:javascript复制
Widget buildStopWatch() {
  return BlocBuilder<StopWatchBloc, StopWatchState>(
    buildWhen: (p, n) => p.duration != n.duration,
    builder: (_, state) => StopWatchWidget(
      duration: state.duration,
      radius: 120,
    ),
  );
}

另外,由于数据已经分离,记录数据已经和 _HomePageState 解除了耦合。这就意味着记录面板可以毫无顾虑地单独分离出来,独立维护。这又进一步简化了 _HomePageState 中的构建逻辑,简化代码,便于阅读,这就是一个良性的反馈链。

到这里,关于通过状态管理如何分离 业务逻辑构建逻辑 就介绍的差不多了,大家可以细细品味。其实所有的状态管理库都大同小异,它们的目的不是在于 优化性能 ,而是在于 优化结构层次 。这里用的是 flutter_bloc ,你完全也可以使用其他的状态管理来实现类似的分离。工具千变万化,但思想万变不离其宗。谢谢观看 ~

0 人点赞