第10讲:导航与路由:在页面间穿梭

掌握页面导航技术,构建真正的多页面Flutter应用。

你好,欢迎回到《Flutter入门到精通》专栏。在上一讲中,我们学习了如何使用ListView和GridView展示动态数据。现在,让我们来学习如何将多个页面连接起来,构建一个完整的、可以导航的多页面应用。

一、导航基础:Navigator 与 Route

在Flutter中,导航由两个核心类管理:

  • Navigator:管理一组Route对象的Widget

  • Route:表示应用中的一个"屏幕"或"页面"

你可以把Navigator想象成一个栈管理器,Route就是栈中的页面。

1.1 基础导航方法

跳转到新页面

dart

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => NewPage()),
);
返回上一页

dart

Navigator.pop(context);
带返回值的导航

dart

// 在第一个页面中跳转并等待返回值
final result = await Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SelectionPage()),
);

// 在第二个页面中返回数据
Navigator.pop(context, '选中的数据');

二、静态路由:基本导航操作

2.1 基础跳转示例

让我们创建一个简单的多页面应用来演示基本导航:

main.dart

dart

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '导航演示',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(), // 应用启动页面
    );
  }
}

home_page.dart

dart

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        backgroundColor: Colors.blue,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                // 跳转到详情页
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const DetailPage(
                      title: '来自首页的详情',
                      content: '这是从首页传递过来的内容。',
                    ),
                  ),
                );
              },
              child: const Text('跳转到详情页'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 跳转到设置页
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const SettingsPage(),
                  ),
                );
              },
              child: const Text('跳转到设置页'),
            ),
          ],
        ),
      ),
    );
  }
}

detail_page.dart

dart

class DetailPage extends StatelessWidget {
  final String title;
  final String content;

  const DetailPage({
    super.key,
    required this.title,
    required this.content,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
        backgroundColor: Colors.green,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              content,
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 返回上一页
                Navigator.pop(context);
              },
              child: const Text('返回'),
            ),
          ],
        ),
      ),
    );
  }
}

settings_page.dart

dart

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设置'),
        backgroundColor: Colors.orange,
      ),
      body: ListView(
        children: [
          ListTile(
            leading: const Icon(Icons.person),
            title: const Text('个人资料'),
            onTap: () {
              // 跳转到个人资料页
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const ProfilePage(),
                ),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.notifications),
            title: const Text('通知设置'),
            onTap: () {
              // 跳转到通知设置页
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const NotificationSettingsPage(),
                ),
              );
            },
          ),
          ListTile(
            leading: const Icon(Icons.security),
            title: const Text('隐私设置'),
            onTap: () {
              // 跳转到隐私设置页
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => const PrivacySettingsPage(),
                ),
              );
            },
          ),
        ],
      ),
    );
  }
}

2.2 带返回值的导航

这是一个非常实用的功能,比如从列表中选择一项:

selection_page.dart

dart

class SelectionPage extends StatefulWidget {
  const SelectionPage({super.key});

  @override
  State<SelectionPage> createState() => _SelectionPageState();
}

class _SelectionPageState extends State<SelectionPage> {
  String? _selectedItem;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('选择项目'),
      ),
      body: ListView(
        children: [
          _buildSelectionItem('选项 A', 'A'),
          _buildSelectionItem('选项 B', 'B'),
          _buildSelectionItem('选项 C', 'C'),
          _buildSelectionItem('选项 D', 'D'),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _selectedItem != null
            ? () {
                // 返回选中的值
                Navigator.pop(context, _selectedItem);
              }
            : null,
        child: const Icon(Icons.check),
      ),
    );
  }

  Widget _buildSelectionItem(String title, String value) {
    return Card(
      margin: const EdgeInsets.all(8),
      color: _selectedItem == value ? Colors.blue[50] : null,
      child: ListTile(
        title: Text(title),
        leading: Radio<String>(
          value: value,
          groupValue: _selectedItem,
          onChanged: (String? value) {
            setState(() {
              _selectedItem = value;
            });
          },
        ),
        onTap: () {
          setState(() {
            _selectedItem = value;
          });
        },
      ),
    );
  }
}

在首页中使用:

dart

ElevatedButton(
  onPressed: () async {
    // 等待选择页面返回结果
    final selectedValue = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const SelectionPage()),
    );
    
    // 显示选择结果
    if (selectedValue != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('你选择了: $selectedValue')),
      );
    }
  },
  child: const Text('打开选择页面'),
),

三、命名路由:更清晰的导航管理

对于复杂的应用,使用命名路由可以让导航逻辑更清晰、易于维护。

3.1 配置命名路由

在MaterialApp中配置路由表:

main.dart

dart

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '命名路由演示',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      // 定义应用的初始路由
      initialRoute: '/',
      // 配置路由表
      routes: {
        '/': (context) => const HomePage(),
        '/detail': (context) => const DetailPage(
              title: '默认标题',
              content: '默认内容',
            ),
        '/settings': (context) => const SettingsPage(),
        '/profile': (context) => const ProfilePage(),
        '/notifications': (context) => const NotificationSettingsPage(),
        '/privacy': (context) => const PrivacySettingsPage(),
      },
    );
  }
}

3.2 使用命名路由导航

dart

// 简单的命名路由跳转
Navigator.pushNamed(context, '/settings');

// 带参数的命名路由跳转
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {
    'title': '动态标题',
    'content': '动态内容',
  },
);

3.3 在路由页面中接收参数

修改DetailPage来接收命名路由的参数:

dart

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

  @override
  Widget build(BuildContext context) {
    // 从路由参数中获取数据
    final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>?;
    
    final title = args?['title'] ?? '默认标题';
    final content = args?['content'] ?? '默认内容';

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Text(content),
            ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('返回'),
            ),
          ],
        ),
      ),
    );
  }
}

3.4 更优雅的参数传递

创建一个路由参数类来管理参数:

route_arguments.dart

dart

// 详情页参数
class DetailPageArguments {
  final String title;
  final String content;

  DetailPageArguments({
    required this.title,
    required this.content,
  });
}

// 个人资料页参数
class ProfilePageArguments {
  final String userId;
  final bool isEditable;

  ProfilePageArguments({
    required this.userId,
    this.isEditable = false,
  });
}

使用类型安全的参数:

dart

// 传递参数
Navigator.pushNamed(
  context,
  '/detail',
  arguments: DetailPageArguments(
    title: '类型安全的标题',
    content: '类型安全的内容',
  ),
);

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

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as DetailPageArguments;

    return Scaffold(
      appBar: AppBar(title: Text(args.title)),
      body: Text(args.content),
    );
  }
}

四、导航高级技巧

4.1 清除导航栈

有时候我们需要跳转到新页面时清除所有历史记录:

dart

// 跳转到新页面并清除所有历史记录(用户无法返回)
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => const LoginPage()),
  (route) => false, // 移除所有现有路由
);

// 跳转到新页面并保留首页
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => const MainPage()),
  (route) => route.isFirst, // 只保留第一个路由(首页)
);

4.2 替换当前路由

用新页面替换当前页面:

dart

Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => const NewPage()),
);

// 命名路由版本
Navigator.pushReplacementNamed(context, '/newpage');

4.3 弹出到指定页面

返回到导航栈中的特定页面:

dart

// 弹出直到找到首页
Navigator.popUntil(context, (route) => route.isFirst);

// 弹出直到找到指定名称的路由
Navigator.popUntil(context, ModalRoute.withName('/home'));

五、综合实战:完整的用户流程

让我们创建一个完整的用户认证流程来演示复杂的导航场景:

auth_flow_example.dart

dart

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '用户认证流程',
      theme: ThemeData(primarySwatch: Colors.blue),
      initialRoute: '/',
      routes: {
        '/': (context) => const SplashPage(),
        '/login': (context) => const LoginPage(),
        '/register': (context) => const RegisterPage(),
        '/forgot-password': (context) => const ForgotPasswordPage(),
        '/home': (context) => const MainHomePage(),
        '/profile': (context) => const UserProfilePage(),
      },
    );
  }
}

class SplashPage extends StatefulWidget {
  const SplashPage({super.key});

  @override
  State<SplashPage> createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  @override
  void initState() {
    super.initState();
    _checkAuthStatus();
  }

  void _checkAuthStatus() async {
    // 模拟检查用户登录状态
    await Future.delayed(const Duration(seconds: 2));
    
    final isLoggedIn = false; // 这里应该是实际的认证检查

    if (mounted) {
      if (isLoggedIn) {
        // 已登录,跳转到首页并清除启动页
        Navigator.pushAndRemoveUntil(
          context,
          MaterialPageRoute(builder: (context) => const MainHomePage()),
          (route) => false,
        );
      } else {
        // 未登录,跳转到登录页并清除启动页
        Navigator.pushAndRemoveUntil(
          context,
          MaterialPageRoute(builder: (context) => const LoginPage()),
          (route) => false,
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.blue,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const CircularProgressIndicator(color: Colors.white),
            const SizedBox(height: 20),
            Text(
              '加载中...',
              style: TextStyle(color: Colors.white, fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

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

  void _performLogin(BuildContext context) async {
    // 显示加载指示器
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const Center(child: CircularProgressIndicator()),
    );

    // 模拟登录过程
    await Future.delayed(const Duration(seconds: 2));

    // 关闭加载指示器
    Navigator.pop(context);

    // 登录成功,跳转到首页
    Navigator.pushAndRemoveUntil(
      context,
      MaterialPageRoute(builder: (context) => const MainHomePage()),
      (route) => false,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            TextField(decoration: const InputDecoration(labelText: '用户名')),
            const SizedBox(height: 16),
            TextField(decoration: const InputDecoration(labelText: '密码'), obscureText: true),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () => _performLogin(context),
              child: const Text('登录'),
            ),
            const SizedBox(height: 16),
            TextButton(
              onPressed: () {
                Navigator.pushNamed(context, '/register');
              },
              child: const Text('还没有账号?立即注册'),
            ),
            TextButton(
              onPressed: () {
                Navigator.pushNamed(context, '/forgot-password');
              },
              child: const Text('忘记密码?'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        actions: [
          IconButton(
            icon: const Icon(Icons.person),
            onPressed: () {
              Navigator.pushNamed(context, '/profile');
            },
          ),
        ],
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('欢迎来到主页!'),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 模拟退出登录
                Navigator.pushAndRemoveUntil(
                  context,
                  MaterialPageRoute(builder: (context) => const LoginPage()),
                  (route) => false,
                );
              },
              child: const Text('退出登录'),
            ),
          ],
        ),
      ),
    );
  }
}

导航最佳实践

  1. 保持导航简洁:避免过深的导航层级

  2. 使用命名路由:便于维护和团队协作

  3. 类型安全:为路由参数创建专门的类

  4. 错误处理:处理导航失败的情况

  5. 用户体验:提供清晰的导航反馈

结语

恭喜!通过本讲的学习,你已经掌握了Flutter中页面导航的核心技术。从基础跳转到复杂的命名路由,从简单传参到完整的用户流程,你现在可以构建真正的多页面应用了。

记住这些关键导航模式:

  • push/pop:基本的页面跳转和返回

  • 命名路由:适合复杂应用的清晰结构

  • pushAndRemoveUntil:实现登录流程等场景

  • 带返回值导航:用于选择和数据传递

在下一讲中,我们将进入Flutter开发的一个重要分水岭——状态管理。我们将深入学习StatefulWidget的状态管理,并介绍Provider等高级状态管理方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

移动端开发者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值