December 6, 2018 · flutter

Flutter中的数据传递:InheritedWidget, InheritedModel

Flutter中标准的数据传递方法是通过属性传递,类似于react中的props。对于复杂点的UI,这样有时候会比较繁琐。一个属性可能需要跨越很多层传递给子子组件。

对于跨层传递,flutter原生支持如下两种方案:InheritedWidget和InheritedModel。

InheritedWidget

有些Flutter使用经验的开发同学,可能比较熟悉这样的代码。

通过Theme访问当前的界面风格:Theme.of(context).primaryColor
通过MediaQuery访问屏幕大小:MediaQuery.of(context).size

这两个类都是通过InheritedWidget实现的典范。调用组件可以通过Theme.of方法找到上层最近的Theme父组件,跨层获取到其属性,同时会建立调用组件和上层组件的观察者关系。当上层组件属性修改的时候,触发调用组件的更新。

我们通过下面简单的计数器例子,演示下InheritedWidget如何用。如下两个类:InheritedContainer包含了状态计数num属性,Counter通过inheritFromWidgetOfExactType方法找到上层的InheritedContainer组件,并获取计数num并展示在一个按钮里面。


class InheritedContainer extends InheritedWidget {
  static InheritedContainer of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(InheritedContainer) as InheritedContainer;
  }

  int num = 0;

  InheritedContainer({
    Key key,
    @required Widget child,
  }): super(key: key, child: child);

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

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    InheritedContainer state = InheritedContainer.of(context);
    return RaisedButton(
      child: Text('${state.num}'),
      onPressed: () {},
    );
  }
}

这样把Counter放到InheritedContainer下层任何位置,Counter都能获取到InheritedContainer的计数属性num,不用手动传递属性。

InheritedContainer(
  child: Counter(),
)

细心的小伙伴发现我们的计数器现在是无法修改状态的。支持修改的话,我们就需要InheritedWidget和StatefulWidget联用了。我们可以把InheritedContainer放在一个StatefulWidget下面,并把状态移动到StatefulWidget的状态上。

class DataContainer extends StatefulWidget {
  @override
  DataContainerState createState() => DataContainerState();
}

class DataContainerState extends State<DataContainer> {
  int num = 0;

  void incNum() {
    setState(() {
      this.num++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return InheritedContainer(
      num: num,
      parent: this,
      child: Counter(),
    );
  }
}

class InheritedContainer extends InheritedWidget {
  final int num;
  final DataContainerState parent;

  static InheritedContainer of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(InheritedContainer) as InheritedContainer;
  }

  InheritedContainer({
    Key key,
    @required Widget child,
    @required this.parent,
    @required this.num,
  }): super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedContainer oldWidget) {
    return num != oldWidget.num;
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    InheritedContainer c = InheritedContainer.of(context);
    return RaisedButton(
      child: Text('${c.num}'),
      onPressed: c.parent.incNum,
    );
  }
}

注意updateShouldNotify方法,DateContainerState重新构建一个InheritedContainer的时候,就会调用这个方法。该方法返回bool,表示是否需要下层的观察者组件更新。

InheritedModel

InheritedModel可以理解为InheritedWidget的升级版。

前面提到InheritedWidget更新的时候,会调用updateShouldNotify方法询问是否需要通知观察者更新。这个更新粒度很粗,所有观察者都会更新。设想如果InheritedWidget有多个属性a, b, c,下层有A观察a属性,B组件观察b属性,C组件观察c属性。当只有a属性修改的时候,也会同时触发A、B、C三个组件的更新。其中两个更新的浪费的。

InheritedModel使用方法略微不同。获取上层组件属性的时候,需要通过InheritedModel.inheritFrom<Inherited>(context, aspect)方法,并提供一个aspect,表示观察上层组件的什么属性。这样当InheritedModel的属性修改的时候,InheritedModel会对观察者们一一调用updateShouldNotifyDependent方法,确定观察者是否需要更新。

如下例子两个组件Counter1和Counter2分别观察了num1, num2属性。修改其中一个状态的时候不会引起两个组件同时更新。

class DataContainer extends StatefulWidget {
  @override
  DataContainerState createState() => DataContainerState();
}

class DataContainerState extends State<DataContainer> {
  int num1 = 0;

  void incNum1() {
    setState(() {
      this.num1++;
    });
  }

  int num2 = 0;

  void incNum2() {
    setState(() {
      this.num2++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Inherited(
        num1: num1,
        num2: num2,
        parent: this,
        child: const Child()
    );
  }
}

class Child extends StatelessWidget {
  const Child();

  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        Counter1(),
        Counter2(),
      ],
    );
  }
}

class Inherited extends InheritedModel<String> {
  final int num1;
  final int num2;
  final DataContainerState parent;

  static Inherited of(BuildContext context, String aspect) {
    return InheritedModel.inheritFrom<Inherited>(context, aspect: aspect) as Inherited;
  }

  Inherited({
    Key key,
    @required Widget child,
    this.num1,
    this.num2,
    this.parent,
  }): super(key: key, child: child);

  @override
  bool updateShouldNotify(Inherited oldWidget) {
    return num1 != oldWidget.num1 || num2 != oldWidget.num2;
  }

  @override
  bool updateShouldNotifyDependent(Inherited oldWidget,
      Set<String> dependencies) {
    var updateA =  num1 != oldWidget.num1 && dependencies.contains('num1');
    var updateB = num2 != oldWidget.num2 && dependencies.contains('num2');
    return updateA || updateB;
  }
}

class Counter1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Inherited c = Inherited.of(context, 'num1');
    return RaisedButton(
      child: Text('${c.num1}'),
      onPressed: c.parent.incNum1,
    );
  }
}

class Counter2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Inherited c = Inherited.of(context, 'num2');
    return RaisedButton(
      child: Text('${c.num2}'),
      onPressed: c.parent.incNum2,
    );
  }
}

const Child()是什么鬼?

const组件使flutter在更新的时候可以重用cache,提高性能。如果本次构建的组件和上次的属性相同又是const组件,则当前分支的更新停止。如果没有中间的const Child,Counter1, Counter2仍然会同时更新的。因为没有const时候整个分支都会重新构建。

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket