3.7 路由与导航

基础知识

在移动应用程序中,路由与导航是构建多页面应用的核心。它允许用户在不同的屏幕(页面)之间进行切换。Flutter 提供了强大的导航系统,主要通过 NavigatorRoute 来管理页面栈。

1. Navigator (导航器)

Navigator 是一个管理 Route 栈的 Widget。它是一个堆栈,每个 Route 都代表一个页面。当用户导航到新页面时,新页面被推入堆栈顶部;当用户返回时,顶部页面被弹出。

2. Route (路由)

Route 是应用程序中屏幕或页面的抽象。Flutter 提供了几种内置的 Route 类型:

  • MaterialPageRoute:用于创建 Material Design 风格的页面过渡动画,是 Flutter 应用中最常用的路由。
  • CupertinoPageRoute:用于创建 iOS 风格的页面过渡动画。

3. 导航操作

Navigator 提供了多种方法来操作页面栈:

  • Navigator.push():将新页面推入堆栈顶部。新页面会覆盖旧页面。
    dart
    复制代码
    Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const SecondScreen()),
    );
  • Navigator.pop():从堆栈中弹出当前页面,返回到上一个页面。
    dart
    复制代码
    Navigator.pop(context);
  • Navigator.pushReplacement():将新页面推入堆栈,并替换当前页面。旧页面会被销毁。
    dart
    复制代码
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) => const LoginScreen()),
    );
  • Navigator.pushAndRemoveUntil():将新页面推入堆栈,并移除所有旧页面,直到满足某个条件。常用于登录后清除所有历史页面。
    dart
    复制代码
    Navigator.pushAndRemoveUntil(
      context,
      MaterialPageRoute(builder: (context) => const HomeScreen()),
      (Route<dynamic> route) => false, // 移除所有路由
    );
  • Navigator.popAndPushNamed():弹出当前路由,然后将命名路由推入堆栈。
  • Navigator.of(context):获取最近的 Navigator 实例。通常在 build 方法中使用 context 来获取。

4. 命名路由 (Named Routes)

除了直接创建 MaterialPageRoute,Flutter 还支持命名路由。命名路由允许你通过一个字符串名称来引用页面,这使得导航代码更简洁,并且更容易管理复杂的导航流。

  • 定义命名路由:在 MaterialAppCupertinoApproutes 属性中定义。
    dart
    复制代码
    MaterialApp(
      routes: {
        
        // 定义命名路由
        '/': (context) => const HomeScreen(),
        '/details': (context) => const DetailScreen(),
        '/settings': (context) => const SettingsScreen(),
      },
    );
  • 导航到命名路由:使用 Navigator.pushNamed()Navigator.popAndPushNamed()
    dart
    复制代码
    Navigator.pushNamed(context, '/details');
  • 传递参数:可以通过 arguments 属性传递参数。
    dart
    复制代码
    Navigator.pushNamed(
      context,
      '/details',
      arguments: {
        'id': 123,
        'name': 'Product A',
      },
    );
    在接收页面中获取参数:
    dart
    复制代码
    final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
    final int id = args['id'];
    final String name = args['name'];

5. onGenerateRoute (动态路由生成)

对于更复杂的路由逻辑,例如需要根据参数动态生成页面,或者处理未定义的路由,可以使用 onGenerateRoute 回调。它会在 routes 中找不到匹配路由时被调用。

dart
复制代码
MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == '/product') {
      final args = settings.arguments as Map<String, dynamic>;
      return MaterialPageRoute(
        builder: (context) => ProductScreen(productId: args['id']),
      );
    }
    // 处理其他路由或返回一个错误页面
    return MaterialPageRoute(builder: (context) => const UnknownRouteScreen());
  },
);

官方文档链接

Flutter 开发中的应用案例

路由与导航是所有多页面 Flutter 应用的基础。无论是简单的页面跳转,还是复杂的带参数导航、动态路由,掌握 Flutter 的导航系统都是必不可少的。

案例:一个简单的多页面应用,包含命名路由和参数传递

我们将创建一个包含三个页面的应用:主页、详情页和设置页。主页可以导航到详情页(带参数)和设置页。详情页和设置页可以返回主页。

dart
复制代码
import 'package:flutter/material.dart';

// 主页
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: <Widget>[
            ElevatedButton(
              onPressed: () {
                // 导航到详情页,并传递参数
                Navigator.pushNamed(
                  context,
                  '/details',
                  arguments: {'productId': 101, 'productName': 'Flutter T-Shirt'},
                );
              },
              child: const Text('前往详情页 (带参数)'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 导航到设置页
                Navigator.pushNamed(context, '/settings');
              },
              child: const Text('前往设置页'),
            ),
          ],
        ),
      ),
    );
  }
}

// 详情页
class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // 获取传递的参数
    final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
    final int productId = args?['productId'] ?? 0;
    final String productName = args?['productName'] ?? '未知产品';

    return Scaffold(
      appBar: AppBar(
        title: const Text('详情页'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('产品 ID: $productId', style: const TextStyle(fontSize: 20)),
            Text('产品名称: $productName', style: const TextStyle(fontSize: 20)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 返回上一页
                Navigator.pop(context);
              },
              child: const Text('返回主页'),
            ),
          ],
        ),
      ),
    );
  }
}

// 设置页
class SettingsScreen extends StatelessWidget {
  const SettingsScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设置页'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('这是设置页面', style: TextStyle(fontSize: 24)),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 返回上一页
                Navigator.pop(context);
              },
              child: const Text('返回主页'),
            ),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(
    title: 'Flutter 导航示例',
    // 定义命名路由
    routes: {
      '/': (context) => const HomeScreen(),
      '/details': (context) => const DetailScreen(),
      '/settings': (context) => const SettingsScreen(),
    },
    // 如果需要处理未定义的路由,可以使用 onGenerateRoute
    // onGenerateRoute: (settings) {
    //   if (settings.name == '/unknown') {
    //     return MaterialPageRoute(builder: (context) => const Text('未知路由'));
    //   }
    //   return null; // 让 routes 处理
    // },
  ));
}

案例分析:

  • MaterialApproutes 属性:这是定义命名路由的地方。我们将 '/' 映射到 HomeScreen'/details' 映射到 DetailScreen'/settings' 映射到 SettingsScreen
  • Navigator.pushNamed(context, '/details', arguments: {...}):在 HomeScreen 中,我们使用 pushNamed 方法通过命名路由导航到 DetailScreenarguments 参数用于传递数据。这里传递了一个 Map,包含 productIdproductName
  • ModalRoute.of(context)!.settings.arguments:在 DetailScreen 中,我们通过 ModalRoute.of(context)!.settings.arguments 来获取传递过来的参数。由于 arguments 可能是 null,并且类型是 Object?,所以需要进行类型转换和空安全处理(例如使用 as Map<String, dynamic>??? 操作符)。
  • Navigator.pop(context):在 DetailScreenSettingsScreen 中,我们使用 pop 方法返回到上一个页面。当页面栈中只有一个页面时,pop 会关闭应用(在 Android 上)。

这个案例清晰地展示了 Flutter 中路由与导航的基本用法,包括命名路由的定义、页面之间的跳转以及参数的传递。掌握这些概念是构建任何多页面 Flutter 应用的基础。