34

FlutterDojo设计之道—状态管理之路(四)

 3 years ago
source link: https://blog.csdn.net/x359981514/article/details/108373565
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在Flutter中,跨Widget的数据共享,可以如下图这样表示。

VVvIBf.png!mobile

当Child Widget想要跨Widget拿到其它Widget的数据时,通常就需要使用构造函数,将数据一层层传递到Child Widget,这显然不是一个好的解决方案,不仅让Widget之间有了很大的耦合,也产生很多的冗余数据。

为了解决这个问题,Flutter SDK提供了InheritedWidget这个Widget,InheritedWidget是除了StatefulWidget和StatelessWidget之外的第三个常用的Widget。当把InheritedWidget作为Widget Tree的根节点时,这个Widget Tree就具有了一些新的功能,例如,Child Widget可以根据BuildContext找到最近的指定类型的InheritedWidget,而不是通过Widget Tree的构造函数一层层进行传递,如下图所示。

naumueb.png!mobile

InheritedWidget的使用其实非常简单,即共享数据给Child。所以它的核心点,其实就是两个。

  • 需要共享的数据

  • 重新updateShouldNotify的条件

通过BuildContext的dependOnInheritedWidgetOfExactType函数,就可以直接获取父Widget中的InheritedWidget。所以在InheritedWidget内部,通常会有一个of函数,用过调用BuildContext的dependOnInheritedWidgetOfExactType函数来获取对应的父InheritedWidget。

只读的InheritedWidget

InheritedWidget默认情况下都是只读的,即只能将某个数据共享给Child Widget,而不能让Child Widget对数据做更新。下面这个例子演示了一个最基本的InheritedWidget是如何共享数据的。

class InheritedWidgetReadOnlyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ReadOnlyRoot(
      count: 1008,
      child: ChildReadOnly(),
    );
  }
}

class ChildReadOnly extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build');
    ReadOnlyRoot root = ReadOnlyRoot.of(context);
    return Column(
      children: <Widget>[
        SubtitleWidget('InheritedWidget本身不具有写数据的功能,需要结合State来获取数据修改的能力'),
        Text(
          'show ${root.count}',
          style: TextStyle(fontSize: 20),
        ),
      ],
    );
  }
}

// 仅支持读取属性
class ReadOnlyRoot extends InheritedWidget {
  static ReadOnlyRoot of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ReadOnlyRoot>();

  final int count;

  ReadOnlyRoot({
    Key key,
    @required this.count,
    @required Widget child,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(ReadOnlyRoot oldWidget) => count != oldWidget.count;
}

给InheritedWidget增加读写功能

数据的状态通常情况下都是保存在StatefulWidget的State中的,所以,InheritedWidget必须要结合StatefulWidget才能具有修改数据的能力,因此,思路就是在InheritedWidget中持有一个StatefulWidget的State实例,同时,使用一个StatefulWidget,将原本的Child Widget之上,插入这个InheritedWidget,这样就可以借助StatefulWidget来完成数据的修改能力,通过InheritedWidget来实现数据的共享能力。

class RootContainer extends StatefulWidget {
  final Widget child;

  RootContainer({
    Key key,
    this.child,
  }) : super(key: key);

  @override
  _RootContainerState createState() => _RootContainerState();

  static _RootContainerState of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<Root>().state;
}

class _RootContainerState extends State<RootContainer> {
  int count = 0;

  void incrementCounter() => setState(() => count++);

  @override
  Widget build(BuildContext context) {
    return Root(state: this, child: widget.child);
  }
}

// 同时支持读取和写入
class Root extends InheritedWidget {
  final _RootContainerState state;

  Root({
    Key key,
    @required this.state,
    @required Widget child,
  }) : super(key: key, child: child);

  // 判断是否需要更新
  @override
  bool updateShouldNotify(Root oldWidget) => true;
}

在这种写法中,InheritedWidget(Root)是在StatefulWidget(RootContainer)中初始化的,当使用StatefulWidget(RootContainer)的setState函数时,InheritedWidget(Root)重建了,但是其child并不会重建,因为它是widget.child,并不会因为State的重建而重建。

要注意的是,虽然这里的StatefulWidget通过setState来修改数据了,但其子Widget并不会全部重绘,因为InheritedWidget的存在,Child Widget会有选择性的进行重绘。

在这基础上,使用就比较简单了,代码如下所示。

class InheritedWidgetWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RootContainer(
      child: Column(
        children: <Widget>[
          Widget1(),
          Widget2(),
          Widget3(),
        ],
      ),
    );
  }
}

class Widget1 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget1');
    return SubtitleWidget('InheritedWidget本身不具有写数据的功能,需要结合State来获取数据修改的能力');
  }
}

class Widget2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget2');
    return Text(
      'show ${RootContainer.of(context).count}',
      style: TextStyle(fontSize: 20),
    );
  }
}

class Widget3 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    debugPrint('build Widget3');
    return RaisedButton(
      onPressed: () {
        RootContainer.of(context).incrementCounter();
      },
      child: Text('Add'),
    );
  }
}

相关代码 Flutter Dojo-Widgets-Async-InheritedWidget

在上面这个Demo中,Widget2、3分别获取和修改了InheritedWidget中的共享数据,实现了跨Widget的数据共享。

通过Log我们可以发现,初始化的时候,Widget1、2、3都执行了build,但点击的时候,只有Widget2、3重新build了,但是Widget1并不会重新build。

这是什么原因呢?

其实这就是RootContainer.of(context)导致的。

当我们执行RootContainer.of(context)这个函数的时候,实际上调用的是context.dependOnInheritedWidgetOfExactType函数,这个函数不仅仅会返回指定类型的InheritedWidget,同时也会将Context对应的Widget添加到订阅者列表中,也就是说,即使你调用这个函数,只是为了执行某个函数,并不是想刷新UI,但是系统依然认为你需要刷新,从而导致Widget2、3都会执行rebuild。而Widget1,由于没有调用过of函数,所以不会被添加到订阅者列表中,所以不会执行rebuild。

要想解决这个问题也非常简单,那就是在不需要监听的时候,使用findAncestorWidgetOfExactType即可,这个函数只会返回指定类型的Widget,而不会将监听加入订阅者列表中。

static _RootContainerState ofNoBuild(BuildContext context) => context.findAncestorWidgetOfExactType<Root>().state;

点击按钮的函数,只需要调用上面的这个函数,在点击的时候,Widget3就不会执行rebuild了。

除了这种方式以外,还有一个方式,那就是通过const关键字,将一个Widget设置为常量Widget,即不会发生改变,这个时候rebuild的时候,系统会发现const Widget并没有发生改变,就不会rebuild了,这也是为什么在Flutter中,很多不需要改变的Padding、Margin、Theme、Size等参数需要尽可能设置为const的原因,这样可以在rebuild的时候,提高效率。

在Flutter中,Theme的实现,就是采用的这种方式。

Widget Tree的遍历

前面提到了两种方式来获取Widget Tree中的InheritedWidget,dependOnInheritedWidgetOfExactType和findAncestorWidgetOfExactType,从调用结果上来看,一种是会被加入订阅者名单,一种只是单纯的查找。

下面再来继续仔细的看看这两个函数的区别。

findAncestorWidgetOfExactType

首先来看下这个函数的注释。

ZRR7raF.png!mobile

从中我们可以提取几个关键信息。

  • 不会触发rebuild

  • O(n)复杂度

  • 最好在didChangeDependencies中调用

所以findAncestorWidgetOfExactType有几个比较常用的使用场景。

  • 在断言中判断父Widget的使用条件

  • 获取父Widget对象,调用其方法

例如在一些Widget中,可以通过Assert来判断当前是否有使用该Widget的条件,例如Hero Widget。

UBZzeuU.png!mobile

dependOnInheritedWidgetOfExactType

首先也来看下这个函数的注释。

7NJBNfV.png!mobile
  • 会触发rebuild

  • O(1)复杂度

  • 最好在didChangeDependencies中调用

可以发现,其实他跟findAncestorWidgetOfExactType是非常类似的,主要的区别还是在于是否会rebuild,另外,dependOnInheritedWidgetOfExactType的效率很高。

项目地址 Flutter Dojo


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK