原文:Reactive Programming – Streams – BLoC – Practical Use Cases 是作者 Didier Boelens 为 Reactive Programming – Streams – BLoC 写的后续
阅读本文前建议先阅读前篇,前篇中文翻译有两个版本:
[译]Flutter响应式编程:Streams和BLoC by JarvanMo
忠于原作的版本Flutter中如何利用StreamBuilder和BLoC来控制Widget状态 by 吉原拉面
省略了一些初级概念,补充了一些个人解读
前言
在了解 BLoC, Reactive Programming 和 Streams 概念后,我又花了些时间继续研究,现在非常高兴能够与你们分享一些我经常使用并且个人觉得很有用的模式(至少我是这么认为的)。这些模式为我节约了大量的开发时间,并且让代码更加易读和调试。
目录
(由于原文较长,翻译发布时进行了分割)
BlocProvider 性能优化
结合 StatefulWidget 和 InheritedWidget 两者优势构建 BlocProviderBLoC 的范围和初始化
根据 BLoC 的使用范围初始化 BLoC事件与状态管理
基于事件(Event) 的状态 (State) 变更响应表单验证
根据表单项验证来控制表单行为 (范例中包含了表单中常用的密码和重复密码比对)Part Of 模式
允许组件根据所处环境(是否在某个列表/集合/组件中)调整自身的行为
文中涉及的完整代码可在 GitHub 查看。
2. BLoC 的范围和初始化
要回答「要在哪初始化 BLoC?」这个问题,需要先搞清楚 BLoC 的可用范围(scope)。
2.1. 应用中任何地方可用
在实际应用中,常常需要处理如用户鉴权、用户档案、用户设置项、购物篮等等需要在 App 中任何组件都可访问的数据或状态,这里总结了适用这种情况的两种 BLoC 方案:
2.1.1. 全局单例 (Global Singleton)
这种方案使用了一个不在Widget视图树中的 Global 对象,实例化后可用供所有 Widget 使用。
import 'package:rxdart/rxdart.dart';
class GlobalBloc {
///
/// Streams related to this BLoC
///
BehaviorSubject<String> _controller = BehaviorSubject<String>();
Function(String) get push => _controller.sink.add;
Stream<String> get stream => _controller;
///
/// Singleton factory
///
static final GlobalBloc _bloc = new GlobalBloc._internal();
factory GlobalBloc(){
return _bloc;
}
GlobalBloc._internal();
///
/// Resource disposal
///
void dispose(){
_controller?.close();
}
GlobalBloc globalBloc = GlobalBloc();
要使用全局单例 BLoC,只需要 import 后调用定义好的方法即可:
import 'global_bloc.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
globalBloc.push('building MyWidget'); //调用 push 方法添加数据
return Container();
}
}
如果你想要一个唯一的、可从应用中任何组件访问的 BLoC 的话,这个方案还是不错的,因为:
- 简单易用
- 不依赖任何 BuildContext
- 当然也不需要通过 context 查找 BlocProvider 的方式来获取 BLoC
- 释放资源也很简单,只需将 application Widget 基于 StatefulWidget 实现,然后重写其 dispose() 方法,在 dispose() 中调用 globalBloc.dispose() 即可
我也不知道具体是为啥,很多较真的人反对全局单例方案,所以…我们再来看另一种实现方案吧…
2.1.2. 注入到视图树顶层
在 Flutter 中,包含所有页面的ancestor本身必须是 MaterialApp 的父级。 这是因为页面(或者说Route)其实是作为所有页面共用的 Stack 中的一项,被包含在 OverlayEntry 中的。
换句话说,每个页面都有自己独立于任何其它页面的 Buildcontext。这也解释了为啥不用任何技巧是没办法实现两个页面(或路由)之间数据共享的。
因此,必须将 BlocProvider 作为 MaterialApp 的父级才能实现在应用中任何位置都可使用 BLoC,如下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: InitializationPage(),
),
);
}
}
2.2. 在子视图树(多个页面或组件)中可用
大多数时候,我们只需要在应用的部分页面/组件树中使用 BLoC。举个例子,在一个 App 中有类似论坛的功能模块,在这个功能模块中我们需要用到 BLoC 来实现:
- 与后端服务器交互,获取、添加、更新帖子
- 在特定的页面列出需要显示的数据
- …
显然我们不需要将论坛的 BLoC 实现成全局可用,只需在涉及论坛的视图树中可用就行了。
那么可采用通过 BlocProvider将 BLoC 作为模块子树的根(父级)注入的方式,如下所示:
class MyTree extends StatelessWidget {
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: MyBloc(),
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
MyBloc = BlocProvider.of<MyBloc>(context);
return Container();
}
}
这样,该模块下所有 Widget 都可以通过调用 BlocProvider.of 来获取 BLoC.
注意
上面给出的并不是最佳方案,因为每次 MyTree 重构(rebuild)时都会重新初始化 BLoC ,带来的结果是:
- 丢失 BLoC 中已经存在的数据内容
- 重新初始化BLoC 要占用 CPU 时间
在这个例子中更好的方式是使用 StatefulWidget ,利用其持久化 State 的特性解决上述问题,代码如下:
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
MyBloc bloc;
@override
void initState(){
super.initState();
bloc = MyBloc();
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: bloc,
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
这样实现的话,即使 MyTree 组件重构,也不会重新初始化 BLoC,而是直接使用之前的BLoC实例。
2.3. 单一组件中可用
如果只在某一个组件(Widget)中使用 BLoC,只需要在该组件内构建 BLoC 实例即可。