一:InheritedWidget是什么
InheritedWidget是flutter中非常重要的功能性组件,它提供了一种在widget树从上到下共享数据的方式。比如在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子Widget中来获取该共享的数据。
如上图所示,比起普通的widget,逐级传递数据来看,InheritedWidget可以实现子控件跨级传递数据。这个特性在一些需要在整个widget树中共享数据的场景中非常方便,例如Flutter SDK正是通过InheritedWidget来共享应用主题Theme和语言环境Locale。代码上看区别:
当这个跨级跨的越来越大,传递数据越来越多时,InheritedWidget的特性就显得非常重要。
二:InheritedWidget怎么用
通过上面这张图来讲,在ShareDataWidget中,定义了一个data参数,由外部传入,在ShareDataWidget子控件中,ChildWidget2可以通过ShareDataWidget.of(context)?.data
获取到数据。
先来看下InheritedWidget中比较重要的几个方法:
context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()
当在ChildWidget2中,调用该方法时,ChildWidget2和ShareDataWidget就会创建依赖关系,当ShareDataWidget的数据更改了,并且updateShouldNotify方法返回true时,ChildWidget2就会触发didChangeDependencies、build方法。
context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget
这个方法和第一个方法的区别是,在子控件中调用该方法时,并不会将子控件和ShareDataWidget进行依赖关系绑定,所以当ShareDataWidget的数据更改,updateShouldNotify返回true时,也不会触发该控件的build和didChangeDependencies。(当然要注意写法,后面会说明)
上述代码中,dependOnInheritedElement
方法中主要是注册了依赖关系,之后当InheritedWidget
发生变化时,就会更新依赖他的子组件(调用didChangeDependencies、build方法),如果没有依赖的子组件也不会更新。
bool updateShouldNotify(covariant ShareDataWidget oldWidget)
当ShareDataWidget的data变化了之后,InheritedWidget可以决定是否更新其子控件,当然也可以选择不更新,更新返回true,不更新返回false。
讲的有点抽象,看个例子:
当运行后,打印日志如下:
I/flutter (11723): rebuild common widget:0
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:1
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:1
I/flutter (11723): rebuild common widget:1
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild common widget:2
-------------过了3秒后,调用setstate,打印日志如下-----------
I/flutter (11723): rebuild common widget:0
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild getElementForInheritedWidgetOfExactType widget:2
I/flutter (11723): rebuild common widget:2
I/flutter (11723): rebuild dependOnInheritedWidgetOfExactType widget:1
第一段日志,按照控件顺序,调用了build方法。当过了3秒后,调用了TestState.setState方法后,TestState的build方法被触发。
- getCommonWidget(0):无缓存,触发build,打印日志
- depend1:虽然有缓存,但是由于依赖了ShareDataWidget,且data发生变化,因此触发build。但是build顺序在最后。
- notDepend1:有缓存,但是不依赖ShareDataWidget,因此不触发build。
- common:有缓存,不触发build
- getDependWidget(1):因为依赖ShareDataWidget,data发生变化,触发build。
- getNotDependWidget(2)、getCommonWidget(2):不依赖ShareDataWidget, 且没有缓存,触发build。
对上述日志进行总结就是:
- 父控件(TestState)的setState会造成build触发,触发TestState内的全局刷新
- 如果子控件无缓存,每次父控件build都会触发子控件build。(getCommonWidget(0)、getDependWidget(1)、getNotDependWidget(2)、getCommonWidget(2))
- 如果子控件有缓存,但是依赖了InheritedWidget,且数据发生变化,则触发build(depend1)。若不依赖InheritedWidget,则不触发build(common、notDepend1)。
所以如果是不依赖InheritedWidget的子widget,需要有缓存,否则还是会触发build。
三:InheritedWidget进一步优化
上述代码的例子中,如果TestState.data变化,我们只想更新依赖了ShareDataWidget的子控件,而现在更新data字段,需要调用TestState.setState方法,会导致没有缓存的子节点都被重新build,这很没有必要。解决办法就是缓存,但是我们平时写代码,不可能像上面示例代码中那样,在State中声明这样的控件去缓存,一个简单的办法就是,通过封装一个StatefulWidget,将InheritedWidget(ShareDataWidget)封装起来。
优化前 | 优化后 |
---|---|
优化前,页面widget给InheritedWidget传入data,更新的时候调用页面Widget的setState,会造成未缓存的子widget都刷新。 | 用一个新封装widget来封装InheritedWidget,且是唯一的子组件。页面Widget给新封装widget传入T data和Widget child(图中蓝色的子widget),当页面Widget发现data变化时,通知新封装widget调用setState,重新构建InheritedWidget。 |
优化点在于:
- setState范围缩小:data变化,不会调用页面Widget.setState,降低build成本,淡橙色的子widget都不会受影响。只会调用新封装widget.setState,只会重新构建InheritedWidget。
- InheritedWidget子组件缓存:由于InheritedWidget都是页面Widget传入缓存在新封装widget里的,因此当InheritedWidget重建时,也只会重新构建依赖的子组件,不依赖的子组件则不rebuild。
那么问题来了,data变化的时候,页面Widget如何通知新封装Widget呢?当然实现的方式有很多种,比如ChangeNotifier,让T data
继承ChangeNotifier,然后在数据更改的时候进行通知。当把data传入到新封装Widget中后,在initState里,给data添加listener,去监听数据变化。
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) {
return context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>().data;
}
@override
_ChangeNotifierProviderState<T> createState() => _ChangeNotifierProviderState<T>();
}
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
@override
void initState() {
// 给model添加监听器
widget.data.addListener(update);
super.initState();
}
@override
void didUpdateWidget(ChangeNotifierProvider<T> oldWidget) {
//当Provider更新时,如果新旧数据不"==",则解绑旧数据监听,同时添加新数据监听
if (widget.data != oldWidget.data) {
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
void update() {
//如果数据发生变化(model类调用了notifyListeners),重新构建InheritedProvider
setState(() => {});
}
@override
Widget build(BuildContext context) {
return InheritedProvider<T>(
data: widget.data,
child: widget.child,
);
}
@override
void dispose() {
// 移除model的监听器
widget.data.removeListener(update);
super.dispose();
}
}
// 一个通用的InheritedWidget,保存需要跨组件共享的状态
class InheritedProvider<T> extends InheritedWidget {
InheritedProvider({required this.data, required Widget child});
final T data;
@override
bool updateShouldNotify(InheritedProvider<T> old) {
//在此简单返回true,则每次更新都会调用依赖其的子孙节点的`didChangeDependencies`。
return true;
}
}
再画个更清楚的流程图,当然画的不全,比如removeListener这些都没画进去,就是大概表示一下意思:
讲了这么多,看一下最终代码使用的例子:
class _ProviderRouteState extends State<ProviderRoute> {
@override
Widget build(BuildContext context) {
return 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(builder: (context) {
print("ElevatedButton build"); //在后面优化部分会用到
return ElevatedButton(
child: Text("添加商品"),
onPressed: () {
//给购物车中添加商品,添加后总价会更新
ChangeNotifierProvider.of<CartModel>(context)
.add(Item(20.0, 1));
});
})
]);
})));
}
}
上面这个代码中,添加商品的按钮,每次点击后,会导致CardModel刷新,但是由于按钮自身依赖了InheritedWidget,所以也会导致rebuild,这里可以优化一下,让其不依赖。
至此上面讲的,基本上是Provider(一个用于管理状态的包)的底层原理。
上面的例子看似简单,不能体现Provider的强大,但是如果当我们的业务变得很复杂,一个页面内部层级比较深,状态比较多,各个子组件不断嵌套,那么如果要逐级传递数据的话,就会显得不那么优雅,用Provider就能很好的解决跨级传递数据问题。如果在App内,是多个页面共享数据的话,那么则需要将Provider设置的层级更高一些,比如在main.dart中。
四:Provider的使用
1:provider类型
Provider、ChangeNotifierProvider、ProxyProvider、ListenableProxyProvider等。
下面以ChangeNotifierProvider的使用为例讲解:
2:声明Provider的位置
如果某个Provider的数据是全局共享(例如跨页面)的话,那么可以放在main()中;如果是单个页面内几个子Widget共享的话,则在子widget最近的父widget处声明即可。
// 如果有多个provider,则用MultiProvider封装
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider(create: (ctx) => userLoginState),
ChangeNotifierProvider(create: (context) => DoctorStudioProvider())
], child: const MyApp()));
}
// 如果只有单个provider的话
void main() {
runApp(ChangeNotifierProvider(
create: (ctx) => UserLoginStateChange(), child: const MyApp()));
}
这里需要注意的是:推荐使用create:Builder的方式去创建Model,而不是使用.value的方式。
// bad: do not do this
ChangeNotifierProvider.value(value: UserLoginStateChange(),child: MyApp());
原因:如果你想在开始监听时再创建一个对象,不推荐使用.value。create回调函数是延迟调用的,也就是说变量被读取时,create才会被调用。
3:创建Model(extends ChangeNotifier)
class UserLoginStateChange extends ChangeNotifier {
bool? _userHadLogin;
bool get userHadLogin {
_userHadLogin ??= !Util.isStrEmpty(UserDefault.shared.short_access_token);
return _userHadLogin!;
}
changeState(bool isLogin) {
_userHadLogin = isLogin;
//重点是这句,当登录状态改变时,会通知监听该ChangeNotifier的观察者更新
notifyListeners();
}
}
4:监听并读取数据
context.watch<T>()
: widget可以监听到T类型的provider发生的改变。context.read<T>()
: 只是读取T,并不监听改变context.selector<T,R>
: 允许widget监听T上一部分内容的改变。Provider.of<T>(BuildContext ctx, {bool listen = true})
: 默认监听(同watch),如果传入false,则不监听(同read)。- Consumer、Selector: 监听provider发生的改变。
需要注意的是:上述1-4点,都使用到了context,如果是监听变化的话,那么在发生变化时,会触发context.setState方法,而不是在context中某个子组件的build。
举个例子:在下面这个例子中,虽然只有第一个Text需要使用到Provider的数据,但是使用的是页面的context去监听Provider的改变,因此当provider发生改变时,页面的build会被调用,TextButton也会重新创建,虽然他并没有监听使用数据。
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Column(children: [
Text(Provider.of<UserLoginStateChange>(context).userHadLogin
? '已登录'
: '未登录'),
TextButton(onPressed: () {}, child: Text('按钮'))
]));
}
那如何刷新缩小到最小范围呢?请使用Consumer或者Selector:
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
child: Column(children: [
Consumer<UserLoginStateChange>(builder: (ctx, model, child) {
return Text(model.userHadLogin ? '已登录' : '未登录');
}),
TextButton(onPressed: () {}, child: Text('按钮'))
]));
}
这样就可以在provider变化时,只刷新Text,而不会影响页面其他的widget。看下consumer源码:
class Consumer<T> extends SingleChildStatelessWidget {
Consumer({Key? key, required this.builder, Widget? child,}) : super(key: key, child: child);
final Widget Function(BuildContext context, T value, Widget? child) builder;
@override
Widget buildWithChild(BuildContext context, Widget? child) {
// Consumer就是封装了一层,在调用builder时,将Consumer的ctx与Provider进行绑定并获取数据
return builder(context, Provider.of<T>(context), child);
}
}
Flutter社区还有其他用于状态管理的包,例如:Scoped Model、Redux、MobX、BLoC。等我一一研究再分享。
上述描述有疏漏的,请大家指正。