Flutter从2018年底首次在谷歌开发者大会上亮相至今已3年多,其发展也算如火如荼。中小企业中大受欢迎,大厂也相继投入技术研究。 但依然有不少开发者疑惑于为自己的项目要选择哪个状态管理框架,今天笔者将对社区内相对火热的状态管理库(Provider、BLoC、GetX
)做一个技术分析和对比,帮助大家更好地为项目找到合适的状态管理库。
我们在开发过程中,为了提高项目的可维护度和性能,也为了让页面UI跟数据(本地或服务端数据)有效分离的同时又能有效同步,都会让项目保持清晰的目录结构、同时启用状态管理库。
而MVVM模式已然成为前端项目中的主流架构。
MVVM 即 mode + view + viewModel
通过MVVM可以实现视图、数据、业务逻辑完全分离,使项目数据流向清晰明朗,提高性能,提高可维护度。
用户对页面的操作触发数据的处理,数据的变动驱动页面UI的刷新。所以单一数据源
和单向数据流
是做好状态管理的关键。
Flutter中的状态管理库,基本也都遵循MVVM原则,所以在遵循这个原则的基础上,如何使得状态管理性能更好且易于使用,是这些库的设计宗旨。本篇文章我主要对比以下三个库:Provider、BLoC、GetX
。
在Flutter中,状态管理一直是老生常谈的问题。直到Flutter将Provider替代Provide作为官方推荐的状态管理库,Flutter关于状态管理的争论才开始趋于平静,但2021年GetX异军突起,又让众多初学者开始争论究竟使用哪个库来做状态管理。
那么状态管理为何这么重要呢?这里有一个业务场景可以给大家体会下:
假设服务器每隔十秒通过websocket给APP推送一次数据,数据包含文章内容,同时也包含阅读数、点赞数。APP有两个页面,A页面显示文章列表,点击列表项进入B页面查看文章详情。每隔十秒服务器的消息到达后,需要实时更新A、B页面的内容。
Flutter中的发布订阅模式,可以使用stream流机制。stream系统学习
以上面的需求为例:
1. 我们需要一个websocket接收器,收到消息后通过streamController.skin.add发布事件;
2. 页面中stream注册监听器streamController.stream.listen,在监听回调中通过setState刷新视图。
事实上,Flutter目前已有的状态管理,如rxdart、BLoC、fluter_redux、provider、GetX等,都离不开对stream流进行封装,再加上对Flutter InheritedWidget的封装演化出StreamBuilder、BlocBuilder等布局组件,从而达到无需setState就能实时更新视图的效果。Flutter状态管理的演变
BLoC是谷歌提出的一种设计模式,利用stream流的方式实现界面的异步渲染和重绘,我们可以非常顺利的通过BLoC实现业务与界面的分离。一般情况下,我们会在项目中引入flutter_bloc这个库。
一个BLoC状态管理,通常会有三个文件:bloc、event、state
当一个组件需要使用到BLoC状态管理时,需要在调用组件之前,需要声明下BLoC的提供者,具体写法如下:
BlocProvider(create: (context)=> BadgesBloc(),child:UserPage());
当页面有多个BLoC提供者,或者整个App通用的BLoC提供者,即可提前在加载App之前全局声明。可以使用MultiBlocProvider进行声明,具体写法如下:
MultiBlocProvider(
providers: [
BlocProvider(create:(context) => BadgesBloc()),
BlocProvider(create: (context) =>XXX()),
],
child: MaterialApp(),
)
页面布局将使用BlocBuilder创建widget,用户在页面中通过BlocProvider.of(context).add()发起事件
/// 布局示例
BlocBuilder(
// 接收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(context).add(ResetBadgeEvent(true));
此时在bloc中就会接收到事件,判断发起的事件是event中的哪个事件,然后返回对应的state,具体写法如下:
@override
Stream mapEventToState(BadgesEvent event) async* {
if (event is ResetBadgeEvent) {
yield BadgesInitialState(event.unReadNotification);
}
}
Stream _mapGetActivityCountState(isShow) async* {
// 此处更改状态的值,让上面的视图代码可以根据此值进行更新
yield BadgesInitialState(isShow);
}
补上event、state的代码截图
BLoC的目录结构清晰
,完全符合mvvm的习惯。对于工程化项目来说会比较受欢迎,,团队协作起来会减少出错的概率,大家都跟着一个模式去做,维护性也提高了;业务流清晰
。使用dart stream事件流作为基础原理,event和state都是事件驱动的,用户行为触发事件,事件处理完推出状态流,稳定的数据流向往往能提高代码的可靠性;BLoC使用起来相对复杂
,需要创建多个文件。虽然官方引入了cubit,把event组合到bloc文件中,但强烈的结构化依然让不少初学者难以入门;颗粒度的把控相对困难。
通过BlocBuilder构建的视图,在state变更时,视图都会rebuild,想要控制颗粒度只能把bloc再拆细,这会极大的增加代码复杂度和工作量;不过这个问题可通过引入freezed生成代码,然后通过buildWhen等方式减少视图刷新的频次。Provider是Flutter官方开发维护的,也是近些年官方最为推荐的状态管理库。Provider
是建立在 InheretedWidget
之上做了封装,大大减少了我们需要编写的代码量。其特点是:不复杂、好理解,可控度高。我们会在项目中引入provider这个库。
ChangeNotifierProvider.value(
notifier: LoginViewModel(),
child:LoginPage(),
)
MultiProvider(
providers: [
ChangeNotifierProvider( create: (_) => LoginViewModel(),),
ChangeNotifierProvider( create: (_) => HomeViewModel(),),
],
child: MaterialApp()
)
/// 创建provider对象
var loginVM = Provider.of(context);
Column(
children: [
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),
),
),
)
)]
Provider
直接继承于InheritedProvider
,通过工厂构造函数Provider.value
传入model和child节点,然后通过context.dependOnInheritedWidgetOfExactType<_InheritedProviderScope>();
对值进行监听。InheritedWidget
的,所以Provider的实现真的很简单,有用过InheritedWidget
的小伙伴可以去看下源码。使用简单
。model继承ChangeNotifier,没有更多的布局widget,只需要通过context.read / context.watch操作或者监听model即可;颗粒度把控简单
。为了解决widget重新build太频繁的问题,官方推出了context.select
来监听对象的部分属性。也可使用Consumer/Selector进行布局;基于官方InheritedWidget的封装
,不存在任何风险,很稳定且不会给性能方面加负担context强关联
,有Flutter开发经验的都知道,context大多时候基本都是在widget中才能获取到,在其他地方想随时获取 BuildContext
是不切实际的,也就意味着大多时候只能在view层去获取到Provider提供的信息。GetX是一个轻量级且强大的状态管理库,这个库试图完成很多工作,它不仅支持状态管理,也支持路由、国际化、Theme等一大堆功能。GetX在Flutter状态管理中绝对算是异军突起,一经发布就因其简单且全面的优势,引得一大批簇拥者;我并未认真研究GetX,但简单接触后我个人并不喜欢,这种全家桶式的库会让我们的项目相对局限,同时让项目开发者处于没有进步且被动的局面。
我们直接使用 GetX 演示官方example"计数器",
void main() => runApp(GetMaterialApp(home: Home()));
class Controller extends GetxController{
var count = 0.obs;
increment() => count++;
}
class Home extends StatelessWidget {
@override
Widget build(context) {
// 使用Get.put()实例化你的类,使其对当下的所有子路由可用。
final Controller c = Get.put(Controller());
return Scaffold(
// 使用Obx(()=>每当改变计数时,就更新Text()。
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),
// 用一个简单的Get.to()即可代替Navigator.push那8行,无需上下文!
body: Center(child: ElevatedButton(
child: Text("Go to Other"), onPressed: () => Get.to(Other()))),
floatingActionButton:
FloatingActionButton(child: Icon(Icons.add), onPressed: c.increment));
}
}
class Other extends StatelessWidget {
// 你可以让Get找到一个正在被其他页面使用的Controller,并将它返回给你。
final Controller c = Get.find();
@override
Widget build(context){
// 访问更新后的计数变量
return Scaffold(body: Center(child: Text("${c.count}")));
}
}
可以看出确实使用非常的简单,而且已经不太遵循MVC和MVVM结构了,但影响不大,能高效的开发才是国内团队最关心的问题。更多详情见GetX readme!
实现原理这块,我只简单解析这三点:① 如何做到数据驱动;② 如何管理路由;③ 脱离了context后,资源该如何回收。
RxNotifier
,而RxNotifier
with NotifyManager
,NotifyManager
就是提供streamSubscription的扩展类;class RxNotifier = RxInterface with NotifyManager;
mixin NotifyManager {
// 注释:通过GetStream提供onListen;onPause;onResume的回调
GetStream subject = GetStream();
// 注释:Map对象,后续通过key-value键值对进行通知
final _subscriptions = >{};
bool get canUpdate => _subscriptions.isNotEmpty;
// 注释:内部方法,订阅内部流的更改
void addListener(GetStream rxGetx) {
if (!_subscriptions.containsKey(rxGetx)) {
final subs = rxGetx.listen((data) {
if (!subject.isClosed) subject.add(data);
});
final listSubscriptions =
_subscriptions[rxGetx] ??= [];
// 发出通知
listSubscriptions.add(subs);
}
}
StreamSubscription listen(
void Function(T) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) =>
subject.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError ?? false,
);
/// 注释:关闭订阅,释放资源
void close() {
_subscriptions.forEach((getStream, _subscriptions) {
for (final subscription in _subscriptions) {
subscription.cancel();
}
});
_subscriptions.clear();
subject.close();
}
}
NavigatorState
还是调用了pushNamed
;Future? toNamed(
String page, {
dynamic arguments,
int? id,
bool preventDuplicates = true,
Map? parameters,
}) {
if (preventDuplicates && page == currentRoute) {
return null;
}
if (parameters != null) {
final uri = Uri(path: page, queryParameters: parameters);
page = uri.toString();
}
// 注释:global(id).currentState的就是GetMaterialApp.router中的navigatorKey
return global(id).currentState?.pushNamed(
page,
arguments: arguments,
);
}
通过widget的dispose生命钩子调用close,从而释放资源。
@override
void dispose() {
if (widget.dispose != null) widget.dispose!(this);
if (_isCreator! || widget.assignId) {
if (widget.autoRemove && GetInstance().isRegistered(tag: widget.tag)) {
GetInstance().delete(tag: widget.tag);
}
}
_subs.cancel();
// 注释:在这里释放资源
_observer.close();
controller = null;
_isCreator = null;
super.dispose();
}
使用最简单
,用起来确实很简单,极易上手;脱离context,随时随地想用就用,解决了BLoC和Provider的痛点;全家桶式功能
,使用GetX后,我们无需再单独去做路由管理、国际化、主题、全局context等,甚至还支持服务端开发;让项目极度依赖GetX
,而在Flutter更新迭代这么快的情况下,谁也不敢保证GetX全家桶的更新节奏,一旦更新慢了,开发者只能等GetX(当然能够参与社区开源的另当别论)。另外,GetX的使用真的太基础了,让初学者易上手的同时,技术也容易停留于表面
。除了上诉的几种方案,还有其他的库,如redux
/ fish_redux
/ RiverPod
,这些库有的过于复杂,有的刚出不久,笔者调研过程中有留意但并没有用过,活跃度确实也没有上面方案多。
总之,BLoC适合相对大的工程化项目团队使用,架构清晰;Provider很纯粹,也很好用;GetX全家桶一把梭,极度适合新手开发者…
大家在正式学习了flutter之后就能够深入体会,flutter学习并不算难,难得是需要具有一个清晰、系统的思维,为了帮助大家更好的理解flutter,我给大家准备了一份《Flutter进阶学习笔记》,相信大家能在它的帮助下快速掌握flutter的知识,有需要的朋友可以扫码自取。