本文共 11150 字,大约阅读时间需要 37 分钟。
想看原文请出门右转
版本所有,转载请注明。本文主要介绍Streams,Bloc和Reactive Programming(响应式编程)的概念。 理论和实践范例。难度:中级
我花了很长时间才找到介绍Reactive Programming,BLoC和Streams概念的方法。
由于这可以对构建应用程序的方式做出重大改变,我想要一个实际示例来说明:用我做的伪应用程序作为一个例子,简而言之,它允许用户从在线目录中查看电影列表,按类型和发布日期过滤它们,标记/取消标记为收藏夹。 当然,一切都是互动的,用户可以在不同的页面中或在同一个页面内发生各种动作,并且可以实时观察到结果。
下面的动画展示了该程序:当您进入此页面以获取有关Reactive Programming,BLoC和Streams的信息时,我将首先介绍它们。 此后,我将向您展示如何在实践中实施和使用它们。
为了便于想象Stream的概念,我们可以简单把Stream想象为一个有两个端口的管道,只有其中的一个允许插入一些东西。 当您将某物插入管道时,它会在管道内流动并从另一端流出。
In Flutter,在Flutter中,
Stream
中插入一些东西,StreamController
公开了一个名为StreamSink
的“入口”,可以通过sink
属性访问Stream
流出方式是由StreamController
通过stream
属性暴露的。(*):我故意使用术语“通常”,因为很可能不使用任何StreamController
。 但是,正如您将在本文中看到的那样,我将只使用StreamControllers
。所有类型以及任何类型。 从值,事件,对象,集合,映射,错误或甚至另一个流,任何类型的数据都可以由Stream
传递 。
当您需要通知`Stream`传达某些内容时,您只需要监听`StreamController`的`stream`属性。
定义监听时,你会得到对象。 通过StreamSubscription
对象,你将会接受到通知由于Stream
发生变化而带来的的通知。
只要至少有一个活动侦听器,Stream就会开始生成事件,以便每次都通知活动的StreamSubscription对象:
StreamSubscription
也允许以下操作:不,Stream
还允许在流出之前处理流入其中的数据。
StreamTransformer可用于进行任何类型的处理,例如:
Stream
有两种类型。
这种类型的Stream
只允许在该Stream
的整个生命周期内使用单个监听器。
即使在第一个订阅被取消后,也无法在此类流上收听两次。
这是第二种类型Stream,这种Stream
允许任意个数的监听器。
可以随时向广播流添加监听器。 新的监听器将在它开始收听
Stream
时收到事件。
第一个示例显示了“单订阅”Stream,它只是打印输入的数据。 你可能会看到无关紧要的数据类型。
第二个示例显示“广播”Stream,它传达整数值并仅打印偶数。 为此,我们应用StreamTransformer来过滤(第14行)值,只让偶数经过。
如今,如果我不提及,那么Streams的介绍将不再完整。
RxDart
是ReactiveX API的Dart实现,它扩展了原始的Dart Streams API
以符合ReactiveX
标准。
由于它最初并未由Google定义,因此它使用不同的词汇表。 下表给出了Dart
和RxDart
之间的相关性:
RxDart
正如我刚刚所说的,继承了原生的[Dart Streams API]() 并且提供了3种主要的StreamController
变种: 是一个普通的广播StreamController
,但有一种情况是例外的:当stream返回一个而不是一个[Stream]()时。
Stream
的事件。 也是一个广播StreamController,它返回一个[Observable]()而不是一个[Stream]()。
PublishSubject
的主要区别在于BehaviorSubject
还将最后发送的事件发送给刚刚订阅的监听器。 也是一个广播StreamController,它返回一个[Observable]()而不是一个[Stream]()。
ReplaySubject
将Stream
已经发出的所有事件作为第一个事件发送到任何新的监听器。 始终释放不再需要的Resources是一种非常好的做法。
适用于:
StreamSubscription
- 当您不再需要收听Stream时,取消订阅;StreamController
- 当你不再需要StreamController时,关闭它;RxDart Subjects
,当你不再需要BehaviourSubject
,PublishSubject
...时,请将其关闭。Flutter提供了一个非常方便的StatefulWidget,称为。
StreamBuilder监听Stream,每当某些数据输出Stream时,它会自动重建,调用其builder回调。
下面的代码演示了如何使用StreamBuilder:
StreamBuilder( 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 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 },)
以下示例模仿默认的“ counter”应用程序,但我们将使用Stream而不再使用任何setState。
注:counter是flutter的默认生成的demo。
解释和说明:
FloatingActionButton
时,我们递增计数器并通过接收器将其发送到Stream; 在流中注入值的事实导致侦听它的StreamBuilder重建并“刷新”计数器;Stream
接收;setState()
方法会强制整个Widget(和任何子窗口小部件)重建。 在这里,只重建StreamBuilder(当然还有子窗口小部件);响应式编程是使用异步数据流进行编程。换句话说,从事件(例如,点击),变量的变化,消息,......到构建请求,可能改变或发生的所有事物的所有内容将被传送,由数据流触发。
很明显,所有这些意味着,通过响应应式编程,应用程序将会:
组件之间不再存在紧密耦合。
简而言之,当Widget
向Stream
发送内容时,该Widget
不再需要知道:
...... Widget只关心自己的业务,就是这样!
乍一看,读到这个,这似乎可能导致应用程序的“无法控制”,但正如我们将看到的,情况恰恰相反。 它给你:
BLoC模式由来自Google的Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次展示。 在上观看此视频。
BLoC代表业务逻辑组件(Business Logic Component)。
简而言之,业务逻辑(Business Logic )需要:
BLoC模式利用了我们刚才讨论过的概念:Streams。
从上面来看,我们可以直接看到使用BLoC的一个巨大的好处。
感谢业务逻辑与UI的分离:
- 我们可以随时更改业务逻辑,对应用程序的影响最小,
- 我们可能会更改UI而不会对业务逻辑产生任何影响,
- 现在,测试业务逻辑变得更加容易。
将BLoC模式应用于Counter 应用可能看起来有点矫枉过正,但请允许我先向你展示......
我已经听到你说“哇......为什么这一切? 这一切都是必要的吗?“
如果你检查CounterPage(第21-45行),你会发现其中绝对没有任何业务逻辑。
此页面现在仅负责:
此外,整个业务逻辑集中在一个单独的类“IncrementBloc”中。
现在如果你需要更改业务逻辑,您只需更新方法_handleLogic(第77-80行)。 也许新的业务逻辑会要求做非常复杂的事情...... CounterPage永远不会知道它,这非常好!
现在,测试业务逻辑变得更加容易。
无需再通过UI测试业务逻辑。 只需要测试IncrementBloc
。
由于使用了Streams,你现在可以独立于业务逻辑组织布局。
可以从应用程序中的任何位置启动任何操作:只需调用.incrementCounter sink即可。
您可以在任何页面的任何位置显示counter,只需听取.outCounter stream。
不使用setState()而是使用StreamBuilder大大减少了“build”的数量。
从性能角度来看,这是一个巨大的进步。
为了使所有这些工作,BLoC需要可以被访问到。
有几种方法可以访问它:
这种方式可以实现,但不是真的推荐。 此外,由于Dart中没有类析构函数,因此你永远无法正确释放资源。
你可以实例化BLoC的局部实例。 在某些情况下,此解决方案完全符合某些需求。 在这种情况下,你应该始终考虑在StatefulWidget中初始化,以便您
可以利用dispose()方法来释放相关资源。使其可访问的最常见方式是通过父级Widget访问,通过StatefulWidget实现。
以下代码显示了通用BlocProvider的示例。
首先,如何将其作为provider使用?
如果你查看示例代码“streams_4.dart”,你将看到以下代码行(第12-15行)
home: BlocProvider( bloc: IncrementBloc(), child: CounterPage(), ),
通过这些代码,我们只需实例化一个新的BlocProvider,它将处理一个IncrementBloc,并将CounterPage作为子项呈现。
从那一刻开始,从BlocProvider开始的子树的任何Widget都将能够通过以代码访问IncrementBloc:
IncrementBloc bloc = BlocProvider.of(context);
当然,这是非常可取的。建议如下:
该示例还显示了如何检索两个bloc。
在与BLoC相关的大多数文章中,你会看到通过InheritedWidget实现Provider。
当然,没有什么能阻止这种类型的实现。 然而,
这三点解释了我为什么选择通过StatefulWidget实现BlocProvider,这样做可以让我在Widget dispose时释放相关资源。
Flutter无法实例化泛型类型
不幸的是,Flutter无法实例化泛型类型,我们必须将BLoC的实例传递给BlocProvider。 为了在每个BLoC中强制执行dispose()方法,所有BLoC都必须实现BlocBase接口。
在使用InheritedWidget并通过context.inheritFromWidgetOfExactType(...)获取指定类型最近的Widget时,每当InheritedWidget的父级或者子布局发生变化时,这个方法会自动将当前“context”(= BuildContext)注册到要重建的widget当中。
链接到BuildContext的Widget(Stateful或Stateless)的类型无关紧要。
与BLoC相关的第三条规则是:“依赖于Streams对输入(Sink)和输出(stream)的独占使用”。
我的个人经历稍微关系到这个说法......让我解释一下。
起初,BLoC模式被设想为跨平台共享相同的代码(AngularDart,...),并且从这个角度来看,该语句非常有意义。
但是,如果您只打算开发一个Flutter应用程序,那么根据我的谦逊经验,这有点矫枉过正。
如果我们坚持这种说法,那么就没有getter或settr,只有sink和stream。缺点是“所有这些都是异步的”。
我们来看两个样本来说明缺点:
正如本文开头所提到的,我构建了一个伪应用程序来展示如何使用所有这些概念。 完整的源代码可以在上找到。
请放纵,因为这段代码远非完美,可能会做的更好和(或)有更好的架构,但唯一的目标只是告诉你这一切是如何工作的。
由于源代码太多很多,我只会解释主要的几条。
我使用免费的来获取所有电影的列表,以及海报,评级和描述。
为了能够运行此示例应用程序,您需要注册并获取API密钥(完全免费),然后将您的API密钥放在文件“/api/tmdb_api.dart”第15行。
该应用程序使用到了:
3个主要的BLoC:
3.MovieCatalogBloc(在2个主要页面之上),负责根据过滤器提供电影列表;
6个页面:
1.HomePage:登陆页面,允许导航到3个子页面; 2.ListPage:将电影列为GridView的页面,允许过滤,收藏夹选择,访问收藏夹以及在后续页面中显示电影详细信息; 3.ListOnePage:类似于ListPage,但电影列表显示为水平列表,下面是详细信息;6. Details详细信息:页面仅由ListPage调用以显示电影的详细信息,但也允许选择/取消选择电影作为收藏;
下图显示了如何使用主要3个BLoC:
例如,当MovieDetailsWidget调用inAddFavorite Sink时,会触发2个stream:
outFavorites流
大多数Widget和Page都是StatelessWidgets,这意味着:
强制重建的setState()几乎从未使用过。 例外情况是:
一个实际的例子是FavoriteButton,它显示徽章中所选收藏夹的数量。 该应用程序共有3个FavoriteButton实例,每个实例显示在3个不同的页面中。
要显示符合过滤条件的电影列表,我们使用(ListPage)或(ListOnePage)作为无限滚动列表。
电影是通过TMDB API获取的,每次拉取20个。
提醒一下,GridView.builder和ListView.builder都将itemCount作为输入,如果提供了item数量,则表示要根据itemCount的数量来显示列表。itemBuilder的index从0到itemCount - 1不等。
正如您将在代码中看到的那样,我随意为GridView.builder添加了30多个。 理由是,在这个例子中,我们正在操纵假定的无限数量的项目(这不是完全正确但是又有谁关心这个例子)。 这将强制GridView.builder请求显示“最多30个”项目。
此外,GridView.builder和ListView.builder只在认为必须在视口中呈现某个项目(索引)时才调用itemBuilder。
MovieCatalogBloc.outMoviesList返回一个List ,它被迭代以构建每个Movie Card。 第一次,这个List 是空的,但是由于itemCount:... + 30,我们欺骗系统,它将要求通过_buildMovieCard(...)呈现30个不存在的项目。
正如您将在代码中看到的,此例程对Sink进行了一次奇怪的调用:
// Notify the MovieCatalogBloc that we are rendering the MovieCard[index]//通知MovieCatalogBloc我们正在渲染MovieCard[index] movieBloc.inMovieIndex.add(index);
这个调用告诉MovieCatalogBloc我们要渲染MovieCard [index]。
然后_buildMovieCard(...)继续验证与MovieCard [index]相关的数据是否存在。 如果是,则渲染后者,否则显示CircularProgressIndicator。
对StreamCatalogBloc.inMovieIndex.add(index)的调用由StreamSubscription监听,StreamSubscription将索引转换为某个pageIndex数字(一页最多可计20部电影)。 如果尚未从TMDB API获取相应页面,则会调用API。 获取页面后,所有已获取电影的新列表将发送到_moviesController。 当GridView.builder监听该Stream(= movieBloc.outMoviesList)时,后者请求重建相应的MovieCard。 由于我们现在拥有数据,我们可以渲染它了。
介绍PublishSubject,BehaviorSubject和ReplaySubject的图片由发布。
其他一些有趣的文章值得一读:
很长的文章,但还有更多的话要说,因为对我而言,这是展开Flutter应用程序的方法。 它提供了很大的灵活性。
很快就会继续关注新文章。 快乐写代码。
版本所有,转载请注明。