1.背景与动机
在Navigator 2.0
推出之前,Flutter
主要通过Navigator 1.0
和其提供的 API(如push()
, pop()
, pushNamed()
等)来管理页面路由。然而,Navigator 1.0
存在一些局限性,如难以实现复杂的页面操作(如移除栈内中间页面、交换页面等)、不支持嵌套路由以及无法满足全平台(尤其是Web
平台)的新需求。因此,Flutter
官方团队决定对路由系统进行改造,推出了Navigator 2.0
。
2.主要特性
-
声明式API
Navigator 2.0
提供的声明式API
使得路由管理更加直观和易于理解。开发者只需声明页面的配置信息,而无需编写复杂的导航逻辑代码。这种方式不仅减少了代码量,还提高了代码的可读性和可维护性。 -
嵌套路由
Navigator 2.0
满足了嵌套路由的需求场景,允许开发者在应用中创建嵌套的路由结构。这使得应用的结构更加清晰,同时也提高了页面导航的灵活性。 -
全平台支持
Navigator 2.0
提供的API
能够满足不同平台(如iOS
、Android
、Web
等)的导航需求,使得开发者能够更加方便地构建跨平台的应用。 -
强大的页面操作能力
Navigator 2.0
提供了更加丰富的页面操作能力,如移除栈内中间页面、交换页面等。这些操作在Navigator 1.0
中很难实现或需要编写复杂的代码,而在Navigator 2.0
中则变得简单直接。
3.核心组件
-
Router
在Navigator 2.0
中,Router
组件是路由管理的核心。它负责根据当前的路由信息(RouteInformation
)和路由信息解析器(RouteInformationParser
)来构建和更新UI
。Router
组件接收三个主要参数:
1.routeInformationProvider:提供当前的路由信息。
2.routeInformationParser:将路由信息解析为路由配置。
3.routerDelegate:根据路由配置构建和更新UI
。 -
RouteInformationProvider
RouteInformationProvider
是一个提供当前路由信息的组件。它通常与平台相关的路由信息源(如浏览器的URL
、Android
的Intent
等)集成,以获取当前的路由信息。 -
RouteInformationParser
RouteInformationParser
负责将RouteInformation
解析为RouteConfiguration
。这个过程允许开发者根据路由信息的格式(如URL
)来定义如何将其映射到应用内的路由配置。 -
RouterDelegate
RouterDelegate
是与UI
构建紧密相关的组件。它必须实现RouterDelegate
接口,并提供两个主要方法:
1.build(BuildContext context):根据当前的路由配置构建UI
。
2.setNewRoutePath(List<RouteSettings> configuration):设置新的路由路径,并更新UI
。
3.Future<bool> popRoute() :实现后退逻辑。
4.简单实例
首先通过MaterialApp.router()
来创建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
对象。其中根据路由配置构建和更新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':
// 这里可以传递参数,例如 DetailsPage(arguments: someData)
return DetailsPage();
default:
return NotFoundPage();
}
}
@override
String get currentConfiguration => _pages.last;
}
其中build()
一般返回的是一个Navigator
对象,popRoute()
实现后退逻辑,setNewRoutePath()
实现新页面的逻辑。
定义了一个_pages
数组对象,记录每个路由的path
,可以理解为是一个路由栈,这个路由栈对我们来说非常友好,在有复杂的业务逻辑时,我们可以自行定义相应的栈管理逻辑。currentConfiguration
返回的是栈顶的page
信息。
创建一个类继承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);
}
}
好的,接下来我们看一下调用:
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'),
),
);
}
}
非常简单,直接调用Router.of(context).routerDelegate.setNewRoutePath()
即可。
到此为止,一个使用Navigator2.0
的最简单的路由实例就完成了。和Navigator1.0
相比,看上去繁杂了不少。但是可以根据业务需求自定义路由栈进行管理,大大的提升了灵活性。接来看我们看一下Navigator2.0
是如何对路由进行实现的。
5.源码简析
我们在使用Navigator2.0
时,是通过MaterialApp.router()
创建的MaterialApp
对象,之前章节提到过,传了RouteInformationParser
和RouterDelegate
这两个对象。当传递了RouterDelegate
对象时,_MaterialAppState
中的_usesRouter
会被设置为true
。
bool get _usesRouter => widget.routerDelegate != null || widget.routerConfig != null;
在build()
时,通过WidgetsApp.router()
方法创建了一个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
中根据routerDelegate
设置了成员变量_usesRouterWithDelegates
的值:
bool get _usesRouterWithDelegates => widget.routerDelegate != null;
在build()
时会创建一个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,
);
}
......
}
在上一章节的实例中我们可得知,页面的切换都是依靠RouterDelegate
对象进行的。
每当切换到新的页面时,都会调用setNewRoutePath()
方法,因此我们来看一下setNewRoutePath()
是什么时候被调用的,有两处:
第一处:
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
的调用时机:
@override
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
}
我们可以看到在initState()
时,也就是在Router
被初始化的时候由widget.routeInformationProvider
来监听一些状态实现新页面的切换。我们来看一下routeInformationProvider
。RouteInformationProvider
在我们自己没有创建的情况下,系统会默认为我们创建一个PlatformRouteInformationProvider
对象。它实际上是个ChangeNotifier
。系统会监听每一帧的信号发送,调用其父类routerReportsNewRouteInformation()
方法,我们看看它的实现:
@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()
的实现如下:
/// 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');
}
这个方法是由各个平台自行实现的。从注释中我们可得知如果是在Web
平台下,它会切换成history
模式,并从history stack
中追踪所有的变化。 在history
发生变化时,会发送信号给Flutter
层等待处理。SystemNavigator.routeInformationUpdated()
方法是用来更新路由的,我们先不做分析。
接着我们回到PlatformRouteInformationProvider
,看看它什么时候会执行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();
}
在监听到有push
路由的情况下时,会调用notifyListeners()
,从而实现页面的切换。
我们再来看第二处调用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()
方法中调用的parseRouteInformation()
其实就是我们自定义RouteInformationParser
来进行的实现。
Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
return parseRouteInformation(routeInformation);
}
看到当其与父的依赖关系被改变的时候会调用setNewRoutePath()
。大概率就是App
初始化的时候被调用一次。
6.根据狐友业务的Web端实践
我们的Flutter
团队会承担一些运营活动的H5
需求。在实现时我们对路由有如下需求:
1.可以根据业务自由的管理路由栈。
2.分享链接只能分享出去默认入口链接,不希望中间的路由链接被分享出去。
3.不管有多少个路由页面,history
始终不变,在响应浏览器返回键时不响应路由栈的pop
操作。
在之前使用Navigator1.0
时体验并不太好,一个是不够灵活,另外还需对分享出去的链接做处理。因此我们利用Navigator2.0
设计了一套新的路由:
MyRouterDelegate delegate = MyRouterDelegate();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
routeInformationParser: MyRouteParser(),
routerDelegate: delegate,
);
}
Parser
实现非常简单:
class MyRouteParser extends RouteInformationParser<RouteSettings> {
@override
///parseRouteInformation() 方法的作用就是接受系统传递给我们的路由信息 routeInformation
Future<RouteSettings> parseRouteInformation(
RouteInformation routeInformation) {
// Uri uri = Uri.parse(routeInformation.location??"/");
return SynchronousFuture(RouteSettings(name: routeInformation.location));
}
@override
///恢复路由信息
RouteInformation restoreRouteInformation(RouteSettings configuration) {
return RouteInformation(location: configuration.name);
}
}
Delegate
的实现如下:
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,
),
);
}
/// 按下返回的回调
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) {
// 要求栈中只允许有一个同样的页面的实例 否则开发模式热更新会报错
// 要打开的页面在栈中已存在,则将该页面和它上面的所有页面进行出栈
tempPages = tempPages.sublist(0, index);
// 或者删除之前存在栈里的页面,重新创建
// 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
,每执行一个新的路由跳转,都会创建一个RouteSettings
对象并赋值给_setting
,最终在插入_stack
里。buildPage()
的实现如下:
//建造页面
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()
实现如下:
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
参数,而是BaseArgument
的name
参数,这是为什么呢?这是在于我们为了实现无论页面怎么跳转,从头到尾浏览器只保留一个history
,因此我们在页面跳转时RouteSettings
的name
并不发生变化,通过其arguments
里面的参数变化返回不同的Widget
。这样在路由跳转时,其实MaterialPage
由于name
一直会被直接复用,从而不会创建新的MaterialPage
也就不会产生history
。
NavigatorUtil
是由业务调用的,创建跳转方法的抽象类,提供了onJumpTo()
,onReplaceAndJumpTo()
,onClearStack()
,onBack()
四个方法供业务调用,我们可以看一下onJumpTo()
的实现:
@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);
}
可以看到在创建RouteSettings
对象时,name
为RouterName.rootPage
,arg
时由业务传的真正的跳转页面相关的参数。
我们看一下业务的调用:
@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
不会发生变化,history
也不会发生变化,完全符合我们的预期。
6.总结
Flutter
的Navigator 2.0
引入了声明式的API
,使页面路由管理更加灵活和强大。相较于Navigator 1.0
,Navigator 2.0
支持更复杂的路由操作,如嵌套路由和动态路由配置。它使用不可变的Page
对象列表来表示路由历史,与Flutter
的不可变Widgets
设计理念一致。Navigator 2.0
还支持命名路由,通过简单的路由名称即可实现页面跳转,大大简化了路由管理的复杂度。此外,它还提供了更丰富的路由回调和状态管理功能,使开发者能够更轻松地构建复杂的Flutter
应用。