Flutter路由
路由作为一种页面切换的能力,非常重要。Flutter 中路由管理有几个重要的点。
Navigator 1.0:Flutter 早期路由系统,侧重于移动端 ,命令式编程风格,使用 Navigator.push() 和 Navigator.pop() 等方法来管理路由栈。
Navigator 2.0:Flutter1.22 版本以后新增,侧重于桌面端/网页端,声明式编程风格,使用 Router 和 RouteInformationParser 等类来描述和管理路由树。
Flutter路由重要的类
1.Route:应用程序页面的抽象, Navigator 管理 Route。
2.Navigator:负责路由管理的重要类,通过 push 和 pop 进行页面跳转。
Flutter 跳转方式
动态路由
适于单次导航的场景,直接在代码中创建和导航的路由。
Navigator.push(context,
MaterialPageRoute(builder: (context) => RouterPageA()));
Navigator.pop(context);
import 'package:flutter/material.dart';
import 'dart_test_router1.dart';
class TestRouterPage extends StatelessWidget {
const TestRouterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test Navigator"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Test MaterialPageRoute"),
ElevatedButton(
onPressed: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => RouterPageA()));
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
}
import 'package:flutter/material.dart';
class RouterPageA extends StatelessWidget {
const RouterPageA({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("RouterPageA"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Test Navigator.pop"),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
}
Navigator.push有两个参数,一个是BuildContext,另一个是Route。代码中使用的是MaterialPageRoute,执行与对应平台风格一致的切换动画(android 与 ios平台不同)。如果使用 CupertinoPageRoute,页面切换效果是左右滑动。
PageRoute可以自定义,实现自定义页面切换的动画和行为。如果想自定义下过渡动画,使用 PageRouteBuilder 创建自定义路由,通过 pageBuilder 和 transitionsBuilder 属性来定义页面和过渡动画。
通过 pageBuilder 实现页面渐入动画
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation, child: const RouterPageA());
}));
pageBuilder + transitionsBuilder 实现过渡动画
Navigator.of(context).push(PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) =>
const RouterPageA(),
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
//动画的起始位置,轴y方向屏幕下侧偏移起点
var start = const Offset(0, 1);
//动画的结束位置,0表示没有偏移
var end = Offset.zero;
//动画曲线,easeInOut 表示开始慢,中间加速,结束慢
var curve = Curves.easeInOut;
// 创建一个从begin到end的补间动画,.chain 表示与曲线结合
var tween = Tween(begin: start, end: end)
.chain(CurveTween(curve: curve));
// SlideTransition 是一个动画Widget
return SlideTransition(
position: animation.drive(tween), child: child);
}));
静态路由
静态路由需要提前注册,首先给每个路由定义一个名称,通过这个名称来导航到对应的路由。
在应用根级别 (MaterialApp 或者 CupertinoApp ) 中定义路由,使用routes参数将路由名称映射到对应的Widget。
不带参数与返回值案例
Navigator.of(context).pushNamed("/dart_test_router1");
import 'package:flutter/material.dart';
import 'dart_test_router1.dart';
class TestStaticRouterPage extends StatelessWidget {
const TestStaticRouterPage({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test Router Demo",
theme: ThemeData(
useMaterial3: false,
primarySwatch: Colors.blue,
textButtonTheme: const TextButtonThemeData(
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
),
),
home: const RealTestStaticRouterPage(),
routes: {
// "/": (context) => const RealTestStaticRouterPage(),
"/dart_test_router1": (context) => const RouterPageA()
},
// initialRoute: "/",
);
}
}
class RealTestStaticRouterPage extends StatelessWidget {
const RealTestStaticRouterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("TEST STATIC ROUTER PAGE"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Test Static Router Page"),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushNamed("/dart_test_router1");
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
带参数与返回值
import 'package:flutter/material.dart';
import 'dart_test_router1.dart';
import 'dart_test_router3.dart';
class TestStaticRouterPage extends StatelessWidget {
const TestStaticRouterPage({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test Router Demo",
theme: ThemeData(
useMaterial3: false,
primarySwatch: Colors.blue,
textButtonTheme: const TextButtonThemeData(
// 鍘绘帀 TextButton 鐨勬按娉㈢汗鏁堟灉
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
),
),
home: const RealTestStaticRouterPage(),
routes: {
// "/": (context) => const RealTestStaticRouterPage(),
"/dart_test_router1": (context) => const RouterPageA(),
"/dart_test_router3": (context) => const RouterPage3()
},
// initialRoute: "/",
);
}
}
class RealTestStaticRouterPage extends StatelessWidget {
const RealTestStaticRouterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("TEST STATIC ROUTER PAGE"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Test Static Router Page"),
ElevatedButton(
onPressed: () async {
var result = await Navigator.of(context).pushNamed(
"/dart_test_router3",
arguments: {"title": "Hello"});
print(result);
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
import 'package:flutter/material.dart';
class RouterPage3 extends StatefulWidget {
const RouterPage3({super.key});
@override
State<StatefulWidget> createState() {
return _RouterPage3State();
}
}
class _RouterPage3State extends State<RouterPage3> {
@override
Widget build(BuildContext context) {
var args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
final title = args?['title'] ?? "DEFAULT TITLE";
return Scaffold(
appBar: AppBar(
title: Text("RouterPageA"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("GET TITLE===========銆?title"),
ElevatedButton(
onPressed: () {
Navigator.pop(context, "I went from RouterPage3");
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
}
2024-09-12 10:01:46.569 25438-25629 flutter I I went from RouterPage3
路由操作
路由替换
像登录页跳首页的场景,我们希望页面跳转成功后,回到上上个页面。我们可以通过pushReplacement、pushReplacementNamed实现。
API一:动态路由替换
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => RouterPageA()));
API二:静态路由替换
var result = await Navigator.of(context).pushReplacementNamed(
"/dart_test_router3",
arguments: {"title": "Hello"});
新路由入栈+移除之前的路由,直到条件满足
pushAndRemoveUntil 将给定路由推送给Navigator,删除先前的路由,直到该函数的参数predicate返回true为才停止。
import 'package:flutter/material.dart';
import 'dart_test_router1.dart';
import 'dart_test_router3.dart';
class TestRouterPage4 extends StatelessWidget {
const TestRouterPage4({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test pushAndRemoveUntil",
theme: ThemeData(
useMaterial3: false,
primarySwatch: Colors.blue,
textButtonTheme: const TextButtonThemeData(
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)),
home: const RealTestRouterPage4(),
);
}
}
class RealTestRouterPage4 extends StatelessWidget {
const RealTestRouterPage4({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test pushAndRemoveUntil"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("test pushAndRemoveUntil"),
ElevatedButton(
onPressed: () {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => RouterPageA()),
(Route<dynamic> route) => route.isFirst);
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
]),
),
);
}
}
这个方式是跳转到某个页面,然后移除路由直到...为止
路由出栈,直到条件满足
在flutter 路由跳转中,我们想要回到特定的一个页面 比如:从 A -> B-> C ->D,我们向从 D页面 pop至 B 页面。我们可以使用 popUtil方法回到 B 页面。
popUntil 反复执行pop 直到该函数的参数predicate返回true为止。
import 'package:flutter/material.dart';
import 'dart_test_router6.dart';
class TestRouterPage5 extends StatelessWidget {
const TestRouterPage5({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Test popUntil TestRouterPage5",
theme: ThemeData(
useMaterial3: false,
primarySwatch: Colors.blue,
textButtonTheme: const TextButtonThemeData(
style: ButtonStyle(splashFactory: NoSplash.splashFactory),
)),
home: const RealTestRouterPage5(),
);
}
}
class RealTestRouterPage5 extends StatelessWidget {
const RealTestRouterPage5({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test popUntil TestRouterPage5"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("test popUntil TestRouterPage5"),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TestRouterPage6()));
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
]),
),
);
}
}
import 'package:flutter/material.dart';
import 'dart_test_router7.dart';
class TestRouterPage6 extends StatelessWidget {
const TestRouterPage6({super.key});
@override
Widget build(BuildContext context) {
return RealTestRouterPage6();
}
}
class RealTestRouterPage6 extends StatelessWidget {
const RealTestRouterPage6({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test popUntil TestRouterPage6"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("test popUntil TestRouterPage6"),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TestRouterPage7()));
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
]),
),
);
}
}
import 'package:flutter/material.dart';
class TestRouterPage7 extends StatelessWidget {
const TestRouterPage7({super.key});
@override
Widget build(BuildContext context) {
return RealTestRouterPage7();
}
}
class RealTestRouterPage7 extends StatelessWidget {
const RealTestRouterPage7({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test popUntil TestRouterPage7"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("test popUntil TestRouterPage7"),
ElevatedButton(
onPressed: () {
Navigator.of(context)
.popUntil((Route<dynamic> route) => route.isFirst);
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
]),
),
);
}
}
上面的代码中调用Navigator.of(context)
.popUntil((Route<dynamic> route) => route.isFirst);直接回到了 TestRouterPage5。
删除指定路由
获得当前路由
ModalRoute.of(context);
移除指定路由
if (route != null) {
Navigator.of(context).removeRoute(route);
}
移除指定路由下方的单个路由
if (route != null) {
Navigator.of(context).removeRouteBelow(route);
}
页面传参与数据回传
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:gsy_flutter_demo/widget/dart_test_router2.dart';
import 'dart_test_router1.dart';
class TestRouterPage extends StatelessWidget {
const TestRouterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Test Navigator"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text("Test MaterialPageRoute"),
ElevatedButton(
onPressed: () async {
String backContent = await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => RouterPage2(title: "Custom Title")));
print(backContent);
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
}
import 'package:flutter/material.dart';
class RouterPage2 extends StatefulWidget {
final String title;
const RouterPage2({super.key, required this.title});
@override
State<StatefulWidget> createState() {
return _RouterPage2State();
}
}
class _RouterPage2State extends State<RouterPage2> {
@override
Widget build(BuildContext context) {
var widgitTitle = widget.title;
return Scaffold(
appBar: AppBar(
title: Text("RouterPageA"),
),
body: Container(
alignment: Alignment.center,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("GET TITLE===========銆?widgitTitle"),
ElevatedButton(
onPressed: () {
Navigator.pop(context, "I went from RouterPageA");
},
child: Text("Click ElevatedButton"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
elevation: 10,
minimumSize: Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
)
],
),
),
);
}
}
2024-09-11 14:51:16.211 24765-24840 flutter I I went from RouterPageA
Navigator 的工作流程
Navigator 的核心是对路由栈的管理。当你调用 Navigator.push 时,一个新的路由被创建并推入栈顶;当你调用 Navigator.pop 时,栈顶的路由被移除。
const Navigator({
super.key,
this.pages = const <Page<dynamic>>[],
this.onPopPage,
this.initialRoute,
this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
this.onGenerateRoute,
this.onUnknownRoute,
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.reportsRouteUpdateToEngine = false,
this.clipBehavior = Clip.hardEdge,
this.observers = const <NavigatorObserver>[],
this.requestFocus = true,
this.restorationScopeId,
this.routeTraversalEdgeBehavior = kDefaultRouteTraversalEdgeBehavior,
});
Navigator.push
#Navigator.push
@optionalTypeArgs
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
Navigator.push 调用Navigator.of(context).push(route),内部 调用NavigatorState 的 push 方法:
@optionalTypeArgs
Future<T?> push<T extends Object?>(Route<T> route) {
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
return route.popped;
}
#NavigatorState
void _pushEntry(_RouteEntry entry) {
assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
assert(entry.route._navigator == null);
assert(entry.currentState == _RouteLifecycle.push);
_history.add(entry);
_flushHistoryUpdates();
assert(() {
_debugLocked = false;
return true;
}());
_afterNavigation(entry.route);
}
这里发生了一系列操作:
- _history.add(route) : 将 新路由 添加到 路由栈 中。
- route.install() : 安装路由,将页面挂载到 widget 树上。
- route.didPush() : 通知 路由 已经被推入 栈顶 。
- _cancelActivePointers() : 取消所有正在进行的触摸事件,防止发生意外事件。
- route.didChange() : 通知 路由 状态发生变化。
push 方法返回一个 Future,该 Future 会在路由完成push操作时完成。
Navigator.pop
#Navigator.pop
Navigator.of(context).pop();
@optionalTypeArgs
void pop<T extends Object?>([ T? result ]) {
assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
if (entry.pageBased) {
if (widget.onPopPage!(entry.route, result) && entry.currentState == _RouteLifecycle.idle) {
// The entry may have been disposed if the pop finishes synchronously.
assert(entry.route._popCompleter.isCompleted);
entry.currentState = _RouteLifecycle.pop;
}
entry.route.onPopInvoked(true);
} else {
entry.pop<T>(result);
assert (entry.currentState == _RouteLifecycle.pop);
}
if (entry.currentState == _RouteLifecycle.pop) {
_flushHistoryUpdates(rearrangeOverlay: false);
}
assert(entry.currentState == _RouteLifecycle.idle || entry.route._popCompleter.isCompleted);
assert(() {
_debugLocked = false;
return true;
}());
_afterNavigation(entry.route);
}
- history.lastWhere:找到最后一个处于 “存在”状态 的路由条目。
- entry.pageBased:路由是否是基于页面的。
- widget.onPopPage:执行页面pop操作。
- entry.route.onPopInvoked(true):通知路由pop操作被调用。
- entry.pop<T>(result):直接调用pop。
- _afterNavigation(entry.route):导航完成后回调。