前言

  在Flutter开发中,状态管理是一个永恒的话题。

  一般的原则是:如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理。

  对于组件私有的状态管理很好理解,但对于跨组件共享的状态,管理的方式就比较多了,如使用全局事件总线EventBus,它是一个观察者模式的实现,通过它就可以实现跨组件状态同步:状态持有方(发布者)负责更新、发布状态,状态使用方(观察者)监听状态改变事件来执行一些操作。

  但是观察者模式来实现跨组件状态共享有一些明显的缺点:

  1. 必须显示定义各种事件,不好管理;
  2. 订阅者必须显式注册状态改变回调,也必须在组件销毁时手动去解绑回调以避免内存泄漏;

  是否还有更好的跨组件状态管理方式了?

  我们知道,InheritedWidget能绑定与它依赖的子孙组件的依赖关系,并且当InheritedWidget数据发生变化时,可以自动更新依赖的子孙组件。基于此,可以将需要跨组件共享的状态保存在InheritedWidget中,然后在子组件中引用InheritedWidget即可。Flutter社区的Provider包就是基于这个思想实现的。

Provider

  基于上面的思想,实现一个最小功能的Provider。

定义一个保存共享数据的类

  为了通用性,使用泛型。

class InheritedProvider<T> extends InheritedWidget{
InheritedProvider({@required this.data, Widget child}): super(child: child); // 共享状态使用泛型
final T data; bool updateShouldNotify(InheritedProvider<T> old){
// 返回true,则每次更新都会调用依赖其的子孙节点的didChangeDependencies
return true;
}
}

数据发生变化时如何重构InheritedProvider

  存在两个问题:

  1. 数据发生变化怎么通知?
  2. 谁来重新构建InheritedProvider?

  对于第一个问题,可以使用之前介绍的eventBus来进行事件通知,但是为了更贴近Flutter开发,使用Flutter中SDK中提供的ChangeNotifier类 ,它继承自Listenable,也实现了一个Flutter风格的发布者-订阅者模式,可以通过调用addListener()和removeListener()来添加、移除监听器(订阅者);通过调用notifyListeners() 可以触发所有监听器回调。

  对于第二个问题,将要共享的状态放到一个Model类中,然后让它继承自ChangeNotifier,这样当共享的状态改变时,我们只需要调用notifyListeners() 来通知订阅者,然后由订阅者来重新构建InheritedProvider。

// 该方法用于Dart获取模板类型
Type _typeOf<T>() => T; class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget{
ChangeNotifierProvider({
Key key,
this.data,
this.child,
}); final Widget child;
final T data; // 定义一个便捷方法,方便子树中的widget获取共享数据
// static T of<T>(BuildContext context){
// final type = _typeOf<InheritedProvider<T>>();
// final provider = context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>;
// return provider.data;
// } // 替换上面的便捷方法,按需求是否注册依赖关系
static T of<T>(BuildContext context, {bool listen = true}){
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget as InheritedProvider<T>;
return provider.data;
} _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>(); } // _ChangeNotifierProviderState类的主要作用就是监听到共享状态(model)改变时重新构建Widget树。
// 注意,在_ChangeNotifierProviderState类中调用setState()方法,widget.child始终是同一个,
// 所以执行build时,InheritedProvider的child引用的始终是同一个子widget,
// 所以widget.child并不会重新build,这也就相当于对child进行了缓存!当然如果ChangeNotifierProvider父级Widget重新build时,则其传入的child便有可能会发生变化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
void update(){
// 如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
setState(() => {});
} @override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
// 当Provider更新时,如果新旧数据不相等,则解绑旧数据监听,同时添加新数据监听
if(widget.data != oldWidget.data){
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
} @override
void initState(){
// 给model添加监听器
widget.data.addListener(update);
super.initState();
} @override
void dispose(){
// 移除model的监听器
widget.data.removeListener(update);
super.dispose();
} @override
Widget build(BuildContext context){
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
} }

  可以看到_ChangeNotifierProviderState类的主要作用就是监听到共享状态(model)改变时重新构建Widget树。注意,在_ChangeNotifierProviderState类中调用setState()方法,widget.child始终是同一个,所以执行build时,InheritedProvider的child引用的始终是同一个子widget,所以widget.child并不会重新build,这也就相当于对child进行了缓存!当然如果ChangeNotifierProvider父级Widget重新build时,则其传入的child便有可能会发生变化。

代码示例

// 跨组件状态共享(Provider)

// 一个通用的InheritedWidget,保存任需要跨组件共享的状态
import 'dart:collection'; import 'package:flutter/material.dart'; class InheritedProvider<T> extends InheritedWidget{
InheritedProvider({@required this.data, Widget child}): super(child: child); // 共享状态使用泛型
final T data; bool updateShouldNotify(InheritedProvider<T> old){
// 返回true,则每次更新都会调用依赖其的子孙节点的didChangeDependencies
return true;
}
} // 该方法用于Dart获取模板类型
Type _typeOf<T>() => T; class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget{
ChangeNotifierProvider({
Key key,
this.data,
this.child,
}); final Widget child;
final T data; // 定义一个便捷方法,方便子树中的widget获取共享数据
// static T of<T>(BuildContext context){
// final type = _typeOf<InheritedProvider<T>>();
// final provider = context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>;
// return provider.data;
// } // 替换上面的便捷方法,按需求是否注册依赖关系
static T of<T>(BuildContext context, {bool listen = true}){
final type = _typeOf<InheritedProvider<T>>();
final provider = listen
? context.inheritFromWidgetOfExactType(type) as InheritedProvider<T>
: context.ancestorInheritedElementForWidgetOfExactType(type)?.widget as InheritedProvider<T>;
return provider.data;
} _ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>(); } // _ChangeNotifierProviderState类的主要作用就是监听到共享状态(model)改变时重新构建Widget树。
// 注意,在_ChangeNotifierProviderState类中调用setState()方法,widget.child始终是同一个,
// 所以执行build时,InheritedProvider的child引用的始终是同一个子widget,
// 所以widget.child并不会重新build,这也就相当于对child进行了缓存!当然如果ChangeNotifierProvider父级Widget重新build时,则其传入的child便有可能会发生变化。
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{
void update(){
// 如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
setState(() => {});
} @override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget){
// 当Provider更新时,如果新旧数据不相等,则解绑旧数据监听,同时添加新数据监听
if(widget.data != oldWidget.data){
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
} @override
void initState(){
// 给model添加监听器
widget.data.addListener(update);
super.initState();
} @override
void dispose(){
// 移除model的监听器
widget.data.removeListener(update);
super.dispose();
} @override
Widget build(BuildContext context){
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
} } // 购物车示例:实现一个显示购物车中所有商品总价的功能 // 用于表示商品信息
class Item{
// 商品单价
double price;
// 商品份数
int count; Item(this.price, this.count); } // 保存购物车内商品数据,跨组件共享
class CartModel extends ChangeNotifier{
// 用于保存购物车中商品列表
final List<Item> _items = []; // 禁止改变购物车里的商品信息
UnmodifiableListView<Item> get items => UnmodifiableListView(_items); // 购物车中商品的总价
double get totalPrice => _items.fold(0, (value, item) => value + item.count * item.price); // 将[item]添加到购物车,这是唯一一种能从外部改变购物车的方法
void add(Item item) {
_items.add(item);
// 通知监听者(订阅者),重新构建InheritedProvider,更新状态
notifyListeners();
} } // 优化
// 一个便捷类,封装一个Consumer的Widget
class Consumer<T> extends StatelessWidget{
final Widget child;
final Widget Function(BuildContext context, T value) builder; Consumer({
Key key,
@required this.builder,
this.child,
}): assert(builder != null),
super(key: key); Widget build(BuildContext context){
return builder(
context,
// 自动获取Model
ChangeNotifierProvider.of<T>(context),
);
}
} // 页面
class ProviderRoute extends StatefulWidget{
_ProviderRouteState createState() => _ProviderRouteState();
} class _ProviderRouteState extends State<ProviderRoute>{
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('跨组件状态共享(Provider)'),
),
body: Center(
child: ChangeNotifierProvider<CartModel>(
data: CartModel(),
child: Builder(builder: (context){
return Column(
children: <Widget>[
// Builder(builder: (context){
// var cart = ChangeNotifierProvider.of<CartModel>(context);
// return Text("总价:${cart.totalPrice}");
// },), // 进行优化,替换上面Builder
Consumer<CartModel>(
builder: (context, cart) => Text("总价:${cart.totalPrice}"),
), Builder(builder: (context){
// 控制台打印出这句,说明按钮在每次点击时其自身都会重新build!
print("RaisedButton build");
return RaisedButton(
child: Text("添加商品"),
onPressed: (){
// 给购物车中添加商品,添加后总价会更新
// ChangeNotifierProvider.of<CartModel>(context).add(Item(20.0, 1));
// listen设为false,不建立依赖关系,因为按钮不需要每次重新build
ChangeNotifierProvider.of<CartModel>(context, listen: false).add(Item(20.0, 1));
},
);
},)
],
);
},),
),
),
);
}
}

代码优化

  两个地方可以进行代码优化,详细看代码示例。

总结

Provider原理图



  Model变化后会自动通知ChangeNotifierProvider(订阅者),ChangeNotifierProvider内部会重新构建InheritedWidget,而依赖该InheritedWidget的子孙Widget就会更新。

  可以发现使用Provider,将会带来如下收益:

  1. 业务代码更关注数据了,只要更新Model,则UI会自动更新,而不用在状态改变后再去手动调用setState()来显式更新页面。
  2. 数据改变的消息传递被屏蔽了,我们无需手动去处理状态改变事件的发布和订阅了,这一切都被封装在Provider中了。这真的很棒,帮我们省掉了大量的工作!
  3. 在大型复杂应用中,尤其是需要全局共享的状态非常多时,使用Provider将会大大简化我们的代码逻辑,降低出错的概率,提高开发效率。

其他状态管理包

  Provider & Scoped Model、Redux、MobX、BLoC,这里特别推荐阿里咸鱼团队推出的开源项目:fish redux。

【Flutter】功能型组件之跨组件状态共享的更多相关文章

  1. Vue兄弟组件(非父子组件)状态共享与传值

      前言:网上大部分文章写的有点乱,很少有讲得易懂的文章. 所以,我写了篇在我能看得懂的基础上又照顾到大家的文章 =.= 作者:X1aoYE 备注:此文原创,转载请注明~  内容里的<br> ...

  2. vue任意关系组件通信与跨组件监听状态 vue-communication

    大家好!我是木瓜太香! 众所周知,组件式开发方式给我们带来了方便,不过也引入了新的问题,组件之间的数据就像被一道无形的墙隔开,如果我们希望临时让两个组件直接通信,vuex 太巨,而 $emit 又不好 ...

  3. vue组件之间的传值——中央事件总线与跨组件之间的通信($attrs、$listeners)

    vue组件之间的通信有很多种方式,最常用到的就是父子组件之间的传值,但是当项目工程比较大的时候,就会出现兄弟组件之间的传值,跨级组件之间的传值.不可否认,这些都可以类似父子组件一级一级的转换传递,但是 ...

  4. react context跨组件传递信息

    从腾讯课堂看到的一则跨组件传递数据的方法,贴代码: 使用步骤: 1.在产生参数的最顶级组建中,使用childContextTypes静态属性来定义需要放入全局参数的类型 2.在父组件中,提供状态,管理 ...

  5. python 全栈开发,Day100(restful 接口,DRF组件,DRF跨域(cors组件))

    昨日内容回顾 1. 为什么要做前后端分离? - 前后端交给不同的人来编写,职责划分明确.方便快速开发 - 针对pc,手机,ipad,微信,支付宝... 使用同一个接口 2. 简述http协议? - 基 ...

  6. React-Native子组件修改父组件的几种方式,兄弟组件状态修改(转载)

    子组件修改父组件的状态,在开发中非常常见,下面列举了几种方式.DeviceEventEmitter可以跨组件,跨页面进行数据传递,还有一些状态的修改.http://www.jianshu.com/p/ ...

  7. Context - React跨组件访问数据的利器

    Context提供了一种跨组件访问数据的方法.它无需在组件树间逐层传递属性,也可以方便的访问其他组件的数据 在经典的React应用中,数据是父组件通过props向子组件传递的.但是在某些特定场合,有些 ...

  8. Flutter学习笔记(16)--Scaffold脚手架、AppBar组件、BottomNavigationBar组件

    如需转载,请注明出处:Flutter学习笔记(15)--MaterialApp应用组件及routes路由详解 今天的内容是Scaffold脚手架.AppBar组件.BottomNavigationBa ...

  9. Flutter 目录结构介绍、入口、自定义 Widget、MaterialApp 组件、Scaffold 组件

    Flutter 目录结构介绍 文件夹 作用 android android 平台相关代码 ios ios 平台相关代码 lib flutter 相关代码,我们主要编写的代 码就在这个文件夹 test ...

随机推荐

  1. 【题解】「UVA1149」装箱 Bin Packing

    做法显然:贪心,但是怎么贪? 首先从大到小或从小到大排序,窝这次是从大到小排,这样更容易理解(从小到大更方变) 然后设置两个变量 front 和 after 作为前指针和后指针. 循环判断: 当前后两 ...

  2. 一个实现浏览器网页与本地程序之间进行双向调用的轻量级、强兼容、可扩展的插件开发平台—PluginOK中间件

    通过PluginOK中间件插件平台(原名本网通WebRunLocal)可实现在网页中的JavaScript脚本无障碍访问本地电脑的硬件.调用本地系统的API及相关组件,同时可彻底解决ActiveX组件 ...

  3. 一、eclipse配置TestNG

    eclipse配置TestNG可以通过eclipse直接下载,但我没有vpn,所以使用线下配置. 1-下载TestNG的配置文件,有两个文件 features 和 plugins 2-eclipse配 ...

  4. Consul 多数据中心下的服务注册发现与配置共享

    1. Consul简介   Consul是HashiCorp公司推出的开源软件,它提供了一套分布式高可用可横向扩展的解决方案,能为微服务提供服务治理.健康检查.配置共享等能力.   Eurake2.x ...

  5. Taro 周报 #7: 收获「e代驾」案例,发布 v2.2.16 和 v3.2.0-canary.2

    Taro 周报 2020 年 12 月 05 日 - 2020 年 12 月 12 日 ,更多的Taro周报点击 Taro 大事件 58 技术发布文章<开源 | Taro 3 支持 React ...

  6. 从面试角度分析LinkedList源码

    注:本系列文章中用到的jdk版本均为java8 LinkedList类图如下: LinkedList底层是由双向链表实现的.链表好比火车,每节车厢包含了车厢和连接下一节车厢的连接点.而双向链表的每个节 ...

  7. react第十一单元(受控组件和非受控组件-实现类似于vue双向绑定的功能)

    第十一单元(受控组件和非受控组件-实现类似于vue双向绑定的功能) #课程目标 理解因为react的单向数据流 理解表单组件会因为react数据流变的不好维护 理解受控组件与非受控组件的实质区别 理解 ...

  8. CTF练习三 —— 命令注入&命令执行绕过

    这个题是第四届强网杯也就是2020.8.22号开始的那场一道简单的命令注入题,再这之前我并没有学习过命令注之类的知识,,,看到题之后先搜在学,,误打误撞解了出来,过段时间wp就会放出来,所以这里就不对 ...

  9. 注解 @CrossOrigin

    在Controller中看到@CrossOrigin ,这是什么?有什么用?为什么要用? what? @CrossOrigin是用来处理跨域请求的注解 先来说一下什么是跨域: (站在巨人的肩膀上) 跨 ...

  10. Vue必须必须要注意的几个细节

    1.每次执行完,尽量npm run dev 一次,有时候又缓存问题 2.安装sass 一.使用save会在package.json中自动添加.因为sass-loader依赖于node-sass npm ...