阅读 1292

Flutter之跨Widget传递数据

在Flutter中,UI是由不同粒度的Widget组成,这也导致了一些数据需要在Widget间传递。一般情况下,都是通过属性来进行数据的传递。但如果涉及到跨层传递时,属性可能需要跨越很多层才能传递给子组件,导致中间很多并不需要这个属性的组件,也得接收其子Widget的数据,繁琐且冗余。

所以这时候就需要使用其他方案来进行跨层传输,目前主要有InheritedWidgetNotificationEventBus三种方案来实现数据的跨层传输。

1、InheritedWidget

InheritedWidget主要用于底层Widget向上层Widget获取数据,也就是数据的传递方向是从由父Widget传递给子Widget,也就是说两个Widget必须有父子关系才能使用InheritedWidget。

1.1、InheritedWidget的使用

下面来看InheritedWidget的使用,如下。

//创建一个继承自InheritedWidget的类
class CountContainer extends InheritedWidget {
  //获取类型是CountContainer的InheritedWidget,时间复杂度是O(1)
  static CountContainer of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountContainer>();

  final int count;

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

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

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    //将CountContainer作为根节点,并使用0作为初始化count
    return CountContainer(count: 0, child: Counter());
  }
}
//CountContainer的使用
class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //获取InheritedWidget节点
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${state.count}',
      ),
    );
  }
}
复制代码

在上面代码中,有一个问题。那就是count的值是无法修改的。但如果要修改count的值,可以使用ChangeNotifier来进行值的更新。

class CountContainer<T extends ValueNotifier> extends InheritedNotifier {
  static CountContainer of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountContainer>();

  const CountContainer({
    Key key,
    this.notifier,
    @required Widget child,
  })  : assert(child != null),
        super(key: key, child: child);
  final T notifier;
}
class _MyHomePageState extends State<MyHomePage> {
  //ValueNotifier继承自ChangeNotifier
  ValueNotifier _valueNotifier;
  int count = 0;

  @override
  void initState() {
    //创建一个ValueNotifier对象
    _valueNotifier = new ValueNotifier(0);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return CountContainer(
      notifier: _valueNotifier,
      child: GestureDetector(
        onTap: () {
          count++;
          //值的更新
          _valueNotifier.value = count;
        },
        child: Counter(),
      ),
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${CountContainer.of(context).notifier.value}',
      ),
    );
  }
}
复制代码

通过上面代码,就能够使用InheritedWidge+ChangeNotifier来获取父Widget中可变化的数据。

provider中数据更新就是通过InheritedWidge+ChangeNotifier来实现的。

1.2、InheritedWidget的实现原理

再来看InheritedWidget的实现原理。它的核心实现再其InheritedElement中,代码如下。

class InheritedElement extends ProxyElement {
  /// Creates an element that uses the given widget as its configuration.
  InheritedElement(InheritedWidget widget) : super(widget);

  @override
  InheritedWidget get widget => super.widget as InheritedWidget;

  //存储Element
  final Map<Element, Object> _dependents = HashMap<Element, Object>();

  @override
  void _updateInheritance() {
    //获取父Element的_inheritedWidgets
    final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
    if (incomingWidgets != null)
      _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
    else
      _inheritedWidgets = HashMap<Type, InheritedElement>();
    //将InheritedElement以widget.runtimeType为key添加到_inheritedWidgets中
    _inheritedWidgets[widget.runtimeType] = this;
  }
  
  @protected
  Object getDependencies(Element dependent) {
    //根据Element从_dependents中获取存储的值
    return _dependents[dependent];
  }

  @protected
  void setDependencies(Element dependent, Object value) {
    //将Element添加到_dependents中,value可为null
    _dependents[dependent] = value;
  }

  @protected
  void updateDependencies(Element dependent, Object aspect) {
    setDependencies(dependent, null);
  }

  @protected
  void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
    //调用Element的didChangeDependencies方法,在didChangeDependencies方法中会重新绘制dependent所对应的Widget
    dependent.didChangeDependencies();
  }

  @override
  void updated(InheritedWidget oldWidget) {
    if (widget.updateShouldNotify(oldWidget))
      super.updated(oldWidget);
  }
  @override
  void notifyClients(InheritedWidget oldWidget) {
    //根据_dependents来遍历
    for (final Element dependent in _dependents.keys) {
      notifyDependent(oldWidget, dependent);
    }
  }
}

abstract class Element extends DiagnosticableTree implements BuildContext {
  //...
  void _updateInheritance() {
    //将父Element中_inheritedWidgets的值赋给当前Element的_inheritedWidgets
    _inheritedWidgets = _parent?._inheritedWidgets;
  }
  //...
}
复制代码

先来看属性_inheritedWidgets,它是Element中的一个Map。如果当前Element是InheritedElement,则将当前Element添加到_inheritedWidgets中,否则就会将其父Element中的_inheritedWidgets的数据拷贝给当前Element的_inheritedWidgets。也就是每个Element中会存储当前Element到根Element中所有的InheritedElement对象,所以这也是dependOnInheritedWidgetOfExactType的时间复杂度为O(1)的原因。

abstract class Element extends DiagnosticableTree implements BuildContext {
  //...
  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    //将当前Element添加到`InheritedElement`的`_dependents`中
    ancestor.updateDependencies(this, aspect);
    //返回一个InheritedWidget对象
    return ancestor.widget;
  }
  //...
  @override
  T dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object aspect}) {
    //根据T的类型从_inheritedWidgets中获取InheritedElement,时间复杂度为O(1)
    final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
    if (ancestor != null) {
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }
  //...
}
复制代码

在通过dependOnInheritedWidgetOfExactType获取InheritedWidget时,也会把当前Element添加到InheritedElement的_dependents中。

当调用InheritedElement的notifyClients方法时,就会遍历其_dependents,然后执行_dependents中Element的didChangeDependencies方法来更新UI。

abstract class Element extends DiagnosticableTree implements BuildContext {
  //...
  @mustCallSuper
  void didChangeDependencies() {
    //将当前Element标记为脏,在setState中最终调用的也是markNeedsBuild方法。
    markNeedsBuild();
  }
  //...
}
复制代码

2、Notification

Notification主要用于底层Widget向上层Widget传递数据,也就是数据方向的传递方向是从子Widget传递给父Widget。也就是两个Widget必须有父子关系才能使用Notification。

2.1、Notification的使用

Notification需要与NotificationListener搭配使用。

//创建一个Notification对象
class CountContainer extends Notification {
  final int count;

  CountContainer(this.count);
}

class _MyHomePageState extends State<MyHomePage> {
  int count = 0;
  int num = 0;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: NotificationListener<CountContainer>(
        //接受子widget传递过来的值
        onNotification: (CountContainer container) {
          setState(() {
            num = container.count + 1;
          });
          //返回false则表示还可以继续向上传递数据
          return false;
        },
        child: Column(
          children: [
            Text("当前num的值:$num"),
            NotificationListener<CountContainer>(
              child: Column(
                children: [Text("当前count的值:$count"), Counter()],
              ),
              //接受子widget传递过来的值
              onNotification: (CountContainer container) {
                setState(() {
                  count = container.count;
                });
                //返回false则表示还可以继续向上传递数据
                return false;
              },
            )
          ],
        ),
      ),
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
      child: Text("更新UI"),
      onPressed: () {
        //向父Widget传递数据
        CountContainer(1).dispatch(context);
      },
    );
  }
}
复制代码

使用很简单,这里要注意一点的是onNotification对应函数的返回值。如果返回值为false,则表示数据还可以继续向上传递,直到根Widget。否则就到当前Widget为止。

2.2、Notification的实现原理

Notification的实现原理就很简单了。就是从当前Widget出发,向上遍历,找到对应的NotificationListener并把数据交给NotificationListener的_dispatch方法进行分发。代码实现如下。

abstract class Notification {
  const Notification();

  
  @protected
  @mustCallSuper
  bool visitAncestor(Element element) {
    if (element is StatelessElement) {
      final StatelessWidget widget = element.widget;
      if (widget is NotificationListener<Notification>) {
        //找到NotificationListener并交给其的_dispatch方法来处理数据
        if (widget._dispatch(this, element)) // that function checks the type dynamically
          return false;
      }
    }
    return true;
  }

  void dispatch(BuildContext target) {
    //从当前Widget向上遍历查找指定的NotificationListener,时间复杂度为O(n)
    target?.visitAncestorElements(visitAncestor);
  }
}
复制代码

visitAncestorElements就是向上遍历Element的实现,而visitAncestor则是判断当前Widget是否是指定的NotificationListener的实现。

abstract class Element extends DiagnosticableTree implements BuildContext {
  //...
  @override
  void visitAncestorElements(bool visitor(Element element)) {
    Element ancestor = _parent;
    //如果visitor返回true且ancestor不为null,则表示继续向上遍历
    while (ancestor != null && visitor(ancestor))
      ancestor = ancestor._parent;
  }
  //...
}
复制代码

visitAncestorElements的实现很简单,如上。所以查找指定NotificationListener的时间复杂度为O(n)。

3、EventBus

InheritedWidget与Notification必须要求两个Widget有父子关系才可进行数据的传输。但如果两个Widget是兄弟关系的话,那么就无法使用InheritedWidget及Notification。这时候就可以使用EventBus来进行数据的传输,当然具有父子关系的Widget也可以使用EventBus来进行数据的传输。

3.1、EventBus的使用

首先需要导入EventBus,如下。

  event_bus: ^1.1.1
复制代码

导入成功后,下面就可以直接来使用EventBus。

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("EventBus Demo"),
      ),
      body: Column(
        children: [
          //接受消息Widget
          MessageWidget(),
          //发送消息Widget
          OtherWidget(),
        ],
      ),
    );
  }
}

class Message {
  final String text;

  const Message(this.text);
}

class MessageWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => MessageWidgetState();
}

EventBus eventBus = new EventBus();

//展示消息Widget
class MessageWidgetState extends State<MessageWidget> {
  String text = "没任何消息";
  StreamSubscription subscription;

  @override
  void initState() {
    subscription = eventBus.on<Message>().listen((event) {
      setState(() {
        text = event.text;
      });
    });
    super.initState();
  }

  @override
  void dispose() {
    //不取消则存在内存泄漏
    subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text("当前消息:$text");
  }
}
//发送消息Widget
class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TextButton(
        onPressed: () {
          eventBus.fire(Message("消息测试"));
        },
        child: Text("发送消息"));
  }
}
复制代码

在上面代码中,MessageWidget与OtherWidget是兄弟关系,所以无法使用InheritedWidget与Notification来实现两个Widget间的通信,而使用EventBus正好实现了两者间的通信。

EventBus是基于Stream来实现的,关于其实现原理可以参考Flutter完整开发实战详解(十一、全面深入理解Stream)这篇文章。

4、总结

本文主要是对Flutter中Widget间通信做了一个介绍。虽然目前都推荐使用Provider,但还是需要了解本文中的三种方案,这样才能更好的使用provider及阅读provider源码。当然本文也是分析provider实现原理的前篇。

文章分类
Android
文章标签