Flutter | 状态管理探索篇——BLoC(三)

前言

Flutter的不少灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。因此在Flutter中,它的Widget是immutable的,而它的动态部分所有放到了状态(State)中。react

在以前的文章中,咱们已经介绍了scoped model与redux两种状态管理方案在flutter中的应用。他们彷佛都还不错,但都仍是美中不足。今天我将介绍Google提出的一种全新的解决方案——BLoC。git

在正式开始介绍前,我但愿您已经阅读并理解了stream的相关知识,后面的内容都基于此。若是您还未了解过dart:stream 的话,我建议您先阅读这篇文章:Dart:什么是Streamgithub

BLoC

为何须要状态管理

咱们一直在找寻强大的状态管理方式。也许你并无想过,flutter自身已经为咱们提供了状态管理,并且你常常都在用到。编程

没错,它就是 Stateful widget。当咱们接触到flutter的时候,首先须要了解的就是有些小部件是有状态的,有些则是无状态的。stateless widgetstateful widgetredux

在stateful widget中,咱们widget的描述信息被放进了State,而stateful widget只是持有一些immutable的数据以及建立它的状态而已。它的全部成员变量都应该是final的,当状态发生变化的时候,咱们须要通知视图从新绘制,这个过程就是setState。网络

这看上去很不错,咱们改变状态的时候setState一下就能够了。 在咱们一开始构建应用的时候,也许很简单,咱们这时候可能并不须要状态管理。app

可是随着功能的增长,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。less

一旦当app的交互变得复杂,setState出现的次数便会显著增长,每次setState都会从新调用build方法,这势必对于性能以及代码的可阅读性带来必定的影响。

能不能不使用setState就能刷新页面呢?如何在多个页面中共享状态?咱们但愿有一种更增强大的方式,来管理咱们的状态。异步

BLoC是什么

BLoC是一种利用reactive programming方式构建应用的方法,这是一个由流构成的彻底异步的世界。 async

  • 用StreamBuilder包裹有状态的部件,streambuilder将会监听一个流
  • 这个流来自于BLoC
  • 有状态小部件中的数据来自于监听的流。
  • 用户交互手势被检测到,产生了事件。例如按了一下按钮。
  • 调用bloc的功能来处理这个事件
  • 在bloc中处理完毕后将会吧最新的数据add进流的sink中
  • StreamBuilder监听到新的数据,产生一个新的snapshot,并从新调用build方法
  • Widget被从新构建

BLoC可以容许咱们完美的分离业务逻辑!不再用考虑何时须要刷新屏幕了,一切交给StreamBuilder和BLoC!和StatefulWidget说拜拜!!

BLoC表明业务逻辑组件(Business Logic Component),由来自Google的两位工程师 Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次展现。点击观看Youtube视频。

Lets do it!

这里咱们以一个最简单的CountApp举例。简单介绍BLoC的用法。该项目完整代码已上传Github

这是一个在不一样页面使用BLoC共享状态信息的app。这两个页面都依赖于一个数字,这个数字会随着咱们按下按钮的次数而增长。

第一步:建立BLoC

咱们这里的要求很简单,仅仅只是输出一个数字而已,而后有一个方法可以让数字加一。因此咱们须要建立一条可以经过int类型数据的流。

import 'dart:async';

class CountBLoC {
 int _count;
 StreamController<int> _countController;

 CountBLoC() {
   _count = 0;
   _countController = StreamController<int>();
 }
 
 Stream<int> get value => _countController.stream;

 increment() {
   _countController.sink.add(++_count);
 }

 dispose() {
   _countController.close();
 }
}
复制代码

为何要使用私有变量“_”

一个应用须要大量开发人员参与,你写的代码也许在几个月以后被另一个开发看到了,这时候假如你的变量没有被保护的话,也许一样是让count++,他会用countController.sink.add(++_count)这种方法,而不是调用 increment方法。

虽然两种方式的效果彻底同样,可是第二种方式将会让咱们的business logic零散的混入其余代码中,提升了代码耦合程度,很是不利于代码的维护以及阅读,因此为了让BLoC彻底分离咱们的业务逻辑,请务必使用私有变量。

第二步:建立BLoC实例

这里有三种方式建立bloc

  • 全局单例建立
  • 局部建立
  • scoped

因为咱们须要在两个屏幕中访问同一个bloc,因此咱们只能选择全局单例模式或者scoped模式。

全局单例模式

全局单例咱们只须要在bloc类的文件中建立一个bloc实例便可。不过我并不推荐这种作法,由于不须要用这个bloc的时候,咱们应该释放它。

可是为了让我解释的尽可能简单,后面我将会基于全局单例模式来介绍。

Scoped模式

建立一个bloc provider类,这里咱们须要借助InheritWidget,实现of方法并让updateShouldNotify返回true。

class BlocProvider extends InheritedWidget {
  CountBLoC bLoC = CountBLoC();

  BlocProvider({Key key, Widget child}) : super(key: key, child: child);

  @override
  bool updateShouldNotify(_) => true;

  static CountBLoC of(BuildContext context) =>
      (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bLoC;
}
复制代码

小提示: 这里updateShouldNotify须要传入一个InheritedWidget oldWidget,可是咱们强制返回true,因此传一个“_”占位。

第三步:在页面中使用StreamBuilder

这里以第一个页面为例,仅仅显示文字+数字。

StreamBuilder<int>(
            stream: bloc.value,
            initialData: 0,
            builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
              return Text(
                'You hit me: ${snapshot.data} times',
                style: Theme.of(context).textTheme.display1,
              );
            })
复制代码
  • StreamBuilder中stream参数表明了这个stream builder监听的流,咱们这里监听的是countBloc的value(它是一个stream)。
  • initData表明初始的值,由于当这个控件首次渲染的时候,还未与用户产生交互,也就不会有事件从流中流出。因此须要给首次渲染一个初始值。
  • builder函数接收一个位置参数BuildContext 以及一个snapshot。snapshot就是这个流输出的数据的一个快照。咱们能够经过snapshot.data访问快照中的数据。也能够经过snapshot.hasError判断是否有异常,并经过snapshot.error获取这个异常。
  • StreamBuilder中的builder是一个AsyncWidgetBuilder,它可以异步构建widget,当检测到有数据从流中流出时,将会从新构建。

在第二个页面中调用increment

floatingActionButton: FloatingActionButton(
          onPressed: ()=> bloc.increment(),
          child: Icon(Icons.add),
      )
复制代码

因为这里并不涉及widget的重构,咱们只须要调用bloc的功能便可。

处理广播流

咱们构建好ui后,运行程序将会发现这件奇怪的事。

第二个页面的数字没法显示,并且控制台抛出了这个异常。

flutter: Bad state: Stream has already been listened to.
复制代码

这是因为流被重复监听致使的。 两个页面中都须要显示这个数字,那么就使用了两个StreamBuilder。而StreamBuilder都监听的同一个流,因此致使了流被重复监听了。

还记得咱们在Dart|什么是Stream中说的两种流吗。没错,咱们建立StreamController的时候,默认是建立的单订阅流。因此咱们须要将流改为广播流。

_countController = StreamController.broadcast<int>();
复制代码

只须要在建立StreamController的时候调用broadcast方法便可。

来看看效果

可是咱们这里还有一个小问题, 你发现了吗

Q&A

为何第二次进入UnderPage的时候,计数器显示为0,按了一下才好

这是因为咱们在第一次pop UnderPage的时候,这个页面已经被销毁了。当咱们再push进去的时候,StreamBuilder没法收听到最后一次事件(已经流过去了),只能显示initiaData。而再次点击时,正确的数字被add进了流,StreamController收听到了它,因此又能显示正确的数据了。

这个问题可以解决吗?

答案是确定的,使用rxdart!rxdart极大的加强了流的功能,解决方法将会在后续rxdart篇介绍。

大型应用中应该如何组织BLoC

大型应用程序须要多个BLoC。一个好的模式是为每一个屏幕使用一个顶级组件,并为每一个复杂足够的小部件使用一个。可是,太多的BLoC会变得很麻烦。此外,若是您的应用中有数百个可观察量(流),则会对性能产生负面影响。换句话说:不要过分设计你的应用程序。

——Filip Hracek

一个更加复杂的app

filip提供了一个 更复杂的BLoC样本。他将购物应用程序从新建立为一个更现实的例子,其中产品目录逐页从网络中获取,咱们有无限的这些产品列表。此外,对于目录中的每一个产品,咱们但愿在产品已在目录中时稍微更改ProductSquare的显示。

了解更多

下面有一些优秀的文章可以给您更多参考

写在最后

本次所用到的代码已经上传Github:github.com/OpenFlutter…

bloc是一个优秀的状态管理方式,它可以帮助咱们更好的构建复杂的大型应用。可是他还不是完美的(至少目前不是)。它在处理大量异步事件以及分离业务逻辑上表现很优秀,可是在共享状态上还有一些缺陷。

有人尝试将redux与bloc结合使用,试图找到突破口。这里有一个专门为它编写的库:rebloc。感兴趣的朋友能够自行了解一下。

若是你在使用bloc进行状态管理的时候有任何好的点子,或者是疑问,欢迎在下方评论区以及个人邮箱1652219550a@gmail.com留言,我会在24小时内与您联系!

下一篇文章将会为你们介绍Reactive Programming的最佳库RxDart在BLoC上的实践,敬请期待。