路由作为一种页面切换的能力,非常重要。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});@overrideWidget 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});@overrideWidget 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 是一个动画Widgetreturn 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});@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideState<StatefulWidget> createState() {return _RouterPage3State();}
}class _RouterPage3State extends State<RouterPage3> {@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideWidget 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});@overrideWidget build(BuildContext context) {return RealTestRouterPage6();}
}class RealTestRouterPage6 extends StatelessWidget {const RealTestRouterPage6({super.key});@overrideWidget 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});@overrideWidget build(BuildContext context) {return RealTestRouterPage7();}
}class RealTestRouterPage7 extends StatelessWidget {const RealTestRouterPage7({super.key});@overrideWidget 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});@overrideWidget 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});@overrideState<StatefulWidget> createState() {return _RouterPage2State();}
}class _RouterPage2State extends State<RouterPage2> {@overrideWidget 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
@optionalTypeArgsstatic 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 方法:
@optionalTypeArgsFuture<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();
@optionalTypeArgsvoid 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):导航完成后回调。