上一期文章 Flutter 主流状态管理框架 provider get 分析与思考 中,我和大家一起分析了 Flutter 中状态管理框架出现的原因,以及他们的设计思路。从整体上来看,状态管理可以分为两大类,一种是依赖于「Flutter 树机制」的框架,例如 provider、bloc 等;另一类是 get 这种「依赖注入」的框架。今天就来和大家分享一下,我在日常开发中遇到过的问题,以及我认为他们不太合理的一些设计。希望能帮助大家在日常开发中减少出错的可能。
一、ProviderNotFoundException!!!
依赖树机制的这类状态框架的问题,千言万语汇做一个异常
ProviderNotFoundException !!!!
是的,至今遇到的绝大部分的问题,都来自于这个异常。因为树机制依赖于我们使用的 context,一旦 context 不对,我们就能碰到这个异常,具体有以下场景:
1、context 层级高于 Provider
这个问题其实和 上路 在 Flutter 深入理解BuildContext 文章中提到的例子一样。
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FlatButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondPage()));
},
child: Text('跳转')),
),
),
);
}
}
复制代码
当我们点击「跳转」按钮,会报出异常,
flutter: Navigator operation requested with a context that does not include a Navigator.
flutter: The context used to push or pop routes from the Navigator must be that of a widget that is a
flutter: descendant of a Navigator widget.
复制代码
这种错很好理解,Flutter 主流状态管理框架 provider get 分析与思考 中我们提到过,context 对应的是 Widget 在 Element 树中的节点,通过向上查找父节点中的 Presenter。如果使用的 context 过高,那自然无法获取。
这点使用 provider 也一样,知道原理之后解法也很简单,使用 Provider 之下的 context 即可。
2、context 已从树中移除
这一点我在实践中遇到过,一般的页面在打开时,显示 loading 进行请求,之后根据网络状态展示内容或者异常页面。如果展示异常我们会展示为一个「重新加载」页面,点击进行网络请求。
那次的问题在于,再次请求时网络状态成功了,页面展示为正常的状态。但是业务逻辑中还在用「重新加载」widget 对应的 context 获取 Provider。因为「重新加载」widget 已经从树中移除了,所以在使用结果自然报错。
![](/qrcode.jpg)
这个问题看起来很简单,实际在第一次遇到时压根没有往这个方向联想,一步步 Debug 当前 context 所对应的 widget,以及查看他的 parent 节点最终才发现了这个问题。
总结一下,对于
ProviderNotFoundException
这一类的异常,往往是由于 context 对象错误导致。遇到的时候不要慌,抓住 Flutter 树机制的核心。Debug 当前的 context 对象,观察他的 widget、parent 等属性,看是否符合预期,答案往往就在里面。
二、get 中的一些实践问题
上一期我们提到,get 其实核心的设计在于将 Presenter 存到一个单例的 Map 中,这样在任何地方都能随时访问。
其中单例存储的 key 是 Presenter.runtimeType + tag
tag 是一个 String 类型的标识,value 是对应的 Presenter 实例。
这种全局单例存储其实有个很蛋疼的问题,类型重复:
1、类型重复
举个场景,淘宝商品详情页之间的跳转。
因为页面都是同一个类,只是不同实例。由于第一个页面已经将 Presenter 存放,这时如果你在第二个页面直接使用
Get.find<A>
会发现获取的还是上一个页面的 Presenter。
为了解决这个问题,我们必须要在 Get.put
的时候必须给每一个 Presenter 加上唯一 tag,比如商品 id。
但如果这样,获取 Presenter 时必须知道 tag,在跨页面访问 presenter 时如何共享 tag 又成了一个新的问题。
你可能会想 provider 中就没有这个问题了么?因为一般我们在使用 Provider 的时候,都是跟随页面的。相当于每个页面都有一个 Map 进行存储,打开不同的页面对应是不同的 Map,所以不用考虑类型重复的问题。
但是跨页面访问 Presenter 任然是个麻烦的问题。
2、Getbuilder<T>
最后这一点也是 fluttercandidate 中一个群友实际遇到的问题,也是我个人认为这个框架不合理的地方。就是 Getbuilder<T> 通过传递 Presenter 泛型在 builder
中将其返回使用。
GetBuilder<Presenter>(
/// builder 中直接将 Controller 对应的实例返回
builder: (presenter) =>
Text('clicks: ${presenter.count}')
)
复制代码
当时那个兄 dei,也是和上面一样商品详情页重复跳转的场景,为了解决类型重复的问题,他使用时间戳作为 tag。
final Presenter presenter = Get.put(Presenter(),tag: DateTime.now().toString());
复制代码
结果在使用 GetBuilder 的时候傻眼了:
GetBuilder<Presenter>(
/// 和上面 tag 不是一个,所以查找不到实例
tag: DateTime.now().toString(),
builder: (presenter) =>
Text('clicks: ${presenter.count}')
)
复制代码
理解 Get 的注入之后,GetBuilder 的原理其实非常简单。因为是全局单例 Map 存储,所以只需要 key(runtimeType+tag)
就可以获取到对应的 Presenter,GetBuilder 相当于自动为我们做了查找这么一件事。这个例子中,因为 put 和 GetBuidler 的 tag 不一样(当前的时间戳),所以获取 GetBuilder 获取不到。只需要简单的将 tag 存起来在 GetBuilder 中使用即可。
但这个自动查找泛型在我看来并没有什么必要,完全可以参考系统的 ValueListenableBuilder 的做法,让使用者手动传入 Presenter 实例变成下面这样。
GetBuilder<Presenter>(
/// 手动传入 presenter ,表示这个 presenter 与 GetBuilder 建立了监听关系。
presenter: presenter,
builder: (presenter) =>
Text('clicks: ${presenter.count}')
)
复制代码
这么对比起来看似多了一个参数 presenter,但这个 presenter 一定是你能提前获取的。手动的指定「监听对象」和「监听者」的绑定我认为会让开发者更加清楚当前这个 GetBuilder 监听的 presenter 是谁。这样压根从源头上避免了上面使用时间戳做 tag 的问题。
三、总结
以上便是我在实践中遇到的一些问题,其实本质还是由于他们不同的设计思路导致。例如,使用树机制的 provider,就肯定存在 context 异常的可能。而 get 使用全局单例的方式自然避免不了类型重复,或者回收一类的问题。所以对于框架,更重要的是去理解他的设计思路,而不是无脑的跟风。相关内容在上期也有提过:Flutter 主流状态管理框架 provider get 分析与思考,希望能帮助你避开一些坑。
状态管理相关内容到这期就结束了,下一部分我专注学习 Dart 虚拟机和 isolate 的相关内容,欢迎关注。
如果你有任何疑问可以通过公众号与联系我,如果文章对你有所启发,希望能得到你的点赞、关注和收藏,这是我持续写作的最大动力。Thanks~
公众号:进击的Flutter或者 runflutter 里面整理收集了最详细的Flutter进阶与优化指南,欢迎关注。
往期精彩内容: