網站首頁 編程語言 正文
一、 WillPopScope用法
WillPopScope
本質是一個widget用于攔截物理按鍵返回事件(Android的物理返回鍵和iOS的側滑返回),我們先了解一下這個類, 很簡單,共有兩個參數,子widget child
和用于監聽攔截返回事件的onWillPop
方法
const WillPopScope({ super.key, required this.child, required this.onWillPop, }) : assert(child != null);
下面我們以Android為例看一下用法,用法很簡單
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 和不實現onWillPop一樣,自動返回, *返回 false route不再響應物理返回事件,攔截返回事件自行處理 */ return false; }, ),
在需要攔截返回事件的頁面添加WillPopScope后,返回值為false時,點擊物理返回鍵頁面沒有任何反應,需要自己實現返回邏輯。
二、使用WillPopScope遇到的問題
當flutter項目中只有一個Navigator
時,使用上面的方式是沒有問題的,但是一個項目中往往有多個Navigator
,我們就會遇到WillPopScope
失效的情況(具體原理后面會解釋),先來看一個嵌套示例
主頁面main page, 由于MaterialApp就是一個Navigator, 所以我們在里面嵌套一個Navigator,示例只寫關鍵代碼
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 和不實現onWillPop一樣,自動返回, *返回 false route不再響應物理返回事件,攔截返回事件自行處理 */ return true; },
first page, 嵌入到主頁,創建路由可以跳轉第二頁
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( child: Center( child: InkWell( child: const Text("第一頁"), onTap: () { //跳轉到第二頁 Navigator.push(context, MaterialPageRoute(builder: (context) { return SecondPage(); })); }, )), onWillPop: () async { //監聽物理返回事件并打印 print("first page onWillScope"); return false; }); } }
第二頁
class SecondPage extends StatelessWidget { @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async{ //監聽物理返回事件并打印 print("second page onWillPop"); return false; }, child: const Center( child: Text("第二頁"), ), ); } }
運行后會發現,點擊返回鍵只有主頁的onWillPop 監聽到了物理返回事件,第一頁和第二頁的onWillPop沒有任何反應
I/flutter: onWillPop
看上去只響應了最初的Navigator,嵌套后的Navigator的監聽沒有任何效果,為什么會出現這樣的問題呢?下面是對WillPopScope原理的講解,如果只想看解決辦法請直接跳到文章最后。
三、 WillPopScope原理
我們先看WillPopScope的源碼,WillPopScope的主要源碼就是下面兩段,很容易理解,就是在UI或者數據更新后,對比onWillPop有沒有變化并更新。
@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!); } } }
重點看這一段,獲取ModalRoute并將onWillPop注冊到ModalRoute中
_route = ModalRoute.of(context); if (widget.onWillPop != null) { //該方法就是將onWillScope放到route持有的_willPopCallbacks數組中 _route?.addScopedWillPopCallback(widget.onWillPop!); }
進入到ModalRoute中,看到注冊到_willPopCallbacks中的onWillPop在WillPop中被調用,注意看當 onWillPop返回值為false時,WillPop的返回值為RoutePopDisposition.doNotPop。
這里解決了一個小疑點,onWillPop返回值的作用,返回false就不pop。但是還沒有解決我們的主要疑問,只能接著往下看。
@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) { //當返回值為false時,doNotPop return RoutePopDisposition.doNotPop; } } return super.willPop(); }
接著找到調用WillPop的方法,是一個MaybePop的方法,這個方法里包含了同一個 Navigator
里面頁面的彈出邏輯,這里我們不做分析,感興趣的可以自己研究。但是如果涉及到不同的Navigator
呢?我們先看這個方法里面的返回值,這個很重要。但我們的問題同樣不是在這里能解答的,只能繼續向上追溯。
@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; } }
那又是誰調用了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(); }
根據層層的追溯,我們現在來到下面的方法,這個方法很好理解,也是讓我很疑惑的地方。for循環遍歷_observes
數組中的所有WidgetsBindingObserver
。但是——注意這個轉折 如果數組中的第一個元素的didPopRoute
方法返回true
,那么遍歷結束,如果返回false
那么最終會調用SystemNavigator.pop()
,這個方法的意思是直接退出應用。也就是說handlePopRoute
這個方法要么執行數組里的第一個WidgetBindingObserver
的didPopRoute
要么退出應用。感覺這個for循環然并卵。
那為什么要講這個方法呢,因為應用監聽到物理返回按鍵事件后會調用這個方法。
@protected Future<void> handlePopRoute() async { for (final WidgetsBindingObserver observer in List<WidgetsBindingObserver>.of(_observers)) { if (await observer.didPopRoute()) { return; } } SystemNavigator.pop(); }
現在我們知道了,應用監聽到物理返回按鍵事件后會調用handlePopRoute
方法。但是handlePopRoute
中要么調用_observers
數組的第一個item的didPopRoute
方法,要么就退出應用。也就是說想要監聽系統的返回事件要有一個注冊到_observers的WidgetBindingObserver
并且還要是_observers
數組里的第一個元素。通過搜索_observers
的相關操作方法可以知道_observers
添加元素只用到了add
方法,所以第一個元素永遠不會變。那誰是第一個WidgetBindingObserver呢?那就是上文提到的_WidgetsAppState
, 而_WidgetsAppState
會持有一個NavigatorKey
,這個NavigatorKey
就是應用最初Navigator
的持有者。
綜上,我們了解了應用的物理返回鍵監聽邏輯,永遠只會調用到應用的第一個Navigator,所以我們所有的監聽返回邏輯只能用系統的第一個Navigator里面實現。那對于嵌套的Navigator我們該怎么辦呢?
四、嵌套Navigator無法監聽物理返回按鍵的解決辦法
既然不能直接處理嵌套Navigator的物理返回事件,那就只能曲線救國了。 首先去掉無效的WillPopScope
。
first page
class FirstPage extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: InkWell( child: const Text("第一頁"), 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"), ); } }
重頭戲來到了main page里面, 還是將onWillPop
設置為false。攔截所有的物理返回事件。只需要給Navigator設置一個GlobalKey
,然后在onWillPop
中實現對應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 和不實現onWillPop一樣,自動返回, *返回 false route不再響應物理返回事件,攔截返回事件自行處理 */ return false; }, ), ); } }
原文鏈接:https://juejin.cn/post/7144316484611735582
相關推薦
- 2022-04-18 pytorch自定義loss損失函數_python
- 2022-12-01 Go初學者踩坑之go?mod?init與自定義包的使用_Golang
- 2023-06-16 Golang調用FFmpeg轉換視頻流的實現_Golang
- 2022-12-05 c++入門必學庫函數sort的基本用法_C 語言
- 2022-04-28 shell?腳本中獲取命令的輸出的實現示例_linux shell
- 2022-09-08 詳解Dijkstra算法原理及其C++實現_C 語言
- 2022-09-02 Oracle11g調整SGA方法詳解_oracle
- 2023-03-20 C#字符集編碼的使用及說明_C#教程
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支