Flutter 之 通知 Notification (五十二)

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

注意:通知冒泡和Web开发中浏览器事件冒泡原理是相似的,都是事件从出发源逐层向上传递,我们可以在上层节点任意位置来监听通知/事件,也可以终止冒泡过程,终止冒泡后,通知将不会再向上传递。

1. 监听通知

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


class MSNotificationDemo1 extends StatelessWidget {
  const MSNotificationDemo1({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("NotificationDemo1")),
      body: NotificationListener(
        onNotification: (notification) {
          switch (notification.runtimeType) {
            case ScrollStartNotification:
              print("开始滚动");
              break;
            case ScrollUpdateNotification:
              print("正在滚动");
              break;
            case ScrollEndNotification:
              print("结束滚动");
              break;
            default:
          }
          return true;
        },
        child: ListView.builder(
          itemBuilder: (ctx, index) {
            return ListTile(
              title: Text("$index"),
            );
          },
          itemCount: 100,
          itemExtent: 56,
        ),
      ),
    );
  }
}

image.png

上例中的滚动通知如ScrollStartNotification、ScrollUpdateNotification等都是继承自ScrollNotification类,不同类型的通知子类会包含不同的信息,比如ScrollUpdateNotification有一个scrollDelta属性,它记录了移动的位移。

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

  const NotificationListener({
    Key? key,
    required this.child,
    this.onNotification,
  }) :

我们可以看到:

    1. NotificationListener 继承自StatelessWidget类,所以它可以直接嵌套到 Widget 树中。
    1. NotificationListener 可以指定一个模板参数,该模板参数类型必须是继承自Notification;当显式指定模板参数时,NotificationListener 便只会接收该参数类型的通知。举个例子,如果我们将上例子代码改为:
//指定监听通知的类型为滚动结束通知(ScrollEndNotification)
NotificationListener<ScrollEndNotification>(
  onNotification: (notification) {
    // 只有滚动结束时才会回调
    print(notification.runtimeType);
    return true;
  },
  child: ListView.builder(
    itemBuilder: (ctx, index) {
      return ListTile(title: Text("$index"));
    },
    itemCount: 100,
  ),
),

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

    1. onNotification回调为通知处理回调,其函数签名如下:
typedef NotificationListenerCallback<T extends Notification> = bool Function(T notification);

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

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

2. 自定义通知

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

  1. 定义一个通知类,要继承自Notification类;
class MSCustomNotification extends Notification {
  MSCustomNotification(this.msg);
  final String msg;
}
  1. 分发通知。

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

示例


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


class MSNotificationDemo3 extends StatefulWidget {
  const MSNotificationDemo3({Key? key}) : super(key: key);

  @override
  State<MSNotificationDemo3> createState() => _MSNotificationDemo3State();
}

class _MSNotificationDemo3State extends State<MSNotificationDemo3> {
  String _msg = "";
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("NotificationDemo3")),
      // 监听指定通知 MSCustomNotification
      body: NotificationListener<MSCustomNotification>(
        onNotification: (notification) {
          _msg += notification.msg;
          setState(() {});
          return true; // 阻止冒泡
        },
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              // 不能这样写 
              // 此处context是根Context,而NotificationListener监听的是子Widget
              // ElevatedButton(
              //   onPressed: () => MSCustomNotification("Hi").dispatch(context),
              //   child: Text("Send Notification"),
              // ),
              Builder(
                builder: (context) {
                  return ElevatedButton(
                    onPressed: () {
                      // 分发通知
                      MSCustomNotification("Hi ").dispatch(context);
                    },
                    child: Text("Send Notification"),
                  );
                },
              ),
              Text(
                _msg,
                textScaleFactor: 1.5,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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

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

image.png

3. 阻止通知冒泡

我们将上面的例子改为:

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

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

4. 冒泡原理

我们在上面介绍了通知冒泡的现象及使用,现在我们更深入一些,介绍一下Flutter框架中是如何实现通知冒泡的。
我们从通知分发的的源头出发,然后再顺藤摸瓜。由于通知是通过Notification的dispatch(context)方法发出的,那我们先看看dispatch(context)方法中做了什么,下面是相关源码

  void dispatch(BuildContext? target) {
    // The `target` may be null if the subtree the notification is supposed to be
    // dispatched in is in the process of being disposed.
    target?.visitAncestorElements(visitAncestor);
  }

dispatch(context)中调用了当前context的visitAncestorElements方法,该方法会从当前Element开始向上遍历父级元素;visitAncestorElements有一个遍历回调参数,在遍历过程中对遍历到的父级元素都会执行该回调。遍历的终止条件是:已经遍历到根Element或某个遍历回调返回false。源码中传给visitAncestorElements方法的遍历回调为visitAncestor方法,我们看看visitAncestor方法的实现:

//遍历回调,会对每一个父级Element执行此回调
bool visitAncestor(Element element) {
  //判断当前element对应的Widget是否是NotificationListener。
  
  //由于NotificationListener是继承自StatelessWidget,
  //故先判断是否是StatelessElement
  if (element is StatelessElement) {
    //是StatelessElement,则获取element对应的Widget,判断
    //是否是NotificationListener 。
    final StatelessWidget widget = element.widget;
    if (widget is NotificationListener<Notification>) {
      //是NotificationListener,则调用该NotificationListener的_dispatch方法
      if (widget._dispatch(this, element)) 
        return false;
    }
  }
  return true;
}

visitAncestor会判断每一个遍历到的父级Widget是否是NotificationListener,如果不是,则返回true继续向上遍历,如果是,则调用NotificationListener的_dispatch方法,我们看看_dispatch方法的源码:

  bool _dispatch(Notification notification, Element element) {
    // 如果通知监听器不为空,并且当前通知类型是该NotificationListener
    // 监听的通知类型,则调用当前NotificationListener的onNotification
    if (onNotification != null && notification is T) {
      final bool result = onNotification(notification);
      // 返回值决定是否继续向上遍历
      return result == true; 
    }
    return false;
  }

我们可以看到NotificationListener的onNotification回调最终是在_dispatch方法中执行的,然后会根据返回值来确定是否继续向上冒泡。

注意:1. Context上也提供了遍历Element树的方法。2. 我们可以通过Element.widget得到element节点对应的widget

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容