前言

  在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. 测试window安装的客户端

    1.win10 安装了客户端,测试一下,

  2. 2020/12月最新WinSpy/WinSpy++下载exe

    >>>下载地址 https://wws.lanzous.com/iFUsVj931xa 密码:5hp7 解压密码:yunmuq 夹带私货:在这里希望大家分享文件别再用百度云了,不用百 ...

  3. 阿里云OSS生成sts令牌

    业务场景: 如果前端直接上传文件到OSS,势必要暴露令牌,无法精准控制上传内容等,使用临时令牌即可解决这个问题. 先去阿里云后台设置好token,角色,地区等 pom.xml <dependen ...

  4. windows 远程连接报错

    在windows7上或者windows10上远程连接服务器报错("连接错误"),试了网上的方法,发现是服务器安装ssl证书关闭了ssh服务,开启ssh服务后,重启电脑就可以解决这个 ...

  5. c++笔试题3

    一.[阿里C++面试题]1.如何初始化一个指针数组.答案: 错题解析:首先明确一个概念,就是指向数组的指针,和存放指针的数组. 指向数组的指针:char (*array)[5];含义是一个指向存放5个 ...

  6. centos7下安装iostat命令

    [root@node01 yum.repos.d]# yum intall -y sysstat Loaded plugins: fastestmirror No such command: inta ...

  7. Mysql8.0新特性【详细版本】

    1.  账户与安全 用户创建与授权 之前:创建用户并授权 1 grant all privileges on *.* to 'myuser'@'%' identified by '3edc#EDC'; ...

  8. Java 8 Lambda表达式-接口实现

    Java 8 Lambda表达式在只有一个方法的接口实现代码编写中,可以起到简化作用: (argument list) -> body 具体看Runnable接口的例子 public class ...

  9. js上 初识JavaScript

    1.JavaScript简介 **JavaScript ** 是什么?(重点) Js是一种专门为网页交互设计的客户端(浏览器端)的脚本语言: Js与html和css有相似之处,都在浏览器端解析: Js ...

  10. go-slice实现的使用和基本原理

    目录 摘要 Slice数据结构 使用make创建Slice 使用数组创建Slice Slice 扩容 Slice Copy 特殊切片 总结 参考 你的鼓励也是我创作的动力 Posted by 微博@Y ...