Flutter Bloc状态管理 简单上手
我们都知道,Flutter中Widget的状态控制了UI的更新,比如最常见的StatefulWidget,通过调用setState({})方法来刷新控件。那么其他类型的控件,比如StatelessWidget就不能更新状态来吗?答案当然是肯定可以的。前文已经介绍过几种状态管理
Stream
Stream 是 Dart 提供的一种数据流订阅管理的"工具",感觉有点像 Android 中的 EventBus 或者 RxBus,Stream 可以接收任何对象,包括是另外一个 Stream,接收的对象通过 StreamController 的 sink 进行添加,然后通过 StreamController 发送给 Stream,通过 listen 进行监听,listen 会返回一个 StreamSubscription 对象,StreamSubscription 可以操作对数据流的监听,例如 pause,resume,cancel 等。Stream 分两种类型:
Single-subscription Stream:单订阅 stream,整个生命周期只允许有一个监听,如果该监听 cancel 了,也不能再添加另一个监听,而且只有当有监听了,才会发送数据,主要用于文件IO流的读取等。Broadcast Stream:广播订阅 stream,允许有多个监听,当添加了监听后,如果流中有数据存在就可以监听到数据,这种类型,不管是否有监听,只要有数据就会发送,用于需要多个监听的情况。
还是看下例子会比较直观:
class _StreamHomeState extends State<StreamHome> {
  StreamController _controller = StreamController();  // 创建单订阅类型 `StreamController`
  Sink _sink;
  StreamSubscription _subscription;
  @override
  void initState() {
    super.initState();
    _sink = _controller.sink; // _sink 用于添加数据
    // _controller.stream 会返回一个单订阅 stream,
    // 通过 listen 返回 StreamSubscription,用于操作流的监听操作
    _subscription = _controller.stream.listen((data) => print('Listener: $data'));
    // 添加数据,stream 会通过 `listen` 方法打印
    _sink.add('A');
    _sink.add();
    _sink.add(11.16);
    _sink.add([, , ]);
    _sink.add({'a': , 'b': });
  }
  @override
  void dispose() {
    super.dispose();
    // 最后要释放资源...
    _sink.close();
    _controller.close();
    _subscription.cancel();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
    );
  }
}
运行后看下控制台的输出:

listen 后才会发送数据,不试试我还是不相信的,我们把 _sink.add 放到 listen 前面去执行,再看控制台的打印结果。居然真的是一样的,Google 粑粑果然诚不欺我。pause,resume 方法,看下数据如何监听,修改代码:_sink = _controller.sink;
_subscription = _controller.stream.listen((data) => print('Listener: $data'));
_sink.add('A');
_subscription.pause(); // 暂停监听
_sink.add();
_sink.add(11.16);
_subscription.resume(); // 恢复监听
_sink.add([, , ]);
_sink.add({'a': , 'b': });
pause 方法后,stream 被堵住了,数据不继续发送了。
接下来看下广播订阅 stream,对代码做下修改:
StreamController _controller = StreamController.broadcast(); //广播订阅 stream
Sink _sink;
StreamSubscription _subscription; @override
void initState() {
super.initState(); _sink = _controller.sink; // _sink 用于添加数据 _sink.add('A');
_subscription = _controller.stream.listen((data) => print('Listener: $data')); // 添加数据,stream 会通过 `listen` 方法打印
_sink.add();
_subscription.pause();
_sink.add(11.16);
_subscription.resume(); _sink.add([, , ]);
_sink.add({'a': , 'b': });
} @override
void dispose() {
super.dispose();
// 最后要释放资源...
_sink.close();
_controller.close();
_subscription.cancel();
}
我们再看下控制台的打印:

总结:
单订阅 Stream 只有当存在监听的时候,才发送数据,广播订阅 Stream 则不考虑这点,有数据就发送;当监听调用 pause 以后,不管哪种类型的 stream 都会停止发送数据,当 resume 之后,把前面存着的数据都发送出去。
sink 可以接受任何类型的数据,也可以通过泛型对传入的数据进行限制,比如我们对 StreamController 进行类型指定 StreamController<int> _controller = StreamController.broadcast(); 因为没有对 Sink 的类型进行限制,还是可以添加除了 int 外的类型参数,但是运行的时候就会报错,_controller 对你传入的参数做了类型判定,拒绝进入。
Stream 中还提供了很多 StremTransformer,用于对监听到的数据进行处理,比如我们发送 0~19 的 20 个数据,只接受大于 10 的前 5 个数据,那么可以对 stream 如下操作:
_subscription = _controller.stream
.where((value) => value > )
.take()
.listen((data) => print('Listen: $data')); List.generate(, (index) => _sink.add(index));
那么打印出来的数据如下图:

除了 where,take 还有很多 Transformer, 例如 map,skip 等等,小伙伴们可以自行研究。了解了 Stream 的基本属性后,就可以继续往下了~
StreamBuilder。所以,StreamBuilder是Stream在UI方面的一种使用场景,通过它我们可以在非StatefulWidget中保存状态,同时在状态改变时及时地刷新UI。
StreamBuilder
StreamBuilder其实是一个StatefulWidget,它通过监听Stream,发现有数据输出时,自动重建,调用builder方法。前面提到了 stream 通过 listen 进行监听数据的变化,Flutter 就为我们提供了这么个部件 StreamBuilder 专门用于监听 stream 的变化,然后自动刷新重建。接着来看下源码
StreamBuilder<T>(
key: ...可选...
stream: ...需要监听的stream...
initialData: ...初始数据,否则为空...
builder: (BuildContext context, AsyncSnapshot<T> snapshot){
if (snapshot.hasData){
return ...基于snapshot.hasData返回的控件
}
return ...没有数据的时候返回的控件
},
)
下面是一个模仿官方自带demo“计数器”的一个例子,使用了StreamBuilder,而不需要任何setState:
import 'package:flutter/material.dart';
import 'dart:async'; class CounterPage extends StatefulWidget {
@override
_CounterPageState createState() => _CounterPageState();
} class _CounterPageState extends State<CounterPage> {
int _counter = ;
final StreamController<int> _streamController = StreamController<int>(); @override
void dispose(){
_streamController.close();
super.dispose();
} @override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Stream version of the Counter App')),
body: Center(
child: StreamBuilder<int>( // 监听Stream,每次值改变的时候,更新Text中的内容
stream: _streamController.stream,
initialData: _counter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
return Text('You hit me: ${snapshot.data} times');
}
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: (){
// 每次点击按钮,更加_counter的值,同时通过Sink将它发送给Stream;
// 每注入一个值,都会引起StreamBuilder的监听,StreamBuilder重建并刷新counter
_streamController.sink.add(++_counter);
},
),
);
}
}
setState是一个很大的改进,因为我们不需要强行重建整个控件和它的子控件,只需要重建我们希望重建的StreamBuilder(当然它的子控件也会被重建)。我们之所以依然使用StatefulWidget的唯一原因就是:StreamController需要在控件dispose()的时候被释放。能不能完全抛弃StatefulWidget?BLoC了解下
setState 方法,那么下一步,我们试试把 StatefulWidget 替换成 StatelessWidget 吧,而且官方也推荐使用 StatelessWidget 替换 StatefulWidget,这里就需要提下 BLoC 模式了。BLoC是Business Logic Component(业务逻辑组建)的缩写,就是将UI与业务逻辑分离,有点MVC的味道。
InheritedWidget 来实现的,但是 InheritedWidget 没有提供 dispose 方法,那么就会存在 StreamController 不能及时销毁等问题,所以,参考了一篇国外的文章,Reactive Programming - Streams - BLoC 这里通过使用 StatefulWidget 来实现,当该部件销毁的时候,可以在其 dispose 方法中及时销毁 StreamController,这里我还是先当个搬运工,搬下大佬为我们实现好的基类import 'package:flutter/material.dart';
abstract class BaseBloc {
  void dispose(); // 该方法用于及时销毁资源
}
class BlocProvider<T extends BaseBloc> extends StatefulWidget {
  final Widget child; // 这个 `widget` 在 stream 接收到通知的时候刷新
  final T bloc; 
  BlocProvider({Key key, @required this.child, @required this.bloc}) : super(key: key);
  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();
  // 该方法用于返回 Bloc 实例
  static T of<T extends BaseBloc>(BuildContext context) {
    final type = _typeOf<BlocProvider<T>>(); // 获取当前 Bloc 的类型
    // 通过类型获取相应的 Provider,再通过 Provider 获取 bloc 实例
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }
  static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BaseBloc>> {
  @override
  void dispose() {
    widget.bloc.dispose(); // 及时销毁资源
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}
接着我们对前面的例子使用 BLoC 进行修改。
首先,我们需要创建一个 Bloc 类,用于修改 count 的值:
import '../widget/baseBloc.dart';
import 'dart:async';
class CounterBloc extends BaseBloc {
  int _count = ;
  int get count => _count;
  // stream
  StreamController<int> _countController = StreamController.broadcast();
  Stream<int> get countStream => _countController.stream; // 用于 StreamBuilder 的 stream
  void dispatch(int value) {
    _count = value;
    _countController.sink.add(_count); // 用于通知修改值
  }
  @override
  void dispose() {
    _countController.close(); // 注销资源
  }
}
在使用 Bloc 前,需要在最上层的容器中进行注册,也就是 MaterialApp 中.
import 'package:flutter/material.dart';
import './widget/baseBloc.dart';
import './bloc/counter.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 这里对创建的 bloc 类进行注册,如果说有多个 bloc 类的话,可以通过 child 进行嵌套注册即可
// 放在最顶层,可以全局调用,当 App 关闭后,销毁所有的 Bloc 资源,
// 也可以在路由跳转的时候进行注册,至于在哪里注册,完全看需求
// 例如实现主题色的切换,则需要在全局定义,当切换主题色的时候全局切换
// 又比如只有某个或者某几个特殊界面调用,那么完全可以通过在路由跳转的时候注册
return BlocProvider(
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: StreamHome(),
),
bloc: CounterBloc());
}
} class StreamHome extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 获取注册的 bloc,必须先注册,再去查找
final CounterBloc _bloc = BlocProvider.of<CounterBloc>(context);
return Scaffold(
body: SafeArea(
child: Container(
alignment: Alignment.center,
child: StreamBuilder(
initialData: _bloc.count,
stream: _bloc.countStream,
builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 20.0)),
),
)),
floatingActionButton:
// 通过 bloc 中的 dispatch 方法进行值的修改,通知 stream 刷新界面
FloatingActionButton(onPressed: () =>
_bloc.dispatch(_bloc.count + ), child: Icon(Icons.add)),
);
}
}
重新运行后,查看效果还是一样的。所以我们成功的对 StatefulWidget 进行了替换。
先总结下 Bloc:
 1. 成功的把页面和逻辑分离开了,页面只展示数据,逻辑通过 BLoC 进行处理
 2. 减少了 setState 方法的使用,提高了性能
 3. 实现了状态管理
多个Bloc的使用
- 每一个有业务逻辑的页面的顶层都应该有自己的BLoC;
 - 每一个“足够复杂的组建(complex enough component)”都应该有相应的BLoC;
 - 可以使用一个
ApplicationBloc来处理整个App的状态。 
下面的例子展示了在整个App的顶层使用ApplicationBloc,在CounterPage的顶层使用IncrementBloc:
void main() => runApp(
BlocProvider<ApplicationBloc>(
bloc: ApplicationBloc(),
child: MyApp(),
)
); class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context){
return MaterialApp(
title: 'Streams Demo',
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
);
}
} class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context){
final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);
final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context); ...
}
}
一个实践Demo
大佬构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在Github上找到。
Flutter Bloc状态管理 简单上手的更多相关文章
- (转)flutter 新状态管理方案 Provide (一)-使用
		
flutter 新状态管理方案 Provide (一)-使用 版权声明:本文为博主原创文章,基于CC4.0协议,首发于https://kikt.top ,同步发于csdn,转载必须注明出处! ...
 - 为了弄懂Flutter的状态管理, 我用10种方法改造了counter app
		
为了弄懂Flutter的状态管理, 我用10种方法改造了counter app 本文通过改造flutter的counter app, 展示不同的状态管理方法的用法. 可以直接去demo地址看代码: h ...
 - Flutter 对状态管理的认知与思考
		
前言 由 编程技术交流圣地[-Flutter群-] 发起的 状态管理研究小组,将就 状态管理 相关话题进行为期 两个月 的讨论. 目前只有内定的 5 个人参与讨论,如果你对 状态管理 有什么独特的见解 ...
 - Flutter 状态管理 flutter_Provide
		
项目的商品类别页面将大量的出现类和类中间的状态变化,这就需要状态管理.现在Flutter的状态管理方案很多,redux.bloc.state.Provide. Scoped Model : 最早的状态 ...
 - flutter Provide 状态管理篇
		
Provide是Google官方推出的状态管理模式.官方地址为: https://github.com/google/flutter-provide 现在Flutter的状态管理方案很多,redux. ...
 - Flutter状态管理Provider,简单上手
		
在之前的文章中介绍了 Google 官方仓库下的一个状态管理 Provide.乍一看这俩玩意可能很容易就被认为是同一个东西,仔细一看,这不就差了一个字吗,有什么区别呢. 首先,你要知道的最大的一个区别 ...
 - Flutter 状态管理之BLoC
		
在正式介绍 BLoC之前, 为什么我们需要状态管理.如果你已经对此十分清楚,那么建议直接跳过这一节.如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可 ...
 - Flutter | 状态管理特别篇——Provide
		
前言 今天偶然发现在谷歌爸爸的仓库下出现了一个叫做flutter-provide的状态管理框架,2月8日才第一次提交,非常新鲜.在简单上手之后感觉就是一个字--爽!所以今天就跟大家分享一下这个新的状态 ...
 - 比Redux更容易上手的状态管理库
		
前言 当项目越发复杂时,我们发现仅仅是提升状态已经无法适应如此复杂的状态管理了,程序状态变得比较难同步,操作,到处是回调,发布,订阅,这意味着我们需要更好的状态管理方式,于是就引入了状态管理库,如Re ...
 
随机推荐
- C# 模式匹配
			
最近在使用vs编码时,重构提示:模式匹配 Element view = bindable as Element; if (view == null) { return; } 运用模式匹配可以简写为: ...
 - 【angularJS】学习笔记
			
一.一个html中多个ng-app //对于ng-app初始化一个AngularJS程序属性的使用需要注意,在一个页面中AngularJS自动加载第一个ng-app,其他ng-app会忽略 //如果需 ...
 - Linux环境下面安装Perl环境
			
1.下载安装 wget http://www.cpan.org/src/5.0/perl-5.26.1.tar.gz tar zxvf perl-5.26.1.tar.gz cd perl-5.26. ...
 - 洛谷 P1855 榨取kkksc03 题解
			
P1855 榨取kkksc03 题目描述 洛谷2的团队功能是其他任何oj和工具难以达到的.借助洛谷强大的服务器资源,任何学校都可以在洛谷上零成本的搭建oj并高效率的完成训练计划. 为什么说是搭建oj呢 ...
 - 在Xilinx ISE中生成ROM时需要注意的事
			
在Xilinx ISE中生成ROM时,需要指定.coe文件.需要做到两件事.其一,要使用memory_initialization_radix= ; memory_initializatoin_vec ...
 - java学习笔记(3)数据类型、源码、反码、补码、精度损失、基本数据类型互相转换
			
关于java中的数据类型: 1.数据类型的作用是什么? 程序当中有很多数据,每一个数据都是有相关类型的,不同数据类型的数据占用的空间大小不同. 数据类型的作用是指导java虚拟机(JVM)在运行程序的 ...
 - S标签和C标签
			
<s:if test="#attr.info.RLZT==1"> <a style="cursor:hand;" onclick=" ...
 - nginx压力测试和并发预估
			
一.Nginx并发预估 预估算法:{(?G)*1024-system}/请求大小 (?G):表示内存大小1024:表示内存容量标准进制system:表示系统和服务占用的额外内存和需要预留的内存请求大小 ...
 - IDEA中新建子模块
			
在IDEA中新建子模块简单步骤: 找到父模块 ->new Module ,然后: next之后,输入ArtifactId: next之后,再输入子模块名,其中,要注意,在contentRoot和 ...
 - thymeleaf错误解决办法
			
Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "username&q ...