Flutter實(shí)戰(zhàn) Notification

2021-03-08 18:01 更新

通知(Notification)是 Flutter 中一個(gè)重要的機(jī)制,在 widget 樹中,每一個(gè)節(jié)點(diǎn)都可以分發(fā)通知,通知會(huì)沿著當(dāng)前節(jié)點(diǎn)向上傳遞,所有父節(jié)點(diǎn)都可以通過NotificationListener來(lái)監(jiān)聽通知。Flutter 中將這種由子向父的傳遞通知的機(jī)制稱為通知冒泡(Notification Bubbling)。通知冒泡和用戶觸摸事件冒泡是相似的,但有一點(diǎn)不同:通知冒泡可以中止,但用戶觸摸事件不行。

通知冒泡和 Web 開發(fā)中瀏覽器事件冒泡原理是相似的,都是事件從出發(fā)源逐層向上傳遞,我們可以在上層節(jié)點(diǎn)任意位置來(lái)監(jiān)聽通知/事件,也可以終止冒泡過程,終止冒泡后,通知將不會(huì)再向上傳遞。

Flutter 中很多地方使用了通知,如可滾動(dòng)組件(Scrollable Widget)滑動(dòng)時(shí)就會(huì)分發(fā)滾動(dòng)通知(ScrollNotification),而 Scrollbar 正是通過監(jiān)聽 ScrollNotification 來(lái)確定滾動(dòng)條位置的。

下面是一個(gè)監(jiān)聽可滾動(dòng)組件滾動(dòng)通知的例子:

NotificationListener(
  onNotification: (notification){
    switch (notification.runtimeType){
      case ScrollStartNotification: print("開始滾動(dòng)"); break;
      case ScrollUpdateNotification: print("正在滾動(dòng)"); break;
      case ScrollEndNotification: print("滾動(dòng)停止"); break;
      case OverscrollNotification: print("滾動(dòng)到邊界"); break;
    }
  },
  child: ListView.builder(
      itemCount: 100,
      itemBuilder: (context, index) {
        return ListTile(title: Text("$index"),);
      }
  ),
);

上例中的滾動(dòng)通知如ScrollStartNotificationScrollUpdateNotification等都是繼承自ScrollNotification類,不同類型的通知子類會(huì)包含不同的信息,比如ScrollUpdateNotification有一個(gè)scrollDelta屬性,它記錄了移動(dòng)的位移,其它通知屬性讀者可以自己查看 SDK 文檔。

上例中,我們通過NotificationListener來(lái)監(jiān)聽子ListView的滾動(dòng)通知的,NotificationListener定義如下:

class NotificationListener<T extends Notification> extends StatelessWidget {
  const NotificationListener({
    Key key,
    @required this.child,
    this.onNotification,
  }) : super(key: key);
 ...//省略無(wú)關(guān)代碼 
}  

我們可以看到:

  1. NotificationListener 繼承自StatelessWidget類,所以它可以直接嵌套到 Widget 樹中。

  1. NotificationListener 可以指定一個(gè)模板參數(shù),該模板參數(shù)類型必須是繼承自Notification;當(dāng)顯式指定模板參數(shù)時(shí),NotificationListener 便只會(huì)接收該參數(shù)類型的通知。舉個(gè)例子,如果我們將上例子代碼改為:

   //指定監(jiān)聽通知的類型為滾動(dòng)結(jié)束通知(ScrollEndNotification)
   NotificationListener<ScrollEndNotification>(
     onNotification: (notification){
       //只會(huì)在滾動(dòng)結(jié)束時(shí)才會(huì)觸發(fā)此回調(diào)
       print(notification);
     },
     child: ListView.builder(
         itemCount: 100,
         itemBuilder: (context, index) {
           return ListTile(title: Text("$index"),);
         }
     ),
   );

上面代碼運(yùn)行后便只會(huì)在滾動(dòng)結(jié)束時(shí)在控制臺(tái)打印出通知的信息。

  1. onNotification回調(diào)為通知處理回調(diào),其函數(shù)簽名如下:

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

它的返回值類型為布爾值,當(dāng)返回值為true時(shí),阻止冒泡,其父級(jí) Widget 將再也收不到該通知;當(dāng)返回值為false 時(shí)繼續(xù)向上冒泡通知。

Flutter 的 UI 框架實(shí)現(xiàn)中,除了在可滾動(dòng)組件在滾動(dòng)過程中會(huì)發(fā)出ScrollNotification之外,還有一些其它的通知,如SizeChangedLayoutNotification、KeepAliveNotificationLayoutChangedNotification等,F(xiàn)lutter 正是通過這種通知機(jī)制來(lái)使父元素可以在一些特定時(shí)機(jī)來(lái)做一些事情。

#自定義通知

除了 Flutter 內(nèi)部通知,我們也可以自定義通知,下面我們看看如何實(shí)現(xiàn)自定義通知:

  1. 定義一個(gè)通知類,要繼承自 Notification 類;

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

  1. 分發(fā)通知。

Notification有一個(gè)dispatch(context)方法,它是用于分發(fā)通知的,我們說過context實(shí)際上就是操作Element的一個(gè)接口,它與Element樹上的節(jié)點(diǎn)是對(duì)應(yīng)的,通知會(huì)從context對(duì)應(yīng)的Element節(jié)點(diǎn)向上冒泡。

下面我們看一個(gè)完整的例子:

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


class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //監(jiān)聽通知  
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        setState(() {
          _msg+=notification.msg+"  ";
        });
       return true;
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
//          RaisedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),  
            Builder(
              builder: (context) {
                return RaisedButton(
                  //按鈕點(diǎn)擊時(shí)分發(fā)通知  
                  onPressed: () => MyNotification("Hi").dispatch(context),
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}


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

上面代碼中,我們每點(diǎn)一次按鈕就會(huì)分發(fā)一個(gè)MyNotification類型的通知,我們?cè)?Widget 根上監(jiān)聽通知,收到通知后我們將通知通過 Text 顯示在屏幕上。

注意:代碼中注釋的部分是不能正常工作的,因?yàn)檫@個(gè)context是根Context,而 NotificationListener 是監(jiān)聽的子樹,所以我們通過Builder來(lái)構(gòu)建 RaisedButton,來(lái)獲得按鈕位置的 context。

運(yùn)行效果如圖8-6所示:

圖8-6

#阻止冒泡

我們將上面的例子改為:

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //監(jiān)聽通知
    return NotificationListener<MyNotification>(
      onNotification: (notification){
        print(notification.msg); //打印通知
        return false;
      },
      child: NotificationListener<MyNotification>(
        onNotification: (notification) {
          setState(() {
            _msg+=notification.msg+"  ";
          });
          return false; 
        },
        child: ...//省略重復(fù)代碼
      ),
    );
  }
}

上列中兩個(gè)NotificationListener進(jìn)行了嵌套,子NotificationListeneronNotification回調(diào)返回了false,表示不阻止冒泡,所以父NotificationListener仍然會(huì)受到通知,所以控制臺(tái)會(huì)打印出通知信息;如果將子NotificationListeneronNotification回調(diào)的返回值改為true,則父NotificationListener便不會(huì)再打印通知了,因?yàn)樽?code>NotificationListener已經(jīng)終止通知冒泡了。

#通知冒泡原理

我們?cè)谏厦娼榻B了通知冒泡的現(xiàn)象及使用,現(xiàn)在我們更深入一些,介紹一下 Flutter 框架中是如何實(shí)現(xiàn)通知冒泡的。為了搞清楚這個(gè)問題,就必須看一下源碼,我們從通知分發(fā)的的源頭出發(fā),然后再順藤摸瓜。由于通知是通過Notificationdispatch(context)方法發(fā)出的,那我們先看看dispatch(context)方法中做了什么,下面是相關(guān)源碼:

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

dispatch(context)中調(diào)用了當(dāng)前context的visitAncestorElements方法,該方法會(huì)從當(dāng)前 Element 開始向上遍歷父級(jí)元素;visitAncestorElements有一個(gè)遍歷回調(diào)參數(shù),在遍歷過程中對(duì)遍歷到的父級(jí)元素都會(huì)執(zhí)行該回調(diào)。遍歷的終止條件是:已經(jīng)遍歷到根 Element 或某個(gè)遍歷回調(diào)返回false。源碼中傳給visitAncestorElements方法的遍歷回調(diào)為visitAncestor方法,我們看看visitAncestor方法的實(shí)現(xiàn):

//遍歷回調(diào),會(huì)對(duì)每一個(gè)父級(jí)Element執(zhí)行此回調(diào)
bool visitAncestor(Element element) {
  //判斷當(dāng)前element對(duì)應(yīng)的Widget是否是NotificationListener。

  
  //由于NotificationListener是繼承自StatelessWidget,
  //故先判斷是否是StatelessElement
  if (element is StatelessElement) {
    //是StatelessElement,則獲取element對(duì)應(yīng)的Widget,判斷
    //是否是NotificationListener 。
    final StatelessWidget widget = element.widget;
    if (widget is NotificationListener<Notification>) {
      //是NotificationListener,則調(diào)用該NotificationListener的_dispatch方法
      if (widget._dispatch(this, element)) 
        return false;
    }
  }
  return true;
}

visitAncestor會(huì)判斷每一個(gè)遍歷到的父級(jí) Widget 是否是NotificationListener,如果不是,則返回true繼續(xù)向上遍歷,如果是,則調(diào)用NotificationListener_dispatch方法,我們看看_dispatch方法的源碼:

  bool _dispatch(Notification notification, Element element) {
    // 如果通知監(jiān)聽器不為空,并且當(dāng)前通知類型是該NotificationListener
    // 監(jiān)聽的通知類型,則調(diào)用當(dāng)前NotificationListener的onNotification
    if (onNotification != null && notification is T) {
      final bool result = onNotification(notification);
      // 返回值決定是否繼續(xù)向上遍歷
      return result == true; 
    }
    return false;
  }

我們可以看到NotificationListeneronNotification回調(diào)最終是在_dispatch方法中執(zhí)行的,然后會(huì)根據(jù)返回值來(lái)確定是否繼續(xù)向上冒泡。上面的源碼實(shí)現(xiàn)其實(shí)并不復(fù)雜,通過閱讀這些源碼,一些額外的點(diǎn)讀者可以注意一下:

  1. Context上也提供了遍歷 Element 樹的方法。
  2. 我們可以通過Element.widget得到element節(jié)點(diǎn)對(duì)應(yīng)的 widget;我們已經(jīng)反復(fù)講過 Widget 和 Element 的對(duì)應(yīng)關(guān)系,讀者通過這些源碼來(lái)加深理解。

#總結(jié)

Flutter 中通過通知冒泡實(shí)現(xiàn)了一套自低向上的消息傳遞機(jī)制,這個(gè)和 Web 開發(fā)中瀏覽器的事件冒泡原理類似,Web 開發(fā)者可以類比學(xué)習(xí)。另外我們通過源碼了解了 Flutter 通知冒泡的流程和原理,便于讀者加深理解和學(xué)習(xí) Flutter 的框架設(shè)計(jì)思想,在此,再次建議讀者在平時(shí)學(xué)習(xí)中能多看看源碼,定會(huì)受益匪淺。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)