FlutterDojo设计之道—状态管理之路(三)
Dart作为一个现代化的编程语言,吸收了很多语言的特点,特别是响应式编程的风格。
通过Dart提供的Stream机制,Flutter可以很轻松的构建响应式的编程方式,同时也让跨页面、跨Widget的数据管理问题迎刃而解。
Flutter的响应式编程,具有下面几个特点。
- 数据的管理,围绕Stream进行,通过Stream的sink和listen,来进行数据的管理
- Widget发出Stream后,无需感知外界的影响,同样的,Widget在listen Stream时,只需要根据数据的改变来构建UI
- Widget之间不再耦合,通过Stream管道获取数据,互相无依赖
借助Flutter的这个特性,Google在数据管理之路上提出了BLoC模式。
BLoC模式由Paolo Soares和Cong Hui设计,并谷歌在2018的DartConf首次提出,全称Business Logic Component。
在BLoC模式下,Widget与Data彻底解耦:
- App的业务逻辑处理都在BLoC中
- Widget通过Sink向BLoC发送数据
- BLoC通过Stream通知Widget重建UI
这其实有点类似MVP、MVC模式,BLoC模式将整个App分为三层,Data Layer、BLoC Layer、UI Layer,Data Layer和UI Layer都只能和BLoC Layer双向通信,但它们之间彼此隔离。
下面将官方的counter demo,用BLoC模式重写下,让大家了解下创建BLoC模式的一般范式。
创建BLoC业务处理类
BLoC类是一个业务逻辑处理类,不包含任何UI逻辑,且一个BLoC类只处理一种独立的业务逻辑,在官方的Demo中,业务逻辑有下面几个部分构成。
- 记录点击数
- 点击后增加点击数
所以创建的BLoC类,只对外暴露这两个业务,即对外的Stream和increment函数。
abstract class BlocBase {
void dispose();
}
class IncrementBloc implements BlocBase {
// _私有化控制访问权限
int _count;
StreamController<int> _countController;
IncrementBloc() {
_count = 0;
_countController = StreamController<int>();
}
Stream<int> get value => _countController.stream;
increment() {
_countController.sink.add(++_count);
}
dispose() {
_countController.close();
}
}
BlocBase仅仅封装了dispose函数,用于资源的释放。IncrementBloc就是这个业务的处理核心,通过Stream,让外界可以监听数据的改变。
一个标准的BLoC类通常包含下面几个部分。
- 私有的model和StreamController
- 公开的get方法返回Stream
- 公开的业务处理函数
- dispose函数
创建BLoC管理类
BLoC管理类是一个通用的处理类,借助StatefulWidget来实现了BLoC业务处理类的管理。同时,它也是数据和UI的粘合剂,用于将指定业务的BLoC类注入到具体的业务UI中。
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}) : super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
BlocProvider<T> provider = context.findAncestorWidgetOfExactType<BlocProvider<T>>();
return provider.bloc;
}
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>> {
@override
void dispose() {
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
BLoC管理类实际上只做了两件事。
- 将业务UI作为其子Widget
- 给业务UI提供指定的BLoC逻辑处理类
创建BLoC UI
@override
Widget build(BuildContext context) {
return BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
);
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
return Scaffold(
body: Center(
child: StreamBuilder<int>(
stream: bloc.value,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return Text('You hit me: ${snapshot.data} times');
},
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => bloc.increment(),
),
);
}
}
在UI层中,可以通过BlocProvider.of<IncrementBloc>(context)来获取指定类型的BLoC,这样就可以使用它内部定义好的接口和数据。
在UI层中,有两种写法,一种是直接使用StatelessWidget,在build函数中初始化BlocProvider.of<IncrementBloc>(context),另一种是使用StatefulWidget,在didChangeDependencies()中进行初始化,因为didChangeDependencies相比initState来说,可以更加安全的获取Context。
这种方式做到了完全的解耦,只要定义好BLoC中的接口和数据模型,前端展示UI,就完全和数据无关了。
在UI层中,需要做的就是通过StreamBuilder来解析要监听的数据,StreamBuilder的builder函数是一个AsyncWidgetBuilder,它能够异步构建widget,其参数AsyncSnapshot<int> snapshot就是流中的数据快照,可以通过snapshot.data来访问流中的数据,或者通过snapshot.hasError、snapshot.error来获取异常信息。
BLoC流的单播与广播
Flutter中的Stream分为两种,单播与多播,默认情况下创建的是单播Stream,这样的话,只能有一个StreamBuilder来监听,如果存在多个StreamBuilder监听同一个BLoC Stream,则需要将默认创建的Stream改成多播Stream。
_countController = StreamController.broadcast<int>();
在多页面使用的时候,有个地方需要注意,那就是流是实时的,不具有粘滞性。举个例子,比如在第一个界面在流中添加了一些数据,再打开第二个界面的时候,创建StreamBuilder之后,是无法直接获取流的最新数据的,因为这时候流中的的数据在StreamBuilder监听之前就已经结束了。所以这种情况下,要么是在创建StreamBuilder前,初始化initialData的值为流中最新的数据;要么是使用RxDart来强化流的功能。
- 二帮主:央行数字货币的崛起,会给比特币带来什么影响
- cas原理介绍
- 结合Jexus + Kestrel 部署 asp.net core 生产环境
- C#全角和半角转换
- 浅谈Scala在大数据处理方面的优势
- 利用mybatis-generator自动生成代码
- 微信年终放大招!小程序再次升级,这个功能超想要!
- WordPress 中禁止文章自动保存和修订版本的方法
- 第一届机器人学习大会总结
- Hadoop: MapReduce2的几个基本示例
- 使用 nRoute 框架来实现基于 Silverlight 的桌面应用
- oracle:如何用sql生成日历
- 极品双拼“马仔”mazai.com易主终端
- Hadoop:pig 安装及入门示例
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- android 自定义圆角button效果的实例代码(自定义view Demo)
- Android开发简易音乐播放器
- Android 自定义ListView实现QQ空间界面(说说内包含图片、视频、点赞、评论、转发功能)
- Android自定义View实现自动吸附功能
- Qt音视频开发35-Onvif图片参数
- alpine安装openssl
- iOS14中的PHPicker
- Android 如何实现动态申请权限
- Android录屏的三种解决方案
- Android 实现将Bitmap 保存到本地
- Android Gradle依赖管理、去除重复依赖、忽略的方式
- Andriod Studio实现保存QQ密码功能(案例代码详解)
- Android Studio编写微信页面提交功能
- android 实现按钮浮动在键盘上方的实例代码
- 创建Android守护进程实例(底层服务)