[EP-007] 优雅弹窗处理
问题提出
在 EP-006 中我们提到了状态管理三个原语,其中加载状态可能会有特殊的需求。例如我们的 UI 设计师和产品一致的认为:我们需要弹出一个加载框,这样可以显式的表示加载状态,并中断用户操作实现伪防抖。或许有他们的道理,即使不够优雅。
但是无论如何加载这个动作总得有个地方去写,写在控制器里?还是说直接封装进网络服务里?请求之前弹出,请求之后关闭?显然这都不够优雅,而且易错,我们需要考虑以下情况:
- 你的网络请求可能正常执行了,但需要记得关闭加载框(主观的心智负担);
- 你的网络请求可能会变成错误,你需要在错误的时候关闭加载框;
- 你可能有多个网络请求,在哪里关闭加载呢?
- 不只是网络请求,本地的耗时操作也需要加载,完成的时候记得关闭加载框。
你可能会说这有什么,挨个写好就行了。但是错综复杂的业务逻辑,迟早会埋藏 bug,但是解决它仅仅只是个习惯问题,不需要添加太多代码。
优雅实践
弹窗处理需要 context,自然的我们需要在 UI 层去弹窗(TODO: EP-009 理解上下文),但是业务都在控制器层、服务层,我怎么知道什么时候应该弹窗?
思路改变一下,既然弹窗是 UI 层干的事,UI 层关心的肯定也不是业务发生了什么,而是关心当前的状态,我们只需要让控制器层暴露一个加载状态,就可以了,那么回到三个原语里,watch
、read
、listen
,首先排除用于调用方法的 read
,其次排除 watch
,因为我不需要返回小组件,而且 watch
并不方便知道什么时候加载状态结束了。所以我们需要使用 listen
监听加载状态。
bloc
伪代码如下:
BlocListener<BlocA, BlocAState>(
listenWhen: (previousState, state) {
return previousState == BlocALoadingState
|| state == BlocALoadingState;
},
listener: (context, state) {
if (state == BlocALoadingState) showDialog(context);
else closeDialog(context);
},
child: const SizedBox(),
);
riverpod
伪代码如下:
ref.listen(otherProvider, (previous, next) {
if (next == BlocALoadingState) showDialog(context);
if (previous == BlocALoadingState) closeDialog(context);
});
当进入加载状态,UI 弹出加载框,当退出加载状态,UI 关闭加载框。就是这样一个简单而优雅的逻辑,模块间解耦,状态明确。