Flutter Navigator2.0的原理和Web端實踐
1.背景與動機
在Navigator 2.0推出之前,F(xiàn)lutter主要通過Navigator 1.0和其提供的 API(如push(), pop(), pushNamed()等)來管理頁面路由。然而,Navigator 1.0存在一些局限性,如難以實現(xiàn)復(fù)雜的頁面操作(如移除棧內(nèi)中間頁面、交換頁面等)、不支持嵌套路由以及無法滿足全平臺(尤其是Web平臺)的新需求。因此,F(xiàn)lutter官方團隊決定對路由系統(tǒng)進行改造,推出了Navigator 2.0。
2.主要特性
- 聲明式API Navigator 2.0提供的聲明式API使得路由管理更加直觀和易于理解。開發(fā)者只需聲明頁面的配置信息,而無需編寫復(fù)雜的導(dǎo)航邏輯代碼。這種方式不僅減少了代碼量,還提高了代碼的可讀性和可維護性。
- 嵌套路由 Navigator 2.0滿足了嵌套路由的需求場景,允許開發(fā)者在應(yīng)用中創(chuàng)建嵌套的路由結(jié)構(gòu)。這使得應(yīng)用的結(jié)構(gòu)更加清晰,同時也提高了頁面導(dǎo)航的靈活性。
- 全平臺支持 Navigator 2.0提供的API能夠滿足不同平臺(如iOS、Android、Web等)的導(dǎo)航需求,使得開發(fā)者能夠更加方便地構(gòu)建跨平臺的應(yīng)用。
- 強大的頁面操作能力 Navigator 2.0提供了更加豐富的頁面操作能力,如移除棧內(nèi)中間頁面、交換頁面等。這些操作在Navigator 1.0中很難實現(xiàn)或需要編寫復(fù)雜的代碼,而在Navigator 2.0中則變得簡單直接。
3.核心組件
- Router 在Navigator 2.0中,Router組件是路由管理的核心。它負責根據(jù)當前的路由信息(RouteInformation)和路由信息解析器(RouteInformationParser)來構(gòu)建和更新UI。Router組件接收三個主要參數(shù):1.routeInformationProvider:提供當前的路由信息;2.routeInformationParser:將路由信息解析為路由配置;3.routerDelegate:根據(jù)路由配置構(gòu)建和更新UI。
- RouteInformationProvider RouteInformationProvider是一個提供當前路由信息的組件。它通常與平臺相關(guān)的路由信息源(如瀏覽器的URL、Android的Intent等)集成,以獲取當前的路由信息。
- RouteInformationParser RouteInformationParser負責將RouteInformation解析為RouteConfiguration。這個過程允許開發(fā)者根據(jù)路由信息的格式(如URL)來定義如何將其映射到應(yīng)用內(nèi)的路由配置。
- RouterDelegate RouterDelegate是與UI構(gòu)建緊密相關(guān)的組件。它必須實現(xiàn)RouterDelegate接口,并提供兩個主要方法: 1.build(BuildContext context):根據(jù)當前的路由配置構(gòu)建UI;2.setNewRoutePath(List configuration):設(shè)置新的路由路徑,并更新UI;3.Future popRoute() :實現(xiàn)后退邏輯。
4.簡單實例
首先通過MaterialApp.router()來創(chuàng)建MaterialApp:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final routerDelegate = MyRouterDelegate();
final routeInformationParser = MyRouteInformationParser();
return MaterialApp.router(
title: 'Flutter Navigator 2.0 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerDelegate: routerDelegate,
routeInformationParser: routeInformationParser,
);
}
}
需要定義一個RouterDelegate對象和一個RouteInformationParser對象。其中根據(jù)路由配置構(gòu)建和更新UI,RouteInformationParser負責將RouteInformation解析為RouteConfiguration。 RouterDelegate可以傳個泛型,定義其currentConfiguration對象的類型。
class MyRouterDelegate extends RouterDelegate<String>
with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
private List<String> _pages = ['/home'];
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: _pages.map((route) => MaterialPage(
key: Key(route),
child: generatePage(route),
)).toList(),
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
_pages.removeLast();
notifyListeners();
return true;
},
);
}
@override
Future<void> setNewRoutePath(String path) async {
if (!_pages.contains(path)) {
_pages.add(path);
notifyListeners();
}
}
Widget generatePage(String route) {
switch (route) {
case '/home':
return HomePage();
case '/details':
// 這里可以傳遞參數(shù),例如 DetailsPage(arguments: someData)
return DetailsPage();
default:
return NotFoundPage();
}
}
@override
String get currentConfiguration => _pages.last;
}
其中build()一般返回的是一個Navigator對象,popRoute()實現(xiàn)后退邏輯,setNewRoutePath()實現(xiàn)新頁面的邏輯。定義了一個_pages數(shù)組對象,記錄每個路由的path,可以理解為是一個路由棧,這個路由棧對我們來說非常友好,在有復(fù)雜的業(yè)務(wù)邏輯時,我們可以自行定義相應(yīng)的棧管理邏輯。currentConfiguration返回的是棧頂?shù)膒age信息。創(chuàng)建一個類繼承RouteInformationParser,主要的作用是包裝解析路由信息,這里有一個最簡單的方式,如下:
class MyRouteInformationParser extends RouteInformationParser<String> {
@override
Future<String> parseRouteInformation(RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location);
return SynchronousFuture(uri.path);
}
@override
RouteInformation restoreRouteInformation(String configuration) {
return RouteInformation(location: configuration);
}
}
好的,接下來我們看一下調(diào)用:
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
Router.of(context).routerDelegate.setNewRoutePath("/details");
},
child: Text('Go to Details'),
),
),
);
}
}
class DetailsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details')),
body: Center(
child: Text('This is Details Page'),
),
);
}
}
class NotFoundPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Not Found')),
body: Center(
child: Text('Page not found'),
),
);
}
}
非常簡單,直接調(diào)用Router.of(context).routerDelegate.setNewRoutePath()即可。
到此為止,一個使用Navigator2.0的最簡單的路由實例就完成了。和Navigator1.0相比,看上去繁雜了不少。但是可以根據(jù)業(yè)務(wù)需求自定義路由棧進行管理,大大的提升了靈活性。接來看我們看一下Navigator2.0是如何對路由進行實現(xiàn)的。
5.源碼簡析
我們在使用Navigator2.0時,是通過MaterialApp.router()創(chuàng)建的MaterialApp對象,之前章節(jié)提到過,傳了RouteInformationParser和RouterDelegate這兩個對象。當傳遞了RouterDelegate對象時,_MaterialAppState中的_usesRouter會被設(shè)置為true。
bool get _usesRouter => widget.routerDelegate != null || widget.routerConfig != null;
在build()時,通過WidgetsApp.router()方法創(chuàng)建了一個WidgetsApp對象:
if (_usesRouter) {
return WidgetsApp.router(
key: GlobalObjectKey(this),
routeInformationProvider: widget.routeInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate,
routerConfig: widget.routerConfig,
backButtonDispatcher: widget.backButtonDispatcher,
builder: _materialBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
textStyle: _errorTextStyle,
color: materialColor,
locale: widget.locale,
localizationsDelegates: _localizationsDelegates,
localeResolutionCallback: widget.localeResolutionCallback,
localeListResolutionCallback: widget.localeListResolutionCallback,
supportedLocales: widget.supportedLocales,
showPerformanceOverlay: widget.showPerformanceOverlay,
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
);
}
在_WidgetsAppState中根據(jù)routerDelegate設(shè)置了成員變量_usesRouterWithDelegates的值:
bool get _usesRouterWithDelegates => widget.routerDelegate != null;
在build()時會創(chuàng)建一個Router對象,其中Router繼承了StatefulWidget:
@override
Widget build(BuildContext context) {
Widget? routing;
if (_usesRouterWithDelegates) {
routing = Router<Object>(
restorationScopeId: 'router',
routeInformationProvider: _effectiveRouteInformationProvider,
routeInformationParser: widget.routeInformationParser,
routerDelegate: widget.routerDelegate!,
backButtonDispatcher: _effectiveBackButtonDispatcher,
);
}
......
}
在上一章節(jié)的實例中我們可得知,頁面的切換都是依靠RouterDelegate對象進行的。每當切換到新的頁面時,都會調(diào)用setNewRoutePath()方法,因此我們來看一下setNewRoutePath()是什么時候被調(diào)用的,有兩處。第一處:
void _handleRouteInformationProviderNotification() {
_routeParsePending = true;
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
_RouteSetter<T> _processParsedRouteInformation(Object? transaction, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
return (T data) async {
if (_currentRouterTransaction != transaction) {
return;
}
await delegateRouteSetter()(data);
if (_currentRouterTransaction == transaction) {
_rebuild();
}
};
}
我們看看_handleRouteInformationProviderNotification的調(diào)用時機:
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
}
我們可以看到在initState()時,也就是在Router被初始化的時候由widget.routeInformationProvider來監(jiān)聽一些狀態(tài)實現(xiàn)新頁面的切換。我們來看一下routeInformationProvider。RouteInformationProvider在我們自己沒有創(chuàng)建的情況下,系統(tǒng)會默認為我們創(chuàng)建一個PlatformRouteInformationProvider對象。它實際上是個ChangeNotifier。系統(tǒng)會監(jiān)聽每一幀的信號發(fā)送,調(diào)用其父類routerReportsNewRouteInformation()方法,我們看看它的實現(xiàn):
@override
void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) {
final bool replace =
type == RouteInformationReportingType.neglect ||
(type == RouteInformationReportingType.none &&
_equals(_valueInEngine.uri, routeInformation.uri));
SystemNavigator.selectMultiEntryHistory();
SystemNavigator.routeInformationUpdated(
uri: routeInformation.uri,
state: routeInformation.state,
replace: replace,
);
_value = routeInformation;
_valueInEngine = routeInformation;
}
其中SystemNavigator.selectMultiEntryHistory()的實現(xiàn)如下:
/// Selects the multiple-entry history mode.
///
/// On web, this switches the browser history model to one that tracks all
/// updates to [routeInformationUpdated] to form a history stack. This is the
/// default.
///
/// Currently, this is ignored on other platforms.
///
/// See also:
///
/// * [selectSingleEntryHistory], which forces the history to only have one
/// entry.
static Future<void> selectMultiEntryHistory() {
return SystemChannels.navigation.invokeMethod<void>('selectMultiEntryHistory');
}
這個方法是由各個平臺自行實現(xiàn)的。從注釋中我們可得知如果是在Web平臺下,它會切換成history模式,并從history stack中追蹤所有的變化。在history發(fā)生變化時,會發(fā)送信號給Flutter層等待處理。SystemNavigator.routeInformationUpdated()方法是用來更新路由的,我們先不做分析。接著我們回到PlatformRouteInformationProvider,看看它什么時候會執(zhí)行notifyListeners()方法:
@override
Future<bool> didPushRouteInformation(RouteInformation routeInformation) async {
assert(hasListeners);
_platformReportsNewRouteInformation(routeInformation);
return true;
}
void _platformReportsNewRouteInformation(RouteInformation routeInformation) {
if (_value == routeInformation) {
return;
}
_value = routeInformation;
_valueInEngine = routeInformation;
notifyListeners();
}
在監(jiān)聽到有push路由的情況下時,會調(diào)用notifyListeners(),從而實現(xiàn)頁面的切換。我們再來看第二處調(diào)用setNewRoutePath()的地方:
@override
void didChangeDependencies() {
_routeParsePending = true;
super.didChangeDependencies();
// The super.didChangeDependencies may have parsed the route information.
// This can happen if the didChangeDependencies is triggered by state
// restoration or first build.
if (widget.routeInformationProvider != null && _routeParsePending) {
_processRouteInformation(widget.routeInformationProvider!.value, () => widget.routerDelegate.setNewRoutePath);
}
_routeParsePending = false;
_maybeNeedToReportRouteInformation();
}
void _processRouteInformation(RouteInformation information, ValueGetter<_RouteSetter<T>> delegateRouteSetter) {
assert(_routeParsePending);
_routeParsePending = false;
_currentRouterTransaction = Object();
widget.routeInformationParser!
.parseRouteInformationWithDependencies(information, context)
.then<void>(_processParsedRouteInformation(_currentRouterTransaction, delegateRouteSetter));
}
parseRouteInformationWithDependencies()方法中調(diào)用的parseRouteInformation()其實就是我們自定義RouteInformationParser來進行的實現(xiàn)。
Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
return parseRouteInformation(routeInformation);
}
看到當其與父的依賴關(guān)系被改變的時候會調(diào)用setNewRoutePath()。大概率就是App初始化的時候被調(diào)用一次。
6.根據(jù)狐友業(yè)務(wù)的Web端實踐
我們的Flutter團隊會承擔一些運營活動的H5需求。在實現(xiàn)時我們對路由有如下需求:
- 可以根據(jù)業(yè)務(wù)自由的管理路由棧;
- 分享鏈接只能分享出去默認入口鏈接,不希望中間的路由鏈接被分享出去;
- 不管有多少個路由頁面,history始終不變,在響應(yīng)瀏覽器返回鍵時不響應(yīng)路由棧的pop操作。
在之前使用Navigator1.0時體驗并不太好,一個是不夠靈活,另外還需對分享出去的鏈接做處理。因此我們利用Navigator2.0設(shè)計了一套新的路由:
MyRouterDelegate delegate = MyRouterDelegate();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routeInformationParser: MyRouteParser(),
routerDelegate: delegate,
);
}
Parser實現(xiàn)非常簡單:
class MyRouteParser extends RouteInformationParser<RouteSettings> {
@override
///parseRouteInformation() 方法的作用就是接受系統(tǒng)傳遞給我們的路由信息 routeInformation
Future<RouteSettings> parseRouteInformation(
RouteInformation routeInformation) {
// Uri uri = Uri.parse(routeInformation.location??"/");
return SynchronousFuture(RouteSettings(name: routeInformation.location));
}
@override
///恢復(fù)路由信息
RouteInformation restoreRouteInformation(RouteSettings configuration) {
return RouteInformation(location: configuration.name);
}
}
Delegate的實現(xiàn)如下:
import 'package:ai_chatchallenge/router/exit_util.dart';
import 'package:ai_chatchallenge/router/navigator_util.dart';
import 'package:ai_chatchallenge/router/my_router_arg.dart';
import 'package:flutter/material.dart';
import 'route_page_config.dart';
class MyRouterDelegate extends RouterDelegate<RouteSettings>
with PopNavigatorRouterDelegateMixin<RouteSettings>, ChangeNotifier {
///頁面棧
List<Page> _stack = [];
//當前的界面信息
RouteSettings _setting = RouteSettings(
name: RouterName.rootPage,
arguments: BaseArgument()..name = RouterName.rootPage);
//重寫navigatorKey
@override
GlobalKey<NavigatorState> navigatorKey;
MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() {
//初始化兩個方法 一個是push頁面 另一個是替換頁面
NavigatorUtil()
.registerRouteJump(RouteJumpFunction(onJumpTo: (RouteSettings setting) {
// _setting = setting;
// changePage();
addPage(name: setting.name, arguments: setting.arguments);
}, onReplaceAndJumpTo: (RouteSettings setting) {
if (_stack.isNotEmpty) {
_stack.removeLast();
}
_setting = setting;
changePage();
}, onClearStack: () {
_stack.clear();
_setting = RouteSettings(
name: RouterName.rootPage,
arguments: BaseArgument()..name = RouterName.rootPage);
changePage();
}, onBack: () {
if (_stack.isNotEmpty) {
_stack.removeLast();
if (_stack.isNotEmpty) {
_setting = _stack.last;
} else {
_setting = RouteSettings(
name: RouterName.rootPage,
arguments: BaseArgument()..name = RouterName.rootPage);
}
changePage();
}
}));
}
@override
RouteSettings? get currentConfiguration {
return _stack.last;
}
@override
Future<bool> popRoute() {
if (_stack.length > 1) {
_stack.removeLast();
_setting = _stack.last;
changePage();
//非最后一個頁面
return Future.value(true);
}
//最后一個頁面確認退出操作
return _confirmExit();
}
Future<bool> _confirmExit() async {
bool result = ExitUtil.doubleCheckExit(navigatorKey.currentContext!);
// bool result = await ExitUtil.backToDesktop();
return !result;
}
void addPage({required name, arguments}) {
_setting = RouteSettings(name: name, arguments: arguments);
changePage();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
//解決物理返回建無效的問題
onWillPop: () async => !await navigatorKey.currentState!.maybePop(),
child: Navigator(
key: navigatorKey,
pages: _stack,
onPopPage: _onPopPage,
),
);
}
/// 按下返回的回調(diào)
bool _onPopPage(Route<dynamic> route, dynamic result) {
debugPrint("這里的試試");
if (!route.didPop(result)) {
return false;
}
return true;
}
changePage() {
int index = getCurrentIndex(_stack, _setting!);
List<Page> tempPages = _stack;
if (index != -1) {
// 要求棧中只允許有一個同樣的頁面的實例 否則開發(fā)模式熱更新會報錯
// 要打開的頁面在棧中已存在,則將該頁面和它上面的所有頁面進行出棧
tempPages = tempPages.sublist(0, index);
// 或者刪除之前存在棧里的頁面,重新創(chuàng)建
// tempPages.removeAt(index);
}
Page page;
if (_setting?.arguments is BaseArgument) {
if ((_setting?.arguments as BaseArgument).name == RouterName.rootPage) {
_stack.clear();
}
} else {
if (_setting?.name == RouterName.rootPage) {
_stack.clear();
}
}
page = buildPage(name: _setting?.name, arguments: _setting?.arguments);
tempPages = [...tempPages, page];
NavigatorUtil().notify(tempPages, _stack);
_stack = tempPages;
notifyListeners();
}
@override
Future<void> setInitialRoutePath(RouteSettings configuration) {
return super.setInitialRoutePath(_setting);
}
@override
Future<void> setNewRoutePath(RouteSettings configuration) async {
if (configuration.arguments is BaseArgument) {
if ((configuration.arguments as BaseArgument).name ==
RouterName.rootPage) {
_stack.clear();
}
} else {
if (configuration.name == RouterName.rootPage) {
_stack.clear();
}
}
addPage(name: configuration.name, arguments: configuration.arguments);
}
}
其中_stack是我們的路由棧,_setting是RouteSettings,每執(zhí)行一個新的路由跳轉(zhuǎn),都會創(chuàng)建一個RouteSettings對象并賦值給_setting,最終在插入_stack里。buildPage()的實現(xiàn)如下:
//建造頁面
buildPage({required name, arguments}) {
return MaterialPage(
child: getPageChild(name: name, arguments: arguments),
arguments: arguments,
name: name,
key: ValueKey(
arguments is BaseArgument ? (arguments as BaseArgument).name : name));
}
其中MaterialPage繼承了Page。getPageChild()實現(xiàn)如下:
Widget getPageChild({required name, arguments}) {
Widget page;
Map? arg;
if (arguments is Map) {
arg = arguments;
}
if (arguments is BaseArgument) {
switch ((arguments as BaseArgument).name) {
case RouterName.rootPage:
page = TestHomePage();
break;
case RouterName.testChild1Page:
page = TestChildPage1(
argument: arguments.arguments as TestChild1PageArgument,
);
break;
case RouterName.testChild2Page:
page = TestChildPage2();
break;
default:
page = TestHomePage();
}
} else {
page = TestHomePage();
}
return page;
}
class RouterName {
static const rootPage = "/";
static const testChild1Page = "/testChild1Page";
static const testChild2Page = "/testChild2Page";
}
我們可以看到,在真正返回Widget時,我們并沒有使用傳入的name參數(shù),而是BaseArgument的name參數(shù),這是為什么呢?這是在于我們?yōu)榱藢崿F(xiàn)無論頁面怎么跳轉(zhuǎn),從頭到尾瀏覽器只保留一個history,因此我們在頁面跳轉(zhuǎn)時RouteSettings的name并不發(fā)生變化,通過其arguments里面的參數(shù)變化返回不同的Widget。這樣在路由跳轉(zhuǎn)時,其實MaterialPage由于name一直會被直接復(fù)用,從而不會創(chuàng)建新的MaterialPage也就不會產(chǎn)生history。 NavigatorUtil是由業(yè)務(wù)調(diào)用的,創(chuàng)建跳轉(zhuǎn)方法的抽象類,提供了onJumpTo(),onReplaceAndJumpTo(),onClearStack(),onBack()四個方法供業(yè)務(wù)調(diào)用,我們可以看一下onJumpTo()的實現(xiàn):
@override
void onJumpTo(
{required name,
Object? stackArguments,
Map<String, dynamic>? historyArgMap,
BuildContext? context}) {
var arg = BaseArgument();
arg.name = name;
arg.arguments = stackArguments;
RouteSettings settings =
RouteSettings(name: RouterName.rootPage, arguments: arg);
return _function!.onJumpTo!(settings);
}
可以看到在創(chuàng)建RouteSettings對象時,name為RouterName.rootPage,arg時由業(yè)務(wù)傳的真正的跳轉(zhuǎn)頁面相關(guān)的參數(shù)。我們看一下業(yè)務(wù)的調(diào)用:
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
children: [
Text("TestHomePage"),
Text("history length is : " + window.history.length.toString()),
Text("href: " + WebUtil.get().getWindow().location.href),
TextButton(
onPressed: () {
var arg = TestChild1PageArgument()..isSuccess = "false";
NavigatorUtil().onJumpTo(
name: RouterName.testChild1Page,
stackArguments: arg,
historyArgMap: arg.toJson(),
context: context);
},
child: Text("Go to TestChildPage1"))
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
children: [
Text("TestChildPage1"),
Text("history length is : " + window.history.length.toString()),
Text("href: " + WebUtil.get().getWindow().location.href),
TextButton(
onPressed: () {
NavigatorUtil().onJumpTo(
name: RouterName.testChild2Page, context: context);
},
child: Text("Go to TestChildPage2")),
TextButton(
onPressed: () {
NavigatorUtil().onBack();
},
child: Text("Back to TestHomePage")),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Column(
children: [
Text("TestChildPage2"),
Text("history length is : " + window.history.length.toString()),
Text("href: " + WebUtil.get().getWindow().location.href),
TextButton(
onPressed: () {
NavigatorUtil().onBack();
},
child: Text("Back to TestChild1page")),
TextButton(
onPressed: () {
NavigatorUtil().onClearStack();
},
child: Text("Back to Root")),
],
),
),
);
}
我們看一下截圖展示:
圖片
圖片
圖片
在這個過程中href不會發(fā)生變化,history也不會發(fā)生變化,完全符合我們的預(yù)期。
7.總結(jié)
Flutter的Navigator 2.0引入了聲明式的API,使頁面路由管理更加靈活和強大。相較于Navigator 1.0,Navigator 2.0支持更復(fù)雜的路由操作,如嵌套路由和動態(tài)路由配置。它使用不可變的Page對象列表來表示路由歷史,與Flutter的不可變Widgets設(shè)計理念一致。Navigator 2.0還支持命名路由,通過簡單的路由名稱即可實現(xiàn)頁面跳轉(zhuǎn),大大簡化了路由管理的復(fù)雜度。此外,它還提供了更豐富的路由回調(diào)和狀態(tài)管理功能,使開發(fā)者能夠更輕松地構(gòu)建復(fù)雜的Flutter應(yīng)用。