Flutter - BLoC 第一讲
本篇已同步到 个人博客 ,欢迎常来。
【译文】Reactive Programming - Streams - BLoC
[toc]
注:此处的"toc"应显示为目录,但是简书不支持,显示不出来。
本译文介绍Streams、Bloc 和 Reactive Programming 的概念。理论和实践范例。对于作者的个人note没有进行翻译,请自行翻阅原文地址 原文原码。和iOS开发中的RAC相似,本文推荐重点在 <如何基于流出的数据构建Widge>!
难度:中级
本文纪实
本译文的原文是在学 BLoC 的 第三方框架 (框架的教程)而看到的推荐链接进入该文章,为了更好的实现Flutter的BLoC而进行的翻译学习,翻译完也到了文章底部竟然有推荐中文翻译 链接, 那本篇就孤芳自赏吧!也顺便记录下自己的第一篇国外技术译文吧!推荐读者结合原文 看译文效果会更佳。
笔者本文学习目的: 解耦
什么是流?
介绍 :为了便于想象Stream的概念,只需考虑一个带有两端的管道,只有一个允许在其中插入一些东西。当您将某物插入管道时,它会在管道内流动并从另一端流出。
在Flutter中
- 管道称为流
- 我们通常控制Stream(*)使用StreamController
- 插入东西到流中,StreamController暴露了“ 入口 ”,称为StreamSink,通过访问水槽财产
- StreamController通过stream属性公开了Stream的出路
注意: (*):我故意使用术语“ 通常 ”,因为很可能不使用任何StreamController。但是,正如您将在本文中阅读的那样,我将只使用StreamControllers。
流可以传达什么?
所有东西都可以通过流传递。从值,事件,对象,集合,映射,错误或甚至另一个流,可以由流传达任何类型的数据。
我怎么知道Stream传达的东西?
当您需要通知Stream传达某些内容时,您只需要监听 StreamController 的stream属性。
定义监听器时,您会收到StreamSubscription对象。这是通过StreamSubscription对象,您将收到通知,在Stream级别发生某些事情。
只要有至少一个活动 侦听器,Stream就会开始生成事件,以便每次都通知活动的 StreamSubscription对象:
- 一些数据来自流,
- 当一些错误发送到流时,
- 当流关闭时。
该StreamSubscription对象,您还可以:
- 停止听
- 暂停,
- 恢复。
Stream只是一个简单的管道吗?
不,流还允许在流出之前处理流入其中的数据。
为了控制Stream内部数据的处理,我们使用StreamTransformer,它只是
- 一个“ 捕获 ” Stream内部流动数据的函数
- 对数据做了些什么
- 这种转变的结果也是一个流
您将直接从该声明中了解到,可以按顺序使用多个StreamTransformer。
StreamTransformer可以用来做任何类型的处理,如,例如:
- 过滤(filtering):根据任何类型的条件过滤数据,
- 重新组合(regrouping):重新组合数据,
- 修改(modification):对数据应用任何类型的修改,
- 将数据注入其他流,
- 缓冲,
- 处理(processing):根据数据进行任何类型的操作/操作,
- ...
Stream流的类型
有两种类型的Streams。
单订阅流
这种类型的Stream只允许在该Stream的整个生命周期内使用单个监听器。
即在第一个订阅被取消后,也无法在此类流上收听两次。
广播流
第二种类型的Stream允许任意数量的监听器。
可以随时向广播流添加侦听器。新的侦听器将在它开始收听Stream时收到事件。
基本的例子
任何类型的数据
第一个示例显示了“ 单订阅 ” 流,它只是打印输入的数据。您可能会看到数据类型无关紧要。
streams_1.dart
import 'dart:async';
void main() {
//
// 初始化“单订阅”流控制器
//
final StreamController ctrl = StreamController();
//
//初始化一个只打印数据的监听器
//一收到它
//
final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));
//
// We here add the data that will flow inside the 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
第二个示例显示“ 广播 ” 流,它传达整数值并仅打印偶数。为此,我们应用StreamTransformer来过滤(第14行)值,只让偶数经过。
import 'dart:async';
void main() {
//
// Initialize a "Broadcast" Stream controller of integers
//
final StreamController<int> ctrl = StreamController<int>.broadcast();
//
// Initialize a single listener which filters out the odd numbers and
// only prints the even numbers
//
final StreamSubscription subscription = ctrl.stream
.where((value) => (value % 2 == 0))
.listen((value) => print('$value'));
//
// We here add the data that will flow inside the stream
//
for(int i=1; i<11; i++){
ctrl.sink.add(i);
}
//
// We release the StreamController
//
ctrl.close();
}
RxDart
所述RxDart包是用于执行 Dart 所述的ReactiveX API,它扩展了原始达特流 API符合ReactiveX标准。
由于它最初并未由Google定义,因此它使用不同的词汇表。下表给出了Dart和RxDart之间的相关性。
Dart | RxDart |
---|---|
Stream | Observable |
StreamController | Subject |
正如我刚才所说,RxDart 扩展了原始的Dart Streams API并提供了StreamController的 3个主要变体:
PublishSubject
PublishSubject是正常广播 StreamController有一个例外:流返回一个可观察到,而不是流。
PublishSubject仅向侦听器发送在订阅之后添加到Stream的事件。
![](https://img.haomeiwen.com/i699599/317db649dd785a8f.png)
BehaviorSubject
该BehaviorSubject也是广播 StreamController它返回一个可观察到,而不是流。
与PublishSubject的主要区别在于BehaviorSubject还将最后发送的事件发送给刚刚订阅的侦听器。
![](https://img.haomeiwen.com/i699599/ec883ab1dc660e96.png)
ReplaySubject
该ReplaySubject也是广播 StreamController它返回一个可观察到,而不是流。
ReplaySubject,默认情况下,发送的所有事件是已经由发射流到任何新的听众,甚至第一个事件。
![](https://img.haomeiwen.com/i699599/374b541a5bca7819.png)
关于资源的重要说明
始终释放不再需要的资源是一种非常好的做法。
本声明适用于:
StreamSubscription - 当您不再需要收听流时,取消订阅;
StreamController - 当你不再需要StreamController时,关闭它;
这同样适用于RxDart主题,当您不再需要BehaviourSubject,PublishSubject ...时,请将其关闭。
如何基于流出的数据构建Widget?(重点)
Flutter提供了一个非常方便的StatefulWidget,名为StreamBuilder。
StreamBuilder监听到流 和 stream每次消失,它都会自动重建,调用他builder callback
这是如何使用StreamBuilder:
StreamBuilder<T>(
key: ...optional, the unique ID of this Widget...
stream: ...the stream to listen to...
initialData: ...any initial data, in case the stream would initially be empty...
builder: (BuildContext context, AsyncSnapshot<T> snapshot){
if (snapshot.hasData){
return ...the Widget to be built based on snapshot.data
}
return ...the Widget to be built if no data is available
},
)
以下示例模仿默认的“ 计数器 ”应用程序,但使用Stream而不再使用任何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>();
@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(
// 我们正在监听流,每次有一个新值流出这个流时,我们用该值更新Text ;
child: StreamBuilder<int>(
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: (){
//当我们点击FloatingActionButton时,增加计数器并通过sink将其发送到Stream;
//事实上 注入到stream中值会导致侦听它(stream)的StreamBuilder重建并 ‘刷新’计数器;
_streamController.sink.add(++_counter);
},
),
);
}
}
注意点:
我们不再需要state的概念,所有东西都通过Stream接受;
这是一个很大的改进,因为实际调用setState()方法的,会强制整个 Widget(和任何子小部件)重建。这里,只有StreamBuilder被重建(当然它的子部件,被streamBuilder包裹的子控件);
我们仍然在为页面使用StatefulWidget的唯一原因,仅仅是因为我们需要通过dispose方法第15行释放StreamController ;
什么是反应式编程?
反应式编程是使用异步数据流进行编程。
换句话说,任何东西 比如从事件(例如点击),变量的变化,消息,......到构建请求,可能改变或发生的所有事件的所有内容都将被传送,由数据流触发。
很明显,所有这些意味着,通过反应式编程,应用程序:
- 变得异步
- 围绕Streams和listeners的概念进行架构
- 当某事发生在某处(事件,变量的变化......)时,会向Stream发送通知
- 如果 "某人" 监听该流(无论其在应用程序中的任何位置),它将被通知并将采取适当的行动.
组件之间不再存在紧密耦合。
简而言之,当Widget向Stream发送内容时,该Widget 不再需要知道:
- 接下来会发生什么
- 谁可能使用这些信息(没有一个,一个或几个小部件......)
- 可能使用此信息的地方(无处,同一屏幕,另一个,几个...)
- 当这些信息可能被使用时(几乎是直接,几秒钟之后,永远不会......)
- ...... Widget只关心自己的事业,就是这样!
乍一看,读到这个,这似乎会导致应用程序“ 无法控制 ”,但正如我们将看到的,情况正好相反。它给你:
- 构建仅负责特定活动的部分应用程序的机会
- 轻松模拟一些组件的行为,以允许更完整的测试覆盖
- 轻松重用组件(应用程序或其他应用程序中的其他位置),
- 重新设计应用程序,并能够在不进行太多重构的情况下将组件从一个地方移动到另一个地方,
我们将很快看到优势......但在我需要介绍最后一个主题之前:BLoC模式。
BLoC模式
BLoC模式由Paolo Soares 和 Cong Hui设计,并谷歌在2018的 DartConf 首次提出,可以在 YouTube 上观看。
BLoC表示为业务逻辑组件 (Business Logic Component)
简而言之, Business Logic需要:
- 移动到一个或几个BLoC,
- 尽可能从表示层(Presentation Layer)中删除。换句话说,UI组件应该只关心UI事物而不关心业务
- 依赖Streams 独家使用输入(Sink)和输出(stream)
- 保持平台独立
- 保持环境独立
事实上,BLoC模式最初被设想为允许独立于平台重用相同的代码:Web应用程序,移动应用程序,后端。
它究竟意味着什么?
该 BLoC模式 是利用我们刚才上面所讨论的观念:Streams (流)
![](https://img.haomeiwen.com/i699599/2924b4cd35807a27.png)
- Widgets通过Sinks向 BLoC 发送事件(event)
- BLoC通过流(stream)通知小部件(widgets)
- 由BLoC实现的业务逻辑不是他们关注的问题。
从这个声明中,我们可以直接看到一个巨大的好处。
由于业务逻辑与UI的分离:
- 我们可以随时更改业务逻辑,对应用程序的影响最小
- 我们可能会更改UI而不会对业务逻辑产生任何影响,
- 现在,测试业务逻辑变得更加容易。
如何将此BLoC模式应用于Counter Application示例中
将BLoC模式应用于此计数器应用程序似乎有点矫枉过正,但让我先向您展示......
代码: streams_4.dart
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget 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 {
@override
Widget 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>(
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;
//
// Stream来处理计数器
//
StreamController<int> _counterController = StreamController<int>();
StreamSink<int> get _inAdd => _counterController.sink;
Stream<int> get outCounter => _counterController.stream;
//
// Stream来处理计数器上的操作
//
StreamController _actionController = StreamController();
StreamSink get incrementCounter => _actionController.sink;
//
// Constructor
//
IncrementBloc(){
_counter = 0;
_actionController.stream
.listen(_handleLogic);
}
void dispose(){
_actionController.close();
_counterController.close();
}
void _handleLogic(data){
_counter = _counter + 1;
_inAdd.add(_counter);
}
}
我已经听到你说“ 哇......为什么这一切?这都是必要的吗?”。
第一 是责任分离
如果你检查CounterPage(第21-45行),其中绝对没有任何业务逻辑。
此页面现在仅负责:
显示计数器,现在只在必要时刷新(即使没有页面必须知道它)
提供按钮,当按下时,请求在柜台上执行动作
此外,整个业务逻辑集中在一个单独的类“ IncrementBloc ”中。
如果现在,您需要更改业务逻辑,您只需更新方法_handleLogic(第77-80行)。也许新的业务逻辑将要求做非常复杂的事情...... CounterPage永远不会知道它,这是非常好的!
第二 可测试性
现在,测试业务逻辑变得更加容易。
无需再通过用户界面测试业务逻辑。只需要测试IncrementBloc类。
第三 自由组织布局
由于使用了Streams,您现在可以独立于业务逻辑组织布局。
可以从应用程序中的任何位置启动任何操作:只需调用.incrementCounter接收器即可。
您可以在任何页面的任何位置显示计数器,只需听取.outCounter流。
第四 减少“建设”的数量
不使用setState()而是使用StreamBuilder这一事实大大减少了“ 构建 ”的数量,只减少了所需的数量。
从性能角度来看,这是一个巨大的进步。
只有一个约束...... BLoC的可访问性
为了使所有这些工作,BLoC需要可访问。
有几种方法可以访问它:
通过全球单例
这种方式很有可能,但不是真的推荐。此外,由于Dart中没有类析构函数,因此您永远无法正确释放资源。作为本地实例
您可以实例化BLoC的本地实例。在某些情况下,此解决方案完全符合某些需求。在这种情况下,您应该始终考虑在StatefulWidget中初始化,以便您可以利用dispose()方法来释放它。由父类提供
使其可访问的最常见方式是通过祖先 Widget,实现为StatefulWidget。
以下代码显示了通用 BlocProvider的示例。
代码: streams_5
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
```.dart
```dart
//所有BLoC的通用接口
abstract class BlocBase {
void dispose();
}
//通用BLoC提供商
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>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
关于这种通用BlocProvider的一些解释
首先,如何将其用作提供者?
如果您查看示例代码“ streams_4.dart ”,您将看到以下代码行(第12-15行)
home: BlocProvider<IncrementBloc>(
bloc: IncrementBloc(),
child: CounterPage(),
),
使用这些行,我们只需实例化一个新的BlocProvider,它将处理一个IncrementBloc,并将CounterPage作为子项呈现。
从那一刻开始,从BlocProvider开始的子树的任何小部件部分都将能够通过以下行访问IncrementBloc:
IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
我们可以有多个BLoC吗?
当然,这是非常可取的。建议是:
- (如果有任何业务逻辑)每页顶部有一个BLoC,
- 为什么不是ApplicationBloc来处理应用程序状态?
- 每个“足够复杂的组件”都有相应的BLoC。
以下示例代码在整个应用程序的顶部显示ApplicationBloc,然后在CounterPage顶部显示IncrementBloc。
该示例还显示了如何检索两个blocs。
代码 streams_6.dart
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);
...
}
}
为什么不使用InheritedWidget?
在大多数与BLoC相关的文章中,您将看到Provider的实现是一个InheritedWidget。
当然,没有什么能阻止这种类型的实现。然而,
- 一个InheritedWidget没有提供任何dispose方法,记住,在不再需要资源时总是释放资源是一个很好的做法。
- 当然,没有什么能阻止你将InheritedWidget包装在另一个StatefulWidget中,但是,使用> * InheritedWidget增加了什么呢?
- 最后,如果不受控制,使用InheritedWidget经常会导致副作用(请参阅下面的InheritedWidget上的提醒)。
这3个子弹解释了我为将通用 BlocProvider实现为StatefulWidget而做出的选择,这样我就可以在处理这个小部件时释放资源。
Flutter无法实例化泛型类型
不幸的是,Flutter无法实例化泛型类型,我们必须将BLoC的实例传递给BlocProvider。为了在每个BLoC中强制执行dispose()方法,所有BLoC都必须实现BlocBase接口。
提醒InheritedWidget
当我们使用的是InheritedWidget和调用的context.inheritFromWidgetOfExactType(...)方法来获得给定类型的最近的窗口小部件,每次InheritedWidget的父级或者子布局发生变化时,这个方法会自动将当前“context”(= BuildContext)注册到要重建的widget当中。。
请注意,为了完全正确,我刚才解释的与InheritedWidget相关的问题只发生在我们将InheritedWidget与StatefulWidget结合使用时。当您只使用没有State的InheritedWidget时,问题就不会发生。但是......我将在下一篇文章 中回到这句话。
链接到BuildContext的Widget类型(Stateful或Stateless)无关紧要。