Flutter 如何跨组件传递数据

2021-02-26 15:27:55 浏览数 (1)

1. InheritedWidget

InheritedWidget 是 Flutter 中非常重要的一个功能型 Widget,它可以高效的将数据在Widget 树中向下传递、共享,这在一些需要在 Widget 树中共享数据的场景中非常方便,如 Flutter 中,正是通过 InheritedWidget 来共享应用主题( Theme )Locale (当前语言环境)信息的。

假如我们 Widget 要往 Widget3 传值,一般情况下我们是这么写的。一层一层的传值,这样写十分麻烦。

点击按钮,count 1,在最下面的一个 widget 上面显示

代码语言:javascript复制
class InheritedDemo extends StatefulWidget {
  @override
  _InheritedDemoState createState() => _InheritedDemoState();
}

class _InheritedDemoState extends State<InheritedDemo> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        children: [
          Test1(count),
          RaisedButton(
              child: Text('我是按钮'),
              onPressed: () => setState(() {
                count  ;
              }))
        ],
      ),
    );
  }
}

class Test1 extends StatelessWidget {
 final count;
 Test1(this.count);
  @override
  Widget build(BuildContext context) {
    return Test2(count);
  }
}
class Test2 extends StatelessWidget {
 final count;
 Test2(this.count);
  @override
  Widget build(BuildContext context) {
    return Test3(count);
  }
}
class Test3 extends StatefulWidget {
 final count;
 Test3(this.count);
  @override
  _Test3State createState() => _Test3State();
}
class _Test3State extends State<Test3> {
  @override
  Widget build(BuildContext context) {
    return Text(widget.count.toString());
  }
}

Flutter 给我们提供了一个 InheritedWidget 组件,来帮助我们完成上面功能。

首先我们需要定义一个继承 InheritedWidget 的 widget MyData

代码语言:javascript复制
class MyData extends InheritedWidget {
  MyData({this.data, Widget child}) : super(child: child);

  final int data; //需要在子树中共享的数据,保存点击次数
  //定义一个便捷方法,方便子树中的widget获取共享数据
  static MyData of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyData>();
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(MyData old) {
    //如果返回true,则子树中依赖(build函数中有调用)本widget
    //的子widget的`state.didChangeDependencies`会被调用
    return old.data != data;
  }
}

然后使用 MyData 包裹整个 widget

代码语言:javascript复制
class InheritedDemo extends StatefulWidget {
  @override
  _InheritedDemoState createState() => _InheritedDemoState();
}

class _InheritedDemoState extends State<InheritedDemo> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return MyData(
        data: count,
        child: Column(
          children: <Widget>[
            Test1(),
            RaisedButton(
              child: Text('我是按钮'),
              onPressed: () => setState(() {
                count  ;
              }),
            )
          ],
        ));
  }
}

class Test1 extends StatelessWidget {
//  final count;
//  Test1(this.count);
  @override
  Widget build(BuildContext context) {
    return Test2();
  }
}

class Test2 extends StatelessWidget {
//  final count;
//  Test2(this.count);
  @override
  Widget build(BuildContext context) {
    return Test3();
  }
}

class Test3 extends StatefulWidget {
//  final count;
//  Test3(this.count);
  @override
  _Test3State createState() => _Test3State();
}

class _Test3State extends State<Test3> {
  @override
  Widget build(BuildContext context) {
    return Text(MyData.of(context).data.toString());
  }

  @override
  void didChangeDependencies() {
    print("didChangeDependencies来了!");
    super.didChangeDependencies();
  }
}

2. Notification

Notification 是 Flutter 中进行跨层数据共享的另一个重要的机制。如果说 InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递,那 Notificaiton 则恰恰相反,数据流动方式是从子 Widget 向上传递至父 Widget。这样的数据传递机制适用于子 Widget 状态变更,发送通知上报的场景。

Flutter 中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。

如果想要实现自定义通知,我们首先需要继承 Notification 类。Notification 类提供了 dispatch 方法,可以让我们沿着 context 对应的 Element 节点树向上逐层发送通知

2.1 定义一个通知类,要继承自 Notification 类
代码语言:javascript复制
class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}
2.2 分发通知

Notification有一个 dispatch(context) 方法,它是用于分发通知的,我们说过 context 实际上就是操作 Element 的一个接口,它与 Element 树上的节点是对应的,通知会从context 对应的 Element 节点向上冒泡。

代码语言:javascript复制
//子widget
class ChildNotificationWidget extends StatefulWidget {
  @override
  _ChildNotificationWidgetState createState() => _ChildNotificationWidgetState();
}

class _ChildNotificationWidgetState extends State<ChildNotificationWidget> {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 按钮点击时分发通知
      onPressed: () => MyNotification("你好,Flutter").dispatch(context),
      child: Text("Fire Notification"),
    );
  }
}

//父widget
class parentNotificationWidget extends StatefulWidget {
  @override
  _parentNotificationWidgetState createState() => _parentNotificationWidgetState();
}

class _parentNotificationWidgetState extends State<parentNotificationWidget> {
  String _msg = " 通知:";
  @override
  Widget build(BuildContext context) {
    // 监听通知
    return NotificationListener<MyNotification>(
        onNotification: (notification) {
          setState(() {_msg  = notification.msg "  ";});// 收到子 Widget 通知,更新 msg
        },
        child:Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(_msg),
            ChildNotificationWidget()],// 将子 Widget 加入到视图树中
        )
    );
  }
}

3. 事件总线 EventBus

无论是 InheritedWidget 还是 Notificaiton,它们的使用场景都需要依靠 Widget 树,也就意味着只能在有父子关系的 Widget 之间进行数据共享。但是,组件间数据传递还有一种常见场景:这些组件间不存在父子关系。这时,事件总线 EventBus 就登场了。

事件总线是在 Flutter 中实现跨组件通信的机制。它遵循发布 / 订阅模式,允许订阅者订阅事件,当发布者触发事件时,订阅者和发布者之间可以通过事件进行交互。发布者和订阅者之间无需有父子关系,甚至非 Widget 对象也可以发布 / 订阅。这些特点与其他平台的事件总线机制是类似的。

接下来,我们通过一个跨页面通信的例子,来看一下事件总线的具体使用方法。需要注意的是,EventBus 是一个第三方插件,因此我们需要在 pubspec.yaml 文件中声明它:

代码语言:javascript复制
dependencies:  
  event_bus: ^1.1.1
代码语言:javascript复制

本节我们实现一个简单的全局事件总线,我们使用单例模式,代码如下:

代码语言:javascript复制
//订阅者回调签名
typedef void EventCallback(arg);

class EventBus {
  //私有构造函数
  EventBus._internal();

  //保存单例
  static EventBus _singleton = new EventBus._internal();

  //工厂构造函数
  factory EventBus()=> _singleton;

  //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
  var _emap = new Map<Object, List<EventCallback>>();

  //添加订阅者
  void on(eventName, EventCallback f) {
    if (eventName == null || f == null) return;
    _emap[eventName] ??= new List<EventCallback>();
    _emap[eventName].add(f);
  }

  //移除订阅者
  void off(eventName, [EventCallback f]) {
    var list = _emap[eventName];
    if (eventName == null || list == null) return;
    if (f == null) {
      _emap[eventName] = null;
    } else {
      list.remove(f);
    }
  }

  //触发事件,事件触发后该事件所有订阅者会被调用
  void emit(eventName, [arg]) {
    var list = _emap[eventName];
    if (list == null) return;
    int len = list.length - 1;
    //反向遍历,防止订阅者在回调中移除自身带来的下标错位
    for (var i = len; i > -1; --i) {
      list[i](arg);
    }
  }
}

//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = new EventBus();

注意:Dart 中实现单例模式的标准做法就是使用 static 变量 工厂构造函数的方式,这样就可以保证 new EventBus() 始终返回都是同一个实例

上面代码转载自:事件总线

代码语言:javascript复制
class EventBusPage extends StatefulWidget {
  @override
  _EventBusPageState createState() => _EventBusPageState();
}

class _EventBusPageState extends State<EventBusPage> {
  String msg = "通知:";
  @override
  void initState() {
    // TODO: implement initState
    bus.on("eventName", (arg) {
      print(arg);
      setState(() {
        msg  = arg;
      });
    });
    super.initState();
  }
  dispose() {
    bus.off("eventName");//State销毁时,清理注册
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Text(msg),
          RaisedButton(
            child: Text('进入二级界面'),
              onPressed: (){
                Navigator.push(context, MaterialPageRoute(builder: (BuildContext context){
                  return EventBusTwoPage();
                }));
              })
        ],
      ),
    );
  }
}

class EventBusTwoPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("EventBusTwoPage"),),
      body: Center(
        child: RaisedButton(
          child: Text('二级界面'),
          onPressed: (){
            bus.emit("eventName","二级页面来通知了");
            Navigator.pop(context);
          },
        ),
      )
    );
  }
}

0 人点赞