想看原文请出门右转原文传送门
本文主要介绍 Streams,Bloc 和 Reactive Programming(响应式编程)的概念. 理论和实践范例.
难度: 中级
介绍
我花了很长时间才找到介绍 Reactive Programming,BLoC 和 Streams 概念的方法.
由于这可以对构建应用程序的方式做出重大改变, 我想要一个实际示例来说明:
很可能不使用它们, 但有时可能更难以编码和性能更低,
使用它们的好处同时也是
使用它们的影响, 正面的和 (或) 负面的.
用我做的伪应用程序作为一个例子, 简而言之, 它允许用户从在线目录中查看电影列表, 按类型和发布日期过滤它们, 标记 / 取消标记为收藏夹. 当然, 一切都是互动的, 用户可以在不同的页面中或在同一个页面内发生各种动作, 并且可以实时观察到结果.
下面的动画展示了该程序:
image.PNG
当您进入此页面以获取有关 Reactive Programming,BLoC 和 Streams 的信息时, 我将首先介绍它们. 此后, 我将向您展示如何在实践中实施和使用它们.
什么是 Stream?
介绍
为了便于想象 Stream 的概念, 我们可以简单把 Stream 想象为一个有两个端口的管道, 只有其中的一个允许插入一些东西. 当您将某物插入管道时, 它会在管道内流动并从另一端流出.
- In Flutter,
- the pipe is called a
- to control the Stream, we usually<upper style="box-sizing: border-box;">(*)</upper> use a
- to insert something into the Stream, the StreamController exposes the "entrance", called a StreamSink, accessible via the sink property
- the way out of the Stream, is exposed by the StreamController via the streamproperty
在 Flutter 中,
管道称为
为了控制 Stream, 我们通常 (*) 使用
为了在 Stream 中插入一些东西, StreamController 公开了一个名为 StreamSink 的 "入口", 可以通过 sink 属性访问
Stream 流出方式是由 StreamController 通过 stream 属性暴露的.
(*): 我故意使用术语 "通常", 因为很可能不使用任何 StreamController. 但是, 正如您将在本文中看到的那样, 我将只使用 StreamControllers.
Stream 可以传达什么?
所有类型以及任何类型. 从值, 事件, 对象, 集合, 映射, 错误或甚至另一个流, 任何类型的数据都可以由 Stream 传递 .
### 我怎么知道 Stream 传达的东西?
当您需要通知`Stream`传达某些内容时, 您只需要监听`StreamController`的`stream`属性.
定义监听时, 你会得到对象. 通过 StreamSubscription 对象, 你将会接受到通知由于 Stream 发生变化而带来的的通知.
只要至少有一个活动侦听器, Stream 就会开始生成事件, 以便每次都通知活动的 StreamSubscription 对象:
一些数据来自流,
当一些错误发送到流时,
当流关闭时.
StreamSubscription 也允许以下操作:
停止监听
暂时
恢复 Stream 只是一个简单的管道吗? 不, Stream 还允许在流出之前处理流入其中的数据.
为了控制 Stream 内部数据的处理, 我们使用, 它只是:
一个 "捕获"Stream 内部流动数据的函数
对数据做一些处理
这种转变的结果也是一个 Stream
到此你应该很容易意识到你可以按顺序使用多个 StreamTransformer.
StreamTransformer 可用于进行任何类型的处理, 例如:
过滤: 根据任何类型的条件过滤数据,
重新组合: 重新组合数据,
修改: 对数据应用任何类型的修改,
将数据注入其他流,
缓冲,
处理: 根据数据进行任何类型的操作 / 操作,
...Stream 的类型 Stream 有两种类型. 单订阅 Stream 这种类型的 Stream 只允许在该 Stream 的整个生命周期内使用单个监听器. 即使在第一个订阅被取消后, 也无法在此类流上收听两次.
广播 Stream
这是第二种类型 Stream, 这种 Stream 允许任意个数的监听器.
可以随时向广播流添加监听器. 新的监听器将在它开始收听 Stream 时收到事件.
基本例子
任何类型的数据
第一个示例显示了 "单订阅"Stream, 它只是打印输入的数据. 你可能会看到无关紧要的数据类型.
StreamTransformer
第二个示例显示 "广播"Stream, 它传达整数值并仅打印偶数. 为此, 我们应用 StreamTransformer 来过滤 (第 14 行) 值, 只让偶数经过.
RxDart
如今, 如果我不提及 RxDart https://pub.dartlang.org/packages/rxdart , 那么 Streams 的介绍将不再完整.
RxDart 是 ReactiveX API 的 Dart 实现, 它扩展了原始的 Dart Streams API 以符合 ReactiveX 标准.
由于它最初并未由 Google 定义, 因此它使用不同的词汇表. 下表给出了 Dart 和 RxDart 之间的相关性:
Dart | RxDart |
---|---|
Stream | Observable |
StreamController | Subject |
RxDart 正如我刚刚所说的, 继承了原生的 Dart Streams API 并且提供了 3 种主要的 StreamController 变种:
PublishSubject
是一个普通的广播 StreamController, 但有一种情况是例外的: 当 stream 返回一个而不是一个 Stream 时.
image.PNG
如你所见, PublishSubject 仅向监听器发送在订阅之后添加到 Stream 的事件.
BehaviorSubject
也是一个广播 StreamController, 它返回一个 Observable 而不是一个 Stream.
image.PNG
与 PublishSubject 的主要区别在于 BehaviorSubject 还将最后发送的事件发送给刚刚订阅的监听器.
ReplaySubject
也是一个广播 StreamController, 它返回一个 Observable 而不是一个 Stream.
image.PNG
默认情况下, ReplaySubject 将 Stream 已经发出的所有事件作为第一个事件发送到任何新的监听器.
关于 Resources 的重要说明
始终释放不再需要的 Resources 是一种非常好的做法.
适用于:
StreamSubscription - 当您不再需要收听 Stream 时, 取消订阅;
StreamController - 当你不再需要 StreamController 时, 关闭它;
这同样适用于 RxDart Subjects, 当你不再需要 BehaviourSubject,PublishSubject... 时, 请将其关闭.
如何基于由 Stream 提供的数据构建 Widget?
Flutter 提供了一个非常方便的 StatefulWidget, 称为.
StreamBuilder 监听 Stream, 每当某些数据输出 Stream 时, 它会自动重建, 调用其 builder 回调.
下面的代码演示了如何使用 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
- },
- )
以下示例模仿默认的 "counter" 应用程序, 但我们将使用 Stream 而不再使用任何 setState.
注: counter 是 flutter 的默认生成的 demo.
解释和说明:
第 24-30 行: 我们正在监听 stream, 每当 stream 输出一个新的值, 我们将用该值更新 Text;
第 35 行: 当我们点击
FloatingActionButton
时, 我们递增计数器并通过接收器将其发送到 Stream; 在流中注入值的事实导致侦听它的 StreamBuilder 重建并 "刷新" 计数器;
我们不再需要 State 的概念, 所有内容都通过 Stream 接收;
这是一个很大的改进, 因为调用 setState()方法会强制整个 Widget(和任何子窗口小部件)重建. 在这里, 只重建 StreamBuilder(当然还有子窗口小部件);
我们仍然在为页面使用 StatefulWidget 的唯一原因, 仅仅是因为我们需要通过 dispose 方法释放 StreamController, 第 15 行;
什么是响应式编程?
响应式编程是使用异步数据流进行编程. 换句话说, 从事件(例如, 点击), 变量的变化, 消息,...... 到构建请求, 可能改变或发生的所有事物的所有内容将被传送, 由数据流触发.
很明显, 所有这些意味着, 通过响应应式编程, 应用程序将会:
变得异步,
围绕 Streams 和 listeners 的概念进行架构,
当某些事情发生在某个地方 (事件, 变量的变化......) 时, 会向 Stream 发送通知,
如果 "某人" 收听该 Stream, 它将被通知并将采取适当的行动, 无论其在应用程序中的位置如何.
组件之间不再存在紧密耦合.
简而言之, 当 Widget 向 Stream 发送内容时, 该 Widget 不再需要知道:
接下来会发生什么,
谁可能使用这些信息(没有一个, 一个或几个 Widget...)
可能使用此信息的地方(无处, 同一页面, 另一个页面, 或者几个页面...),
当这些信息可能被使用时(几乎是直接, 几秒钟之后, 永远不会......).
...... Widget 只关心自己的业务, 就是这样!
乍一看, 读到这个, 这似乎可能导致应用程序的 "无法控制", 但正如我们将看到的, 情况恰恰相反. 它给你:
构建仅负责特定活动的部分应用程序的机会,
轻松模拟一些组件的行为, 以允许更完整的测试覆盖,
轻松重用组件(当前应用程序或其他应用程序中的其他位置),
重新设计应用程序, 并能够在不进行太多重构的情况下将组件从一个地方移动到另一个地方,
...
我们将很快看到使用响应式编程的好处...... 但在此之前我还需要介绍一下最后一个话题: BLoC 模式.
BLoC 模式
BLoC 模式由来自 Google 的 Paolo Soares 和 Cong Hui 设计, 并在 2018 年 DartConf 期间 (2018 年 1 月 23 日至 24 日) 首次展示. 在 YouTube https://www.YouTube.com/watch?v=PLHln7wHgPE 上观看此视频.
BLoC 代表业务逻辑组件(Business Logic Component).
简而言之, 业务逻辑 (Business Logic ) 需要:
转移到一个或几个 BLoC,
尽可能从表现层中删除. 换句话说, UI 组件应该只关心 UI 事物而不关心业务,
依赖 Streams 独家使用输入 (Sink) 和输出(流),
保持平台独立,
保持环境独立.
事实上, BLoC 模式最初被设想为允许独立于平台重用相同的代码: web 应用程序, 移动应用程序, 后端.
它到底意味着什么?
BLoC 模式利用了我们刚才讨论过的概念: Streams.
image.PNG
Widgets 通过 Sinks 向 BLoC 发送事件,
BLoC 通过 Stream 通知 Widgets,
由 BLoC 实现的业务逻辑不是他们关注的问题.
从上面来看, 我们可以直接看到使用 BLoC 的一个巨大的好处.
感谢业务逻辑与 UI 的分离: 我们可以随时更改业务逻辑, 对应用程序的影响最小,
我们可能会更改 UI 而不会对业务逻辑产生任何影响,
现在, 测试业务逻辑变得更加容易.
如何将此 BLoC 模式应用于 Counter 应用?
将 BLoC 模式应用于 Counter 应用可能看起来有点矫枉过正, 但请允许我先向你展示......
我已经听到你说 "哇...... 为什么这一切? 这一切都是必要的吗?"
首先, 是责任分离
如果你检查 CounterPage(第 21-45 行), 你会发现其中绝对没有任何业务逻辑.
此页面现在仅负责:
显示计数器, 现在只在必要时刷新(即使页面不必知道)
提供按钮, 当按钮按下时, 将会在 counter 面板上请求一个动作
此外, 整个业务逻辑集中在一个单独的类 "IncrementBloc" 中.
现在如果你需要更改业务逻辑, 您只需更新方法_handleLogic(第 77-80 行). 也许新的业务逻辑会要求做非常复杂的事情...... CounterPage 永远不会知道它, 这非常好!
其次, 可测试性
现在, 测试业务逻辑变得更加容易.
无需再通过 UI 测试业务逻辑. 只需要测试 IncrementBloc.
第三, 自由组织布局
由于使用了 Streams, 你现在可以独立于业务逻辑组织布局.
可以从应用程序中的任何位置启动任何操作: 只需调用. incrementCounter sink 即可.
您可以在任何页面的任何位置显示 counter, 只需听取. outCounter stream.
第四, 减少 "build" 的数量
不使用 setState()而是使用 StreamBuilder 大大减少了 "build" 的数量.
从性能角度来看, 这是一个巨大的进步.
只有一个限制...BLoC 的可访问性
为了使所有这些工作, BLoC 需要可以被访问到.
有几种方法可以访问它:
通过全局单例
这种方式可以实现, 但不是真的推荐. 此外, 由于 Dart 中没有类析构函数, 因此你永远无法正确释放资源.
作为局部变量
你可以实例化 BLoC 的局部实例. 在某些情况下, 此解决方案完全符合某些需求. 在这种情况下, 你应该始终考虑在 StatefulWidget 中初始化, 以便您
可以利用 dispose()方法来释放相关资源.
由父级提供
使其可访问的最常见方式是通过父级 Widget 访问, 通过 StatefulWidget 实现.
以下代码显示了通用 BlocProvider 的示例.
关于这种通用 BlocProvider 的一些解释
首先, 如何将其作为 provider 使用?
如果你查看示例代码 "streams_4.dart", 你将看到以下代码行(第 12-15 行)
- home: BlocProvider<IncrementBloc>(
- bloc: IncrementBloc(),
- child: CounterPage(),
- ),
通过这些代码, 我们只需实例化一个新的 BlocProvider, 它将处理一个 IncrementBloc, 并将 CounterPage 作为子项呈现.
从那一刻开始, 从 BlocProvider 开始的子树的任何 Widget 都将能够通过以代码访问 IncrementBloc:
IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
可以使用多个 BLoC 吗?
当然, 这是非常可取的. 建议如下:
(如果有任何业务逻辑)每个页面的顶部有一个 BLoC,
为什么不是 ApplicationBloc 来处理应用程序状态?
每个 "足够复杂的组件" 都有相应的 BLoC.
以下示例代码在整个应用程序的顶部显示 ApplicationBloc, 然后在 CounterPage 顶部显示 IncrementBloc.
该示例还显示了如何检索两个 bloc.
为什么不使用 InheritedWidget?
在与 BLoC 相关的大多数文章中, 你会看到通过 InheritedWidget 实现 Provider.
当然, 没有什么能阻止这种类型的实现. 然而,
一个 InheritedWidget 没有提供任何 dispose 方法, 请记住, 在不再需要资源时总是释放资源是一种很好的做法.
当然, 没有什么能阻止你将 InheritedWidget 包装在另一个 StatefulWidget 中, 但是, 使用 InheritedWidget 增加了什么呢?
最后, 如果不受控制, 使用 InheritedWidget 经常会导致副作用(请参阅下面的 InheritedWidget 上的 Reminder).
这三点解释了我为什么选择通过 StatefulWidget 实现 BlocProvider, 这样做可以让我在 Widget dispose 时释放相关资源.
Flutter 无法实例化泛型类型
不幸的是, Flutter 无法实例化泛型类型, 我们必须将 BLoC 的实例传递给 BlocProvider. 为了在每个 BLoC 中强制执行 dispose()方法, 所有 BLoC 都必
须实现 BlocBase 接口.
InheritedWidget 的一些提醒
在使用 InheritedWidget 并通过 context.inheritFromWidgetOfExactType(...)获取指定类型最近的 Widget 时, 每当 InheritedWidget 的父级或者子布局发生变化时, 这个方法会自动将当前 "context"(= BuildContext)注册到要重建的 widget 当中.
链接到 BuildContext 的 Widget(Stateful 或 Stateless)的类型无关紧要.
关于 BLoC 的个人建议
与 BLoC 相关的第三条规则是:"依赖于 Streams 对输入 (Sink) 和输出 (stream) 的独占使用".
我的个人经历稍微关系到这个说法...... 让我解释一下.
起初, BLoC 模式被设想为跨平台共享相同的代码(AngularDart,...), 并且从这个角度来看, 该语句非常有意义.
但是, 如果您只打算开发一个 Flutter 应用程序, 那么根据我的谦逊经验, 这有点矫枉过正.
如果我们坚持这种说法, 那么就没有 getter 或 settr, 只有 sink 和 stream. 缺点是 "所有这些都是异步的".
我们来看两个样本来说明缺点:
你需要从 BLoC 中检索一些数据, 以便使用这些数据作为应该立即显示这些参数的页面的输入(例如, 想一个参数页面), 如果我们不得不依赖 Streams, 这会使构建异步页面(很复杂). 通过 Streams 使其工作的示例代码可能如下所示...... 丑陋不是它.
在 BLoC 级别, 您还需要转换某些数据的 "假" 注入, 以触发提供您希望通过流接收的数据. 使这项工作的示例代码可以是:
我不知道您的意见, 但就个人而言, 如果我没有任何与代码移植 / 共享相关的限制, 我发现这太笨重了, 我宁愿在需要时使用常规的 getter / setter 并使用 Streams / Sinks 来保持分离责任并在需要的地方广播信息, 这很棒.
现在是时候在实践中看到这一切......
正如本文开头所提到的, 我构建了一个伪应用程序来展示如何使用所有这些概念. 完整的源代码可以在 GitHub 上找到.
请放纵, 因为这段代码远非完美, 可能会做的更好和 (或) 有更好的架构, 但唯一的目标只是告诉你这一切是如何工作的.
由于源代码太多很多, 我只会解释主要的几条.
电影目录的来源
我使用免费的 TMDB API https://www.themoviedb.org/documentation/API 来获取所有电影的列表, 以及海报, 评级和描述.
为了能够运行此示例应用程序, 您需要注册并获取 API 密钥(完全免费), 然后将您的 API 密钥放在文件 "/API/tmdb_api.dart" 第 15 行.
应用程序的体系结构
该应用程序使用到了:
3 个主要的 BLoC:
1. ApplicationBloc(在所有内容之上), 负责提供所有电影类型的列表;
2.FavoriteBloc(就在下面), 负责处理 "收藏夹" 的概念;
3.MovieCatalogBloc(在 2 个主要页面之上), 负责根据过滤器提供电影列表;
6 个页面:
1.HomePage: 登陆页面, 允许导航到 3 个子页面;
2.ListPage: 将电影列为 GridView 的页面, 允许过滤, 收藏夹选择, 访问收藏夹以及在后续页面中显示电影详细信息;
3.ListOnePage: 类似于 ListPage, 但电影列表显示为水平列表, 下面是详细信息;
4. FavoritesPage: 列出收藏夹的页面, 允许取消选择任何收藏夹;
5. Filters: 允许定义过滤器的 EndDrawer: 流派和最小 / 最大发布日期. 从 ListPage 或 ListOnePage 调用此页面;
6. Details 详细信息: 页面仅由 ListPage 调用以显示电影的详细信息, 但也允许选择 / 取消选择电影作为收藏;
1 个子 BLoC:
1.FavoriteMovieBloc, 链接到 MovieCardWidget 或 MovieDetailsWidget, 以处理作为收藏的电影的选择 / 取消选择
5 个主要 Widget:
1.FavoriteButton: 负责显示收藏夹的数量, 实时, 并在按下时重定向到 FavoritesPage;
2.FavoriteWidget: 负责显示一个喜欢的电影的细节并允许其取消选择;
3.FiltersSummary: 负责显示当前定义的过滤器;
4.MovieCardWidget: 负责将一部电影显示为卡片, 电影海报, 评级和名称, 以及一个图标, 表示该特定电影的选择是最喜欢的;
5.MovieDetailsWidget: 负责显示与特定电影相关的详细信息, 并允许其选择 / 取消选择作为收藏.
不同 BLoCs / Streams 的编排
下图显示了如何使用主要 3 个 BLoC:
在 BLoC 的左侧, 哪些组件调用 Sink
在右侧, 哪些组件监听流
例如, 当 MovieDetailsWidget 调用 inAddFavorite Sink 时, 会触发 2 个 stream:
outTotalFavorites 流强制重建 FavoriteButton, 和
outFavorites 流
强制重建 MovieDetailsWidget("最喜欢的" 图标)
强制重建_buildMovieCard("最喜欢的" 图标)
用于构建每个 MovieDetailsWidget
- image.PNG
- image.PNG
- image.PNG
观察
大多数 Widget 和 Page 都是 StatelessWidgets, 这意味着:
强制重建的 setState()几乎从未使用过. 例外情况是:
在 ListOnePage 中, 当用户点击 MovieCard 时, 刷新 MovieDetailsWidget. 这也可能是由一个 stream 驱动的......
在 FiltersPage 中允许用户在接受筛选条件之前通过 Sink 更改过筛选条件.
应用程序不使用任何 InheritedWidget
该应用程序几乎是 100%BLoCs / Streams 驱动, 这意味着大多数小部件彼此独立, 并且它们在应用程序中的位置
一个实际的例子是 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 <MovieCard>, 它被迭代以构建每个 Movie Card. 第一次, 这个 List <MovieCard > 是空的, 但是由于 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 的图片由 ReactiveX http://reactivex.io/ 发布.
其他一些有趣的文章值得一读:
- Fundamentals of Dart Streams https://www.burkharts.NET/apps/blog/ Thomas Burkhart
- rx_command package https://pub.dartlang.org/packages/rx_command Thomas Burkhart
- Build reactive mobile apps in Flutter - companion article Filip Hracek
- Flutter with Streams and RxDart Brian Egan
结论
很长的文章, 但还有更多的话要说, 因为对我而言, 这是展开 Flutter 应用程序的方法. 它提供了很大的灵活性.
很快就会继续关注新文章. 快乐写代码.
来源: https://www.qcloud.com/developer/article/1340170