Flutter通知

487 阅读4分钟

通知

通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。

例如监听ListView

NotificationListener(
  onNotification: (notification){
    switch (notification.runtimeType){
      case ScrollStartNotification: print("开始滚动"); break;
      case ScrollUpdateNotification: print("正在滚动"); break;
      case ScrollEndNotification: print("滚动停止"); break;
      case OverscrollNotification: print("滚动到边界"); break;
    }
    return true;
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"),);
    }
  ),
);
复制代码

NotificationListener的定义。

class NotificationListener<T extends Notification> extends StatelessWidget {
  /// Creates a widget that listens for notifications.
  const NotificationListener({
    Key? key,
    required this.child,
    this.onNotification,
  }) : super(key: key);
复制代码

他继承至StatelessWidget,所以它可以直接嵌套到 Widget 树中。

NotificationListener 可以指定一个模板参数,该模板参数类型必须是继承自Notification;当显式指定模板参数时,NotificationListener 便只会接收该参数类型的通知。举个例子,如果我们将上例子代码改为:

NotificationListener<ScrollEndNotification>(
  onNotification: (notification){
    //只会在滚动结束时才会触发此回调
    print(notification);
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"),);
    }
  ),
);
复制代码

上面代码运行后便只会在滚动结束时在控制台打印出通知的信息

Flutter的UI框架实现中,除了在可滚动组件在滚动过程中会发出ScrollNotification之外,还有一些其它的通知,如SizeChangedLayoutNotificationKeepAliveNotification 、LayoutChangedNotification等,Flutter正是通过这种通知机制来使父元素可以在一些特定时机来做一些事情。

监听通知

Flutter中很多地方使用了通知,如前面介绍的 Scrollable 组件,它在滑动时就会分发滚动通知(ScrollNotification),而 Scrollbar 正是通过监听 ScrollNotification 来确定滚动条位置的。

下面是一个监听可滚动组件滚动通知的例子:

NotificationListener(
  onNotification: (notification){
    switch (notification.runtimeType){
      case ScrollStartNotification: print("开始滚动"); break;
      case ScrollUpdateNotification: print("正在滚动"); break;
      case ScrollEndNotification: print("滚动停止"); break;
      case OverscrollNotification: print("滚动到边界"); break;
    }
  },
  child: ListView.builder(
    itemCount: 100,
    itemBuilder: (context, index) {
      return ListTile(title: Text("$index"),);
    }
  ),
);

上例中的滚动通知如ScrollStartNotificationScrollUpdateNotification等都是继承自ScrollNotification类,不同类型的通知子类会包含不同的信息,比如ScrollUpdateNotification有一个scrollDelta属性,它记录了移动的位移,其它通知属性读者可以自己查看SDK文档。

上例中,我们通过NotificationListener来监听子ListView的滚动通知的,NotificationListener定义如下:

class NotificationListener<T extends Notification> extends StatelessWidget {
  const NotificationListener({
    Key key,
    required this.child,
    this.onNotification,
  }) : super(key: key);
 ...//省略无关代码 
}  

我们可以看到:

  1. NotificationListener 继承自StatelessWidget类,所以它可以直接嵌套到 Widget 树中。

  2. NotificationListener 可以指定一个模板参数,该模板参数类型必须是继承自Notification;当显式指定模板参数时,NotificationListener 便只会接收该参数类型的通知。举个例子,如果我们将上例子代码改为:

    //指定监听通知的类型为滚动结束通知(ScrollEndNotification)
    NotificationListener<ScrollEndNotification>(
      onNotification: (notification){
        //只会在滚动结束时才会触发此回调
        print(notification);
      },
      child: ListView.builder(
        itemCount: 100,
        itemBuilder: (context, index) {
          return ListTile(title: Text("$index"),);
        }
      ),
    );
    

    上面代码运行后便只会在滚动结束时在控制台打印出通知的信息。

  3. onNotification回调为通知处理回调,其函数签名如下:

    typedef NotificationListenerCallback<T extends Notification> = bool Function(T notification);
    

    它的返回值类型为布尔值,当返回值为true时,阻止冒泡,其父级Widget将再也收不到该通知;当返回值为false 时继续向上冒泡通知。

Flutter的UI框架实现中,除了在可滚动组件在滚动过程中会发出ScrollNotification之外,还有一些其它的通知,如SizeChangedLayoutNotificationKeepAliveNotificationLayoutChangedNotification等,Flutter正是通过这种通知机制来使父元素可以在一些特定时机来做一些事情。

自定义通知

除了 Flutter 内部通知,我们也可以自定义通知,下面我们看看如何实现自定义通知:

  1. 定义一个通知类,要继承自Notification类;

    class MyNotification extends Notification {
      MyNotification(this.msg);
      final String msg;
    }
    
  2. 分发通知。

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

下面我们看一个完整的例子:

class NotificationRoute extends StatefulWidget {
  @override
  NotificationRouteState createState() {
    return NotificationRouteState();
  }
}

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //监听通知  
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        setState(() {
          _msg+=notification.msg+"  ";
        });
       return true;
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
//           ElevatedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),  
            Builder(
              builder: (context) {
                return ElevatedButton(
                  //按钮点击时分发通知  
                  onPressed: () => MyNotification("Hi").dispatch(context),
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}

class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}

上面代码中,我们每点一次按钮就会分发一个MyNotification类型的通知,我们在Widget根上监听通知,收到通知后我们将通知通过Text显示在屏幕上。

注意:代码中注释的部分是不能正常工作的,因为这个context是根Context,而NotificationListener是监听的子树,所以我们通过Builder来构建ElevatedButton,来获得按钮位置的context。

运行效果如图8-7所示:

图8-7

阻止通知冒泡

我们将上面的例子改为:

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //监听通知
    return NotificationListener<MyNotification>(
      onNotification: (notification){
        print(notification.msg); //打印通知
        return false;
      },
      child: NotificationListener<MyNotification>(
        onNotification: (notification) {
          setState(() {
            _msg+=notification.msg+"  ";
          });
          return false; 
        },
        child: ...//省略重复代码
      ),
    );
  }
}

上列中两个NotificationListener进行了嵌套,子NotificationListeneronNotification回调返回了false,表示不阻止冒泡,所以父NotificationListener仍然会受到通知,所以控制台会打印出通知信息;如果将子NotificationListeneronNotification回调的返回值改为true,则父NotificationListener便不会再打印通知了,因为子NotificationListener已经终止通知冒泡了。

通知冒泡原理

1、xxxNotification.dispatch(context) 分发通知的时候就是调用 context.visitAncestorElements(visitor) 方法。

2、context.visitAncestorElements()方法用来从当前节点向上遍历父节点,找到一个 NotificationLister 类型的控件,然后调用它的 onNotification() 回调方法。

3、回调方法的入参是来自子节点分发过来的通知 (xxxNotification),回调方法的返回值用来判断是否要拦截通知 (xxxNotification)。

4、一层层的向上找 NotificationLister类型父节点并分发通知 (xxxNotification);若不拦截则继续向上寻找,直到根节点为止。这就是我们说的冒泡通知原理了。