Flutter中BLoC与provider的对比

本文重点分享Flutter中主流状态管理库:BLoC与provider的简单用法和对比

背景

笔者在今年的恶劣行情下,终于勇敢的跳槽了。来到新公司从事自己真心追求的Flutter技术开发。目前手中的项目是一个完整的Flutter项目,正在不断迭代,与我一起共同成长。因此,我会逐步把自己的心得记录下来,与君共享!

前言

作为前端开发者,MVVM开发模式应该不陌生。MVVM模式是目前前端项目中的主流架构。MVVM — — model、view、viewModel。model表示页面状态(即页面绑定的数据)、view表示页面视图、viewModel将model和view进行绑定,并且做业务逻辑(包括网络请求,数据更新、页面视图更新驱动等)的处理。从而实现视图、数据、业务逻辑完全分离,使得项目结构清晰明朗,可维护度高。其中,数据的改变可以看成页面状态的改变,对这些页面状态如何管理的更合理,是MVVM架构的重中之重。本篇文章最主要也是讲解笔者在Flutter中,对状态管理的使用心得

Flutter状态管理讲解

在Flutter中,状态管理早已是老生常谈的问题了。直到Flutter将provider替代provide作为官方推荐的状态管理库,Flutter关于状态管理的争论才开始趋于平静。

那么状态管理为何这么重要呢?这里有一个业务场景可以给大家体会下:

假设服务器每隔十秒通过websocket给APP推送一次数据,数据包含文章内容,同时也包含阅读数、点赞数等;APP有两个页面,A页面显示文章列表,点击列表项进入B页面查看文章详情。每隔十秒服务器的消息到达后,需要实时更新A、B页面的内容。

在Flutter中,该如何实现?你可能想到在每个页面注册一个websocket的接收器,在各个页面收到websocket消息通知的时候,通过setState去更新页面视图;撇开性能不讲,如果有10个页面,就需要定义10个接收器,每个接收器还需要分别处理数据然后setState更新视图。开发效率上已经大打折扣,出错率极高。

在上面的例子中,作为前端开发者肯定会希望只在一个地方接收数据,只要数据一改变,视图就实时更新,无需每个页面都多setState操作。我们把数据接收器定义成更新事件的发布者,每个页面都是一个监听者。发布者发出事件后,监听者马上可以收到,视图马上更新。其实这就是典型的发布订阅模式。显而易见,大部分前端包括Flutter中的状态管理,都是基于发布订阅的设计模式出发出发的。

Flutter中的发布订阅模式,可以直接使用stream流机制。(关于stream的系统学习请见:https://juejin.im/post/6844904163407577096)。

以上面的例子,我们需要一个websocket接收器,接收消息后通过streamController.skin.add发布事件,页面中会注册监听器:streamController.stream.listen,在监听回调中实现setState去更新视图。

事实上,Flutter目前已有的状态管理,如rxdart、BLoC、fluter_redux、provider等,都离不开对stream流进行封装,再加入widget的封装演化出StreamBuilder、BlocBuilder等布局组件,从而达到无需setState就能实时更新视图的效果。(关于Flutter状态管理的演变过程,可见:https://juejin.im/post/6844904035439345671)

进入主题

很感谢大家读到这里,前面花费了大量文字解释并代入Flutter的状态管理,无非就是为了说明状态管理是Flutter项目中不可获取的一部分。这篇文章重点要讲的,就是如何使用、如何选用Flutter状态管理库。目前一般公司最常用的状态管理库是:BLoC和Provider。接下来重点讲解两者的使用和比较。

BLoC

目录结构

BLoC是谷歌提出的一种设计模式,利用流的方式实现界面的异步渲染和重绘,我们可以非常快速的通过BLoC实现业务与界面的分离。一般情况下,我们会在项目中引入flutter_bloc这个库。一个BLoC管理,有三个文件:bloc、event、state;

BLoC目录结构

使用方法

当一个组件需要使用到BLoC状态管理时,需要在调用组件之前,需要声明下BLoC的提供者,具体写法如下:

BlocProvider<BadgesBloc>(

    create: (context) =>BadgesBloc(),

    child: UserPage()

)

当一个页面有多个BLoC提供者,或者整个app有几个通用的BLoC提供者,有多个页面都需要使用,即可提前在加载app之前全局声明。可以使用MultiBlocProvider进行声明,具体写法如下:

MultiBlocProvider(

  providers: [

    BlocProvider<BadgesBloc>(create:(context) => BadgesBloc()),

    BlocProvider(create: (context) =>XXX()),

],

    child: MaterialApp()

)

使用中,页面布局将使用BlocBuilder创建widget,用户在页面中通过BlocProvider.of(context).add()发起事件,

///  布局示例

BlocBuilder<BadgesBloc, BadgesState>(

    // 接收bloc返回的state,视图与state中的变量进行绑定

    builder: (context, state) {

    var isShowBadge = false;

    if (state is BadgesInitialState) {

        isShowBadge = state.unReadNotification;

    }

    return Badge(

        showBadge: isShowBadge,

        shape: BadgeShape.circle,

        position: BadgePosition(top: -3, right: -3),

        child: Icon( Icons.notifications_none, color: Color(0xFFFFFFFF),

        ),

    );

})

/// 页面发起事件

// 发出的重设Badge的事件,事件要求传参为bool

BlocProvider.of<BadgesBloc>(context).add(ResetBadgeEvent(true)); 

此时在bloc中就会接收到事件,判断发起的事件是event中的哪个事件,然后返回对应的state。

@override

Stream<BadgesState> mapEventToState(BadgesEvent event) async* {

    if (event is ResetBadgeEvent) {

        yield BadgesInitialState(event.unReadNotification); 

    } 

}

Stream<BadgesInitialState> _mapGetActivityCountState(isShow) async* {

    // 此处更改状态的值,让上面的视图代码可以根据此值进行更新

     yield BadgesInitialState(isShow);

}

同时需要补上event、state的代码截图

event
state

使用心得

不难得出,BLoC使用起来结构相对复杂,每一个状态则需要三个文件。而且发出事件时,需要用到context。即常理下,只能在build生命周期内发出事件。若是上述案例中的websocket监听中要发出事件,将会显得困难,需要使用全局的context(全局的context可使用GlobalKey<NavigatorState> )。这一点势必会成为BLoC的一个很重大的弊端(后面笔者会有详细解释)。

Provider

Provider是Flutter官方自己维护的,也是官方最为推荐的状态管理库,它的特点是:不复杂、好理解,可控度高。我们会在项目中引入provider这个库,Provider没有像BLoC那样负责的目录结构,下面直接讲解用法。

使用方法

当一个组件需要使用到Provider状态管理时,需要在调用组件之前,需要声明下Provider的提供者,具体写法如下:

ChangeNotifierProvider<LoginViewModel>.value(

    notifier: LoginViewModel(),     

    child:LoginPage(),

)

当一个页面有多个Provider提供者,或者整个app有几个通用的Provider提供者,有多个页面都需要使用,即可提前在加载app之前全局声明。可以使用MultiProvider进行声明,具体写法如下:

MultiProvider(

    providers: [

        ChangeNotifierProvider<LoginViewModel>( create: (_) => LoginViewModel(),),

        ChangeNotifierProvider<HomeViewModel>( create: (_) => HomeViewModel(),),

    ],

    child: MaterialApp()

)

使用中,页面布局需在build中创建一个provider对象,之后直接在widget中绑定viewModel中的数据或者触发事件即可

/// 创建provider对象

var loginVM = Provider.of<LoginViewModel>(context);

Column(

    children: <Widget>[

        new Padding(

            padding: EdgeInsets.only(top: 85),

            child: new Container(

                height: 85.h, width: 486.w,

                child: TextFormField(

                    // 绑定viewModel的数据

                    controller: loginVM.userNameController,

                    decoration: InputDecoration(

                        hintText: "请输入用户名",

                        icon: Icon(Icons.person),

                        hintStyle: TextStyle(color: Colors.grey, fontSize: 24.sp),

            ),

            validator: (value) {

                return value.trim().length > 0 ? null : "必填选项"; }

        )

    )

),

        new Padding(

            padding: EdgeInsets.only(top: 40),

            child: new Container(

                height: 90.h, width: 486.w,

                child: new RaisedButton(

                       // 点击触发viewModel中的方法

                      onPressed: () { loginVM.loginHandel(context)},

            color: const Color(0xff00b4ed), shape: StadiumBorder(),

            child: new Text( "登录",

                    style: new TextStyle(color: Colors.white, fontSize: 32.sp),

            ),

        ),

    )

)]

我们来看看viewModel中的写法,class必须继承ChangeNotifier,当有数据需要更新的时候,调用notifyListeners(),页面就会刷新。

viewModel-1
viewModel-2

使用心得

Provider确实使用起来方便很多。在更新时,无需使用页面手动触发,也就是无需过多的跟context打交道,可以避免BLoC使用全局key的弊端。最重要的一点是,provider非常符合我们对mvvm的一般认识,在build中声明provider后,直接就可以调用provider中的值进行视图渲染。

对比分析

上面简单说明BLoC和Provider两种最常用的状态管理框架的用法。接下来将是这篇文章的重中之重,分析两者的区别:

一、理解难易程度

BLoC是一种设计模式,主要思想是基于Dart Stream流的发布订阅者模式。使用者需要对Stream有较为深刻的理解。另外,在使用上与传统的MVVM模式还是有所差别的。相比之下,Provider就更容易理解,只需在页面中声明一次Provider,即可直接在视图中引用数据,理解起来特别容易。 

二、使用限制

BLoC对上下文context的依赖要更强,BLoC中发布者通过.add发布事件,但发布时必须携带上下文context,视图中通过BlocBuilder作为订阅者,接收到state改变时,rebuild布局。相比之下,Provider的viewModel只需要继承ChangeNotifier,更新时直接notifyListeners(),随时更新,无需绑定上下文。这一点个人认为是provider很大的一个优势。

因为MVVM模式在应用中,无非就是解决视图和数据业务层的耦合。如果在viewModel层还需要依赖context,那么在业务逻辑负责的情况下,势必会更加麻烦。

三、易用程度

这个就无需多说了,Provider完胜BLoC。创建一个BLoC状态管理,需要创建State、Event、Bloc三个文件去管理,Provider一个搞定。

四、 颗粒度掌控

颗粒度即局部刷新的能力,其实这也是状态管理中最头疼的问题。虽然Flutter高效的渲染机制,让我们可以忽略刷新带来的内存问题。

但是追求极致的程序员们,就算我的页面有100个数据,绑定在100个组件上;我改变了一个数据,我就只想页面刷新这一个组件,其他99个不动。

因此就引出一个颗粒度的掌控能力。BLoC中,只要是BlocBuilder包裹下的组件,当State中的任一数据改变,整个组件都会rebuild,这是必然的。为了控制更小的局部刷新,只能不断拆分更细的BLoC

同样,Provider要做到局部刷新,可以使用Consumer控件包裹,(这里跟BlocBuilder是一个原理),也需要拆分更多的viewModel层。在局部刷新这个问题上,其实都是一样不分伯仲,如果一定要比个高低,我觉得BLoC的做法更加直观,反正我BlocBuilder包裹的够细,就越能精致颗粒度。

总结

综上,业界以及个人其实都会比较偏向使用provider作为状态管理的首选库。但BLoC虽然用起来比较负责,但不可否认其在大型项目确实也挺可控;

其实不妨可以结合使用,笔者目前的项目就是结合使用Provider和BLoC,效果也还不错。遇到状态复杂的,就偷偷懒用Provider;一般页面下,就使用BLoC以便更好的把控颗粒度。倒也用的挺香,

总之,选择哪种状态管理没有特定的规律,上手时间、可维护性、开发成本都是需要考虑的主观因素,还是要视团队和具体应用场景而定。如果业务场景只是一个输入框和按钮,过度的模式设计其实就会显得画蛇添足。需要大家理性选择。


希望大家多多指导我!

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