Flutter之導(dǎo)航和路由

?? 目錄

  1. 核心概念
  2. 基本導(dǎo)航
  3. 命名路由
  4. 路由傳參
  5. 返回?cái)?shù)據(jù)
  6. 路由生成器
  7. 高級路由(Router API)
  8. 導(dǎo)航欄和抽屜
  9. 深層鏈接(Deep Linking)
  10. 最佳實(shí)踐
  11. 常見問題

核心概念

什么是導(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)存問題

解決:使用 pushReplacementpushAndRemoveUntil 替換路由。

// 替換當(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):

  1. 基本導(dǎo)航:使用 Navigator.push()Navigator.pop()
  2. 命名路由:適合簡單的路由需求
  3. 路由生成器:提供更靈活的路由控制
  4. Router API:適合復(fù)雜應(yīng)用和深層鏈接

選擇合適的導(dǎo)航方式取決于你的應(yīng)用需求:

  • 簡單應(yīng)用:使用基本導(dǎo)航或命名路由
  • 中等復(fù)雜度:使用路由生成器
  • 復(fù)雜應(yīng)用:使用 Router API(如 go_router)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)航 導(dǎo)航用flutter的自帶組件 思路和vue導(dǎo)航是一樣的1.聲明一個(gè)數(shù)組,放入幾個(gè)導(dǎo)航頁面2.聲明一個(gè)ind...
    明月半倚深秋_f45e閱讀 709評論 0 0
  • 在傳統(tǒng)的 Web 開發(fā)過程中,當(dāng)你需要實(shí)現(xiàn)多個(gè)站內(nèi)頁面時(shí),以前你需要寫很多個(gè) html 頁面,然后通過 a 標(biāo)簽來...
    硅谷干貨閱讀 2,567評論 0 1
  • 我們通常會用屏(Screen)來稱呼一個(gè)頁面(Page),一個(gè)完整的App應(yīng)該是有多個(gè)Page組成的。 在之前的案...
    AlanGe閱讀 188評論 0 0
  • Flutter 的路由機(jī)制主要涉及兩個(gè)核心類:Navigator 和 Route。這兩個(gè)類共同協(xié)作,實(shí)現(xiàn)了應(yīng)用程序...
    土豆騎士閱讀 313評論 0 0
  • 我們通常會用屏(Screen)來稱呼一個(gè)頁面(Page),一個(gè)完整的App應(yīng)該是有多個(gè)Page組成的。 在之前的案...
    Imkata閱讀 685評論 0 1

友情鏈接更多精彩內(nèi)容