参考文章:Reactive Programming - Streams - BLoC
为了便于阅读,略去了原文中的一些跟StreamBuilder和Bloc无关的拓展概念,比如RxDart、Demo的解释等,想要进一步了解的可以移步原文。

先粗略讲点关于Stream的东西

  Stream其实类似于Rx大家族,也是一种对于数据流的订阅管理。Stream可以接受任何类型的数据,值、事件、对象、集合、映射、错误、甚至是另一个Stream,通过StreamController中的sink作为入口,往Stream中插入数据,然后通过你的自定义监听StreamSubscription对象,接受数据变化的通知。如果你需要对输出数据进行处理,可以使用StreamTransformer,它可以对输出数据进行过滤、重组、修改、将数据注入其他流等等任何类型的数据操作。
  Stream有两种类型:单订阅Stream和广播Stream。单订阅Stream只允许在该Stream的整个生命周期内使用单个监听器,即使第一个subscription被取消了,你也没法在这个流上监听到第二次事件;而广播Stream允许任意个数的subscription,你可以随时随地给它添加subscription,只要新的监听开始工作流,它就能收到新的事件。
  下面是一个单订阅的例子:

import 'dart:async';void main() {// 初始化一个单订阅的Stream controllerfinal StreamController ctrl = StreamController();// 初始化一个监听final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));// 往Stream中添加数据ctrl.sink.add('my name');ctrl.sink.add(1234);ctrl.sink.add({'a': 'element A', 'b': 'element B'});ctrl.sink.add(123.45);// StreamController用完后需要释放ctrl.close();
}

  下面是添加了StreamTransformer的例子:

import 'dart:async';void main() {// 初始化一个int类型的广播Stream controllerfinal StreamController<int> ctrl = StreamController<int>.broadcast();// 初始化一个监听,同时通过transform对数据进行简单处理final StreamSubscription subscription = ctrl.stream.where((value) => (value % 2 == 0)).listen((value) => print('$value'));// 往Stream中添加数据for(int i=1; i<11; i++){ctrl.sink.add(i);}// StreamController用完后需要释放ctrl.close();
}

关于RxDart

  之前已经说了,Stream是一种订阅者模式,所以跟Rx大家族很类似。Rx官方已经提供了对Dart语言的官方支持——RxDart。两者的对应关系可以看下下表:

Dart RxDart
Stream Observable
StreamController Subject

  对于RxDart的用法这里不多做讨论。

Flutter中Widget的状态管理和响应式编程的概念

  我们都知道,Flutter中Widget的状态控制了UI的更新,比如最常见的StatefulWidget,通过调用setState({})方法来刷新控件。那么其他类型的控件,比如StatelessWidget就不能更新状态来吗?答案当然是肯定可以的。比如Flutter的Redux插件,就是一种在非StatefulWidget中刷新控件的机制。
  我们上面已经说了,Stream的特性就是当数据源发生变化的时候,会通知订阅者,那么我们是不是可以延展一下,实现当数据源发生变化时,改变控件状态,通知控件刷新的效果呢?Flutter为我们提供了StreamBuilder
  所以,StreamBuilder是Stream在UI方面的一种使用场景,通过它我们可以在非StatefulWidget中保存状态,同时在状态改变时及时地刷新UI。
(Flutter还有其他的一些管理状态的方法跟插件,鄙人暂时没有研究过其他,就不多说了。)
  其实这种数据源改变,UI也跟着改变的方式就是一种响应式编程(Reactive Programming)。响应式编程就是使用异步数据流来编程的方式,换句话说,任何事件(比如点击事件)、变量、消息、请求等等的改变,都会触发数据流的传递。

  如果使用响应式编程,那么App将:

  • 变为异步的;
  • 围绕Streams和listeners的概念来构建;
  • 当任意事件、变量等等发生变化时,会向Stream发送一个通知;
  • Stream的监听者无论位于app中的哪个位置,都会收到这个通知。

什么是StreamBuilder

  StreamBuilder其实是一个StatefulWidget,它通过监听Stream,发现有数据输出时,自动重建,调用builder方法。

StreamBuilder<T>(key: ...可选...stream: ...需要监听的stream...initialData: ...初始数据,否则为空...builder: (BuildContext context, AsyncSnapshot<T> snapshot){if (snapshot.hasData){return ...基于snapshot.hasData返回的控件}return ...没有数据的时候返回的控件},
)

  下面是一个模仿官方自带demo“计数器”的一个例子,使用了StreamBuilder,而不需要任何setState:


import 'dart:async';
import 'package:flutter/material.dart';class CounterPage extends StatefulWidget {@override_CounterPageState createState() => _CounterPageState();
}class _CounterPageState extends State<CounterPage> {int _counter = 0;final StreamController<int> _streamController = StreamController<int>();@overridevoid dispose(){_streamController.close();super.dispose();}@overrideWidget 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了解下

  BLoC模式由来自Google的Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次演示。 你可以在YouTube上观看此视频。
  BLoC是Business Logic Component(业务逻辑组建)的缩写,就是将UI与业务逻辑分离,有点MVC的味道。

  简而言之,BLoC的使用注意点和好处是:

  • 将业务逻辑(Business Logic)转移到一个或者多个BLoC中去;
  • 尽可能地与表现层(Presentation Layer)分离,换句话说就是UI组建只需要关心UI,而不需要关心业务逻辑;
  • input(Sink)和output(Stream)唯一依赖于Streams的使用;
  • 保持了平台独立性;
  • 保持了环境独立性。

  下图是BLoC与Widget交互的简单示意图:

image.png

  • Widget通过Sinks发送事件给BLoC;
  • BLoC通过Streams通知Widget;
  • 不需要关心BLoC实现的业务逻辑。

  这么做的好处显而易见:

多亏了业务逻辑和UI的分离,使得:
 1、我们可以随时更改业务逻辑,最小化对App的影响;
 2、我们可以在完全不影响业务逻辑的前提下更改UI;
 3、业务逻辑测试会更方便。

  通过将StreamBuilder和BLoC结合,我们就可以完全放弃StatefulWidget了:

void main() => runApp(new MyApp());class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return new MaterialApp(title: 'Streams Demo',theme: new ThemeData(primarySwatch: Colors.blue,),home: BlocProvider<IncrementBloc>(bloc: IncrementBloc(),child: CounterPage(),),);}
}class CounterPage extends StatelessWidget {@overrideWidget build(BuildContext context) {final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);return Scaffold(appBar: AppBar(title: Text('Stream version of the Counter App')),body: Center(child: StreamBuilder<int>(// StreamBuilder控件中没有任何处理业务逻辑的代码stream: bloc.outCounter,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.incrementCounter.add(null);},),);}
}class IncrementBloc implements BlocBase {int _counter;// 处理counter的streamStreamController<int> _counterController = StreamController<int>();StreamSink<int> get _inAdd => _counterController.sink;Stream<int> get outCounter => _counterController.stream;// 处理业务逻辑的streamStreamController _actionController = StreamController();StreamSink get incrementCounter => _actionController.sink;// 构造器IncrementBloc(){_counter = 0;_actionController.stream.listen(_handleLogic);}void dispose(){_actionController.close();_counterController.close();}void _handleLogic(data){_counter = _counter + 1;_inAdd.add(_counter);}
}
  • 责任分离:StreamBuilder控件中没有任何处理业务逻辑的代码,所有的业务逻辑处理都在单独的IncrementBloc类中进行。如果你要修改业务逻辑,只需要修改 _handleLogic()方法就行了,无论处理过程多么复杂,CounterPage都不需要知道,不需要关心。
  • 可测试性:只需要测试IncrementBloc类即可。
  • 自由组织布局:有了Streams,你就可以完全独立于业务逻辑地去组织你的布局了。可以在App中任何位置触发操作,只需要通过.incrementCounter sink来传入即可;也可以在任何页面的任何位置来展示counter,只需要监听.outCounter stream
  • 减少了build的数量:使用StreamBuilder放弃setState()大大减少了build的数量,因为只需要build需要刷新的控件,从性能角度来讲是一个重大的提升。

关于Bloc的可访问性

  以上所有功能的实现,都依赖于一点,那就是Bloc必须是可访问的。
  有很多方法都可以保证Bloc的可访问性:

  • 全局单例(global Singleton):这种方式很简单,但是不推荐,因为Dart中对类没有析构函数(destructor)的概念,因此资源永远无法释放。
  • 局部变量(local instance):你可以创建一个Bloc局部实例,在某些情况下可以完美解决问题。但是美中不足的是,你需要在StatefulWidget中初始化,并记得在dispose()中释放它。
  • 由祖先(ancestor)来提供:这也是最常见的一种方法,通过一个实现了StatefulWidget的父控件来获取访问权。
      下面的这个例子展示了一个通用的BlocProvider的实现:
// 所有 BLoCs 的通用接口
abstract class BlocBase {void dispose();
}// 通用 BLoC provider
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){final type = _typeOf<BlocProvider<T>>();BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);return provider.bloc;}static Type _typeOf<T>() => T;
}class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{@overridevoid dispose(){widget.bloc.dispose();super.dispose();}@overrideWidget build(BuildContext context){return widget.child;}
}

  怎么使用这个BlocProvider呢?

 home: BlocProvider<IncrementBloc>(bloc: IncrementBloc(),child: CounterPage(),),

  这段代码实例化了一个新的BlocProvider,用于处理IncrementBloc,然后见CounterPage渲染为一个子控件。那么,BlocProvider下的任意一个子树(sub-tree)都可以访问IncrementBloc,访问方式为:IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

多个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 {@overrideWidget build(BuildContext context){return MaterialApp(title: 'Streams Demo',home: BlocProvider<IncrementBloc>(bloc: IncrementBloc(),child: CounterPage(),),);}
}class CounterPage extends StatelessWidget {@overrideWidget build(BuildContext context){final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context);...}
}

为什么不使用InheritedWidget?

  很多关于BLoC的文章,都将一个InheritedWidget实现为了Provider。
  当然,这么做是可行的,但是:

  • 一个InheritedWidget没有提供任何dispose()方法,但是,在不需要某个资源的时候及时释放它是一个好的编程实践。
  • 没有谁会拦着你在一个StatefulWidget中放一个InheritedWidget,但是请考虑一下,这么做会不会增加什么负担呢?
  • 如果控制的不好,InheritedWidget会产生副作用(下面会讲)。

Flutter无法实例化范型,所以我们需要将BLoC实例传递给BlocProvider。为了在每一个BLoC中执行dispose(),所有BLoCs都需要实现BlocBase接口。

  我们在使用InheritedWidget的context.inheritFromWidgetOfExactType(…)方法来获取制定类型的widget时,每当InheritedWidget的父级或者子布局发生变化,这个方法会自动将当前“context”(= BuildContext)注册到要重建的widget当中。关联至BuildContext的Widget类型(Stateful还是Stateless)并不重要。

BLoC的一些缺点

   BLoC模式起初是希望用在跨平台(如AngularDart)分享代码上的。BLoC没有getter/setter概念,只有sinks/streams,所以说:“rely on exclusive use of Streams for both input (Sink) and output (stream)”,BLoC只依赖于sinks和streams
   我们用两个例子来说一下BLoC的缺点:

  • 我们从BLoC中获取数据,将数据作为页面的输入源,依赖于Streams异步build页面,但是有时候这种方式并不是很优雅:
class FiltersPage extends StatefulWidget {@overrideFiltersPageState createState() => FiltersPageState();
}class FiltersPageState extends State<FiltersPage> {MovieCatalogBloc _movieBloc;double _minReleaseDate;double _maxReleaseDate;MovieGenre _movieGenre;bool _isInit = false;@overridevoid didChangeDependencies() {super.didChangeDependencies();// 在 initState() 的时候,我们是拿不到 context 的。// 如果还没有初始化,我们通过_getFilterParameters获取参数if (_isInit == false){_movieBloc = BlocProvider.of<MovieCatalogBloc>(context);_getFilterParameters();}}@overrideWidget build(BuildContext context) {return _isInit == false? Container(): Scaffold(...);}// 这么写是为了完全100%遵循 BLoC 的规则,我们所有的数据都必须是从Streams里面获取的。// 这很不优雅,这个例子看看就好,只是一个学习性的例子展示void _getFilterParameters() {StreamSubscription subscriptionFilters;subscriptionFilters = _movieBloc.outFilters.listen((MovieFilters filters) {_minReleaseDate = filters.minReleaseDate.toDouble();_maxReleaseDate = filters.maxReleaseDate.toDouble();// Simply to make sure the subscriptions are releasedsubscriptionFilters.cancel();// Now that we have all parameters, we may build the actual pageif (mounted){setState((){_isInit = true;});}});});}
}
  • 在BLoC级别,我们有时候还需要注入一些假数据,来触发stream提供你想要获得的数据:
class ApplicationBloc implements BlocBase {// 提供movie genres的同步StreamStreamController<List<MovieGenre>> _syncController = StreamController<List<MovieGenre>>.broadcast();Stream<List<MovieGenre>> get outMovieGenres => _syncController.stream;// 假的数据处理StreamController<List<MovieGenre>> _cmdController = StreamController<List<MovieGenre>>.broadcast();StreamSink get getMovieGenres => _cmdController.sink;ApplicationBloc() {// 如果我们通过这个sink接收到了任意数据,我们简单地提供一个MovieGenre列表作为输出流_cmdController.stream.listen((_){_syncController.sink.add(UnmodifiableListView<MovieGenre>(_genresList.genres));});}void dispose(){_syncController.close();_cmdController.close();}MovieGenresList _genresList;
}// 外部调用的例子
BlocProvider.of<ApplicationBloc>(context).getMovieGenres.add(null);

一个实践Demo

  大佬构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在Github上找到。

Flutter中如何利用StreamBuilder和BLoC来控制Widget状态相关推荐

  1. flutter bloc_如何使用BLoC模式处理Flutter中的状态

    flutter bloc Last year, I picked up Flutter and I must say it has been an awesome journey so far. Fl ...

  2. Flutter第一部分(UI)第二篇:在Flutter中构建布局

    前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件.数据库.网络)再到Flutter进阶(平台特定代码编写.测试.插件开发等),欢迎感 ...

  3. Flutter中的Wrap

    我们在flutter中使用能够包含多个child的widget的时候,经常会遇到超出边界范围的情况,尤其是在Column和Row的情况下,那么我们有没有什么好的解决办法呢?答案就是今天我们要讲解的Wr ...

  4. flutter bloc_如何在Flutter中使用Streams,BLoC和SQLite

    flutter bloc Recently, I've been working with streams and BLoCs in Flutter to retrieve and display d ...

  5. Flutter实践:深入探索 flutter 中的状态管理方式(1)

    利用 Flutter 内置的许多控件我们可以打造出一款不仅漂亮而且完美跨平台的 App 外壳,我利用其特性完成了类似知乎App的UI界面,然而一款完整的应用程序显然不止有外壳这么简单.填充在外壳里面的 ...

  6. 在Flutter中嵌入Native组件的正确姿势

    引言 在漫长的从Native向Flutter过渡的混合工程时期,要想平滑地过渡,在Flutter中使用Native中较为完善的控件会是一个很好的选择.本文希望向大家介绍AndroidView的使用方式 ...

  7. element中有多个合计_深入理解 Flutter 中的 Widget, Element, RenderObject

    这篇文章基于 Flutter stable v1.7 总结下 Flutter 当前的 UI 系统以及相关的概念, 在最后会通过自己组合一个 Gradient Button 按钮的方式来熟悉 Flutt ...

  8. Flutter 中获取地理位置[Flutter专题61]

    大家好,我是坚果,公众号"坚果前端" Flutter 中获取地理位置 如今,发现用户位置是移动应用程序非常常见且功能强大的用例.如果您曾经尝试过在 Android 中实现位置,您就 ...

  9. Flutter中Row中的子控件左右两端对齐

    Flutter中Row中的子控件左右两端对齐 Container(// padding: EdgeInsets.only(left: 20, right: 20),margin: EdgeInsets ...

最新文章

  1. 过年了,少喝点酒,多喝点茶—绿茶不仅仅是你想的那么简单
  2. 企业网络推广中关键词“出镜率”高会影响企业网络推广吗?
  3. CentOS搭建本地光盘YUM源
  4. MyBatis-Plus 高级功能 —— 乐观锁插件
  5. OpenCV2:幼儿园篇 第一章 创建图像并显示
  6. ASP.NET Core 5.0 Web API 自动集成Swashbuckle
  7. Faster R-CNN源码中ROI Pooling的解析
  8. 【2021杭电多校赛】2021“MINIEYE杯”中国大学生算法设计超级联赛(2)签到题5题
  9. 什么是正则表达式模式修正符?
  10. python整数作为条件_Python基本概念介绍
  11. 阿里云基于NVM的持久化高性能Redis数据库 1
  12. The xor-longest Path poj3764
  13. 【Matlab MTSP】灰狼算法求解多旅行商问题(同始终点)【含源码 1564期】
  14. 研究生学习生活日记——新生见面第一次组会
  15. unity3d 虚拟博物馆_基于Unity3d的博物馆移动信息化系统
  16. 【服务器数据恢复】异常断电导致ESXI系统无法连接存储的数据恢复
  17. 1553B 协议详解
  18. unix 增强工具_适用于任何UNIX系统的10种出色工具
  19. 11届蓝桥杯单片机设计与开发决赛
  20. paddlepaddle 人脸识别爬坑指南

热门文章

  1. INFRARED INDUSTRIES气体分析仪
  2. 多年来,程序员经常加班的真相终于揭开了…
  3. Centos7 Mysql Forgot Login Password
  4. MarchingCubes算法提取等值面的基本原理
  5. 命令行方式运行PHP脚本
  6. ags infoWindow 应用
  7. 解决:Windows照片查看器无法显示此图片,因为计算机上的可用内存可能不足的问题
  8. Scratch3.0——助力新进程序员理解程序(一、基础使用与运动)
  9. 视频怎么压缩小于100兆?压缩视频这么做就可以了
  10. 基于c语言的语法分析器的实现