本文重点分享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的提供者,具体写法如下:
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的代码截图
使用心得
不难得出,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(),页面就会刷新。
使用心得
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以便更好的把控颗粒度。倒也用的挺香,
总之,选择哪种状态管理没有特定的规律,上手时间、可维护性、开发成本都是需要考虑的主观因素,还是要视团队和具体应用场景而定。如果业务场景只是一个输入框和按钮,过度的模式设计其实就会显得画蛇添足。需要大家理性选择。
希望大家多多指导我!