有几个核心概念对于理解如何使用 bloc 包至关重要。
在接下来的部分中,我们将依次详细介绍,并研究如何将它们应用于计数器应用程序。
流是一系列异步数据。
要使用 bloc 库,必须对 Streams
及其工作原理有基本的了解。
如果您不熟悉 Streams
,那么可以想象一下有水流过的管道。管道是 Stream
,水是异步数据。
我们可以通过编写 async*
(异步生成器)函数在 Dart 中创建一个 Stream
。
通过将函数标记为 async*
,我们可以使用 yield
关键字并返回 Stream
数据。在上面的例子中,我们返回一个 Stream
整数,它的最大值是 max
参数。
每次我们在 async*
函数中 yield
时,我们都会通过 Stream
推送该部分数据。
我们可以用多种方式使用上述 Stream
。如果我们想编写一个函数来返回整数 Stream
的总和,它看起来可能像这样:
通过将上述函数标记为 async
,我们可以使用 await
关键字并返回一个 Future
整数。在此示例中,我们正在等待流中的每个值并返回流中所有整数的总和。
我们可以合并上面的代码如下:
现在我们对 Dart 中 Streams
的原理有了一个基本的了解。我们可以学习关于 bloc 包的核心组件:Cubit
了。
Cubit
是扩展自 BlocBase
的类并可以扩展用于管理任何类型的状态。
Cubit
可以公开可调用函数来触发状态的改变。
状态是 Cubit
的输出,代表应用程序状态的一部分。UI 组件可以收到状态通知,并根据当前状态进行部分重绘。
我们可以像这样创建一个 CounterCubit
:
创建 Cubit
时,我们需要定义 Cubit
管理的状态类型。以上面的 CounterCubit
为例,状态类型是 int
,但在更复杂的情况下,可能需要使用 class
而不是值类型。
其次在创建 Cubit
的时候要指定初始状态。我们可以通过调用 super
并赋初始值来实现。在上面的代码片段中,我们在内部将初始状态设置为 0
,但我们也可以通过构造函数参数使 Cubit
更加灵活:
这样我们就可以创建具有不同初始值的 CounterCubit
实例,像这样:
每一个 Cubit
都可以通过 emit
输出一个新的状态。
在上面的代码片段中, CounterCubit
公开了一个名为 increment
的公共方法,可以被外部调用以通知 CounterCubit
增加它的状态值。当调用 increment
时,我们可以通过 状态
的getter访问 Cubit
的当前状态,并通过在当前状态上 +1
来 emit
一个新状态。
我们现在可以使用我们实现的 CounterCubit
了。
在上面的代码片段中,我们从创建一个 CounterCubit
开始。然后我们打印了当前 cubit 的初始状态(由于尚未发出任何新的状态)。接下来,我们调用 increment
函数来触发状态变化。最后,我们再次打印 Cubit
的状态,从 0
变为 1
,并在 Cubit
上调用 close
来关闭内部状态流。
Cubit
公开了一个 Stream
可以用于接收实时的状态更新:
在上面的代码片段中,我们订阅了 CounterCubit
并且在每次状态变化时打印出来。我们调用了 increment
函数来触发新的状态。最后,当我们不再需要接收时关闭了这个 Cubit
, 并且在 subscription
上调用了 cancel
。
当 Cubit
发出一个新的状态时,一个 Change
发生了。我们可以通过重写 onChange
来观察 Cubit
的所有变化。
然后我们可以与 Cubit
交互并观察输出到控制台的所有更改。
上面的示例将会输出:
使用 bloc 库的一个额外好处是我们可以在一个位置访问所有的 Changes
。尽管在这个应用里我们只有一个 Cubit
,但是在大型应用程序中使用多个 Cubits
来管理应用程序状态的不同部分是相当常见的。
如果我们想对所有的 Changes
做出一些反应,仅需创建我们自己的 BlocObserver
即可。
要使用 SimpleBlocObserver
,我们只需要对 main
函数做少许的变更:
上面的代码将会输出:
每一个 Cubit
都有一个 addError
方法,可以用于指示发生了错误。
也可以在 BlocObserver
中重写 onError
,以全局处理所有报告的错误。
如果我们重新运行这个程序,我们会看到下面的输出:
相对于函数来说,Bloc
是一个依赖 事件
触发 状态
变更的更高级的类。Bloc
同样扩展了 BlocBase
,这意味着它和 Cubit
一样拥有类似的公共 API,Blocs
不是调用 Bloc
上的 函数
并直接发出新的 状态
,而是接收 事件
并将传入的 事件
转换为传出的 状态
。
创建 Bloc
跟创建 Cubit
类似,不过除了定义我们要管理的状态以外,我们还必须定义 Bloc
能够处理的事件。
事件是 Bloc 的输入。通常情况下这些事件用于响应用户的交互,类似按下按钮或者页面加载的生命周期事件等等。
和创建 CounterCubit
一样,我们必须通过基类的 super
来传入一个初始状态。
跟 Cubit
里的函数相反,Bloc
必须通过 on<Event>
API注册事件处理程序。事件处理程序负责将任何传入事件转换为零个或多个传出状态。
然后我们可以更新 EventHandler
来处理 CounterIncrementPressed
事件:
在上面的代码片中,我们注册了一个 EventHandler
来管理所有的 CounterIncrementPressed
。针对每个输入的 CounterIncrementPressed
事件我们都可以通过 状态
的 getter 来访问当前的状态并且 emit(state + 1)
。
至此,我们可以创建一个我们的 CounterBloc
实例并使用它了!
在上面的代码片段中,我们先创建了一个 CounterBloc
。然后我们打印了 Bloc
的当前状态(因为还没有新的状态发出)。接下来我们添加了一个 CounterIncrementPressed
事件来出发状态变更。最后,我们再次打印了 Bloc
的状态,从 0
变成了 1
,并且在 Bloc
上调用 close
关闭了内部的状态流。
和 Cubit
一样,Bloc
是一个特殊的 Stream
类型,这意味着我们也可以订阅 Bloc
以实时更新其状态:
在上面的代码片中,我们订阅了 CounterBloc
并且在每次状态变更时进行打印。然后我们添加了 CounterIncrementPressed
事件触发 on<CounterIncrementPressed>
这个 EventHandler
并且发出新的状态。最后,当我们不想再接受更新时,我们在这个订阅上调用了 cancel
并且 close
了这个 Bloc
。
由于 Bloc
扩展了 BlocBase
,我们可以用 onChange
观察 Bloc
的所有状态变更。
然后我们可以更新 main.dart
如下:
现在如果我们运行上面的代码片段,输出将会是:
Bloc
和 Cubit
之间的一个关键区别因素是,由于 Bloc
是事件驱动的,我们还能够捕获有关触发状态变化的信息。
我们可以通过重写 onTransition
来实现。
从一个状态变成另一个状态称为 过渡
。一个 过渡
包含了当前状态,触发事件以及下一个状态。
如果我们重新运行之前相同的 main.dart
代码片段,我们应该看到以下输出:
综前所述,我们可以在一个自定义的 BlocObserver
里重写 onTransition
以实现在一个位置观察所有的过渡。
我们可以像前面一样初始化 SimpleBlocObserver
:
现在如果我们重新运行上面的代码片段,输出应该如下:
另一个 Bloc
实例的特有功能是:它允许我们重写 onEvent
方法,无论什么时候有新的事件被添加到 Bloc
,这个方法都会被调用。和 onChange
和 onTransition
方法一样,onEvent
也可以在本地或者全局被重写。
我们可以像前面一样运行同样的 main.dart
而且应该能看到如下输出:
跟 Cubit
一样,每个 Bloc
都有 addError
和 onError
方法。我们可以在 Bloc
里的任何地方调用 addError
来指示发生了错误。跟 Cubit
里 onError
方法一样,我们可以重写它来响应所有发生的错误。
如果我们重新运行之前的 main.dart
,我们可以看到当错误发生时,输出时下面这样:
现在我们了解了 Cubit
和 Bloc
类的基本信息,你可能会想:什么时候应该用 Cubit
,什么时候则应该用 Bloc
呢?
Cubit
最大优势之一时简单。当创建 Cubit
时,我们只需要定义状态以及公开改变状态的函数。作为对比,当我们创建 Bloc
时,我们要定义状态,事件以及 EventHandler
实现。这样来看,Cubit
更易于理解并且需要写更少的代码。
现在咱们来看看两个计数器的实现:
Cubit
的实现更加简洁,跟单独定义事件相比,函数更像事件。此外,使用 Cubit
时,我们可以简单的从任何地方调用 emit
来触发状态变更。
使用 Bloc
的最大优势之一是了解状态变化的顺序以及触发这些变化的确切原因。处理对应用程序功能至关重要的状态,使用更事件驱动的方法来捕获除状态变化之外的所有事件可能会非常有用。
一个常见的用例就是管理 AuthenticationState
。为了简化,我们用 enum
来表示 AuthenticationState
:
应用程序的状态从 authenticated
到 unauthenticated
的变更可能有很多种原因。比如:用户可能点击了登出来注销。再比如,用户的 access token 被收回了并且他们被强制注销了。使用 Bloc
时,我们可以清楚的追溯应用的状态是如何变更为特定状态的。
上面的 过渡
提供了让我们理解状态变更的所有信息。如果我们用 Cubit
来管理 AuthenticationState
,我们的日志则如下:
这告诉我们用户已登出,但没有解释为什么,这使得调试和理解应用程序状态随时间的变化变得异常困难。
Bloc
优于 Cubit
的另一个领域是当我们需要利用响应式操作符(例如 buffer
、debounceTime
、throttle
等)时。
Bloc
有一个 event 池允许我们控制和转换输入的事件。
例如,如果我们要构建一个实时搜索,我们可能想要实现后端请求的去抖动来避免速率限制抑或是降低后端的成本/负载。
使用 Bloc
的话我们可以提供一个自定义的 EventTransformer
来改变 Bloc
对输入事件的处理。
通过上面的代码,我们只要添加一点点代码就可以很容易的实现对输入事件的去抖动。
如果你不确定应该用哪一种,先用 Cubit
,后面根据需要你可以再重构或者升级为 Bloc
。