Flutter Navigator2.0 指南與原理解析

老孟導(dǎo)讀:此篇文章的作者是:MeandNi,也是《Flutter 開發(fā)之旅從南到北》一書的作者,歡迎大家關(guān)注他的公眾號(hào):MeandNi
歡迎喜歡分享Flutter文章的朋友來投稿,讓更多的人認(rèn)識(shí)您。
本文要討論的話題是 Flutter Navigator2.0。Flutter 1.22?后,大家可以發(fā)現(xiàn),官方對(duì)路由相關(guān) API 的改動(dòng)很大,設(shè)計(jì)文檔中表示,由于傳統(tǒng)的命令式并沒有給開發(fā)者一種靈活的方式去直接管理路由棧,甚至覺得已經(jīng)過時(shí)了,一點(diǎn)也不 Flutter。
As mentioned by a participant in one of Flutter's user studies, the API also feels outdated and not very Flutter-y.
而 Navigator 2.0 引入了一套全新的聲明式 API,全新的實(shí)現(xiàn)方式與調(diào)用方法與以往都截然不同,在官方推薦的?Flutter Navigator 2.0 全面解析(改編自Learning Flutter’s new navigation and routing system)文章中,許多讀者也表示并不能立即適應(yīng) Navigator 2.0 的一些反差。
本文就來帶領(lǐng)讀者們進(jìn)一步深入 Navigator2.0 的基本原理,幫助大家從中探索出最佳的使用方式。
為什么需要新的 API
在探究具體細(xì)節(jié)之前,我們有必要了解一下 Flutter 團(tuán)隊(duì)為什么要不惜這些代價(jià)對(duì) Navigator API 做這次的重構(gòu),主要有如下幾點(diǎn)原因。
原始 API 中的 initialRoute 參數(shù),即系統(tǒng)默認(rèn)的初始頁面,在應(yīng)用運(yùn)行后就不能在更改了。這種情況下,如果用戶接收到一個(gè)系統(tǒng)通知,點(diǎn)擊后想要從當(dāng)前的路由棧狀態(tài)?[Main -> Profile -> Settings]?重啟切換到新的?[Main -> List -> Detail[id=24]?路由棧,Navigator1.0 并沒有一種優(yōu)雅的實(shí)現(xiàn)方式實(shí)現(xiàn)這種效果。
原始的命令式 ?Navigator API 只提供給了開發(fā)者一些非常針對(duì)性的接口,如 push、pop 等,而沒有給出一種更靈活的方式讓我們直接操作路由棧。這也是我上一篇文章中提到的,這種做法其實(shí)與 Flutter 理念相違背,試想如果我們想要改變某個(gè) Widget 的所有子組件只需要重建所有子組件并且創(chuàng)建一系列新的 Widget 即可,而將此概念應(yīng)用在路由中...en?當(dāng)應(yīng)用中存在一系列路由頁面并想要更改時(shí),我們只能調(diào)用 ?push、pop 這類接口來回操作,?Flutter 味道全無。
嵌套路由下,手機(jī)設(shè)備自帶的回退按鈕只能由根 Navigator 響應(yīng)。在目前的應(yīng)用中,我們很多場景都需要在某個(gè)子 tab 內(nèi)單獨(dú)管理一個(gè)子路由棧,假設(shè)有這個(gè)場景,用戶在子路由棧中做一系列路由操作之后,點(diǎn)擊系統(tǒng)回退按鈕,消失的將是整個(gè)上層的根路由,我們當(dāng)然可以使用某種措施來避免這種狀況,但歸咎起來,這也不應(yīng)該是應(yīng)用開發(fā)者應(yīng)該考慮的問題。
于是,Navigator2.0 就肩負(fù)著這千里之任來了。
Navigator2.0
Navigator2.0 新增的聲明式 API 主要包含 Page API、Router API 兩個(gè)部分,它們各自強(qiáng)大的功能為 Navigator2.0 提供了強(qiáng)有力的基石,本節(jié)我就帶讀者們看看它們各自的實(shí)現(xiàn)細(xì)節(jié)。
Page
Page 是 Navigator2.0 中最常見的類之一,從名字就能知道它的含義就是 “頁面”,正如 Widget 就是組件一樣,但 Page 與 Widget 的關(guān)系也更加微妙。
與?Flutter 中三棵樹?的概念保持一致。Widget 只保存組件配置信息,框架層內(nèi)置了一個(gè)?createElement()?可以創(chuàng)建與之對(duì)應(yīng)的 Element 實(shí)例。Page 同樣只保存頁面路由相關(guān)信息,框架層也存在一個(gè)?createRoute()?方法可以創(chuàng)建與之對(duì)應(yīng)的 Route 實(shí)例。

Widget 和 Page 中也都有一個(gè)?canUpdate()?方法,幫助 Flutter 判斷其是否已更新或改變:
//?Page
bool?canUpdate(Page<dynamic>?other)?{
??return?other.runtimeType?==?runtimeType?&&
?????????other.key?==?key;
}
//?Widget
static?bool?canUpdate(Widget?oldWidget,?Widget?newWidget)?{
??return?oldWidget.runtimeType?==?newWidget.runtimeType
??????&&?oldWidget.key?==?newWidget.key;
}
甚至連比較的條件都是運(yùn)行時(shí)類型與 key。
而在代碼層面,Page 類就繼承自我們在 Navigator1.0 時(shí)就用過的 RouteSettings:
abstract?class?Page?extends?RouteSettings
其中就保存了包含路由名稱(name,如 "/settings"),路由參數(shù)(arguments)等信息。
pages
在新的 Navigator 組件中,接受一個(gè)?pages?參數(shù),它接受的就是一個(gè) Page 對(duì)象列表,如下這段代碼:
class?_MyAppState?extends?State<MyApp>?{
??final?pages?=?[
????MyPage(
??????key:?Key('/'),
??????name:?'/',
??????builder:?(context)?=>?HomeScreen(),
????),
????MyPage(
??????key:?Key('/category/5'),
??????name:?'/category/5',
??????builder:?(context)?=>?CategoryScreen(id:?5),
????),
????MyPage(
??????key:?Key('/item/15'),
??????name:?'/item/15',
??????builder:?(context)?=>?ItemScreen(id:?15),
????),
??];
??@override
??Widget?build(BuildContext?context)?{
????return?//...
??????Navigator(
??????????key:?_navigatorKey,
??????????pages:?List.of(pages),
????????),
??}
}
此時(shí),運(yùn)行應(yīng)用,Flutter 就會(huì)根據(jù)這里 pages 列表中的所有 Page 對(duì)象在底層的路由棧生成對(duì)應(yīng)的 Route 實(shí)例,即與 pages 對(duì)應(yīng)的三個(gè)路由頁面。
應(yīng)用打開某個(gè)頁面,就表示在 pages 中添加一個(gè) Page 對(duì)象,系統(tǒng)接受接收到上層的 pages 改變后就會(huì) ** 比較新的 pages 與舊的 pages **,根據(jù)比較結(jié)果,F(xiàn)lutter 就會(huì)在底層路由棧中新生成一個(gè) Route 實(shí)例,這樣一個(gè)新的頁面就算打開成功了。
void?addPage(MyPage?page)?{
??setState(()?=>?pages.add(page));
}
Navigator 組件同樣也新增了一個(gè)?onPopPage?參數(shù),接受一個(gè)回調(diào)函數(shù)來響應(yīng)頁面的 pop 事件,如下面代碼中的 _onPopPage 函數(shù):
class?_MyAppState?extends?State<MyApp>?{
??
??bool?_onPopPage(Route<dynamic>?route,?dynamic?result)?{
????setState(()?=>?pages.remove(route.settings));
????return?route.didPop(result);
??}
??@override
??Widget?build(BuildContext?context)?{
????print('build:?$pages');
????return?//?...
??????Navigator(
????????key:?_navigatorKey,
????????onPopPage:?_onPopPage,
????????pages:?List.of(pages),
??????)
??}
}
當(dāng)我們調(diào)用?Navigator.pop()?關(guān)閉某個(gè)頁面時(shí),即能觸發(fā)這個(gè)函數(shù)調(diào)用,而函數(shù)接收到的 route 對(duì)象就表示需要在 pages 中被移除的頁面,在這里,我們順勢更新 pages 列表做移除操作即可。
在?_onPopPage?中,如果我們同意關(guān)閉該頁面,則調(diào)用?route.didPop(result),該函數(shù)默認(rèn)返回 true。
當(dāng)然,我們也完全可以選擇在接收到通知時(shí)不更新 pages 列表,這完全由我們控制,如下這段代碼:
bool?_onPopPage(Route<dynamic>?route,?dynamic?result)?{
??//?setState(()?=>?pages.remove(route.settings));
??return?route.didPop(result);
}
那么,此時(shí)會(huì)導(dǎo)致什么現(xiàn)象?route.didPop(result)?函數(shù)被直接觸發(fā),表示在底層路由棧中彈出該頁面,這事,F(xiàn)lutter 就會(huì)比較底層已經(jīng)關(guān)閉了一個(gè)頁面的路由棧和當(dāng)前 Navigator 中存有的 pages,發(fā)現(xiàn)不一致,就會(huì)按照現(xiàn)有的 pages 將多余的一個(gè) Page 當(dāng)做新頁面,再生成一個(gè) Route 對(duì)象,這樣底層路由棧中的內(nèi)容就能隨時(shí)保持與上層 pages 數(shù)據(jù)一致了。
也就是說,某個(gè)頁面是否能夠關(guān)閉完全由我們掌控,而不是單純交給系統(tǒng)的 ?Navigator.pop()?。這里,如果我們不想關(guān)閉某個(gè)頁面也可以在 onPopPage 的回調(diào)函數(shù)中直接返回 false:
bool?_onPopPage(Route<dynamic>?route,?dynamic?result)?{
??if?(...)?{
????return?false;
??}
??setState(()?=>?pages.remove(route.settings));
??return?route.didPop(result);
}
需要注意的是,onPopPage 只響應(yīng)路由棧頂層頁面的推出,中間頁面的移除不會(huì)調(diào)用這個(gè)回調(diào)函數(shù)。
這也合情合理,如果我們想要移除非頂層頁面,那么下次彈出頁面時(shí)候,底層路由棧會(huì)直接與新的 pages 列表比較來做出相應(yīng)改變。
要運(yùn)行上述完整案例,查看完整代碼:https://github.com/MeandNi/flutter_navigator_v2/blob/master/lib/pages_example.dart
Flutter 框架中預(yù)先內(nèi)置了 MaterialPage 和 CupertinoPage 兩種 Page,分別表示 Material 和 Cupertino 風(fēng)格下的頁面,與 Navigator1.0 中的 MaterialPageRoute 和 CupertinoPageRoute 相呼應(yīng),它們都接受一個(gè) child 組件表示該頁面所要呈現(xiàn)的內(nèi)容。例如下面這個(gè)例子,我們可以直接在 pages 中使用 MaterialPage 創(chuàng)建頁面:
List?pages?=?[
??MaterialPage(
????key:?ValueKey('VeggiesListPage'),
????child:?VeggiesListScreen(
??????veggies:?veggies,
??????onTapped:?_handleVeggieTapped,
????),
??),
??if?(show404)
????MaterialPage(key:?ValueKey('UnknownPage'),?child:?UnknownScreen())
??else
????if?(_selectedVeggie?!=?null)
??????VeggieDetailsPage(veggie:?_selectedVeggie)
];
我們也可以直接繼承 Page 定義自己的頁面類型,如下:
class?MyPage?extends?Page?{
??final?Veggie?veggie;
??MyPage({
????this.veggie,
??})?:?super(key:?ValueKey(veggie));
??Route?createRoute(BuildContext?context)?{
????return?MaterialPageRoute(
??????settings:?this,
??????builder:?(BuildContext?context)?{
????????return?VeggieDetailsScreen(veggie:?veggie);
??????},
????);
??}
}
這里,我們重寫了?createRoute()??返回一個(gè) MaterialPageRoute 對(duì)象即可。
Router
Router 是 Navigator2.0 中新增的另一個(gè)非常重要的組件,繼承自 StatefulWidget,可以管理自己的狀態(tài)。
它所管理的狀態(tài)就是應(yīng)用的路由狀態(tài),結(jié)合上節(jié)中提到的 Page 的概念,我們就可以將其中的 pages 看做這里的路由狀態(tài),當(dāng)我們改變 pages 內(nèi)容 / 狀態(tài)時(shí),Router 就會(huì)將該狀態(tài)分發(fā)給子組件,狀態(tài)改變導(dǎo)致子組件重建應(yīng)用最新的狀態(tài)。
所以當(dāng) Navigator 作為 Router 的子組件時(shí),就會(huì)天然具有感知路由狀態(tài)改變的能力了,如下圖所示:

當(dāng)用戶點(diǎn)擊某個(gè)按鈕就會(huì)觸發(fā)類似下面這個(gè)函數(shù)的調(diào)用,該函數(shù)又會(huì)導(dǎo)致狀態(tài)改變而重建子組件。
void?_pushPage()?{
??MyRouteDelegate.of(context).push('Route$_counter');
}
Navigator2.0 所強(qiáng)調(diào)的聲明式 API 的核心就在于此,我們操作路由的方式并非再是 push 或者 pop,而是改變應(yīng)用的狀態(tài)了!我們需要從觀念上理解聲明式 API 與以往的不同之處。
Router 代理
Router 要完成上面所說的功能主要需要通過配置 RouterDelegate(路由代理)實(shí)現(xiàn)。
Navigator2.0 之后,F(xiàn)lutter 也提供了 MaterialApp 的新構(gòu)造函數(shù) router 來幫助我們隱式構(gòu)造出全局的 Router 組件,使用方式如下:
MaterialApp.router(
??title:?'Flutter?Demo',
??theme:?ThemeData(
????primarySwatch:?Colors.blue,
????visualDensity:?VisualDensity.adaptivePlatformDensity,
??),
??routeInformationParser:?MyRouteParser(),
??routerDelegate:?delegate,
)
該構(gòu)造函數(shù)接受一個(gè)?routerDelegate?參數(shù),這里,就可以傳入了我們自己創(chuàng)建的 MyRouteDelegate 對(duì)象,具體代碼如下:
class?MyRouteDelegate?extends?RouterDelegate<String>
????with?PopNavigatorRouterDelegateMixin<String>,?ChangeNotifier?{
??final?_stack?=?<String>[];
??static?MyRouteDelegate?of(BuildContext?context)?{
????final?delegate?=?Router.of(context).routerDelegate;
????assert(delegate?is?MyRouteDelegate,?'Delegate?type?must?match');
????return?delegate?as?MyRouteDelegate;
??}
??MyRouteDelegate({
????@required?this.onGenerateRoute,
??});
??//?...
??@override
??Widget?build(BuildContext?context)?{
????print('${describeIdentity(this)}.stack:?$_stack');
????return?Navigator(
??????key:?navigatorKey,
??????onPopPage:?_onPopPage,
??????pages:?[
????????for?(final?name?in?_stack)
????????????MyPage(
??????????????key:?ValueKey(name),
??????????????name:?name,
??????????????routeFactory:?onGenerateRoute,
????????????),
??????],
????);
??}
}
上面的 MyRouteDelegate 繼承自 RouterDelegate,內(nèi)部可以實(shí)現(xiàn)它的 setInitialRoutePath、setNewRoutePath、build 與 currentConfiguration getter 四個(gè)方法,并且也混入了 PopNavigatorRouterDelegateMixin 類,它的主要作用是響應(yīng) Android 設(shè)備的回退按鈕,而 ChangeNotifier 作用便是做事件通知,下文的 “實(shí)現(xiàn) RouterDelegate” 中我們就會(huì)分析這些方法各自的作用。
這里,我們先看?MyRouteDelegate.build?方法,與上一小節(jié)一樣,我們可以通過傳入 pages 和 onPopPage 參數(shù)創(chuàng)建一個(gè) Navigator 組件返回,這樣,當(dāng) MyRouteDelegate 組件傳入到?MaterialApp.router()?構(gòu)造函數(shù)后,這里的 Navigator 就順利成為了 Router 的子組件了。
大部分情況下,一個(gè)自定義的路由代理就可以這樣實(shí)現(xiàn)完成了。
Router 事件
在應(yīng)用開發(fā)中,Router 最根本的作用還是監(jiān)聽各種來自系統(tǒng)的路由相關(guān)事件,包括:
首次啟動(dòng)應(yīng)用程序時(shí),系統(tǒng)請(qǐng)求的初始路由。
監(jiān)聽來自系統(tǒng)的新 intent,即打開一個(gè)新路由頁面。
監(jiān)聽設(shè)備回退,關(guān)閉路由棧中頂部路由。
而要想完整的響應(yīng)這些事件,還得為 Router 配置 RouteNameProvider Delegate 和 BackButtonDispatcher Delegate。
最初,應(yīng)用啟動(dòng)或者打開新頁面的事件從系統(tǒng)發(fā)出時(shí),會(huì)轉(zhuǎn)發(fā)給應(yīng)用層一個(gè)該事件相關(guān)的字符串,RouteNameParser Delegate 會(huì)將該字符串傳遞給 RouteNameParser,經(jīng)而會(huì)解析成一個(gè)類型 T 的對(duì)象,類型 T 默認(rèn)為 ?RouteSetting,其中就會(huì)包含傳遞的路由名稱和參數(shù)等信息了。
類似地,用戶點(diǎn)擊設(shè)備回退按鈕后,會(huì)將該事件傳遞給 BackButtonDispatcher Delegate。
最終,RouteNameParser 解析的對(duì)象數(shù)據(jù)和 BackButtonDispatcher Delegate 回退事件都會(huì)轉(zhuǎn)發(fā)給上文中的 RouteDelegate,RouteDelegate 接受到這些事就會(huì)響應(yīng),執(zhí)行響應(yīng)的狀態(tài)改變,從而導(dǎo)致含有 pages 的 Navigator 組件重建,在應(yīng)用層中呈現(xiàn)最新的路由狀態(tài)。
整個(gè)過程可以用下圖表示:

需要知道的是,RouteNameProvider Delegate 和 BackButtonDispatcher Delegate 都有 Flutter 內(nèi)置的默認(rèn)實(shí)現(xiàn),因此,大部分情況下,我們并不需要考慮其中的細(xì)節(jié),此時(shí)類型 T 默認(rèn)為 ?RouteSetting(與 Navogator1.0 一致,包含路由信息)。
從以上部分可以看出,一系列的操作只是將最終事件傳遞給 RouterDelegate 而已,之后狀態(tài)更新等操作都可以由我們自定義的 RouterDelegate 決定。
實(shí)現(xiàn) RouterDelegate
正如我們上文說的,F(xiàn)lutter 為 RouteNameProvider Delegate 和 BackButtonDispatcher Delegate 都提供了默認(rèn)實(shí)現(xiàn),而 RouterDelegate 則必須要我們手動(dòng)實(shí)現(xiàn),并傳遞給 ?MaterialApp.router()??構(gòu)造函數(shù)才行。
我們可以在這里完成各種業(yè)務(wù)相關(guān)的操作,RouteDelegate 本身實(shí)現(xiàn)自 Listenable,即可監(jiān)聽對(duì)象,也可以叫做被觀察者,每當(dāng)狀態(tài)改變時(shí),觀察者們就能通知它響應(yīng)該事件,從而觸使 Navigator 組件重建,更新路由狀態(tài)。
RouterDelegate 中的路由事件主要由下面幾個(gè)函數(shù)接受:
backButtonDispatcher 發(fā)出回退按鈕事件時(shí),會(huì)調(diào)用 RouterDelegate 的?popRoute?方法,由混入的 PopNavigatorRouterDelegateMixin 實(shí)現(xiàn)。 發(fā)出應(yīng)用初始路由的通知時(shí),會(huì)調(diào)用 RouterDelegate 的?setInitialRoutePath?方法,該方法接受路由名稱,默認(rèn)此方法會(huì)直接調(diào)用 RouterDelegate 的 setNewRoutePath 函數(shù)。 routeNameProvider 系統(tǒng)出發(fā)打開新路由頁面的通知時(shí),直接調(diào)用?setNewRoutePath?方法,參數(shù)就是由 routeNameParser 解析的結(jié)果。
因此,我們最終就可以實(shí)現(xiàn)如下這樣的 RouterDelegate:
class?MyRouteDelegate?extends?RouterDelegate<String>
????with?PopNavigatorRouterDelegateMixin<String>,?ChangeNotifier?{
??final?_stack?=?<String>[];
??static?MyRouteDelegate?of(BuildContext?context)?{
????final?delegate?=?Router.of(context).routerDelegate;
????assert(delegate?is?MyRouteDelegate,?'Delegate?type?must?match');
????return?delegate?as?MyRouteDelegate;
??}
??MyRouteDelegate({
????@required?this.onGenerateRoute,
??});
??final?RouteFactory?onGenerateRoute;
??@override
??GlobalKey?navigatorKey?=?GlobalKey();
??@override
??String?get?currentConfiguration?=>?_stack.isNotEmpty???_stack.last?:?null;
??List<String>?get?stack?=>?List.unmodifiable(_stack);
??void?push(String?newRoute)?{
????_stack.add(newRoute);
????notifyListeners();
??}
??void?pop()?{
????if?(_stack.isNotEmpty)?{
??????_stack.remove(_stack.last);
????}
????notifyListeners();
??}
??@override
??Future<void>?setInitialRoutePath(String?configuration)?{
????return?setNewRoutePath(configuration);
??}
??@override
??Future<void>?setNewRoutePath(String?configuration)?{
????print('setNewRoutePath?$configuration');
????_stack
??????..clear()
??????..add(configuration);
????return?SynchronousFuture<void>(null);
??}
??bool?_onPopPage(Route<dynamic>?route,?dynamic?result)?{
????if?(_stack.isNotEmpty)?{
??????if?(_stack.last?==?route.settings.name)?{
????????_stack.remove(route.settings.name);
????????notifyListeners();
??????}
????}
????return?route.didPop(result);
??}
??@override
??Widget?build(BuildContext?context)?{
????print('${describeIdentity(this)}.stack:?$_stack');
????return?Navigator(
??????key:?navigatorKey,
??????onPopPage:?_onPopPage,
??????pages:?[
????????for?(final?name?in?_stack)
????????????MyPage(
??????????????key:?ValueKey(name),
??????????????name:?name,
????????????),
??????],
????);
??}
}
這里的 _stack 表示一個(gè)數(shù)據(jù)集,每個(gè)數(shù)據(jù)會(huì)在 build 函數(shù)中創(chuàng)建出一個(gè) MyPage,默認(rèn)為空。應(yīng)用啟動(dòng)時(shí),會(huì)先調(diào)用這里的?setInitialRoutePath(String configuration)?方法,參數(shù)為 ’/‘,此時(shí)路由棧就會(huì)存在一個(gè)首頁了。
完整代碼,參見:https://github.com/MeandNi/flutter_navigator_v2/blob/master/lib/router_example.dart
在子組件中,我們也可以使用 MyRouteDelegate,通過如下方式打開或者關(guān)閉一個(gè)頁面:
MyRouteDelegate.of(context).push('Route$_counter');
MyRouteDelegate.of(context).pop();
與可遺傳組件性質(zhì)相同,這里會(huì)觸發(fā) MyRouteDelegate 中,我們自定義的 push 和 pop 方法操作聲明的路由棧,最終通知更新路由狀態(tài)。
實(shí)現(xiàn) RouteInformationParser
MaterialApp.router?除了需要接受路由代理 routerDelegate 這個(gè)必要參數(shù)外,還需要同時(shí)指定 routeInformationParser 參數(shù),如下:
MaterialApp.router(
??title:?'Flutter?Demo',
??routeInformationParser:?MyRouteParser(),??//?傳入?MyRouteParser
??routerDelegate:?delegate,
)
該參數(shù)接收一個(gè) RouteInformationParser 對(duì)象,定義該類通常有一個(gè)最簡單直接的實(shí)現(xiàn),如下:
class?MyRouteParser?extends?RouteInformationParser?{
??@override
??Future?parseRouteInformation(RouteInformation?routeInformation)?{
????return?SynchronousFuture(routeInformation.location);
??}
??@override
??RouteInformation?restoreRouteInformation(String?configuration)?{
????return?RouteInformation(location:?configuration);
??}
}
MyRouteParser 繼承自 RouteInformationParser,并重寫了父類?parseRouteInformation()?和?restoreRouteInformation()?兩個(gè)方法。
如上文所述,parseRouteInformation()?方法的作用就是接受系統(tǒng)傳遞給我們的路由信息 routeInformation 解析后,返回并轉(zhuǎn)發(fā)給我們之前定義的路由代理 RouterDelegate,解析后的類型為 RouteInformationParser 的泛型類型,即這里的 String。也就是說,下面這個(gè) routerDelegate 中 ?setNewRoutePath()?方法的的參數(shù) configuration 就是從這里轉(zhuǎn)發(fā)而來的:
@override
Future<void>?setNewRoutePath(String?configuration)?{
??print('setNewRoutePath?$configuration');
??_stack
????..clear()
????..add(configuration);
??return?SynchronousFuture<void>(null);
}
restoreRouteInformation()?方法返回一個(gè) RouteInformation 對(duì)象,表示從傳入的 configuration 恢復(fù)路由信息。與 parseRouteInformation 相呼應(yīng)。
例如,在瀏覽器中,F(xiàn)lutter 應(yīng)用所在的標(biāo)簽被關(guān)閉,此時(shí)如果我們想要恢復(fù)整個(gè)頁面的路由棧則需要重寫此方法,
上面 MyRouteParser 的實(shí)現(xiàn),是最簡單的實(shí)現(xiàn)方式,功能就是在?parseRouteInformation()?接受底層 RouteInformation,restoreRouteInformation()?恢復(fù)上層的 configuration。
我們也可以繼續(xù)為這兩個(gè)方法賦能,實(shí)現(xiàn)更符合業(yè)務(wù)需求的邏輯,如下這代碼:
import?'package:flutter/material.dart';
import?'package:flutter_navigator_v2/navigator_v2/model.dart';
class?VeggieRouteInformationParser?extends?RouteInformationParser<VeggieRoutePath>?{
??@override
??Future?parseRouteInformation(
??????RouteInformation?routeInformation)?async?{
????print("parseRouteInformation");
????final?uri?=?Uri.parse(routeInformation.location);
????//?Handle?'/'
????if?(uri.pathSegments.length?==?0)?{
??????return?VeggieRoutePath.home();
????}
????//?Handle?'/veggie/:id'
????if?(uri.pathSegments.length?==?2)?{
??????if?(uri.pathSegments[0]?!=?'veggie')?return?VeggieRoutePath.unknown();
??????var?remaining?=?uri.pathSegments[1];
??????var?id?=?int.tryParse(remaining);
??????if?(id?==?null)?return?VeggieRoutePath.unknown();
??????return?VeggieRoutePath.details(id);
????}
????//?Handle?unknown?routes
????return?VeggieRoutePath.unknown();
??}
??@override
??RouteInformation?restoreRouteInformation(VeggieRoutePath?path)?{
????print("restoreRouteInformation");
????if?(path.isUnknown)?{
??????return?RouteInformation(location:?'/404');
????}
????if?(path.isHomePage)?{
??????return?RouteInformation(location:?'/');
????}
????if?(path.isDetailsPage)?{
??????return?RouteInformation(location:?'/veggie/${path.id}');
????}
????return?null;
??}
}
這里的 VeggieRouteInformationParser 繼承的 RouteInformationParser 泛型類型被指定為了我們自定義的 VeggieRoutePath,在 Navigator2.0 中我們稱這個(gè)解析后的形式為路由 Model。
此時(shí) VeggieRouteInformationParser 作用就凸顯出來了,它在?parseRouteInformation()?方法中接受系統(tǒng)的 RouteInformation 信息后就可以轉(zhuǎn)換成我們上層熟悉的 VeggieRoutePath ?Model 對(duì)象。VeggieRoutePath 類內(nèi)容如下:
class?VeggieRoutePath?{
??final?int?id;
??final?bool?isUnknown;
??VeggieRoutePath.home()
??????:?id?=?null,
????????isUnknown?=?false;
??VeggieRoutePath.details(this.id)?:?isUnknown?=?false;
??VeggieRoutePath.unknown()
??????:?id?=?null,
????????isUnknown?=?true;
??bool?get?isHomePage?=>?id?==?null;
??bool?get?isDetailsPage?=>?id?!=?null;
}
此時(shí),在?RouterDelegate?中,我們就可以根據(jù)該對(duì)象做路由狀態(tài)的更新了。
最佳實(shí)踐
Navigator 2.0 與以往不同的方面主要體現(xiàn)在,將路由狀態(tài)轉(zhuǎn)換成了應(yīng)用本身的狀態(tài),給了開發(fā)者更大的自由與想象空間,此后,我們可以將路由邏輯及其狀態(tài)的管理與我們的業(yè)務(wù)邏輯緊密相連,形成自己的一套方案,相信這又會(huì)是以后 Flutter 體系中一塊大主題。
上述提及的所有代碼包含三個(gè)案例,分別是:
pages_example.dart,Navigator + Page 實(shí)現(xiàn)路由狀態(tài)管理。 router_example.dart,Router + Navigator + Page 實(shí)現(xiàn)路由狀態(tài)的統(tǒng)一管理 水果列表最佳實(shí)踐,相對(duì)完整的一個(gè)案例,包含自定義 RouteInformationParser Model 和路由狀態(tài)管理操作。
源碼地址:https://github.com/MeandNi/flutter_navigator_v2
