通知(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)通知如ScrollStartNotification
、ScrollUpdateNotification
等都是繼承自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)代碼
}
我們可以看到:
NotificationListener
繼承自StatelessWidget
類,所以它可以直接嵌套到 Widget 樹中。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)打印出通知的信息。
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
、KeepAliveNotification
、LayoutChangedNotification
等,F(xiàn)lutter 正是通過這種通知機(jī)制來(lái)使父元素可以在一些特定時(shí)機(jī)來(lái)做一些事情。
除了 Flutter 內(nèi)部通知,我們也可以自定義通知,下面我們看看如何實(shí)現(xiàn)自定義通知:
class MyNotification extends Notification {
MyNotification(this.msg);
final String msg;
}
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所示:
我們將上面的例子改為:
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)行了嵌套,子NotificationListener
的onNotification
回調(diào)返回了false
,表示不阻止冒泡,所以父NotificationListener
仍然會(huì)受到通知,所以控制臺(tái)會(huì)打印出通知信息;如果將子NotificationListener
的onNotification
回調(diào)的返回值改為true
,則父NotificationListener
便不會(huì)再打印通知了,因?yàn)樽?code>NotificationListener已經(jīng)終止通知冒泡了。
我們?cè)谏厦娼榻B了通知冒泡的現(xiàn)象及使用,現(xiàn)在我們更深入一些,介紹一下 Flutter 框架中是如何實(shí)現(xiàn)通知冒泡的。為了搞清楚這個(gè)問題,就必須看一下源碼,我們從通知分發(fā)的的源頭出發(fā),然后再順藤摸瓜。由于通知是通過Notification
的dispatch(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;
}
我們可以看到NotificationListener
的onNotification
回調(diào)最終是在_dispatch
方法中執(zhí)行的,然后會(huì)根據(jù)返回值來(lái)確定是否繼續(xù)向上冒泡。上面的源碼實(shí)現(xiàn)其實(shí)并不復(fù)雜,通過閱讀這些源碼,一些額外的點(diǎn)讀者可以注意一下:
Context
上也提供了遍歷 Element 樹的方法。Element.widget
得到element
節(jié)點(diǎn)對(duì)應(yīng)的 widget;我們已經(jīng)反復(fù)講過 Widget 和 Element 的對(duì)應(yīng)關(guān)系,讀者通過這些源碼來(lái)加深理解。Flutter 中通過通知冒泡實(shí)現(xiàn)了一套自低向上的消息傳遞機(jī)制,這個(gè)和 Web 開發(fā)中瀏覽器的事件冒泡原理類似,Web 開發(fā)者可以類比學(xué)習(xí)。另外我們通過源碼了解了 Flutter 通知冒泡的流程和原理,便于讀者加深理解和學(xué)習(xí) Flutter 的框架設(shè)計(jì)思想,在此,再次建議讀者在平時(shí)學(xué)習(xí)中能多看看源碼,定會(huì)受益匪淺。
更多建議: