?? 目錄
- 核心概念
- 基本導(dǎo)航
- 命名路由
- 路由傳參
- 返回?cái)?shù)據(jù)
- 路由生成器
- 高級路由(Router API)
- 導(dǎo)航欄和抽屜
- 深層鏈接(Deep Linking)
- 最佳實(shí)踐
- 常見問題
核心概念
什么是導(dǎo)航和路由?
- 路由(Route):表示應(yīng)用中的一個(gè)頁面或屏幕。在 Flutter 中,每個(gè)頁面都是一個(gè) Widget。
- 導(dǎo)航器(Navigator):管理路由堆棧的組件,負(fù)責(zé)在路由之間進(jìn)行跳轉(zhuǎn)和管理。
- 路由堆棧(Route Stack):類似瀏覽器的歷史記錄,使用后進(jìn)先出(LIFO)的方式管理頁面。
導(dǎo)航的基本原理
路由堆棧(從下到上):
┌─────────────┐
│ 頁面 C │ ← 當(dāng)前頁面(棧頂)
├─────────────┤
│ 頁面 B │
├─────────────┤
│ 頁面 A │ ← 初始頁面(棧底)
└─────────────┘
- push:將新頁面推入堆棧頂部
- pop:從堆棧頂部移除當(dāng)前頁面
- replace:替換當(dāng)前頁面
- popUntil:返回到指定頁面
基本導(dǎo)航
1. Navigator.push() - 導(dǎo)航到新頁面
使用 Navigator.push() 導(dǎo)航到新頁面,這是最常用的導(dǎo)航方式。
// 基本用法
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
// 或者使用 Navigator.of(context)
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => SecondScreen(),
),
);
完整示例:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '導(dǎo)航示例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const FirstScreen(),
);
}
}
// 第一個(gè)頁面
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第一頁')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 導(dǎo)航到第二個(gè)頁面
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
},
child: const Text('前往第二頁'),
),
),
);
}
}
// 第二個(gè)頁面
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第二頁')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一頁
Navigator.pop(context);
},
child: const Text('返回'),
),
),
);
}
}
2. Navigator.pop() - 返回上一頁
使用 Navigator.pop() 返回上一頁。
Navigator.pop(context);
// 可以返回?cái)?shù)據(jù)
Navigator.pop(context, '返回的數(shù)據(jù)');
3. MaterialPageRoute vs CupertinoPageRoute
- MaterialPageRoute:Android 風(fēng)格的頁面過渡動畫
- CupertinoPageRoute:iOS 風(fēng)格的頁面過渡動畫
// Material 風(fēng)格(Android)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
// Cupertino 風(fēng)格(iOS)
Navigator.push(
context,
CupertinoPageRoute(builder: (context) => SecondScreen()),
);
4. 自定義頁面過渡動畫
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
// 淡入淡出
return FadeTransition(opacity: animation, child: child);
// 或者滑動
// return SlideTransition(
// position: Tween<Offset>(
// begin: const Offset(1.0, 0.0),
// end: Offset.zero,
// ).animate(animation),
// child: child,
// );
},
transitionDuration: const Duration(milliseconds: 300),
),
);
命名路由
命名路由可以減少代碼重復(fù),特別適合在多個(gè)地方導(dǎo)航到同一頁面。
1. 定義路由表
在 MaterialApp 中定義路由表:
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
2. 使用命名路由導(dǎo)航
// 導(dǎo)航到命名路由
Navigator.pushNamed(context, '/details');
// 或者使用 pushReplacementNamed 替換當(dāng)前路由
Navigator.pushReplacementNamed(context, '/details');
3. 完整示例
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '命名路由示例',
initialRoute: '/',
routes: {
'/': (context) => const HomeScreen(),
'/details': (context) => const DetailsScreen(),
'/settings': (context) => const SettingsScreen(),
},
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首頁')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/details');
},
child: const Text('前往詳情頁'),
),
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
child: const Text('前往設(shè)置頁'),
),
],
),
),
);
}
}
?? 注意事項(xiàng)
雖然命名路由提供了便利,但在處理深層鏈接和復(fù)雜導(dǎo)航需求時(shí)存在一定限制。對于大多數(shù)現(xiàn)代應(yīng)用,推薦使用 Router API(見下文)。
路由傳參
1. 通過構(gòu)造函數(shù)傳參
這是最簡單直接的方式:
// 定義接收參數(shù)的頁面
class DetailsScreen extends StatelessWidget {
final String title;
final int id;
const DetailsScreen({
super.key,
required this.title,
required this.id,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Center(
child: Text('ID: $id'),
),
);
}
}
// 導(dǎo)航時(shí)傳遞參數(shù)
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailsScreen(
title: '詳情',
id: 123,
),
),
);
2. 通過命名路由傳參
使用 arguments 參數(shù):
// 定義路由時(shí)使用 settings.arguments
MaterialApp(
routes: {
'/details': (context) {
final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
return DetailsScreen(
title: args['title'],
id: args['id'],
);
},
},
);
// 導(dǎo)航時(shí)傳遞參數(shù)
Navigator.pushNamed(
context,
'/details',
arguments: {
'title': '詳情',
'id': 123,
},
);
3. 使用路由生成器傳參(推薦)
MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/details') {
final args = settings.arguments as Map<String, dynamic>;
return MaterialPageRoute(
builder: (context) => DetailsScreen(
title: args['title'],
id: args['id'],
),
);
}
return null;
},
);
返回?cái)?shù)據(jù)
從新頁面返回?cái)?shù)據(jù)到上一頁:
1. 返回?cái)?shù)據(jù)
// 在第二個(gè)頁面返回?cái)?shù)據(jù)
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('選擇選項(xiàng)')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context, '選項(xiàng)A');
},
child: const Text('選擇選項(xiàng)A'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context, '選項(xiàng)B');
},
child: const Text('選擇選項(xiàng)B'),
),
],
),
),
);
}
}
2. 接收返回的數(shù)據(jù)
// 在第一個(gè)頁面接收數(shù)據(jù)
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
if (result != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('選擇了: $result')),
);
}
},
child: const Text('打開選擇頁面'),
);
3. 完整示例
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});
@override
State<FirstScreen> createState() => _FirstScreenState();
}
class _FirstScreenState extends State<FirstScreen> {
String? _selectedOption;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('第一頁')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_selectedOption != null)
Text('已選擇: $_selectedOption'),
ElevatedButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SecondScreen(),
),
);
if (result != null) {
setState(() {
_selectedOption = result as String;
});
}
},
child: const Text('選擇選項(xiàng)'),
),
],
),
),
);
}
}
路由生成器
onGenerateRoute 允許動態(tài)生成路由,適合處理復(fù)雜的路由邏輯。
1. 基本用法
MaterialApp(
onGenerateRoute: (settings) {
// 根據(jù)路由名稱生成不同的頁面
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const HomeScreen());
case '/details':
return MaterialPageRoute(builder: (_) => const DetailsScreen());
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsScreen());
default:
return MaterialPageRoute(
builder: (_) => const NotFoundScreen(),
);
}
},
);
2. 處理未知路由
MaterialApp(
onGenerateRoute: (settings) {
// 處理已知路由
if (settings.name == '/details') {
return MaterialPageRoute(builder: (_) => const DetailsScreen());
}
// 處理未知路由
return MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(child: Text('頁面未找到')),
),
);
},
// 或者使用 onUnknownRoute
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (_) => const NotFoundScreen(),
);
},
);
3. 結(jié)合參數(shù)使用
MaterialApp(
onGenerateRoute: (settings) {
final uri = Uri.parse(settings.name ?? '/');
switch (uri.path) {
case '/details':
final id = uri.queryParameters['id'];
return MaterialPageRoute(
builder: (_) => DetailsScreen(id: id),
);
default:
return MaterialPageRoute(builder: (_) => const HomeScreen());
}
},
);
高級路由(Router API)
對于具有復(fù)雜導(dǎo)航需求的應(yīng)用,F(xiàn)lutter 提供了 Router API,特別適用于處理深層鏈接和 Web URL 同步。
1. 使用 go_router(推薦)
go_router 是一個(gè)流行的路由包,簡化了 Router 的使用。
安裝:
dependencies:
go_router: ^13.0.0
基本用法:
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
);
// 在 MaterialApp 中使用
MaterialApp.router(
routerConfig: router,
);
導(dǎo)航:
// 導(dǎo)航到新頁面
context.go('/details/123');
// 或者使用 push
context.push('/details/123');
// 返回
context.pop();
2. 嵌套路由
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
],
);
3. 路由守衛(wèi)
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: [
// ... 路由定義
],
);
導(dǎo)航欄和抽屜
1. BottomNavigationBar - 底部導(dǎo)航欄
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
int _currentIndex = 0;
final List<Widget> _screens = [
const HomeScreen(),
const SearchScreen(),
const ProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _screens[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首頁',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: '搜索',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
2. Drawer - 側(cè)邊抽屜
Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('菜單'),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('首頁'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/');
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('設(shè)置'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/settings');
},
),
],
),
),
body: const Center(child: Text('內(nèi)容')),
);
3. TabBar - 標(biāo)簽頁導(dǎo)航
class TabScreen extends StatelessWidget {
const TabScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('標(biāo)簽頁'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.home), text: '首頁'),
Tab(icon: Icon(Icons.search), text: '搜索'),
Tab(icon: Icon(Icons.person), text: '我的'),
],
),
),
body: const TabBarView(
children: [
HomeScreen(),
SearchScreen(),
ProfileScreen(),
],
),
),
);
}
}
深層鏈接(Deep Linking)
深層鏈接允許應(yīng)用通過特定的 URL 直接打開特定的頁面。
1. 使用 go_router 處理深層鏈接
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/product/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductScreen(id: id);
},
),
],
);
2. Android 配置
在 android/app/src/main/AndroidManifest.xml 中:
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<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>
3. 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>
最佳實(shí)踐
1. 路由管理最佳實(shí)踐
- ? 使用 Router API:對于現(xiàn)代應(yīng)用,推薦使用
go_router或類似的包 - ? 集中管理路由:將路由定義集中在一個(gè)文件中
- ? 使用類型安全的路由:避免字符串硬編碼
- ? 處理錯(cuò)誤路由:提供 404 頁面
- ? 使用路由守衛(wèi):保護(hù)需要認(rèn)證的頁面
2. 導(dǎo)航最佳實(shí)踐
- ? 使用 await 接收返回?cái)?shù)據(jù):確保正確處理異步返回
- ? 避免深層嵌套:限制導(dǎo)航堆棧的深度
- ? 提供返回按鈕:確保用戶可以返回
- ? 使用 WillPopScope:處理返回按鈕的攔截
3. 代碼組織
// routes/routes.dart - 集中管理路由
class AppRoutes {
static const String home = '/';
static const String details = '/details';
static const String settings = '/settings';
}
// routes/app_router.dart - 路由配置
final router = GoRouter(
routes: [
GoRoute(
path: AppRoutes.home,
builder: (context, state) => const HomeScreen(),
),
// ...
],
);
4. 攔截返回操作
class EditScreen extends StatelessWidget {
const EditScreen({super.key});
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 顯示確認(rèn)對話框
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('確認(rèn)'),
content: const Text('確定要離開嗎?未保存的更改將丟失。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('確定'),
),
],
),
);
return shouldPop ?? false;
},
child: Scaffold(
appBar: AppBar(title: const Text('編輯')),
body: const Center(child: Text('編輯內(nèi)容')),
),
);
}
}
常見問題
1. Navigator 找不到 context
問題:Navigator operation requested with a context that does not include a Navigator
解決:確保 context 來自包含 MaterialApp 或 CupertinoApp 的 Widget 樹。
// ? 錯(cuò)誤
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(
builder: (context) {
// 這里的 context 是正確的
return ElevatedButton(
onPressed: () {
Navigator.push(context, ...); // ?
},
);
},
),
);
}
}
2. 路由參數(shù)類型轉(zhuǎn)換錯(cuò)誤
問題:type 'String' is not a subtype of type 'int'
解決:確保參數(shù)類型匹配,使用類型轉(zhuǎn)換。
// 從路由參數(shù)獲取時(shí)進(jìn)行類型轉(zhuǎn)換
final id = int.parse(state.pathParameters['id']!);
3. 返回?cái)?shù)據(jù)為 null
問題:從新頁面返回時(shí),上一頁接收到的數(shù)據(jù)為 null
解決:確保使用 await 等待返回結(jié)果。
// ? 正確
final result = await Navigator.push(...);
// ? 錯(cuò)誤
final result = Navigator.push(...); // result 可能是 null
4. 路由堆棧過深
問題:導(dǎo)航堆棧過深導(dǎo)致內(nèi)存問題
解決:使用 pushReplacement 或 pushAndRemoveUntil 替換路由。
// 替換當(dāng)前路由
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
// 清除所有路由并導(dǎo)航到新頁面
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
(route) => false, // 清除所有路由
);
總結(jié)
Flutter 的導(dǎo)航和路由系統(tǒng)提供了多種方式來管理頁面跳轉(zhuǎn):
-
基本導(dǎo)航:使用
Navigator.push()和Navigator.pop() - 命名路由:適合簡單的路由需求
- 路由生成器:提供更靈活的路由控制
- Router API:適合復(fù)雜應(yīng)用和深層鏈接
選擇合適的導(dǎo)航方式取決于你的應(yīng)用需求:
- 簡單應(yīng)用:使用基本導(dǎo)航或命名路由
- 中等復(fù)雜度:使用路由生成器
- 復(fù)雜應(yīng)用:使用 Router API(如 go_router)