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.'),
      ),
    );
  }
}
相关推荐
0wioiw026 分钟前
Android-Kotlin基础(Jetpack③-LiveData)
android·开发语言·kotlin
xzkyd outpaper32 分钟前
Android中Binder缓冲区为什么限制1MB,此外Bundle数据为什么要存储在Binder缓冲区中
android·binder
aqi001 小时前
FFmpeg开发笔记(七十九)专注于视频弹幕功能的国产弹弹播放器
android·ffmpeg·音视频·直播·流媒体
深盾科技3 小时前
Android 安全编程:Kotlin 如何从语言层保障安全性
android·安全·kotlin
whysqwhw3 小时前
RecyclerView的LayoutManager扩展用法
android
whysqwhw3 小时前
RecyclerView的全面讲解
android
whysqwhw3 小时前
RecyclerView的四级缓存机制
android
whysqwhw3 小时前
RecyclerView的设计实现
android
weixin_411191844 小时前
安卓Handler和Looper的学习记录
android·java