02.25 Flutter 之 Notification

學習最好的方式就是多用幾次,我們就從簡單的通知使用開始學習。在此之前,先看一下通知監聽的源碼:

<code>/// A widget that listens for [Notification]s bubbling up the tree.////// Notifications will trigger the [onNotification] callback only if their/// [runtimeType] is a subtype of `T`.////// To dispatch notifications, use the [Notification.dispatch] method.class NotificationListener extends StatelessWidget { /// Creates a widget that listens for notifications. const NotificationListener({ Key key, @required this.child, //回調方法 this.onNotification, }) : super(key: key); ...}/<code>

從註釋中可以瞭解到:

NotificationListener 是一個監聽向上冒泡的 Notification(通知)的 Widget。當通知來臨,onNotification 方法回調將會被觸發,前提是來臨的通知是 T 或者 T 的子類型。用 Notification.dispatch 方法可以發送一個通知。

來一個簡單的例子:

<code>Scaffold( body: NotificationListener( onNotification: (notification) { switch (notification.runtimeType) { case ScrollStartNotification: print("ScrollStartNotification"); break; case ScrollUpdateNotification: print("ScrollUpdateNotification"); break; case ScrollEndNotification: print("ScrollEndNotification"); break; case OverscrollNotification: print("OverscrollNotification"); break; } return false; }, child: ListView.builder( itemBuilder: (BuildContext context, int index) { return ListTile( title: Text("$index"), ); }, itemCount: 40, )))/<code>

當滑動 ListView 時會看到如下輸出:

<code>flutter: ScrollStartNotificationflutter: ScrollUpdateNotification...flutter: ScrollEndNotification/<code>

此處 NotificationListener 我們沒有限制類型,只要是通知,都會被監聽到,稍微改一下:

<code>//指定監聽通知的類型為滾動結束通知(ScrollEndNotification)NotificationListener<scrollendnotification>( onNotification: (notification) { switch (notification.runtimeType) { case ScrollStartNotification: print("ScrollStartNotification"); break; case ScrollUpdateNotification: print("ScrollUpdateNotification"); break; case ScrollEndNotification: print("ScrollEndNotification"); break; case OverscrollNotification: print("OverscrollNotification"); break; } return false; }, child: ListView.builder( itemCount: 100, itemBuilder: (context, index) { return ListTile(title: Text("$index"),); } ),);/<scrollendnotification>/<code>

此時只會打印 ScrollEndNotification,因為 NotificationListener 只監聽 ScrollEndNotification 類型的通知。

還可以看到 NotificationListener 的回調 onNotification 需要一個 bool 類型的返回值,其源碼:

<code>typedef NotificationListenerCallback = bool Function(T notification);/<code>

它的返回值類型為布爾值,當返回值為 true 時,阻止冒泡,其父級 Widget 將再也收不到該通知;當返回值為 false 時繼續向上冒泡通知。如果覺得難以理解可以這麼想,當返回 true 的時候,代表到我這裡通知已經被我完全消費了,所以通知傳遞不到上一層,返回 false 的時候,代表我沒有消費它,只是監聽到它,所以通知得以往上冒泡傳遞。

可以用下面下自定義通知案例證明 返回值 的作用。

自定義通知

自定義通知,只需要繼承自 Notification 類就行。

<code>class TestNotification extends Notification { final String msg; TestNotification(this.msg);}/<code>

發送通知,之前提到 Notification.dispatch 可以發送通知。完整的案例如下:

<code>import 'package:flutter/material.dart';class TestNotification extends Notification { final String msg; TestNotification(this.msg);}class TestNotificationPage extends StatefulWidget { @override _TestNotificationPageState createState() => _TestNotificationPageState();}class _TestNotificationPageState extends State<testnotificationpage> { String msg = ""; @override Widget build(BuildContext context) { return Scaffold( body: NotificationListener<testnotification>( onNotification: (notification) { print(notification.msg); //能否打印取決於子監聽中的onNotification返回值 return true; }, child: NotificationListener<testnotification>( onNotification: (notification) { setState(() { msg += notification.msg + "\\t"; }); //這裡返回true,父節點的監聽將收不到通知 //返回false則可以 return false; }, child: Container( margin: EdgeInsets.only(top: 40), alignment: Alignment.center, child: Column( children: <widget>[ RaisedButton( //按鈕點擊時分發通知 onPressed: () => TestNotification("Hello").dispatch(context), child: Text("無效的通知"), ), Builder( builder: (context) { return RaisedButton( //按鈕點擊時分發通知 onPressed: () => TestNotification("Hello").dispatch(context), child: Text("發送通知"), ); }, ), Text(msg) ], ), )), ), ); }}/<widget>/<testnotification>/<testnotification>/<testnotificationpage>/<code>

以上案例,只要我們點擊按鈕,就會發送一個 TestNotification 類型的通知,被監聽到顯示在界面。但是點擊“無效的通知”按鈕時,界面並沒有任何反應,只是因為我們此時傳入的 context 是 build(BuildContext context)中的 context,和 NotificationListener 是同一級的,因此 NotificationListener 監聽不到通知。套一個 Builder 組件 或者直接創建一個新的 Widget 都行。

當子層 NotificationListener 的 onNotification 返回 true 時,父 NotificationListener 接受不到通知,反之可以接受。

運行效果:

從源碼更深入理解 Notification

要想更深入瞭解 why? 就得從源碼入手。通知完整流程是 發送通知===>冒泡傳遞通知===>監聽消費通知。我們就看源碼是怎麼實現這一流程的。

通知的發送

通知發送是在 Notification 類裡發送的,我們就先看一下 Notification 類:

Notification

<code>/// A notification that can bubble up the widget tree./// To listen for notifications in a subtree, use a [NotificationListener].////// To send a notification, call [dispatch] on the notification you wish to/// send. The notification will be delivered to any [NotificationListener]/// widgets with the appropriate type parameters that are ancestors of the given/// [BuildContext].///一個可以在widget樹上冒泡傳遞的通知///用你想要發送通知的實例調用dispatch方法可以發送一個通知,///通知將會被傳遞到NotificationListener監聽的widget樹中abstract class Notification { @protected @mustCallSuper bool visitAncestor(Element element) { if (element is StatelessElement) { final StatelessWidget widget = element.widget; if (widget is NotificationListener<notification>) { if (widget._dispatch(this, element)) // that function checks the type dynamically return false; } } return true; } //開始發送通知 void dispatch(BuildContext target) { // target?.visitAncestorElements(visitAncestor); } ...}/<notification>/<code>

發送通知時,調用 context 中的 visitAncestorElements 方法,visitAncestorElements 在 Element 類中的實現如下:

<code>@override void visitAncestorElements(bool visitor(Element element)) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element ancestor = _parent; while (ancestor != null && visitor(ancestor)) //把父節點賦值給當前節點 ancestor = ancestor._parent; }/<code>

visitAncestorElements 需要傳入一個回調方法,只要回調方法返回 true,它就會把父節點賦值給當前節點,達到向上遍歷傳遞的效果。

<code>target?.visitAncestorElements(visitAncestor);/<code>

傳入的是 visitAncestor,我們看 visitAncestor 方法如下:

<code> @protected @mustCallSuper bool visitAncestor(Element element) { //判斷當前element對應的Widget是否是NotificationListener。 //由於NotificationListener是繼承自StatelessWidget, //故先判斷是否是StatelessElement if (element is StatelessElement) { final StatelessWidget widget = element.widget; //是StatelessElement,則獲取element對應的Widget,判斷 //是否是NotificationListener 。 if (widget is NotificationListener<notification>) { //是NotificationListener,則調用該NotificationListener的_dispatch方法 if (widget._dispatch(this, element)) return false; } } //默認返回true,這使得visitAncestorElements會 //一直像父節點遍歷傳遞 return true; }/<notification>/<code>

可見,最終會調到 NotificationListener 中的_dispatch 方法,如果_dispatch 方法返回 true,則終止向上遍歷。返回 false 則繼續像父節點遍歷。

NotificationListener

監聽通知

<code>//onNotification回調typedef NotificationListenerCallback = bool Function(T notification);class NotificationListener extends StatelessWidget { /// Creates a widget that listens for notifications. const NotificationListener({ Key key, @required this.child, this.onNotification, }) : super(key: key); final NotificationListenerCallback onNotification; bool _dispatch(Notification notification, Element element) { // 如果通知監聽器不為空,並且當前通知類型是該NotificationListener // 監聽的通知類型,則調用當前NotificationListener的onNotification if (onNotification != null && notification is T) { //調用onNotification回調 final bool result = onNotification(notification); // 返回值決定是否繼續向上遍歷 return result == true; } return false; }}/<code>

我們可以看到 NotificationListener 的 onNotification 回調最終是在_dispatch 方法中執行的,然後會根據返回值來確定是否繼續向上冒泡。

小結

通知是一套自底向上的消息傳遞機制。
調用 dispatch(context)發送通知,dispatch 會調用 context.visitAncestorElements(visitor) 方法。context.visitAncestorElements(visitor) 方法會從當前節點向上遍歷父節點,通過傳入的 visitor 方法回調調用 NotificationListener 的_dispatch 方法。_dispatch 方法會過濾一些通知類型,最後調用 onNotification 回調。根據 onNotification 回調的返回值判斷通知是否繼續向上傳遞。

額外

NestedScrollView 與 ListView 共用時,當 ListView 需要傳入自己的 ScrollController 時,NestedScrollView 不會跟著滑動。此時我們只要把 ScrollController 換成 NotificationListener 即可。

效果