浮昇通知


Flutter 中的事件基本上分為兩個層次:原始指標事件(Raw pointer events)與手勢(Gestures)。事件的觸發與否依 behavior 特性設定有關,不過,對於從 Web 前端開發過來的人而言,這些行為是有些陌生的,因為他們可能想要事件浮昇之類的行為。

在 Flutter 中提供了 Notification,元件可以發送(dispatch)通知(Notification),通知會沿著樹浮昇(bubbling up),父裔中的 NotificationListener 可以獲得通知,預設情況下,通知預設會持續往上浮昇,然而父裔中某個 NotificationListener 可以阻止通知浮昇。

例如,若想模擬一下 Web 中 DOM 的 click 事件,可以自訂一個 ClickNotification

import 'package:flutter/material.dart';

// 自訂 NotificationListener
class ClickNotification extends Notification {
  final Widget target;
  ClickNotification({this.target});
}

void main() => runApp(
  MaterialApp(
    home: Scaffold(

      // 監聽子裔的 ClickNotification
      body: NotificationListener<ClickNotification> (
        child: Center(

          // 監聽 NotificationButton 的 ClickNotification
          child: NotificationListener<ClickNotification> (
            child: NotificationButton('Click me'),
            onNotification: (notification) {
              var button = notification.target as NotificationButton;
              print('"${button.data}" Clicked');
              // return true;  // 傳回 true 可阻止通知浮昇
            }
          )
        ),
        onNotification: (notification) {
          print('bubble up');
        }
      ),
    ),
  )
);

class NotificationButton extends StatelessWidget {
  final String data;

  NotificationButton(this.data);

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ClickNotification>(
      child: Builder(
        builder: (context) {
          return RawMaterialButton(
            child: Text(data),
            onPressed: () {
              ClickNotification(target: this).dispatch(context);
            },
          );
        },
      ),
    );
  }
}

在這個範例中,ClickNotification 繼承了 Notification,主要是用來包裹 target,也就是觸發通知的對象,在 Scaffoldbody 部份,NotificationListener 會監聽子元件是否有通知,在其子裔中有個 NotificationButton,會在點選按鈕時發送 ClickNotification

NotificationButton 的部份,主要將是用 NotificationListener 作為 Builder 的父元件,而在 RawMaterialButton 被按下時,onPressed 執行時,會建立 ClickNotification 並發送。

為什麼要用 Builder,而不是將 RawMaterialButton 實例傳回?主要是在 context,如果你使用以下:

...
class NotificationButton extends StatelessWidget {
  final String data;

  @override
  Widget build(BuildContext context) {
    return NotificationListener<ClickNotification>(
      child: RawMaterialButton(
        child: Text(data),
        onPressed: () {
          ClickNotification(target: this).dispatch(context);
        },
      ),
    );
  }
}

那麼這時的 contextNotificationButtonElement,也就是說,這時 NotificationListener 監聽的子裔,會是從 NotificationButton 以上發出的 ClickNotification,這時作為 NotificationButton 的子元件 RawMaterialButton 發出的 ClickNotification,是不會有作用的。

至於原先指定 Builder 的方式,builder 設定的函式上 context 參數,接收到的是 Builder 對應的 Element,而 BuilderNotificationListener 的子元件,因此發送 ClickNotification 時,Builder 以上的父裔,預設可以收到通知。

範例若按下按鈕,主控台會顯示「“Click me” Clicked」與「bubble up」文字,如果將範例中的 return true 處註解移除,表示阻止通知浮昇,這時就只會顯示「“Click me” Clicked」。

實際上,Flutter 內建了一些 Notification 的實作,像是 DraggableScrollableNotificationKeepAliveNotificationLayoutChangedNotification(子類還有 ScrollNotificationSizeChangedLayoutNotification)、OverscrollIndicatorNotification 等,可以用來傾聽一些系統內建的通知,必要時也可以利用一下。