浮昇通知


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,也就是觸發通知的對象,在 Scaffold 的 body 部份,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);
        },
      ),
    );
  }
}

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

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

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

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