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

前言

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

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

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

BLoC

为什么需要状态管理

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

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

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

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

image

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

image

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

能不能不使用setState就能刷新页面呢?如何在多个页面中共享状态?我们希望有一种更加强大的方式,来管理我们的状态。

BLoC是什么

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


image
  • 用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。这两个页面都依赖于一个数字,这个数字会随着我们按下按钮的次数而增加。


image

第一步:创建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后,运行程序将会发现这件奇怪的事。


image

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

flutter: Bad state: Stream has already been listened to.

这是由于流被重复监听导致的。 两个页面中都需要显示这个数字,那么就使用了两个StreamBuilder。而StreamBuilder都监听的同一个流,所以导致了流被重复监听了。

还记得我们在Dart|什么是Stream中说的两种流吗。没错,我们创建StreamController的时候,默认是创建的单订阅流。所以我们需要将流改成广播流。

    _countController = StreamController.broadcast<int>();

只需要在创建StreamController的时候调用broadcast方法即可。

来看看效果

image

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

Q&A

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

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

这个问题能够解决吗?

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

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

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

——Filip Hracek

一个更加复杂的app

image

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

了解更多

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

写在最后

本次所用到的代码已经上传Github:https://github.com/OpenFlutter/Flutter-Notebook/tree/master/mecury_project/example/bloc_demo

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 前言 Flutter的很多灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。所以在Flutt...
    Vadaski阅读 30,951评论 19 47
  • 壹 暑假我们一家去云南游玩一趟,最喜欢那边宜人的气候,清晨站在窗前,温柔的风轻轻拂过,微凉如水,窗外不远处是烟雾缭...
    玉米婶阅读 2,896评论 22 33
  • 他告诉我 他是笼中鸟而我是林中鸟,他渴望和我一样翱翔在蓝天上.于是结伴而行.很不巧,旅途中出现了意外,当只剩下...
    昕xin阅读 285评论 0 0
  • 邻居的孩子要上大学了,问我每月要给孩子多少生活费合适?每月给孩子的现在不仅仅是生活费,还包含其他各种消费。当然,生...
    mimi播报阅读 288评论 1 5