End

Flutter 陈航 20-通讯 数据传递 Notification EventBus

本文地址


目录

20 | 跨组件传递数据,只需要记住这三招

在 Flutter 中,实现跨组件数据传递的标准方式是通过属性传值。但是,对于视图层级比较深的 UI 样式,一个属性可能需要跨越很多层才能传递给子组件,这种传递方式会导致中间很多并不需要这个属性的组件,也需要接收其子 Widget 的数据,不仅繁琐而且冗余。

对于数据的跨层传递,Flutter 还提供了三种方案:

  • InheritedWidget:数据从父 Widget 传递到子 Widget
  • Notification:数据从子 Widget 传递到父 Widget
  • EventBus:跨组件通信

InheritedWidget

Theme 类是通过 InheritedWidget 实现的典型案例:在子 Widget 中通过 Theme.of 找到上层 Theme 的 Widget,并建立观察者关系,当上层父 Widget 属性修改的时候,子 Widget 也会触发更新。

InheritedWidget 仅提供了数据的能力,如果想要修改它的数据,需要把它和 State 配套使用:把 InheritedWidget 中的数据和相关的数据修改方法,全部移到 StatefulWidget 中的 State 上,而 InheritedWidget 只需要保留对它们的引用。

父 Widget

class CountContainer extends InheritedWidget {
  /// 提供一个 of 方法,方便其子 Widget 在 Widget 树中找到它
  static CountContainer of(BuildContext context) =>
      context.dependOnInheritedWidgetOfExactType<CountContainer>() as CountContainer;

  final IHomePageCount model;
  final String time = formatDate(DateTime.now(), [hh, '-', nn, '-', ss]);

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

  @override
  bool updateShouldNotify(CountContainer oldWidget) => model.getCount() != oldWidget.model.getCount(); // 是否需要重建
}

子 Widget

class Counter extends StatelessWidget {
  const Counter({super.key});

  @override
  Widget build(BuildContext context) {
    CountContainer parent = CountContainer.of(context); //获取 InheritedWidget 节点
    return Container(
      color: Colors.amber,
      child: GestureDetector(
        child: Text('时间 ${parent.time},已点击 ${parent.model.getCount()} 次'),
        onTap: () => parent.model.addCount(10),
      ),
    );
  }
}

Widget 树

class _HomePageState extends State<HomePage> implements IHomePageCount {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: CountContainer(
          model: this,
          // ignore: prefer_const_constructors
          child: Counter(), // 注意,千万别加 const 关键字,否则 Counter 将不会刷新
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => addCount(1),
          child: Text("$_count"),
        ));
  }

  @override
  int getCount() => _count;

  @override
  void addCount(int add) => setState(() => _count += add);
}
abstract class IHomePageCount {
  int getCount();

  void addCount(int add);
}
  • 无论子 Counter 在父 CountContainer 下层什么位置,都能获取到父 Widget 的属性 time 和 model
  • 通过 model 提供的方法,子 Counter 也可以间接更新父 CountContainer 的 time 属性

Notification

通过给 ListView 嵌套一层 NotificationListener,便可在 onNotification 回调中获取到子 Widget 的滚动事件。

Notification 通知的监听则与此类似。

定义通知

Notification 提供的 dispatch 方法,可以沿着 context 对应的 Element 节点树向上逐层发送通知。

class CustomNotification extends Notification {
  CustomNotification(this.msg);

  final String msg;

  @override
  void dispatch(BuildContext? target) {
    super.dispatch(target);
    flog("dispatch target=${target?.widget}");
  }
}

发送通知

点击子 Widget 时发送通知

class CustomChild extends StatelessWidget {
  const CustomChild({super.key});

  @override
  Widget build(BuildContext context) => ElevatedButton(
        onPressed: () => CustomNotification(DateTime.now().millisecond.toString()).dispatch(context),
        child: const Text("发送通知"),
      );
}

监听通知

通过给 子 Widget 嵌套一层 NotificationListener,便可在 onNotification 回调中获取到子 Widget 发送的通知。

NotificationListener<CustomNotification>(
  onNotification: (notification) {
    flog("收到子 Widget 发送的通知:${notification.msg}");
    setState(() => _msg = notification.msg);
    return true;
  },
  child: Column(children: <Widget>[Text(_msg), const CustomChild()]),
)

EventBus

InheritedWidget 和 Notification 的使用场景都需要依靠 Widget 树,只能在有父子关系的 Widget 之间进行数据共享。

事件总线 event_bus 是在 Flutter 中实现跨组件通信的机制。它遵循发布/订阅模式,发布者和订阅者之间无需有父子关系,并且非 Widget 对象也可以发布/订阅

dependencies:
  flutter:
    sdk: flutter
  event_bus: ^2.0.0

自定义事件类

EventBus 可以支持任意对象的传递。

class CustomEvent {
  String msg;

  CustomEvent(this.msg);
}

发送和监听

class _HomePageState extends State<HomePage> {
  EventBus eventBus = EventBus();
  String _msg = "通知";
  late StreamSubscription subscription;

  @override
  initState() {
    subscription = eventBus.on<CustomEvent>().listen((event) {
      flog("onData event=${event.msg}"); // 监听 CustomEvent 事件
      setState(() => _msg = event.msg);
    });
    super.initState();
  }

  @override
  dispose() {
    subscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Text(_msg),
        floatingActionButton: FloatingActionButton(
          onPressed: () => eventBus.fire(CustomEvent(DateTime.now().millisecond.toString())),
          child: Text(_msg),
        ));
  }
}

四种数据共享方式总结

  • 对于层级较深的视图,使用 InheritedWidget 可以实现子 Widget 跨层共享父 Widget 的属性
  • InheritedWidget 中的属性在子 Widget 中只能,和 StatefulWidget 中的 State 配套使用便可以修改
  • 使用 NotificationListener 可以在父 Widget 监听来自子 Widget 的事件
  • EventBus 是一种无需发布者与订阅者之间存在父子关系的数据同步机制

2023-1-7

posted @ 2023-01-07 21:23  白乾涛  阅读(257)  评论(0编辑  收藏  举报