以下内容参考《Flutter实战·第二版》和《Flutter中文网》,仅为个人学习、熟悉Flutter,不同版本可能有稍微不一样。
环境
///
/// 例子来源:
/// 《Flutter实战·第二版》:https://book.flutterchina.club/chapter2/flutter_router.html
/// Flutter中文网:https://flutter.cn/docs/cookbook/navigation
///
/// 环境:
/// Flutter 3.10.1 • channel stable • https://github.com/flutter/flutter.git
/// Framework • revision d3d8effc68 (7 days ago) • 2023-05-16 17:59:05 -0700
/// Engine • revision b4fb11214d
/// Tools • Dart 3.0.1 • DevTools 2.23.1
///
路由管理,其实就是常见的导航管理,push打开新页面,pop关闭页面
MaterialPageRoute和Navigator
Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
- 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
打开页面
Navigator.of(context).push(
MaterialPageRoute(
maintainState: true, //默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
fullscreenDialog: false, //表示新的路由页面是否是一个全屏的模态对话框,[true]iOS从屏幕底部滑入
builder: (context) => TipRoute(text: param),
),
);
关闭页面
Navigator.of(context).pop();
路由传值
传数据到第二页面
- 方式一:跳转的时候,对应
Widget
带参数过去,见DetailScreen
; - 方式二:使用
RouteSettings
传递参数,使用 ModalRoute.of() 方法获取参数,见DetailScreenRoute
。
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 传递数据到新页面
/// https://flutter.cn/docs/cookbook/navigation/passing-data
TodosScreen的数据源
static final todos = List.generate(
20,
(i) => Todo(
title: "Todo $I",
description: "A description of what needs to be done for Todo $I"
)
);
//////////////////////////////////////////////////////////////////////
class Todo {
final String title;
final String description;
const Todo({required this.title, required this.description});
}
class TodosScreen extends StatelessWidget {
const TodosScreen({super.key, required this.todos});
final List<Todo> todos;
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
Todo todo = todos[index];
return ListTile(
title: Text(todo.title),
//subtitle: Text(todos[index].description),
onTap: () {
if (index % 2 == 0) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todo), // 方式一
)
);
} else {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DetailScreenRoute(),
settings: RouteSettings(
arguments: todo, // 方式二
),
)
);
}
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
const DetailScreen({super.key, required this.todo});
final Todo todo; // 方式一默认带参
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: customAppBar(title: "${todo.title} 方式一", backgroundColor: navigationBarColor),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text(todo.description),
),
);
}
}
class DetailScreenRoute extends StatelessWidget {
const DetailScreenRoute({super.key});
@override
Widget build(BuildContext context) {
final todo = ModalRoute.of(context)!.settings.arguments as Todo; // 方式二获取参数
return Scaffold(
appBar: customAppBar(title: "${todo.title} 方式二", backgroundColor: navigationBarColor),
body: Padding(
padding: const EdgeInsets.all(16),
child: Text(todo.description),
),
);
}
}
一些自定义的导航bar
AppBar customAppBar({required String title, required Color backgroundColor}) {
return AppBar(
title: Text(
title,
style: const TextStyle(color: Colors.white)
),
iconTheme: const IconThemeData(color: Colors.white),
backgroundColor: backgroundColor,
);
}
Color navigationBarColor = Colors.red;
回传数据
调用API:Navigator.pop(context, "");
添加参数返回
使用async和await获取返回数据,用SnackBar
显示返回数据
/// 从一个页面回传数据
/// https://flutter.cn/docs/cookbook/navigation/returning-data
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child: SelectionButton(),
),
);
}
}
class SelectionButton extends StatefulWidget {
const SelectionButton({super.key});
@override
State<SelectionButton> createState() => _SelectionButtonState();
}
class _SelectionButtonState extends State<SelectionButton> {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: (){
_navigateAndDisplaySelection(context);
},
child: const Text("请选择")
);
}
Future<void> _navigateAndDisplaySelection(BuildContext context) async {
final result = await Navigator.of(context).push(
MaterialPageRoute(
fullscreenDialog: true, //模态从下到上弹出
builder: (context) => const SelectionScreen()),
);
// When a BuildContext is used from a StatefulWidget, the mounted property
// must be checked after an asynchronous gap.
// 不加这个会有警告 Don't use 'BuildContext's across async gaps. (Documentation)
if (!mounted) return;
ScaffoldMessenger.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
}
class SelectionScreen extends StatelessWidget {
const SelectionScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: customAppBar(title: "请选择", backgroundColor: navigationBarColor),
body: Padding(padding: const EdgeInsets.all(30),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: () {
Navigator.pop(context, "好的");
},
child: const Text("好的"),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: ElevatedButton(
onPressed: (){
Navigator.pop(context, "不行");
},
child: const Text("不行"))
),
],
),
),
);
}
}
命名路由
在main.dart
处理,封装了个路由管理
导航到对应名称的routes里
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 导航到对应名称的routes里
/// https://flutter.cn/docs/cookbook/navigation/named-routes
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
const Padding(
padding: EdgeInsets.only(left: 20, top: 20, right: 20),
child: Text("当使用[initialRoute]时,需要确保没有同时定义[home]属性。"),
),
ElevatedButton(
child: const Text("前往下一页"),
onPressed: () {
Navigator.of(context).pushNamed(RouteManager.secondScreen);
},
),
ElevatedButton(
child: const Text("下一页arguments传参"),
onPressed: () {
Navigator.of(context).pushNamed(
RouteManager.extractArguments,
arguments: const ScreenArguments(
title: "arguments传参",
message: "这个页面是使用pushNamed(arguments:)带入参数,内容使用ModalRoute.of()方法。这个方法返回的是当前路由及其携带的参数。"
),
);
},
),
const Padding(
padding: EdgeInsets.all(10),
child: Text("两种获取参数不兼容", style: TextStyle(color: Colors.red)),
),
const Padding(
padding: EdgeInsets.only(left: 20, top: 20, right: 20),
child: Text("使用[onGenerateRoute]时,不需要定义[initialRoute]和[routes],否则[onGenerateRoute]不会调用。"),
),
ElevatedButton(
child: const Text("下一页onGenerateRoute取参"),
onPressed: () {
Navigator.of(context).pushNamed(
RouteManager.passArguments,
arguments: const ScreenArguments(
title: "onGenerateRoute取参",
message: "通过 onGenerateRoute() 函数提取参数,然后把参数传递给组件。"
),
);
},
),
],
),
),
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: customAppBar(title: "第二页", backgroundColor: navigationBarColor),
body: Column(
children: [
Center(child:
ElevatedButton(
child: const Text("返回"),
onPressed: () {
Navigator.of(context).pop();
},
),
),
]
),
);
}
}
main.dart配置
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: RouteManager.routers,
);
}
}
封装的route管理,包含路由名称,注册Map,还有下一步的onGenerateRoute使用
来着:《Flutter 学习之 命名路由(Navigator 1.0)》https://www.jianshu.com/p/170c3ae8690c
class RouteManager {
/// 定义路由名字
static const String rootPageName = "/"; // 名为"/"的路由作为应用的home(首页)
static const String secondScreen = "/secondScreen";
static const String extractArguments = "/extractArguments";
static const String passArguments = "/passArguments";
/// 注册路由
static final Map<String, WidgetBuilder> routers = {
rootPageName: (context) => const MyHomePage(title: "Flutter Demo"), //首页
secondScreen: (context) => const SecondScreen(),
extractArguments: (context) => const ExtractArgumentsScreen(),
passArguments: (context, {arguments}) => PassArgumentsScreen(arguments: arguments),
};
/// 路由生成钩子,main中不需要routes:{}
Route<dynamic> onGenerateRoute(RouteSettings settings) {
// 获取rotateName
final String? routerName = settings.name;
// 找到对应的页面
final Function? buildPage = routers[routerName];
print("routerName=$routerName, buildPage=$buildPage");
// 如果对应的页面是空的就返回一个空页面
// MaterialPageRoute:页面切换方式为从下往上,
// CupertinoPageRoute:页面切换方式从左往右 还可以自定义切换方式
if (buildPage == null) {
return MaterialPageRoute(
builder: (_) => const Scaffold(
body: Center(
child: Text("Page Not Found!!!!"),
),
)
);
}
// 查看携带的参数
dynamic? args = settings.arguments as dynamic;
if (args != null) {
//buildPage里面的函数,规范带参arguments:,比如Name(arguments:xxx)
return MaterialPageRoute(builder: (context) => buildPage(context, arguments: args));
}
return MaterialPageRoute(builder: (context) => buildPage(context));
}
}
给特定的route传参
使用 onGenerateRoute 提取参数
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
/// 给特定的route传参
/// https://flutter.cn/docs/cookbook/navigation/navigate-with-arguments
class ScreenArguments {
final String title;
final String message;
const ScreenArguments({required this.title, required this.message});
}
class ExtractArgumentsScreen extends StatelessWidget {
const ExtractArgumentsScreen({super.key});
@override
Widget build(BuildContext context) {
//使用ModalRoute.of()方法。这个方法返回的是当前路由及其携带的参数。
final ScreenArguments args = ModalRoute.of(context)!.settings.arguments as ScreenArguments;
return Scaffold(
appBar: customAppBar(title: args.title, backgroundColor: navigationBarColor),
body: Padding(
padding: const EdgeInsets.all(20),
child: Text(args.message),
),
);
}
}
class PassArgumentsScreen extends StatelessWidget {
final ScreenArguments arguments;
const PassArgumentsScreen({super.key, required this.arguments});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: customAppBar(title: arguments.title, backgroundColor: navigationBarColor),
body: Padding(
padding: const EdgeInsets.all(20),
child: Text(arguments.message),
),
);
}
}
main.dart配置
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
/// 使用[onGenerateRoute]时,不需要定义[initialRoute]和[routes],否则[onGenerateRoute]不会调用。
onGenerateRoute: RouteManager().onGenerateRoute,
);
}
}
遇到的问题
1、Could not find a generator for route RouteSettings("xxx", null) in the _WidgetsAppState.
子页面不能使用MaterialApp,该页面无法Navigator.of(context).push()
找了好几个地方都不对,最后这里解决了Navigator中命名路由使用中的问题
======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
Could not find a generator for route RouteSettings("/secondScreen", null) in the _WidgetsAppState.
Make sure your root app widget has provided a way to generate
this route.
Generators for routes are searched for in the following order:
1. For the "/" route, the "home" property, if non-null, is used.
2. Otherwise, the "routes" table is used, if it has an entry for the route.
3. Otherwise, onGenerateRoute is called. It should return a non-null value for any valid route not handled by "home" and "routes".
4. Finally if all else fails onUnknownRoute is called.
Unfortunately, onUnknownRoute was not set.
2、The following NoSuchMethodError was thrown building Builder(dirty)
路由带参数报错,使用路由封装的时候,带了参数,赋值错误导致
// 查看携带的参数
dynamic? args = settings.arguments as dynamic;
if (args != null) {
//buildPage里面的函数,规范带参arguments:,比如Name(arguments:xxx)
return MaterialPageRoute(builder: (context) => buildPage(context, arguments: args));
}
return MaterialPageRoute(builder: (context) => buildPage(context));
错误信息:
======== Exception caught by widgets library =======================================================
The following NoSuchMethodError was thrown building Builder(dirty):
Closure call with mismatched arguments: function 'RouteManager.routers.<anonymous closure>'
Receiver: Closure: (BuildContext, {dynamic arguments}) => PassArgumentsScreen
Tried calling: RouteManager.routers.<anonymous closure>(Instance of 'StatelessElement', _Map len:1)
Found: RouteManager.routers.<anonymous closure>(BuildContext, {dynamic arguments}) => PassArgumentsScreen