前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 状态管理 | 业务逻辑与构建逻辑分离

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

作者头像
张风捷特烈
发布2022-09-20 10:36:05
1.4K0
发布2022-09-20 10:36:05
举报
文章被收录于专栏:Android知识点总结

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

《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 ,你完全也可以使用其他的状态管理来实现类似的分离。工具千变万化,但思想万变不离其宗。谢谢观看 ~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 业务逻辑和构建逻辑
  • 2. 数据的维护
  • 3.秒表状态数据对布局的影响
  • 4.秒表记录值的维护
  • 5. 基于 flutter_bloc 的状态管理
  • 6. 组件状态类对状态的访问
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档