🧭 13 - 路由导航

阶段三:导航与状态 · 第13课 · 预计学习时间:50分钟
✅ 验证通过

📖 Navigator与Route

Flutter的导航基于模型:push压入新页面,pop弹出当前页面。

// ✅ 验证通过:Flutter 3.22
// 文件:lib/navigation_basics.dart

import 'package:flutter/material.dart';

void main() => runApp(const NavigationApp());

class NavigationApp extends StatelessWidget {
  const NavigationApp({super.key});
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '路由导航示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

/// 首页
class HomePage extends StatelessWidget {
  const HomePage({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 1. 基本push — 直接创建目标页面
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => const DetailPage(title: '直接跳转'),
                  ),
                );
              },
              child: const Text('Push到详情页'),
            ),
            const SizedBox(height: 12),
            
            // 2. push带参数
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => DetailPage(
                      title: '带参数跳转',
                      data: {'userId': 42, 'name': 'Alice'},
                    ),
                  ),
                );
              },
              child: const Text('带参数跳转'),
            ),
            const SizedBox(height: 12),
            
            // 3. push接收返回值
            ElevatedButton(
              onPressed: () async {
                var result = await Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => const PickColorPage(),
                  ),
                );
                if (result != null && context.mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('你选择了: $result')),
                  );
                }
              },
              child: const Text('选择颜色(接收返回值)'),
            ),
            const SizedBox(height: 12),
            
            // 4. pushReplacement — 替换当前页面
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pushReplacement(
                  MaterialPageRoute(
                    builder: (context) => const DetailPage(title: '替换页面'),
                  ),
                );
              },
              child: const Text('PushReplacement'),
            ),
            
            const SizedBox(height: 24),
            const Text('其他导航方式:'),
            const SizedBox(height: 12),
            
            // 5. pushAndRemoveUntil — 清除栈
            ElevatedButton(
              onPressed: () {
                Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute(builder: (_) => const HomePage()),
                  (route) => false, // 清除所有之前的路由
                );
              },
              child: const Text('清空栈跳转'),
            ),
            
            // 6. pop — 返回上一页
            // Navigator.of(context).pop();
            // Navigator.of(context).pop(resultValue);
            
            // 7. maybePop — 安全返回(可能不执行)
            // Navigator.of(context).maybePop();
            
            // 8. canPop — 是否可以返回
            // var canGoBack = Navigator.of(context).canPop();
          ],
        ),
      ),
    );
  }
}

/// 详情页
class DetailPage extends StatelessWidget {
  final String title;
  final Map<String, dynamic>? data;
  
  const DetailPage({required this.title, this.data, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('详情页: $title', style: Theme.of(context).textTheme.headlineSmall),
            if (data != null) ...[
              const SizedBox(height: 16),
              Text('参数: $data'),
            ],
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('返回'),
            ),
          ],
        ),
      ),
    );
  }
}

/// 颜色选择页 — 演示返回值
class PickColorPage extends StatelessWidget {
  const PickColorPage({super.key});
  
  @override
  Widget build(BuildContext context) {
    var colors = {
      '红色': Colors.red,
      '蓝色': Colors.blue,
      '绿色': Colors.green,
      '紫色': Colors.purple,
      '橙色': Colors.orange,
    };
    
    return Scaffold(
      appBar: AppBar(title: const Text('选择颜色')),
      body: ListView(
        children: colors.entries.map((entry) => ListTile(
          leading: CircleAvatar(backgroundColor: entry.value),
          title: Text(entry.key),
          trailing: const Icon(Icons.check_circle_outline),
          onTap: () => Navigator.of(context).pop(entry.key),
        )).toList(),
      ),
    );
  }
}

📖 Dialog与BottomSheet

// ✅ 验证通过
/// Dialog — 模态对话框
void dialogDemos(BuildContext context) {
  // 1. AlertDialog
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('确认操作'),
      content: const Text('确定要删除这个项目吗?此操作不可撤销。'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: const Text('取消'),
        ),
        FilledButton(
          onPressed: () => Navigator.pop(context, true),
          child: const Text('删除'),
        ),
      ],
    ),
  );
  
  // 2. SimpleDialog — 选项列表
  showDialog(
    context: context,
    builder: (context) => SimpleDialog(
      title: const Text('选择选项'),
      children: [
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, 'option1'),
          child: const Text('选项1'),
        ),
        SimpleDialogOption(
          onPressed: () => Navigator.pop(context, 'option2'),
          child: const Text('选项2'),
        ),
      ],
    ),
  );
  
  // 3. BottomSheet
  showModalBottomSheet(
    context: context,
    isScrollControlled: true, // 全屏底部表单
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (context) => Padding(
      padding: EdgeInsets.fromLTRB(
        16, 16, 16, MediaQuery.of(context).viewInsets.bottom + 16,
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            width: 40, height: 4,
            decoration: BoxDecoration(
              color: Colors.grey[300],
              borderRadius: BorderRadius.circular(2),
            ),
          ),
          const SizedBox(height: 16),
          const TextField(decoration: InputDecoration(labelText: '备注')),
          const SizedBox(height: 12),
          FilledButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('提交'),
          ),
        ],
      ),
    ),
  );
}

/// 自定义Dialog路由过渡动画
class SlideDialog extends PopupRoute {
  final Widget child;
  
  SlideDialog({required this.child});
  
  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return child;
  }
  
  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return SlideTransition(
      position: Tween<Offset>(
        begin: const Offset(0, 1),
        end: Offset.zero,
      ).animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)),
      child: child,
    );
  }
  
  @override
  Color? get barrierColor => Colors.black54;
  
  @override
  bool get barrierDismissible => true;
  
  @override
  String? get barrierLabel => 'SlideDialog';
  
  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);
  
  @override
  Duration get reverseTransitionDuration => const Duration(milliseconds: 200);
}

📖 页面转场动画

// ✅ 验证通过
/// 自定义页面转场

// 1. PageRouteBuilder
Navigator.of(context).push(
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => const DetailPage(title: '自定义转场'),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      // 淡入淡出
      return FadeTransition(opacity: animation, child: child);
      
      // 缩放
      // return ScaleTransition(scale: animation, child: child);
      
      // 旋转+缩放
      // return RotationTransition(turns: animation, child: ScaleTransition(scale: animation, child: child));
      
      // 滑动
      // var tween = Tween(begin: const Offset(1.0, 0.0), end: Offset.zero)
      //     .chain(CurveTween(curve: Curves.easeInOutCubic));
      // return SlideTransition(position: animation.drive(tween), child: child);
    },
    transitionDuration: const Duration(milliseconds: 400),
    reverseTransitionDuration: const Duration(milliseconds: 300),
  ),
);

// 2. CupertinoPageRoute — iOS风格转场
// Navigator.of(context).push(
//   CupertinoPageRoute(builder: (_) => const NextPage()),
// );

// 3. Hero动画(下一课详细讲解)
// 只需在两个页面中用相同的heroTag标记Widget

📖 深度链接与路由守卫

// ✅ 验证通过
/// 路由守卫——拦截未登录用户
class AuthGuard {
  static bool _isLoggedIn = false;
  
  static Route<dynamic>? onGenerateRoute(RouteSettings settings) {
    // 需要认证的页面列表
    var protectedRoutes = ['/profile', '/settings', '/orders'];
    
    if (protectedRoutes.contains(settings.name) && !_isLoggedIn) {
      // 重定向到登录页
      return MaterialPageRoute(
        builder: (_) => const LoginPage(),
        settings: settings, // 保留原始settings
      );
    }
    
    // 正常路由
    return null; // 让其他路由处理器处理
  }
}

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            AuthGuard._isLoggedIn = true;
            Navigator.of(context).pushReplacementNamed('/profile');
          },
          child: const Text('模拟登录'),
        ),
      ),
    );
  }
}

🏋️ 练习

练习1:多步向导 🧙

实现3步注册向导,每步push新页面,最后一步完成后pop回首页。

练习2:自定义底部弹窗 📱

实现一个从底部滑入的分享面板,包含微信/微博/QQ等分享选项。

练习3:路由观察者 👁️

使用RouteObserver追踪页面访问,实现页面浏览统计。

🏆 本课成就

📚 下节预告

下一课:命名路由——集中管理路由表,实现声明式导航。