# 前言

[Flutter状态管理初接触] 的 上一篇 (opens new window) 中我们介绍了基于provider的状态管理, 也说到它的底层是基于InheritedWidget的. 我在上手尝试过使用后, 个人感觉很像React中的context, 因此觉得在讲解InheritedWidget的过程中, 可以多以React context的使用来作为示例.

还看了下闲鱼技术部的fish-redux, 感觉更熟悉了... action / reducer / store / effect的概念简直梦回redux/dva, 果然状态管理是殊途同归的?

我们先通过回顾下React Context API (opens new window):

const MyContext = React.createContext(defaultValue);
const { Provider, Consumer } = MyContext;
<Provider value={SHARED_CONTEXT}></Provider>
<Consumer>{/* 跨层级消费共享上下文 */}</Consumer>

使用Provider提供状态后, 你就可以跨越任意层级的组件访问消费该状态而无需一级级的传递回调函数与属性了, 说来有趣, 我个人在初学react阶段, 看到context时简直惊为天人...

InheritedWidget的使用同样是类似的的, 将状态提升到顶层(我觉得这是比较好的写法, 不要只将其提升到有消费需求的widget最近公共父节点这种), 而后内部子widget就可以进行使用.

仍然是以计数器为例子, 我们直接来从代码领略其作用(因为它的确属于看了就会的233):

class Counter {
  final int count;
  const Counter(this.count);
}
class SharedContext extends InheritedWidget {
  final Counter counter;
  final void Function() increment;
  final void Function() decrement;
  SharedContext(
      {Key key,
       this.counter,
       this.increment,
       this.decrement,
       Widget child})
      : super(key: key, child: child);
}

SharedContext即是我们要进行共享的上下文, 而其内部的"状态"包括counter与两个对counter修改的方法. 在构造函数中我们要求传入counter & increment & decrement , 是为了在InheritedWidget(可以理解为Provider)内部去实例化实际上要共享的状态, 这么做的好处是你可以将SharedContext视作一个抽象类一般的存在, 实际使用可以基于其共享多种状态(比如widget需要的counter有的从0开始, 有的从100开始).

我们还需要为SharedContext提供一个获取上下文的静态方法, 后代有消费需求的widget需要先以SharedContext.xxx的形式获取到共享上下文, 才能进行消费:

static SharedContext of(BuildContext context) {
    // 该方法用于找到最近的上下文
    return context.dependOnInheritedWidgetOfExactType<SharedContext>();
  }

值得一提的是, context.inheritFromWidgetOfExactType(targetType)这一方法已经被废弃, 请使用dependOnInheritedWidgetOfExactType<targetType>方法

dependOnInheritedWidgetOfExactType<SharedContext>能够找到并返回最近的指定类型的共享上下文.

接着, 我们可以来创建Provider功能的widget了, 它需要是一个StatefulWidget:

import "package:flutter/material.dart";
void main() {
  runApp(InheritedWidgetContainer());
}
class InheritedWidgetContainer extends StatefulWidget {
  InheritedWidgetContainer({Key key}) : super(key: key);
  
  _InheritedWidgetContainerState createState() =>
      _InheritedWidgetContainerState();
}
class _InheritedWidgetContainerState extends State<InheritedWidgetContainer> {
  Counter counter;
  void _initialization() {
    counter = Counter(0);
  }
  
  void initState() {
    _initialization();
    super.initState();
  }
  void _increment() {
    setState(() {
      counter = Counter(counter.count + 1);
    });
  }
  void _decrement() {
    setState(() {
      counter = Counter(counter.count - 1);
    });
  }
  
  Widget build(BuildContext context) {
    return SharedContext(
      counter: counter,
      increment: _increment,
      decrement: _decrement,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text("基于 InheritedWidget 的 Flutter状态管理"),
          ),
          body: Center(
              child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                // ...
              ],
            ),
          )),
        ),
      ),
    );
  }
}

InheritedWidgetContainer中, 我们提供了实例化的counter以及修改方法的实现, 并传入到SharedContext中(注意其形式并不是redux中的value={store}这样.) 在不同的Container中, 我们可以实例化不同的状态.

接着, 我们先创建一个widget用于读取值:

class ValueWidget extends StatefulWidget {
  ValueWidget({Key key}) : super(key: key);
  
  _ValueWidgetState createState() => _ValueWidgetState();
}
class _ValueWidgetState extends State<ValueWidget> {
  
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("Dependencies change");
  }
  
  Widget build(BuildContext context) {
    final inheritedCtx = SharedContext.of(context);
    final counter = inheritedCtx.counter;
    print("Count Value in ValueWidget: ${counter.count}");
    return Padding(
      padding: EdgeInsets.all(15),
      child: Text(
        "count: ${counter.count}",
        style: Theme.of(context).textTheme.headline3,
      ),
    );
  }
}

需要注意的是这里:

    final inheritedCtx = SharedContext.of(context);
    final counter = inheritedCtx.counter;

以及didChangeDeps方法, 了解过Flutter生命周期的话你应该知道它会在依赖变化时调用, 用于执行一些开销比较大的操作如网络请求这种副作用, 但依赖是什么呢? 实际上就是其在widget树的上方所拥有的InheritedWidget父级内部状态是否变化, 我们在SharedContext中新增一个方法:

  
  bool updateShouldNotify(SharedContext oldContext) {
    return true;
  }

这个方法接收变化前的状态, 并且判断是否触发子组件的didChangeDeps事件(根据返回true/false). 父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用. 如果build中没有依赖InheritedWidget,则此回调不会被调用.

如果你在这个控件内没有使用到InheritedWidget传递的值, 那么这个方法就不会在状态变化时被调用.

接着, 我们把+ & -按钮也抽离成组件, 来展示其"共享"能力:

class IncreWidget extends StatelessWidget {
  const IncreWidget({Key key}) : super(key: key);
  
  Widget build(BuildContext context) {
    final inheritedCtx = SharedContext.of(context);
    final counter = inheritedCtx.counter;
    print("Count Value in IncreWidget: ${counter.count}");
    return Padding(
        padding: EdgeInsets.only(bottom: 15),
        child: RaisedButton(
          color: Colors.white,
          onPressed: inheritedCtx.increment,
          child: Text(
            "+",
            style: TextStyle(fontSize: 32, color: Colors.green),
          ),
        ));
  }
}
class DecreWidget extends StatelessWidget {
  const DecreWidget({Key key}) : super(key: key);
  
  Widget build(BuildContext context) {
    final inheritedCtx = SharedContext.of(context);
    final counter = inheritedCtx.counter;
    print("Count Value in DecreWidget: ${counter.count}");
    return Padding(
        padding: EdgeInsets.only(top: 15),
        child: RaisedButton(
          color: Colors.white,
          onPressed: inheritedCtx.decrement,
          child: Text(
            "-",
            style: TextStyle(fontSize: 32, color: Colors.redAccent),
          ),
        ));
  }
}

添加到Container中:

 
  Widget build(BuildContext context) {
    return SharedContext(
      counter: counter,
      increment: _increment,
      decrement: _decrement,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text("基于 InheritedWidget 的 Flutter状态管理"),
          ),
          body: Center(
              child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const IncreWidget(),
                ValueWidget(),
                const DecreWidget(),
              ],
            ),
          )),
        ),
      ),
    );
  }

完整代码见 GitHub (opens new window)

其他注意点:

  • SharedContext会在内部值更新时重新构建, 并且是懒初始化的(我猜provider的懒初始化也是因为这个)
  • SharedContext会跟随指定类型的父级InheritedWidget, 来实现局部widget更新