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)都可以通過(guò)NotificationListener來(lái)監(jiān)聽通知。Flutter 中將這種由子向父的傳遞通知的機(jī)制稱為通知冒泡(Notification Bubbling)。通知冒泡和用戶觸摸事件冒泡是相似的,但有一點(diǎn)不同:通知冒泡可以中止,但用戶觸摸事件不行。

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

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

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

  1. NotificationListener(
  2. onNotification: (notification){
  3. switch (notification.runtimeType){
  4. case ScrollStartNotification: print("開始滾動(dòng)"); break;
  5. case ScrollUpdateNotification: print("正在滾動(dòng)"); break;
  6. case ScrollEndNotification: print("滾動(dòng)停止"); break;
  7. case OverscrollNotification: print("滾動(dòng)到邊界"); break;
  8. }
  9. },
  10. child: ListView.builder(
  11. itemCount: 100,
  12. itemBuilder: (context, index) {
  13. return ListTile(title: Text("$index"),);
  14. }
  15. ),
  16. );

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

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

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

我們可以看到:

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

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

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

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

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

  1. 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)過(guò)程中會(huì)發(fā)出ScrollNotification之外,還有一些其它的通知,如SizeChangedLayoutNotification、KeepAliveNotificationLayoutChangedNotification等,F(xiàn)lutter 正是通過(guò)這種通知機(jī)制來(lái)使父元素可以在一些特定時(shí)機(jī)來(lái)做一些事情。

#自定義通知

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

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

  1. class MyNotification extends Notification {
  2. MyNotification(this.msg);
  3. final String msg;
  4. }

  1. 分發(fā)通知。

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

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

  1. class NotificationRoute extends StatefulWidget {
  2. @override
  3. NotificationRouteState createState() {
  4. return new NotificationRouteState();
  5. }
  6. }
  7. class NotificationRouteState extends State<NotificationRoute> {
  8. String _msg="";
  9. @override
  10. Widget build(BuildContext context) {
  11. //監(jiān)聽通知
  12. return NotificationListener<MyNotification>(
  13. onNotification: (notification) {
  14. setState(() {
  15. _msg+=notification.msg+" ";
  16. });
  17. return true;
  18. },
  19. child: Center(
  20. child: Column(
  21. mainAxisSize: MainAxisSize.min,
  22. children: <Widget>[
  23. // RaisedButton(
  24. // onPressed: () => MyNotification("Hi").dispatch(context),
  25. // child: Text("Send Notification"),
  26. // ),
  27. Builder(
  28. builder: (context) {
  29. return RaisedButton(
  30. //按鈕點(diǎn)擊時(shí)分發(fā)通知
  31. onPressed: () => MyNotification("Hi").dispatch(context),
  32. child: Text("Send Notification"),
  33. );
  34. },
  35. ),
  36. Text(_msg)
  37. ],
  38. ),
  39. ),
  40. );
  41. }
  42. }
  43. class MyNotification extends Notification {
  44. MyNotification(this.msg);
  45. final String msg;
  46. }

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

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

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

圖8-6

#阻止冒泡

我們將上面的例子改為:

  1. class NotificationRouteState extends State<NotificationRoute> {
  2. String _msg="";
  3. @override
  4. Widget build(BuildContext context) {
  5. //監(jiān)聽通知
  6. return NotificationListener<MyNotification>(
  7. onNotification: (notification){
  8. print(notification.msg); //打印通知
  9. return false;
  10. },
  11. child: NotificationListener<MyNotification>(
  12. onNotification: (notification) {
  13. setState(() {
  14. _msg+=notification.msg+" ";
  15. });
  16. return false;
  17. },
  18. child: ...//省略重復(fù)代碼
  19. ),
  20. );
  21. }
  22. }

上列中兩個(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è)問(wèn)題,就必須看一下源碼,我們從通知分發(fā)的的源頭出發(fā),然后再順藤摸瓜。由于通知是通過(guò)Notificationdispatch(context)方法發(fā)出的,那我們先看看dispatch(context)方法中做了什么,下面是相關(guān)源碼:

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

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

  1. //遍歷回調(diào),會(huì)對(duì)每一個(gè)父級(jí)Element執(zhí)行此回調(diào)
  2. bool visitAncestor(Element element) {
  3. //判斷當(dāng)前element對(duì)應(yīng)的Widget是否是NotificationListener。
  4. //由于NotificationListener是繼承自StatelessWidget,
  5. //故先判斷是否是StatelessElement
  6. if (element is StatelessElement) {
  7. //是StatelessElement,則獲取element對(duì)應(yīng)的Widget,判斷
  8. //是否是NotificationListener 。
  9. final StatelessWidget widget = element.widget;
  10. if (widget is NotificationListener<Notification>) {
  11. //是NotificationListener,則調(diào)用該NotificationListener的_dispatch方法
  12. if (widget._dispatch(this, element))
  13. return false;
  14. }
  15. }
  16. return true;
  17. }

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

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

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

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

#總結(jié)

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

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)