目前我的状态管理相关文章有:
《Flutter 状态管理 | 第一论 - 对状态管理的看法与理解》
《Flutter 桌面探索 | 自定义可拖拽导航栏》
《Flutter 状态管理 | 第二论 - 业务逻辑与界面构建分离》
本文秒表的界面基础详见这两篇文章
《Flutter 绘制集录 | 秒表盘的绘制》
《Flutter 绘制集录 | 秒表运动与Ticker》
1. 业务逻辑和构建逻辑
对界面呈现来说,最重要的逻辑有两个部分:业务数据的维护逻辑
和 界面布局的构建逻辑
。其中应用运行中相关数据的获取、修改、删除、存储等操作,就是业务逻辑。比如下面是秒表的三个界面,核心 数据
是秒表的时刻。在秒表应用执行功能时,数据的变化体现在秒数的变化、记录、重置等。
默认情况 | 暂停 | 记录 |
---|---|---|
| | |
界面的构建逻辑主要体现在界面如何布局,维持界面的出现效果。另外,在界面构建过程中,除了业务数据,还有一些数据会影响界面呈现。比如打开秒表时,只有一个启动按钮;在运行中,显示暂停按钮和记录按钮;在暂停时,记录按钮不可用,重置按钮可用。这样在不同的交互场景中,有不同的界面表现,也是构建逻辑处理的一部分。
2. 数据的维护
所以的逻辑本身都是对 数据
的维护,界面能够显示出什么内容,都依赖于数据进行表现。理解需要哪些数据、数据存储在哪里,从哪里来,要传到哪里去,是编程过程中非常重要的一个环节。由于数据需要在构建界面时使用,所以很自然的:在布局写哪里,数据就在哪里维护。
比如默认的计数器项目,其中只有一个核心数据 _counter
,用于表示当前点击的次数。
代码实现时, _counter
数据定义在 _MyHomePageState
中,改数据的维护也在状态类中:
对于一些简单的场景,这样的处理无可厚非。但在复杂的交互场景中,业务逻辑和构建逻辑杂糅在 State
派生类中,会导致代码复杂,逻辑混乱,不便于阅读和维护。
3.秒表状态数据对布局的影响
现在先通过代码来实现如下交互,首先通过 StopWatchType
枚举来标识秒表运行状态。在初始状态 none
时,只有一个开始按钮;点击开始,秒表在运行中,此时显示三个按钮,重置按钮是灰色,不可点击,点击旗子按钮,可以记录当前秒表值;暂停时,旗子按钮不可点击,点击重置按钮时,回到初始态。
enum StopWatchType{
none, // 初始态
stopped, // 已停止
running, // 运行中
}
如下所示,通过 _buildBtnByState
方法根据 StopWatchState
状态值构建底部按钮。根据不同的 state
情况处理不同的显示效果,这就是构建逻辑的体检。而此时的关键数据就是 StopWatchState
对象。
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
处:
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
列表数据,进行界面的展现。
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
的代码量,从而让状态类专注于界面构建逻辑。
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
来维护这三个数据:
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
用于记录值。这就是最核心的三个功能:
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
中。
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
对象。
BlocProvider(
create: (_) => StopWatchBloc(),
child: const HomePage(),
),
比如构建表盘是通过 BlocBuilder
替代 ValueListenableBuilder
,这样当状态量 StopWatchState
发生变化是,且满足 buildWhen
条件时,就会 局部构建
来更新 StopWatchWidget 组件
。其他两个部分同理。这样在保证功能的实现下,就对逻辑进行了分离:
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
,你完全也可以使用其他的状态管理来实现类似的分离。工具千变万化,但思想万变不离其宗。谢谢观看 ~