Flutter WillPopScope攔截返回事件原理示例詳解
一、 WillPopScope用法
WillPopScope
本質(zhì)是一個(gè)widget用于攔截物理按鍵返回事件(Android的物理返回鍵和iOS的側(cè)滑返回),我們先了解一下這個(gè)類(lèi), 很簡(jiǎn)單,共有兩個(gè)參數(shù),子widget child
和用于監(jiān)聽(tīng)攔截返回事件的onWillPop
方法
const WillPopScope({ super.key, required this.child, required this.onWillPop, }) : assert(child != null);
下面我們以Android為例看一下用法,用法很簡(jiǎn)單
body: WillPopScope( child: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Text("back") ), onWillPop: () async { log("onWillPop"); /**返回 true 和不實(shí)現(xiàn)onWillPop一樣,自動(dòng)返回, *返回 false route不再響應(yīng)物理返回事件,攔截返回事件自行處理 */ return false; }, ),
在需要攔截返回事件的頁(yè)面添加WillPopScope后,返回值為false時(shí),點(diǎn)擊物理返回鍵頁(yè)面沒(méi)有任何反應(yīng),需要自己實(shí)現(xiàn)返回邏輯。
二、使用WillPopScope遇到的問(wèn)題
當(dāng)flutter項(xiàng)目中只有一個(gè)Navigator
時(shí),使用上面的方式是沒(méi)有問(wèn)題的,但是一個(gè)項(xiàng)目中往往有多個(gè)Navigator
,我們就會(huì)遇到WillPopScope
失效的情況(具體原理后面會(huì)解釋?zhuān)?先來(lái)看一個(gè)嵌套示例
主頁(yè)面main page, 由于MaterialApp就是一個(gè)Navigator, 所以我們?cè)诶锩媲短滓粋€(gè)Navigator,示例只寫(xiě)關(guān)鍵代碼
main page
body: WillPopScope( child: Center( // Center is a layout widget. It takes a single child and positions it // in the middle of the parent. child: Navigator( onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (context) { return FirstPage(); }), ) ), onWillPop: () async { print("onWillPop"); /**返回 true 和不實(shí)現(xiàn)onWillPop一樣,自動(dòng)返回, *返回 false route不再響應(yīng)物理返回事件,攔截返回事件自行處理 */ return true; },
first page, 嵌入到主頁(yè),創(chuàng)建路由可以跳轉(zhuǎn)第二頁(yè)
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( child: Center( child: InkWell( child: const Text("第一頁(yè)"), onTap: () { //跳轉(zhuǎn)到第二頁(yè) Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, )), onWillPop: () async { //監(jiān)聽(tīng)物理返回事件并打印 print("first page onWillScope"); return false; }); } }
第二頁(yè)
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async{ //監(jiān)聽(tīng)物理返回事件并打印 print("second page onWillPop"); return false; }, child: const Center( child: Text("第二頁(yè)"), ), ); } }
運(yùn)行后會(huì)發(fā)現(xiàn),點(diǎn)擊返回鍵只有主頁(yè)的onWillPop 監(jiān)聽(tīng)到了物理返回事件,第一頁(yè)和第二頁(yè)的onWillPop沒(méi)有任何反應(yīng)
I/flutter: onWillPop
看上去只響應(yīng)了最初的Navigator,嵌套后的Navigator的監(jiān)聽(tīng)沒(méi)有任何效果,為什么會(huì)出現(xiàn)這樣的問(wèn)題呢?下面是對(duì)WillPopScope原理的講解,如果只想看解決辦法請(qǐng)直接跳到文章最后。
三、 WillPopScope原理
我們先看WillPopScope的源碼,WillPopScope的主要源碼就是下面兩段,很容易理解,就是在UI或者數(shù)據(jù)更新后,對(duì)比onWillPop有沒(méi)有變化并更新。
@override void didChangeDependencies() { super.didChangeDependencies(); if (widget.onWillPop != null) { _route?.removeScopedWillPopCallback(widget.onWillPop!); } //獲取ModalRoute _route = ModalRoute.of(context); if (widget.onWillPop != null) { _route?.addScopedWillPopCallback(widget.onWillPop!); } } @override void didUpdateWidget(WillPopScope oldWidget) { super.didUpdateWidget(oldWidget); if (widget.onWillPop != oldWidget.onWillPop && _route != null) { if (oldWidget.onWillPop != null) { _route!.removeScopedWillPopCallback(oldWidget.onWillPop!); } if (widget.onWillPop != null) { _route!.addScopedWillPopCallback(widget.onWillPop!); } } }
重點(diǎn)看這一段,獲取ModalRoute并將onWillPop注冊(cè)到ModalRoute中
_route = ModalRoute.of(context); if (widget.onWillPop != null) { //該方法就是將onWillScope放到route持有的_willPopCallbacks數(shù)組中 _route?.addScopedWillPopCallback(widget.onWillPop!); }
進(jìn)入到ModalRoute中,看到注冊(cè)到_willPopCallbacks中的onWillPop在WillPop中被調(diào)用,注意看當(dāng) onWillPop返回值為false時(shí),WillPop的返回值為RoutePopDisposition.doNotPop。
這里解決了一個(gè)小疑點(diǎn),onWillPop返回值的作用,返回false就不pop。但是還沒(méi)有解決我們的主要疑問(wèn),只能接著往下看。
@override Future<RoutePopDisposition> willPop() async { final _ModalScopeState<T>? scope = _scopeKey.currentState; assert(scope != null); for (final WillPopCallback callback in List<WillPopCallback>.of(_willPopCallbacks)) { if (await callback() != true) { //當(dāng)返回值為false時(shí),doNotPop return RoutePopDisposition.doNotPop; } } return super.willPop(); }
接著找到調(diào)用WillPop的方法,是一個(gè)MaybePop的方法,這個(gè)方法里包含了同一個(gè) Navigator
里面頁(yè)面的彈出邏輯,這里我們不做分析,感興趣的可以自己研究。但是如果涉及到不同的Navigator
呢?我們先看這個(gè)方法里面的返回值,這個(gè)很重要。但我們的問(wèn)題同樣不是在這里能解答的,只能繼續(xù)向上追溯。
@optionalTypeArgs Future<bool> maybePop<T extends Object?>([ T? result ]) async { final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere( (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null, ); if (lastEntry == null) { return false; } assert(lastEntry.route._navigator == this); final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous assert(disposition != null); if (!mounted) { // Forget about this pop, we were disposed in the meantime. return true; } final _RouteEntry? newLastEntry = _history.cast<_RouteEntry?>().lastWhere( (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null, ); if (lastEntry != newLastEntry) { // Forget about this pop, something happened to our history in the meantime. return true; } switch (disposition) { case RoutePopDisposition.bubble: return false; case RoutePopDisposition.pop: pop(result); return true; case RoutePopDisposition.doNotPop: return true; } }
那又是誰(shuí)調(diào)用了maybePop
方法呢, 那就是didPopRoute
, didPopRoute
方法位于_WidgetsAppState
中
@override Future<bool> didPopRoute() async { assert(mounted); // The back button dispatcher should handle the pop route if we use a // router. if (_usesRouterWithDelegates) { return false; } final NavigatorState? navigator = _navigator?.currentState; if (navigator == null) { return false; } return navigator.maybePop(); }
根據(jù)層層的追溯,我們現(xiàn)在來(lái)到下面的方法,這個(gè)方法很好理解,也是讓我很疑惑的地方。for循環(huán)遍歷_observes
數(shù)組中的所有WidgetsBindingObserver
。但是——注意這個(gè)轉(zhuǎn)折 如果數(shù)組中的第一個(gè)元素的didPopRoute
方法返回true
,那么遍歷結(jié)束,如果返回false
那么最終會(huì)調(diào)用SystemNavigator.pop()
,這個(gè)方法的意思是直接退出應(yīng)用。也就是說(shuō)handlePopRoute
這個(gè)方法要么執(zhí)行數(shù)組里的第一個(gè)WidgetBindingObserver
的didPopRoute
要么退出應(yīng)用。感覺(jué)這個(gè)for循環(huán)然并卵。
那為什么要講這個(gè)方法呢,因?yàn)閼?yīng)用監(jiān)聽(tīng)到物理返回按鍵事件后會(huì)調(diào)用這個(gè)方法。
@protected Future<void> handlePopRoute() async { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) { if (await observer.didPopRoute()) { return; } } SystemNavigator.pop(); }
現(xiàn)在我們知道了,應(yīng)用監(jiān)聽(tīng)到物理返回按鍵事件后會(huì)調(diào)用handlePopRoute
方法。但是handlePopRoute
中要么調(diào)用_observers
數(shù)組的第一個(gè)item的didPopRoute
方法,要么就退出應(yīng)用。也就是說(shuō)想要監(jiān)聽(tīng)系統(tǒng)的返回事件要有一個(gè)注冊(cè)到_observers的WidgetBindingObserver
并且還要是_observers
數(shù)組里的第一個(gè)元素。通過(guò)搜索_observers
的相關(guān)操作方法可以知道_observers
添加元素只用到了add
方法,所以第一個(gè)元素永遠(yuǎn)不會(huì)變。那誰(shuí)是第一個(gè)WidgetBindingObserver呢?那就是上文提到的_WidgetsAppState
, 而_WidgetsAppState
會(huì)持有一個(gè)NavigatorKey
,這個(gè)NavigatorKey
就是應(yīng)用最初Navigator
的持有者。
綜上,我們了解了應(yīng)用的物理返回鍵監(jiān)聽(tīng)邏輯,永遠(yuǎn)只會(huì)調(diào)用到應(yīng)用的第一個(gè)Navigator,所以我們所有的監(jiān)聽(tīng)返回邏輯只能用系統(tǒng)的第一個(gè)Navigator里面實(shí)現(xiàn)。那對(duì)于嵌套的Navigator我們?cè)撛趺崔k呢?
四、嵌套Navigator無(wú)法監(jiān)聽(tīng)物理返回按鍵的解決辦法
既然不能直接處理嵌套Navigator的物理返回事件,那就只能曲線(xiàn)救國(guó)了。 首先去掉無(wú)效的WillPopScope
。
first page
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: InkWell( child: const Text("第一頁(yè)"), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, )); } }
second page
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return const Center( child: Text("Second page"), ); } }
重頭戲來(lái)到了main page里面, 還是將onWillPop
設(shè)置為false。攔截所有的物理返回事件。只需要給Navigator設(shè)置一個(gè)GlobalKey
,然后在onWillPop
中實(shí)現(xiàn)對(duì)應(yīng)navigator的返回邏輯。
class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { GlobalKey<NavigatorState> _key = GlobalKey(); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: WillPopScope( child: Center( child: Navigator( key: _key, onGenerateRoute: (RouteSettings settings) => MaterialPageRoute(builder: (context) { return FirstPage(); }), ) ), onWillPop: () async { print("onWillPop"); if(_key.currentState != null && _key.currentState!.canPop()) { _key.currentState?.pop(); } /**返回 true 和不實(shí)現(xiàn)onWillPop一樣,自動(dòng)返回, *返回 false route不再響應(yīng)物理返回事件,攔截返回事件自行處理 */ return false; }, ), ); } }
以上就是Flutter WillPopScope攔截返回事件原理示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter WillPopScope攔截返回的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 給圖片加上水印的示例代碼(支持logo+文字)
本篇文章主要介紹了Android 給圖片加上水印的示例代碼(支持logo+文字),具有一定的參考價(jià)值,有興趣的可以了解一下2017-08-08Android開(kāi)發(fā)之Location用法實(shí)例分析
這篇文章主要介紹了Android開(kāi)發(fā)中Location用法,結(jié)合實(shí)例形式分析了Android使用location控件獲取經(jīng)緯度信息的相關(guān)操作技巧,需要的朋友可以參考下2016-10-10Android編程實(shí)現(xiàn)列表側(cè)滑刪除的方法詳解
這篇文章主要介紹了Android編程實(shí)現(xiàn)列表側(cè)滑刪除的方法,結(jié)合實(shí)例形式詳細(xì)分析了Android列表側(cè)滑刪除功能的原理與具體實(shí)現(xiàn)技巧,注釋中包含詳盡的說(shuō)明,需要的朋友可以參考下2018-01-01Android編程之SurfaceView學(xué)習(xí)示例詳解
這篇文章主要介紹了Android編程之SurfaceView學(xué)習(xí)示例,結(jié)合實(shí)例分析了SurfaceView的功能、使用方法與注意事項(xiàng),具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10Android使用Volley框架定制PostUploadRequest上傳文件
這篇文章主要為大家詳細(xì)介紹了Android使用Volley框架定制PostUploadRequest上傳文件或圖片,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12Android ActionBarActivity設(shè)置全屏無(wú)標(biāo)題的方法總結(jié)
這篇文章主要介紹了Android ActionBarActivity設(shè)置全屏無(wú)標(biāo)題的相關(guān)資料,需要的朋友可以參考下2017-07-07Android自定View流式布局根據(jù)文字?jǐn)?shù)量換行
這篇文章主要為大家詳細(xì)介紹了Android自定View流式布局,根據(jù)文字?jǐn)?shù)量換行,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12Android實(shí)現(xiàn)Tab切換界面功能詳解
這篇文章主要為大家詳細(xì)介紹了Android如何實(shí)現(xiàn)Tab切換界面的功能,以及對(duì)Tab變化事件進(jìn)行監(jiān)聽(tīng)。文中示例代碼講解詳細(xì),感興趣的可以了解一下2022-05-05Android 中圖片和按鈕按下?tīng)顟B(tài)變化實(shí)例代碼解析
這篇文章通過(guò)實(shí)例代碼給大家總結(jié)了android 中圖片和按鈕按下?tīng)顟B(tài)變化問(wèn)題,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-06-06Android 在程序運(yùn)行時(shí)申請(qǐng)權(quán)限的實(shí)例講解
下面小編就為大家分享一篇Android 在程序運(yùn)行時(shí)申請(qǐng)權(quán)限的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01