导航和路由是移动应用的核心功能。Flutter 提供了多种导航方式,从简单的页面跳转到复杂的深层链接处理。本文将全面介绍 Flutter 中的导航和路由方案。
基础导航 - Navigator
1. 基本导航操作
// 导航到新页面
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
// 返回上一页
Navigator.pop(context);
// 返回并传递数据
Navigator.pop(context, '返回的数据');
// 替换当前页面
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
// 清除所有路由并导航到新页面
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomePage()),
(route) => false, // 移除所有之前的路由
);
2. 传递数据
// 传递数据到新页面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondPage(data: 'Hello'),
),
);
// 接收返回的数据
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondPage()),
);
if (result != null) {
print('收到返回数据: $result');
}
3. 自定义路由动画
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondPage(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: child,
);
},
transitionDuration: Duration(milliseconds: 300),
),
);
命名路由
1. 定义路由
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/second': (context) => SecondPage(),
'/third': (context) => ThirdPage(),
},
);
}
}
2. 使用命名路由
// 导航
Navigator.pushNamed(context, '/second');
// 传递参数(需要手动处理)
Navigator.pushNamed(
context,
'/second',
arguments: {'id': 123, 'name': 'Flutter'},
);
3. 接收参数
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return Scaffold(
appBar: AppBar(title: Text('Second Page')),
body: Text('ID: ${args['id']}, Name: ${args['name']}'),
);
}
}
go_router - 现代化路由方案
go_router 是 Flutter 官方推荐的现代化路由解决方案:
1. 基础配置
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/second',
builder: (context, state) => SecondPage(),
),
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserPage(userId: id);
},
),
],
);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
2. 路径参数
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserPage(userId: id);
},
);
// 使用
context.go('/user/123');
3. 查询参数
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
return SearchPage(query: query);
},
);
// 使用
context.go('/search?q=flutter');
4. 嵌套路由
final router = GoRouter(
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => HomePage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => ProfilePage(),
),
],
),
],
),
],
);
5. 重定向和守卫
final router = GoRouter(
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isGoingToLogin = state.matchedLocation == '/login';
if (!isLoggedIn && !isGoingToLogin) {
return '/login';
}
if (isLoggedIn && isGoingToLogin) {
return '/';
}
return null; // 不重定向
},
routes: [
GoRoute(
path: '/login',
builder: (context, state) => LoginPage(),
),
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
],
);
6. 错误处理
final router = GoRouter(
errorBuilder: (context, state) => ErrorPage(error: state.error),
routes: [
// ...
],
);
底部导航栏
1. 使用 IndexedStack
class MainPage extends StatefulWidget {
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final List<Widget> _pages = [
HomePage(),
SearchPage(),
ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
2. 使用 go_router 的 StatefulShellRoute
final router = GoRouter(
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/home',
builder: (context, state) => HomePage(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/search',
builder: (context, state) => SearchPage(),
),
],
),
],
),
],
);
深层链接(Deep Linking)
1. 配置 Android
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp"/>
</intent-filter>
</activity>
2. 配置 iOS
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
3. 处理深层链接
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductPage(productId: id);
},
),
],
);
// 处理应用启动时的链接
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 处理初始链接
final initialLink = await getInitialLink();
if (initialLink != null) {
// 处理深层链接
}
runApp(MyApp());
}
路由过渡动画
1. 自定义过渡
GoRoute(
path: '/second',
pageBuilder: (context, state) => CustomTransitionPage(
key: state.pageKey,
child: SecondPage(),
),
);
class CustomTransitionPage extends CustomTransitionPage {
const CustomTransitionPage({
required super.key,
required super.child,
});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return SlideTransition(
position: animation.drive(
Tween(begin: Offset(1.0, 0.0), end: Offset.zero)
.chain(CurveTween(curve: Curves.ease)),
),
child: child,
);
}
}
最佳实践
- 使用 go_router:对于新项目,推荐使用 go_router
- 类型安全:使用路径参数而不是查询参数传递关键数据
- 错误处理:始终处理路由错误
- 性能优化:使用 IndexedStack 保持页面状态
- 深层链接:从一开始就考虑深层链接支持
常见问题
1. 如何防止返回按钮?
// 使用 WillPopScope
WillPopScope(
onWillPop: () async {
// 返回 false 阻止返回
return false;
},
child: Scaffold(...),
)
2. 如何清除路由栈?
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomePage()),
(route) => false,
);
3. 如何获取当前路由?
// 使用 go_router
final currentLocation = GoRouter.of(context).routerDelegate.currentConfiguration.uri.path;
// 使用 Navigator
final route = ModalRoute.of(context);
总结
Flutter 提供了多种导航方案:
- Navigator:基础导航,适合简单场景
- 命名路由:适合中等复杂度应用
- go_router:现代化方案,推荐用于新项目
选择路由方案时,要考虑:
- 项目复杂度
- 是否需要深层链接
- 团队熟悉度
- 长期维护性
掌握这些导航技巧,能帮助你构建更好的 Flutter 应用。
导航和路由是 Flutter 应用的基础,选择合适的方案能大大提升开发效率和用户体验。