前言

InheritedWidget是Flutter中一个非常重要的功能型Widget,它可以高效快捷的实现共享数据的跨组件传递,并且InheritedWidget在组件树中的数据传递方向始终是从上到下的,这和Notification的传递方向正好相反(Notification讲解详见对应的专题篇),在flutter中我们经常能够看到InheritedWidget式传递的使用场景,比如下面:

Theme.of(context).primaryColor; // 获取主题色
MediaQuery.of(context).size.width;  // 获取屏幕宽度
MaterialLocalizations.of(context).closeButtonLabel; // 当前的语言环境
BlocProvider.of<MyBloc>(context).add(MyEvent()); // 状态管理bloc小部件BlocProvider

试想这样一个场景:一个多层嵌套的组件树,假设在最上层的组件中定义了一个对象,但是该对象在最下层的某个组件中要使用到,我们不可能使用传统的例如构造方法传值等的方式将该对象一级一级向下传递接收传递接收,直到传递到我们需要使用到的组件中,这样想一想都会觉得繁琐,而且中间组件根本没有使用到这个对象,但是为了保证最下层组件能获取到他,还必须通过他向下传递,这样不仅损耗性能还无缘无故写了好多重复的代码,而InheritedWidget的存在就是为了解决此问题而生的。

我们只需要将包含需要共享的这个数据对象所在的最上层组件定义为InheritedWidget类型的组件即可,然后中间组件无需再充当传递媒介,在下层的任何组件我们都可以通过InheritedWidget的of方法获取到最上层组件里面的共享数据,不仅少些很多代码,而且传递也会变的高效很多,InheritedWidget组件的使用具体如下。

具体使用步骤:

1,定义需要与下层共享的数据类,如下:

class ShareData {
  
  String userName;
  ShareData({this.userName = "default"});

  // 规定对象相等的规则,如下userName属性值一样则认为两个对象相等
  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is ShareData && o.userName == userName;
  }
}

2,自定义一个组件继承InheritedWidget,在其中定义要共享的数据以及对应的构造方法,并重写updateShouldNotify方法,同时提供一个可以在下层的子树中很方便的获取这个组件实例的of方法:

class ShareWidget extends InheritedWidget {

  // 需要共享的数据,可以是单个数据,也可以是多个数据
  // 多个数据建议封装成一个类来进行管理,如这里的DataState,里面可以根据实际需求添加更多的字段。
  // final int shareData;  // 单个值可以不用封装成类,直接定义,但实际需求往往更复杂,因此建议统一使用外部的管理类来管理
  final ShareData shareData; // 建议定义类来统一管理共享数据,一个或者多个数据。

  // 构造函数
  const ShareWidget({
    Key key,
    @required this.shareData,
    @required Widget child,
  }) : super(key: key, child: child);

  // 写法1:返回组件对象
  static ShareWidget of(BuildContext context) {
    // return context.inheritFromWidgetOfExactType(ShareWidget);
    // 上面的方法在v1.12.1之后被弃用,改为使用下面的dependOnInheritedWidgetOfExactType。
    return context.dependOnInheritedWidgetOfExactType<ShareWidget>();
  }

  // 写法2:直接返回共享数据
  static ShareData of(BuildContext context) {
    final ShareWidget shareWidget =
        context.dependOnInheritedWidgetOfExactType<ShareWidget>();
    return shareWidget.shareData;
  }

  //该回调决定当状态发生变化时,是否通知子树中依赖的该组件
  @override
  bool updateShouldNotify(ShareWidget oldWidget) {
    // 是否需要更新,返回true则更新
    // 当返回true时,如果在子child的build函数中有调用of获取该InheritedWidget,
    // 那么这个子widget的`state.didChangeDependencies`方法会被调用
    return this.shareData!= oldWidget.shareData;
  }
}

注意:有些时候,of方法返回的可能直接是数据而不是组件,因此下面提供了两种不同返回值的of写法,例如系统的Theme.of(context)返回的就直接是数据ThemeData。

提示:单个数据的共享示例可见:flutter 更优雅的实现app首页底部菜单栏选项切换的两种方式 这篇文章。

3,将InheritedWidget在项目的根Widget上包裹使用,这样InheritedWidget中的数据就能向下传递到项目中的每个角落,从而实现全局的数据共享。

如下,在MaterialApp外面套一层ShareWidget,并且传入一个初始的ShareData:

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 此处的ShareWidget是InheritedWidget类型的组建
    return ShareWidget(
      shareData: ShareData(userName:"初始值"),
      child: MaterialApp(
        title: 'InheritedWidget的使用',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: APage(),
      ),
    );
  }
}

4,获取共享数据dataState,子Widget通过ShareWidget ShareWidget = ShareWidget.of(context);获取到ShareWidget,就可以拿到其中的共享数据dataState了,代码如下:

class APage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          Spacer(),
          Center(
            child: Text("${ShareWidget.of(context).shareData.userName}"),
          ),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              Navigator.of(context).push(MaterialPageRoute(builder: (context) {
                return BPage();
              }));
            },
            child: Text("跳转页面B,在页面B中修改userName的值"),
          ),
          Spacer(),
        ],
      ),
    );
  }
}

在子控件中,context会自动向上查找,省去了我们一级一级往下传递的麻烦,这样就实现了共享数据dataState的向下传递,在下层也就很容易拿到共享数据了。

注意:InheritedWidget组件实现数据传递的优势是跨级传递,级跨得越多优势也就越明显,相反如果仅仅是只有上下二级关系的组件要想实现数据的传递,使用构造方法传值相对就比较简单方便,因为InheritedWidget组件的方式还需对组件进行InheritedWidget的改造,显然没有直接使用构造传值来得直接。

5,Bpage页面如下:

class BPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          Spacer(),
          Text("${ShareWidget.of(context).shareData.userName}"),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              ShareWidget.of(context).shareData= ShareData(userName: "我被改了");
            },
            child: Text("更改userName的值"),
          ),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text("返回"),
          ),
          Spacer(),
        ],
      ),
    );
  }
}

InheritedWidget组件本身不具有刷新页面的功能,因此以上我们只是完成了组件ShareWidget及其共享数据的向下传递,并未实现其状态更新,确实运行之后我们也发现:在B页面点击更改按钮之后我们发现B页面text文本并未更新,点击返回按钮之后,A页面text文本也没有得到更新。

优化:具有状态更新功能的InheritedWidget

以上我们只是完成了数据传递的功能,在B页面点击更改按钮更改共享数据里面的值之后,发现页面上的text文本并未即使更新,这是因为InheritedWidget 和 StatelessWidget一样,本身是没有刷新功能的,众所周知StatefulWidget 是可以用过setState来刷新页面的,因此我们需要在ShareWidget的外层使用StatefulWidget包裹,

下面我们来对上面的例子进行状态更新的优化:

1,定义需要与下层共享的数据类,如下:

class ShareData {
  
  String userName;
  ShareData({this.userName = "default"});

  // 规定对象相等的规则,如下userName属性值一样则认为两个对象相等
  @override
  bool operator ==(Object o) {
    if (identical(this, o)) return true;
    return o is ShareData && o.userName == userName;
  }
}

2,自定义一个组件继承InheritedWidget,然后再定义一个StatefulWidget类型的组件来对其进行包裹,如下:

class _InheritedWidget extends InheritedWidget {
  _InheritedWidget({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final _ShareWidgetState data;

  @override
  bool updateShouldNotify(_InheritedWidget oldWidget) {
    return true;
  }
}

// 额外定义一个StatefulWidget类型的组件来包裹_InheritedWidget组件,使其可以具有状态更新功能
class ShareWidget extends StatefulWidget {
  
  final ShareData shareData; // 共享数据
  final Widget child;

  ShareWidget({Key key, this.shareData, this.child});

  @override
  State<StatefulWidget> createState() => _ShareWidgetState();

  static _ShareWidgetState of(BuildContext context) {
    final _InheritedWidget inheritedConfig = context.dependOnInheritedWidgetOfExactType<_InheritedWidget>();
    return inheritedConfig.data;
  }
}

class _ShareWidgetState extends State<ShareWidget> {

  // 更改共享数据中userName属性的值并触发组件刷新的方法
  void setUserName(String name) {
    setState(() {
      widget.shareData.userName = name;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new _InheritedWidget(data: this, child: widget.child);
  }
}

3,将ShareWidget在项目的根Widget上包裹使用,这样ShareWidget中的数据就能通过InheritedWidget向下传递到项目中的每个角落,从而实现数据共享。

如下,在MaterialApp外面包裹ShareWidget,并且传入一个初始的ShareData:

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 此处的ShareWidget是InheritedWidget类型的组建
    return ShareWidget(
      shareData: ShareData(userName:"初始值"),
      child: MaterialApp(
        title: 'InheritedWidget的使用',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: APage(),
      ),
    );
  }
}

4,获取共享数据dataState,此时子Widget通过ShareWidget.of(context);获取到的是_ShareWidgetState对象,我们知道在state对象中获取widget中的值,需要使用widget去调用,因此通过 ShareWidget.of(context).widget.shareData.userName 就可以拿到其中的共享数据dataState了,代码如下:

class APage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          Spacer(),
          Center(
            child: Text("${ShareWidget.of(context).widget.shareData.userName}"),
          ),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              Navigator.of(context).push(MaterialPageRoute(builder: (context) {
                return BPage();
              }));
            },
            child: Text("跳转页面B,在页面B中修改userName的值"),
          ),
          Spacer(),
        ],
      ),
    );
  }
}

5,Bpage页面如下:

class BPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        children: [
          Spacer(),
          Text("${ShareWidget.of(context).widget.shareData.userName}"),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              ShareWidget.of(context).setUserName("新改的值");
            },
            child: Text("更改userName的值"),
          ),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              Navigator.of(context).pop();
            },
            child: Text("返回"),
          ),
          Spacer(),
        ],
      ),
    );
  }
}

这样当我们再在B页面点击按钮更改了控件的值之后,就会发现两个页面的数据均自动刷新了。

通过上面2个例子我们知道,如果我们只是单纯地想实现数据传递共享,可以使用示例一的方式,如果还需实现数据改变页面刷新的功能,则可以使用示例二的方式,此外为了将两种情况下的代码风格统一,我们也可以使用statelessWidget来包裹_InheritedWidget组件,达到和示例1相同的功能,如下:

只需将上面示例2中第二步的代码更改为如下即可:

class _InheritedWidget extends InheritedWidget {
  _InheritedWidget({
    Key key,
    @required Widget child,
    @required this.data,
  }) : super(key: key, child: child);

  final ShareWidget data;

  @override
  bool updateShouldNotify(_InheritedWidget oldWidget) {
    return true;
  }
}

/// 只传递共享数据,不包含刷新功能,此处只是基于上面的示例1改写成跟示例2相似的写法而已,你也可以使用示例1的写法
class ShareWidget extends StatelessWidget {
  
  final ShareData shareData;
  final Widget child;

  ShareWidget({Key key, this.shareData, this.child});

  static ShareWidget of(BuildContext context) {
    final _InheritedWidget inheritedConfig = context.dependOnInheritedWidgetOfExactType<_InheritedWidget>();
    return inheritedConfig.data;
  }

  @override
  Widget build(BuildContext context) {
    return new _InheritedWidget(data: this, child: child);
  }
}

获取共享数据时:

ShareWidget.of(context).shareData.userName

didChangeDependencies

继承StatefulWidget时State对象有一个回调didChangeDependencies,它会在“依赖”发生变化时被Flutter Framework调用。
而这个“依赖”指的就是是否使用了父widget中InheritedWidget的数据,如果使用了,则代表有依赖,如果没有使用则代表没有依赖。
这种机制可以使子组件在所依赖的主题、locale等发生变化时有机会来做一些事情。

将上面的APage改成StatefulWidget类型的widget,并重写didChangeDependencies方法:

class APage extends StatefulWidget {
  @override
  _APageState createState() => _APageState();
}

class _APageState extends State<APage> {
  @override
  Widget build(BuildContext context) {
    print('-----build');
    return Material(
      child: Column(
        children: [
          Spacer(),
          Center(
            child: Text("${ShareWidget.of(context).dataState.userName}"),
          ),
          SizedBox(
            height: 20,
          ),
          RaisedButton(
            onPressed: () {
              Navigator.of(context).push(MaterialPageRoute(builder: (context) {
                return BPage();
              }));
            },
            child: Text("跳转页面B,在页面B中修改username的值"),
          ),
          Spacer(),
        ],
      ),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    //父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。
    //如果build中没有依赖InheritedWidget,即没有使用到ShareWidget里面的数据,则此回调不会被调用。
    print("Dependencies change");
  }
}

此时再点击B页面的更改按钮,我们会发现,控制台依次输出了:

flutter: Dependencies change
flutter: -----build

说明除了build方法,didChangeDependencies方法的确也被调用了。

那么为什么非要多此一举监听didChangeDependencies,直接监听build方法不就可以了,其实如果数据发生变化是触发请求接口的条件,如果该条件放在build中,则请求接口会发生的很频繁,而didChangeDependencies 则是可控制的,更为合理,(即didChangeDependencies 调用 build必定调用,但是build调用 didChangeDependencies 不一定被调用)。

那么这里如果我只想引用数据不想要didChangeDependencies 回调被调用呢,
按照上面的说法是,如果想要didChangeDependencies 这个回调被调用必须是使用数据并注册,也就是说我们不注册就可以了。

即调用of方法获取会同时执行注册逻辑,借鉴源码的实现,我们在ShareWidget中再增加一个只获取不注册的ofData方法,如下:

  // 只获取数据不注册
  static ShareWidget ofData(BuildContext context) {
    return context
        .getElementForInheritedWidgetOfExactType<ShareWidget>()
        .widget;
  }

这样修改APage中获取文本内容的代码调用为:

child: Text("${ShareWidget.ofData(context).dataState.userName}"),

这样再次点击B页面的更改按钮,发现B页面的text刷新了,但是A页面的text并没有刷新,并且此时控制台也没有任何的输出,说明build和didChangeDependencies均没有被调用。

以上全部就是InheritedWidget实现状态管理的使用方法了,建议:关于页面UI刷新首要还是考虑使用状态管理的方式。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐