flutter学习第 8 节:路由与导航

在移动应用开发中,路由(Route)和导航(Navigation)是实现页面跳转和用户导航体验的核心机制。Flutter 提供了一套完整的路由管理系统,能够轻松实现页面之间的切换、参数传递和返回数据等功能。本节课将详细介绍 Flutter 中的路由与导航机制,帮助你掌握页面跳转的各种实现方式。

一、页面跳转基本原理

Flutter 中的路由管理基于栈(Stack)数据结构,使用 Navigator 组件管理路由栈,实现页面的入栈(push)和出栈(pop)操作。

Navigator 是 Flutter 中管理路由的核心组件,它维护一个路由栈,每个路由对应一个页面:

  • 新页面打开时,对应的路由被 "推入"(push)栈顶
  • 页面关闭时,对应的路由从栈顶 "弹出"(pop)
  • 当前显示的页面始终是栈顶的路由

在 Flutter 应用中,MaterialApp 会自动提供一个 Navigator,我们可以通过 Navigator.of(context) 获取其实例进行操作。

2. MaterialPageRoute

MaterialPageRoute 是 Material Design 风格的路由,它会自动处理页面切换时的过渡动画(如左右滑动、淡入淡出等),适用于大多数场景。

基本用法示例:

dart 复制代码
// 第一个页面
import 'package:flutter/material.dart';

class FirstScreen extends StatelessWidget {
  const FirstScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('First Screen')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 导航到第二个页面
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const SecondScreen()),
            );
          },
          child: const Text('Go to Second Screen'),
        ),
      ),
    );
  }
}

// 第二个页面
class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
        // 导航栏返回按钮
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () {
            // 返回上一个页面
            Navigator.pop(context);
          },
        ),
      ),
      body: const Center(child: Text('This is the second screen')),
    );
  }
}

注意 :在 Scaffold 中,AppBar 会自动添加返回按钮,点击时会触发 Navigator.pop(context),因此通常不需要手动实现返回按钮。

3. 页面切换动画

MaterialPageRoute 提供了一些属性来定制页面切换动画:

dart 复制代码
MaterialPageRoute(
  builder: (context) => const DetailScreen(),
  // 过渡动画持续时间
  transitionDuration: const Duration(milliseconds: 500),
  // 反向过渡动画持续时间
  reverseTransitionDuration: const Duration(milliseconds: 300),
  // 是否维护页面状态
  maintainState: true,
  // 是否为全屏对话框
  fullscreenDialog: true, // 会使用上下滑动动画
)

fullscreenDialog 设为 true 时,页面会从底部滑入,类似对话框的动画效果。


二、命名路由配置

对于复杂应用,直接使用 MaterialPageRoute 可能导致代码冗余且不易维护。Flutter 提供了命名路由(Named Routes)机制,通过路由名称进行页面跳转,使路由管理更加集中和规范。

1. 基本配置

MaterialApp 中通过 routes 属性配置命名路由:

dart 复制代码
void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Named Routes Demo',
      initialRoute: '/', // 初始路由(首页)
      routes: {
        // 路由名称到页面的映射
        '/': (context) => const HomeScreen(),
        '/second': (context) => const SecondScreen(),
        '/detail': (context) => const DetailScreen(),
      },
    );
  }
}

2. 使用命名路由跳转

通过 Navigator.pushNamed() 方法根据路由名称进行跳转

dart 复制代码
// 从首页跳转到第二个页面
ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(context, '/second');
  },
  child: const Text('Go to Second Screen'),
)

// 从第二个页面返回首页
ElevatedButton(
  onPressed: () {
    Navigator.pop(context);
    // 或者跳转到指定路由(会替换当前路由栈)
    // Navigator.pushReplacementNamed(context, '/');
  },
  child: const Text('Go Back'),
)

常用的命名路由导航方法:

  • Navigator.pushNamed(context, '/routeName'):跳转到指定路由
  • Navigator.popAndPushNamed(context, '/routeName'):弹出当前路由并跳转到新路由
  • Navigator.pushReplacementNamed(context, '/routeName'):替换当前路由
  • Navigator.pushNamedAndRemoveUntil(context, '/routeName', (route) => false):跳转到新路由并移除之前的所有路由

三、路由参数传递与接收

在页面跳转时,经常需要传递参数(如 ID、名称等)。Flutter 提供了多种方式在路由之间传递和接收参数。

1. 基本参数传递

使用 Navigator.pushNamed()arguments 参数传递数据:

dart 复制代码
// 传递参数
ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(
      context,
      '/detail',
      arguments: {
        'id': 123,
        'title': 'Flutter 路由教程',
      },
    );
  },
  child: const Text('查看详情'),
)

在目标页面中,使用 ModalRoute.of(context)!.settings.arguments 接收参数:

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

class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // 接收参数
    final Map<String, dynamic> args =
        ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;

    return Scaffold(
      appBar: AppBar(title: Text(args['title'])),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID: ${args['id']}'),
            const SizedBox(height: 16),
            Text('Title: ${args['title']}'),
          ],
        ),
      ),
    );
  }
}

2. 类型安全的参数传递

为了避免类型转换错误,可以创建专门的参数类,实现类型安全的参数传递:

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

// 参数类
class DetailArguments {
  final int id;
  final String title;

  DetailArguments({required this.id, required this.title});
}

// 传递参数
ElevatedButton(
  onPressed: () {
    Navigator.pushNamed(
    context,
    '/detail',
    arguments: DetailArguments(
        id: 123,
        title: 'Flutter 路由教程',
        ),
    );
  },
  child: const Text('查看详情'),
)

// 接收参数
class DetailScreen extends StatelessWidget {
  const DetailScreen({super.key});

  @override
  Widget build(BuildContext context) {
    // 类型安全的参数接收
    final DetailArguments args =
        ModalRoute.of(context)!.settings.arguments as DetailArguments;

    return Scaffold(
      appBar: AppBar(title: Text(args.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('ID: ${args.id}'),
            const SizedBox(height: 16),
            Text('Title: ${args.title}'),
          ],
        ),
      ),
    );
  }
}

不仅可以向前跳转时传递参数,还可以在返回上一个页面时携带数据,这在处理表单提交、选择器等场景非常有用。

1. 返回数据的基本用法

在第二个页面中,通过 Navigator.pop(context, result) 返回数据:

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

class SelectionScreen extends StatelessWidget {
  const SelectionScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Select an Option')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // 返回数据
                Navigator.pop(context, 'Option 1');
              },
              child: const Text('Option 1'),
            ),
            ElevatedButton(
              onPressed: () {
                // 返回数据
                Navigator.pop(context, 'Option 2');
              },
              child: const Text('Option 2'),
            ),
          ],
        ),
      ),
    );
  }
}

在第一个页面中,通过 async/await 获取返回的数据:

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('Home')),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            // 等待返回结果
            final result = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => const SelectionScreen()),
            );

            // 处理返回结果
            if (result != null) {
              ScaffoldMessenger.of(
                context,
              ).showSnackBar(SnackBar(content: Text('Selected: $result')));
            }
          },
          child: const Text('Go to Selection Screen'),
        ),
      ),
    );
  }
}

2. 使用命名路由返回数据

使用命名路由时,同样可以返回数据:

dart 复制代码
// 跳转并等待结果
final result = await Navigator.pushNamed(context, '/selection');

// 返回数据
Navigator.pop(context, 'Selected Value');

五、路由钩子

Flutter 提供了路由钩子(Route Hooks)机制,允许在路由跳转过程中进行拦截和处理,实现更灵活的路由管理。

1. onGenerateRoute

onGenerateRoute 用于动态生成路由,当 routes 中没有匹配的路由名称时,会调用该方法。它适用于需要动态生成路由或对路由进行统一处理的场景(如参数验证、权限检查等)。

dart 复制代码
MaterialApp(
  initialRoute: '/',
  routes: {
    '/': (context) => const HomeScreen(),
    '/second': (context) => const SecondScreen(),
  },
  // 路由生成器
  onGenerateRoute: (settings) {
    // 根据路由名称处理
    if (settings.name == '/detail') {
      // 验证参数
      final args = settings.arguments;
      if (args is DetailArguments) {
        return MaterialPageRoute(
          builder: (context) => DetailScreen(args: args),
        );
      } else {
        // 参数无效时导航到错误页面
        return MaterialPageRoute(
          builder: (context) => const ErrorScreen(message: 'Invalid arguments'),
        );
      }
    }

    // 其他未匹配的路由
    return null;
  },
)

使用 onGenerateRoute 重构详情页:

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

class DetailScreen extends StatelessWidget {
  final DetailArguments args;

  const DetailScreen({
    super.key,
    required this.args,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(args.title)),
      body: Center(
        child: Text('ID: ${args.id}'),
      ),
    );
  }
}

2. onUnknownRoute

routesonGenerateRoute 都无法匹配路由名称时,会调用 onUnknownRoute。通常用于处理 404 情况,显示一个 "页面未找到" 的提示页面。

dart 复制代码
MaterialApp(
// ...其他配置
      onUnknownRoute: (settings) {
        return MaterialPageRoute(
          builder: (context) => NotFoundScreen(
            routeName: settings.name,
          ),
        );
      },
    )
    
// 未找到页面
import 'package:flutter/material.dart';

class NotFoundScreen extends StatelessWidget {
  final String? routeName;

  const NotFoundScreen({
    super.key,
    this.routeName,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Page Not Found')),
      body: Center(
        child: Text('The page "$routeName" does not exist.'),
      ),
    );
  }
}

3. 路由拦截与权限控制

利用 onGenerateRoute 可以实现路由拦截和权限控制,例如检查用户是否登录:

dart 复制代码
onGenerateRoute: (settings) {
  // 需要登录的路由列表
  final requiredAuthRoutes = ['/profile', '/settings'];
  
  // 检查是否需要登录且用户未登录
  if (requiredAuthRoutes.contains(settings.name) && !isLoggedIn()) {
    // 导航到登录页面,并记录目标路由
    return MaterialPageRoute(
      builder: (context) => LoginScreen(
        redirectRoute: settings.name,
      ),
    );
  }
  
  // 正常处理其他路由
  // ...
}

六、路由管理进阶

对于大型应用,推荐使用路由管理库(如 auto_routefluro 等)来简化路由管理。但即使使用这些库,理解 Flutter 原生路由机制仍然很重要。

1. 路由别名与模块化

在大型应用中,可以将路由按模块进行划分,使用路由别名统一管理:

dart 复制代码
// 路由常量类
class AppRoutes {
  static const String home = '/';
  static const String login = '/auth/login';
  static const String register = '/auth/register';
  static const String profile = '/user/profile';
  static const String settings = '/user/settings';
  static const String productList = '/products';
  static const String productDetail = '/products/detail';
}

// 使用时
Navigator.pushNamed(context, AppRoutes.productDetail);

2. 路由动画定制

除了默认的过渡动画,还可以自定义路由切换动画:

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

class CustomPageRoute<T> extends PageRoute<T> {
  final Widget child;

  CustomPageRoute({required this.child});

  @override
  Color? get barrierColor => null;

  @override
  String? get barrierLabel => null;

  @override
  bool get maintainState => true;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);

  @override
  Widget buildPage(
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      ) {
    // 自定义动画
    return FadeTransition(
      opacity: animation,
      child: ScaleTransition(
        scale: animation,
        child: child,
      ),
    );
  }
}

// 使用自定义路由
Navigator.push(
  context,
  CustomPageRoute(child: const DetailScreen()),
);

七、实例:完整的路由管理应用

下面实现一个包含多个页面、参数传递和返回数据功能的完整应用:

dart 复制代码
// 1. 路由常量
import 'package:flutter/material.dart';

class Routes {
  static const String home = '/';
  static const String userList = '/users';
  static const String userDetail = '/users/detail';
  static const String settings = '/settings';
}

// 2. 用户模型
class User {
  final int id;
  final String name;
  final String email;

  User({
    required this.id,
    required this.name,
    required this.email,
  });
}

// 3. 主应用
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Route Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      initialRoute: Routes.home,
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case Routes.home:
            return MaterialPageRoute(builder: (context) => const HomePage());
          case Routes.userList:
            return MaterialPageRoute(builder: (context) => UserListPage());
          case Routes.userDetail:
            final user = settings.arguments as User;
            return MaterialPageRoute(
              builder: (context) => UserDetailPage(user: user),
            );
          case Routes.settings:
            return MaterialPageRoute(
              builder: (context) => const SettingsPage(),
            );
          default:
            return MaterialPageRoute(
              builder: (context) => NotFoundPage(route: settings.name),
            );
        }
      },
    );
  }
}

// 4. 首页
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          ElevatedButton(
            onPressed: () => Navigator.pushNamed(context, Routes.userList),
            child: const Text('View User List'),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () async {
              final result = await Navigator.pushNamed(context, Routes.settings);
              if (result == true) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Settings saved')),
                );
              }
            },
            child: const Text('Go to Settings'),
          ),
        ],
      ),
    );
  }
}

// 5. 用户列表页
class UserListPage extends StatelessWidget {
  UserListPage({super.key});

  // 模拟用户数据
  final List<User> users = [
    User(id: 1, name: 'John Doe', email: 'john@example.com'),
    User(id: 2, name: 'Jane Smith', email: 'jane@example.com'),
    User(id: 3, name: 'Bob Johnson', email: 'bob@example.com'),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('User List')),
      body: ListView.builder(
        itemCount: users.length,
        itemBuilder: (context, index) {
          final user = users[index];
          return ListTile(
            title: Text(user.name),
            subtitle: Text(user.email),
            onTap: () {
              Navigator.pushNamed(
                context,
                Routes.userDetail,
                arguments: user,
              );
            },
          );
        },
      ),
    );
  }
}

// 6. 用户详情页
class UserDetailPage extends StatelessWidget {
  final User user;

  const UserDetailPage({
    super.key,
    required this.user,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(user.name)),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('ID: ${user.id}'),
            const SizedBox(height: 8),
            Text('Name: ${user.name}'),
            const SizedBox(height: 8),
            Text('Email: ${user.email}'),
          ],
        ),
      ),
    );
  }
}

// 7. 设置页面
class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _notificationsEnabled = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: ListView(
        children: [
          SwitchListTile(
            title: const Text('Enable Notifications'),
            value: _notificationsEnabled,
            onChanged: (value) {
              setState(() {
                _notificationsEnabled = value;
              });
            },
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: ElevatedButton(
              onPressed: () {
                // 返回保存成功的结果
                Navigator.pop(context, true);
              },
              child: const Text('Save Settings'),
            ),
          ),
        ],
      ),
    );
  }
}

// 8. 未找到页面
class NotFoundPage extends StatelessWidget {
  final String? route;

  const NotFoundPage({
    super.key,
    this.route,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Page Not Found')),
      body: Center(
        child: Text('The page "$route" does not exist.'),
      ),
    );
  }
}
相关推荐
FinelyYang2 小时前
UniApp + LiveKit:Android 端为什么要用原生做屏幕共享,以及我们怎么落地的
android·uni-app
蓝速科技2 小时前
蓝速科技信创落地:平衡安全合规与项目成本的实战方案
android·运维·人工智能·科技·安全·电脑·鸿蒙
又见情义5 小时前
RK3568 Android 13 SDK 开机自动开启共享热点实践
android
mmsx5 小时前
return false 不是拦截:一次按键触发两个动作的事件冒泡陷阱
android·bug
DU_YULIN6 小时前
Camera HAL ThreadManager概述
android·camera hal
淡淡的香烟7 小时前
Android中的MQTT通信原理解析
android·物联网
淡淡的香烟7 小时前
Android Camera开发详解
android·音视频
mmsx8 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp
xcyxiner9 小时前
flutter wsl2安装记录(使用宿主机虚拟机)
前端·flutter
YF02119 小时前
如何进入Android工程模式
android