Flutter:Navigator2.0介绍及使用
目錄
APP
RouteInformationParser
RouterDelegate
問題
瀏覽器的回退按鈕
總結(jié)
源碼
Navigator1.0
我們學(xué)習(xí)flutter一開始接觸的路由管理就是Navigator1.0,它非常方便,使用簡單,如下:
class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,visualDensity: VisualDensity.adaptivePlatformDensity,),onGenerateRoute: (RouteSettings settings){return PageRouteBuilder(settings: settings,pageBuilder: (BuildContext context, Animation<double> animation,Animation<double> secondaryAnimation) {if(settings.name == "pageB"){return PageB();}else if(settings.name == "pageC"){return PageC();}else{return Container();}});},// routes: {// "pageB" : (BuildContext context) => PageB(),// "pageC" : (BuildContext context) => PageC()// },home: PageA(),);} }通過onGenerateRoute或routes來注冊路由,使用時通過Navigator.of(context).pushNamed()或者其他函數(shù)即可。
Navigator1.0使用簡單,但是問題也一樣,只有push、pop等幾個簡單操作,對于復(fù)雜場景就無能為力了,比如web開發(fā)時地址欄或后退鍵的處理。
所以google后來又推出了Navigator2.0
Navigator2.0
Navigator1.0是通過Navigator來管理處理路由,而Navigator2.0則是通過Router來處理的,但是也需要Navigator,實際上是用Router對Navigator包裹起來。Router相對來說功能就強大很多了,同時使用起來也復(fù)雜很多。
關(guān)于Navigator2.0的原理,網(wǎng)上已經(jīng)有很多文章了,但是我發(fā)現(xiàn)這些文章在使用實例上都不是很清楚,或者說示例過于復(fù)雜。應(yīng)該是大部分參考google官方文檔簡單翻譯的,但是其實我們正常場景使用并不是那么復(fù)雜,而且大部分都沒有講清楚。所以本篇文章不討論原理,只用最簡單的示例來展示如果使用Navigator2.0,或者說如何快速的從Navigator1.0轉(zhuǎn)成Navigator2.0。
APP
首先創(chuàng)建MaterialApp方式有了改變,通過MaterialApp.router()來創(chuàng)建,如下:
class MyApp extends StatelessWidget {final delegate = MyRouteDelegate();@overrideWidget build(BuildContext context) {return MaterialApp.router(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,visualDensity: VisualDensity.adaptivePlatformDensity,),routerDelegate: delegate,routeInformationParser: MyRouteParser(),);} }通過這種方式我們需要設(shè)置routerDelegate和routeInformationParser,這樣就需要實現(xiàn)這兩個類。
RouteInformationParser
創(chuàng)建一個類繼承RouteInformationParser,主要的作用是包裝解析路由信息,這里有一個最簡單的方式,如下:
class MyRouteParser extends RouteInformationParser<String> {@overrideFuture<String> parseRouteInformation(RouteInformation routeInformation) {return SynchronousFuture(routeInformation.location);}@overrideRouteInformation restoreRouteInformation(String configuration) {return RouteInformation(location: configuration);} }我們的路由信息都由一個字符串承載,可以用url的形式,這樣方便處理。
RouterDelegate
RouterDelegate是最重要的部分,這里實現(xiàn)路由切換的邏輯,繼承RouterDelegate的類需要實現(xiàn)下面的函數(shù):
void addListener(listener) void removeListener(listener)Widget build(BuildContext context)Future<bool> popRoute() Future<void> setNewRoutePath(T configuration)其中addListener和removeListener是來自RouterDelegate的繼承Listenable。
build一般返回的是一個Navigator。
popRoute實現(xiàn)后退邏輯
setNewRoutePath實現(xiàn)新頁面的邏輯
單單這么說肯定一頭霧水,我們用一個示例來實現(xiàn)它,具體代碼如下:
class MyRouteDelegate extends RouterDelegate<String> with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier{@overrideGlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();@overrideString get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;final _stack = <String>[];@overrideWidget build(BuildContext context) {return Navigator(key: navigatorKey,pages: [for (final url in _stack)getPage(url)],onPopPage: (route, result){if (_stack.isNotEmpty) {_stack.removeLast();notifyListeners();}return route.didPop(result);},);}Page getPage(String url){return MaterialPage(name: url,arguments: null,child: getWidget(url));}Widget getWidget(String name){switch(name){case "pageB":return PageB();case "pageC":return PageC();default:return PageA();}}@overrideFuture<void> setNewRoutePath(String config) {if(config == "/"){_stack.clear();}if(_stack.isEmpty || config != _stack.last) {_stack.add(config);notifyListeners();}return SynchronousFuture<void>(null);} }首先我們不僅繼承RouterDelegate,同時還繼承ChangeNotifier,這樣就不必實現(xiàn)addListener和removeListener了。
注意:如果這里手動實現(xiàn)了addListener和removeListener但是并沒有實現(xiàn)代碼,這樣會導(dǎo)致頁面無法切換,因為路由變化沒有通知。現(xiàn)象就是點擊切換頁面的按鈕無反應(yīng),build不執(zhí)行。
然后又繼承了PopNavigatorRouterDelegateMixin,它實現(xiàn)了popRoute函數(shù),所以這個函數(shù)也可以不用實現(xiàn)。但是繼承它后需要實現(xiàn)navigatorKey,如上第一行。
通過上面兩個繼承,我們只需要實現(xiàn)setNewRoutePath和build兩個函數(shù)即可。先看setNewRoutePath的代碼:
@overrideFuture<void> setNewRoutePath(String config) {if(config == "/"){_stack.clear();}if(_stack.isEmpty || config != _stack.last) {_stack.add(config);notifyListeners();}return SynchronousFuture<void>(null);}_stack是一個列表,用來存儲所有路由信息,因為前面我們的路由信息用String承載,所以_stack是一個字符串列表。
在這個函數(shù)里將新路由添加進_stack,然后調(diào)用notifyListeners()通知路由變化。
注意這里的兩個邏輯,如果是首頁則先清空;如果新頁面與上一頁一摸一樣,則忽略,因為發(fā)現(xiàn)在web上setNewRoutePath會被重復(fù)調(diào)用。
然后是build函數(shù),如下:
@overrideWidget build(BuildContext context) {return Navigator(key: navigatorKey,pages: [for (final url in _stack)getPage(url)],onPopPage: (route, result){if (_stack.isNotEmpty) {_stack.removeLast();notifyListeners();}return route.didPop(result);},);}?返回一個Navigator,設(shè)置pages和onPopPage。
在onPopPage中實現(xiàn)回退邏輯,可以看到將列表中最后一個remove掉,然后notifyListeners()同時路由變化。上面我們提到PopNavigatorRouterDelegateMixin實現(xiàn)了popRoute函數(shù),它的實現(xiàn)代碼最終就會調(diào)用到onPopPage這里。
pages則是一個Page列表,是當(dāng)前已經(jīng)打開的所有頁面,所以用一個for循環(huán)來創(chuàng)建,我自己定義了一個getPage函數(shù):
Page getPage(String url){return MaterialPage(name: url,arguments: null,child: getWidget(url));}Widget getWidget(String name){switch(name){case "pageB":return PageB();case "pageC":return PageC();default:return PageA();}}注意:因為我們的示例中路由沒有參數(shù),只有路由名稱,所以上面對url沒有進行處理。但是實際使用的時候,在getPage函數(shù)一開始就應(yīng)該對url進行處理,提取出name和參數(shù),并將參數(shù)整理成Object設(shè)置給arguments,這樣頁面中就可以用之前的方式(ModalRoute.of(context).settings.arguments)獲取,不用改變太多。
這里我定義了三個頁面,其中PageA是默認頁面。三個頁面都很簡單,每個頁面有兩個按鈕,一個打開新頁面,一個回退。
打開新頁面用
Router.of(context).routerDelegate.setNewRoutePath("pageB");代替了之前Navigator1.0中的
Navigator.of(context).pushNamed("pageB");回退則使用
Router.of(context).routerDelegate.popRoute();代替了之前Navigator1.0中的
Navigator.of(context).pop();這樣頁面內(nèi)的改動很小,可以很快的轉(zhuǎn)到Navigator2.0。
到這里還差最后一步,實現(xiàn)RouterDelegate中字段currentConfiguration的get方法,如下:
@override String get currentConfiguration => _stack.isNotEmpty ? _stack.last : null;如果不實現(xiàn)這里,雖然頁面可以切換,但是路由信息并沒有更新,比如flutter web的應(yīng)用在瀏覽器中,頁面正常切換,但是地址欄并沒有變化。只有實現(xiàn)了這個get函數(shù),當(dāng)路由發(fā)生變化的時候,其他類才能通過這個函數(shù)獲取到最新路由。
上面就是Navigator2.0的簡單使用,相對于官方的示例更簡單一些,也更容易理解核心部分,尤其方便從Navigator1.0升級到Navigator2.0。
問題
這個過程還是出現(xiàn)不少問題的,記錄一下:
The Navigator.pages must not be empty to use the Navigator.pages API
報錯如下:
════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: The Navigator.pages must not be empty to use the Navigator.pages APIWhen the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28 get current packages/flutter/src/widgets/navigator.dart 3345:33 <fn> packages/flutter/src/widgets/navigator.dart 3361:14 initState packages/flutter/src/widgets/framework.dart 4632:57 [_firstBuild] packages/flutter/src/widgets/framework.dart 4469:5 mount ... ════════════════════════════════════════════════════════════════════════════════════════════════════════════ Exception caught by widgets library ═══════════════════════════════════════════════════════ Navigator.onGenerateRoute was null, but the route named "/" was referenced. The relevant error-causing widget was: MaterialApp file:///Users/bennu/fluttertest/lib/main.dart:62:24 ════════════════════════════════════════════════════════════════════════════════════════════════════════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: The Navigator.pages must not be empty to use the Navigator.pages APIWhen the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28 get current packages/flutter/src/widgets/navigator.dart 3345:33 <fn> packages/flutter/src/widgets/navigator.dart 3361:14 initState packages/flutter/src/widgets/framework.dart 4632:57 [_firstBuild] packages/flutter/src/widgets/framework.dart 4469:5 mount ... ════════════════════════════════════════════════════════════════════════════════════════════════════════════ Exception caught by widgets library ═══════════════════════════════════════════════════════ Navigator.onGenerateRoute was null, but the route named "/" was referenced. The relevant error-causing widget was: MaterialApp file:///Users/bennu/fluttertest/lib/main.dart:62:24 ════════════════════════════════════════════════════════════════════════════════════════════════════════════ Exception caught by widget library ════════════════════════════════════════════════════════ The following assertion was thrown: A HeroController can not be shared by multiple Navigators. The Navigators that share the same HeroController are:- NavigatorState#1f365(lifecycle state: initialized) - NavigatorState#9f699(lifecycle state: initialized) Please create a HeroControllerScope for each Navigator or use a HeroControllerScope.none to prevent subtree from receiving a HeroController. When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/patch/core_patch.dart 906:28 get current packages/flutter/src/widgets/navigator.dart 3501:41 <fn> packages/flutter/src/scheduler/binding.dart 1144:15 [_invokeFrameCallback] packages/flutter/src/scheduler/binding.dart 1090:9 handleDrawFrame packages/flutter/src/scheduler/binding.dart 865:7 <fn> ... ════════════════════════════════════════════════════════════════════════════════════════════════════這里涉及到一開始App的創(chuàng)建,回顧一下代碼:
class MyApp extends StatelessWidget {final delegate = MyRouteDelegate();@overrideWidget build(BuildContext context) {return MaterialApp.router(title: 'Flutter Demo',theme: ThemeData(primarySwatch: Colors.blue,visualDensity: VisualDensity.adaptivePlatformDensity,),routerDelegate: delegate,routeInformationParser: MyRouteParser(),);} }注意MyRouteDelegate并不是在build中創(chuàng)建的,而是在初始化時就創(chuàng)建了。如果在build中才創(chuàng)建就會出現(xiàn)上面的問題,如果像上面代碼一樣在初始化創(chuàng)建就沒有這個問題了。
瀏覽器的回退按鈕
經(jīng)過測試發(fā)現(xiàn),瀏覽器的后退按鈕點擊后并不執(zhí)行pop操作,而是執(zhí)行setNewRoutePath,這樣就會導(dǎo)致回退的時候?qū)嶋H上_stack并沒有移除當(dāng)前頁面,反而將上一個頁面重新添加進來了,這樣_stack路徑就亂了。
這個問題有個官方issues:https://github.com/flutter/flutter/issues/71122
其中官方提到:
the?browser?backward?button?no?longer?tie?to?the?didpopRoute?in?navigator?2.0.?it?is?now?acting?as?deeplinking.?Whenever?backward?or?forward?button?is?pressed,?the?web?engine?will?get?the?new?url?and?send?that?to?the?framework?through?didpushRoute.
BackButtonDispatcher?is?for?android?back?button,?it?will?only?be?triggered?in?android.
這里涉及的BackButtonDispatcher也是Navigator2.0的功能,可以攔截處理返回鍵,但是通過上面可以看出這個功能只對android的返回鍵有效。而在web上,無論是前進還是后退鍵,都是當(dāng)初新的url處理,會執(zhí)行didpushRoute,所以就執(zhí)行到了setNewRoutePath,而不是pop。
issues中也提到了,目前官方?jīng)]有解決這個問題,不過已經(jīng)列入todo列表了,目前想要解決這個問題需要我們自己手動開發(fā)一個plugin,可能需要在native層處理,即在html中通過history處理并暴露api給flutter,比較復(fù)雜,所以目前這個問題并沒有很好的解決方法。
總結(jié)
通過上面可以看出,Navigator2.0相對來說復(fù)雜很多,開發(fā)和學(xué)習(xí)成本大大提高,這也是很多人詬病的原因,所以有人認為Navigator2.0是一個失敗的改造,這也導(dǎo)致目前大家很少使用它。
源碼
關(guān)注公眾號:BennuCTech,發(fā)送“Navi2”獲取源碼。
?
超強干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的Flutter:Navigator2.0介绍及使用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Jetpack:使用 ActivityR
- 下一篇: Flutter混合开发:Android中