Flutter状态管理之InheritedWidget、Provider

1,395 阅读9分钟

一:InheritedWidget是什么

InheritedWidget是flutter中非常重要的功能性组件,它提供了一种在widget树从上到下共享数据的方式。比如在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子Widget中来获取该共享的数据。

image.png

如上图所示,比起普通的widget,逐级传递数据来看,InheritedWidget可以实现子控件跨级传递数据。这个特性在一些需要在整个widget树中共享数据的场景中非常方便,例如Flutter SDK正是通过InheritedWidget来共享应用主题Theme和语言环境Locale。代码上看区别:

image.pngimage.png

当这个跨级跨的越来越大,传递数据越来越多时,InheritedWidget的特性就显得非常重要。

二:InheritedWidget怎么用

image.png

通过上面这张图来讲,在ShareDataWidget中,定义了一个data参数,由外部传入,在ShareDataWidget子控件中,ChildWidget2可以通过ShareDataWidget.of(context)?.data获取到数据。

先来看下InheritedWidget中比较重要的几个方法:

  1. context.dependOnInheritedWidgetOfExactType<ShareDataWidget>()

当在ChildWidget2中,调用该方法时,ChildWidget2和ShareDataWidget就会创建依赖关系,当ShareDataWidget的数据更改了,并且updateShouldNotify方法返回true时,ChildWidget2就会触发didChangeDependencies、build方法。

  1. context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget 这个方法和第一个方法的区别是,在子控件中调用该方法时,并不会将子控件和ShareDataWidget进行依赖关系绑定,所以当ShareDataWidget的数据更改,updateShouldNotify返回true时,也不会触发该控件的build和didChangeDependencies。(当然要注意写法,后面会说明)
image.png

上述代码中,dependOnInheritedElement方法中主要是注册了依赖关系,之后当InheritedWidget发生变化时,就会更新依赖他的子组件(调用didChangeDependencies、build方法),如果没有依赖的子组件也不会更新。

  1. bool updateShouldNotify(covariant ShareDataWidget oldWidget)

当ShareDataWidget的data变化了之后,InheritedWidget可以决定是否更新其子控件,当然也可以选择不更新,更新返回true,不更新返回false。

讲的有点抽象,看个例子:

image.png

当运行后,打印日志如下:

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方法被触发。

  1. getCommonWidget(0):无缓存,触发build,打印日志
  2. depend1:虽然有缓存,但是由于依赖了ShareDataWidget,且data发生变化,因此触发build。但是build顺序在最后。
  3. notDepend1:有缓存,但是不依赖ShareDataWidget,因此不触发build。
  4. common:有缓存,不触发build
  5. getDependWidget(1):因为依赖ShareDataWidget,data发生变化,触发build。
  6. 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)封装起来。

优化前优化后
image.pngimage.png
优化前,页面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,去监听数据变化。

image.png
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这些都没画进去,就是大概表示一下意思:

image.png

讲了这么多,看一下最终代码使用的例子:

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:监听并读取数据

  1. context.watch<T>(): widget可以监听到T类型的provider发生的改变。
  2. context.read<T>(): 只是读取T,并不监听改变
  3. context.selector<T,R>: 允许widget监听T上一部分内容的改变。
  4. Provider.of<T>(BuildContext ctx, {bool listen = true}): 默认监听(同watch),如果传入false,则不监听(同read)。
  5. 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 ModelReduxMobXBLoC。等我一一研究再分享。

上述描述有疏漏的,请大家指正。