Flutter-通知Notification

859 阅读4分钟

通知 Notification

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

注意:通知冒泡事件从出发源逐层向上传递,可以在上层节点任意位置来监听通知事件,也可以终止冒泡过程,终止冒泡后,通知将不会再向上传递。

监听通知

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"),);
    }
  ),
);

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

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之外,还有一些其他通知:SizeChangeLayoutNotification、KeepAliveNotification、LayoutChangeNotification等,Flutter正是通过这种通知机制来使父元素可以在一些特殊时机来做一些事情。

自定义通知

除了Flutter内部通知,也可以自定义通知:

  1. 定义一个通知类,要继承Notification类;
class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}
  1. 分发通知 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;
}

每次点击按钮就会分发一个MyNotificaiton类型的通知,在Widget根上监听通知,收到通知后可以将通知通过Text显示在屏幕上。

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

阻止通知冒泡

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进行来嵌套,子NotificationListener的onNotification回调返回了false,表示不阻止冒泡,所以父NotificationListener的onNotification回调的返回值改为true,则父NotificaitonListenre便不会打印通知,因为子Notification已经终止通知冒泡了。

冒泡原理

通知是通过Notification的dispatch(context)方法发出的:

void dispatch(BuildContext target) {
  target?.visitAncestorElements(visitAncestor);
}

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

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方法中执行的,然后根据返回值来确定是否继续向上冒泡。

注意:

  • Context上也提供了遍历Element树的方法
  • 可以通过Element.widget得到elemetn节点对应的widget。

Flutter中通过通知冒泡实现了一套自低向上的消息传递机制,通过源码了解了Flutter通知冒泡的流程和原理,便于加深理解和学习Flutter的框架设计思想。