Flutter 进阶 - 路由导航(Navigation)

Flutter 进阶 - 路由导航(Navigation)

一、基础路由

1. Navigator.push(跳转)

定义:
将新页面推入路由栈。

基础用法:

// 跳转到新页面
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondPage()),
);

完整示例:

class FirstPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SecondPage()),
            );
          },
          child: Text('跳转到第二页'),
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('第二页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);  // 返回上一页
          },
          child: Text('返回'),
        ),
      ),
    );
  }
}

2. Navigator.pop(返回)

用法:

// 简单返回
Navigator.pop(context);

// 返回并传递数据
Navigator.pop(context, '返回的数据');

接收返回值:

class FirstPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            // 等待返回值
            final result = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => SelectPage()),
            );
            
            if (result != null) {
              print('收到返回值:$result');
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('选择了:$result')),
              );
            }
          },
          child: Text('选择选项'),
        ),
      ),
    );
  }
}

class SelectPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('选择')),
      body: ListView(
        children: [
          ListTile(
            title: Text('选项 A'),
            onTap: () {
              Navigator.pop(context, 'A');  // 返回并传值
            },
          ),
          ListTile(
            title: Text('选项 B'),
            onTap: () {
              Navigator.pop(context, 'B');
            },
          ),
        ],
      ),
    );
  }
}

3. 其他路由方法

// 替换当前路由
Navigator.pushReplacement(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
);

// 清空路由栈并跳转
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
  (route) => false,  // 移除所有路由
);

// 清空路由栈保留首页
Navigator.pushAndRemoveUntil(
  context,
  MaterialPageRoute(builder: (context) => HomePage()),
  ModalRoute.withName('/'),  // 保留首页
);

// 判断是否可以返回
if (Navigator.canPop(context)) {
  Navigator.pop(context);
} else {
  print('没有可返回的页面');
}

二、命名路由

4. 配置路由表

在 MaterialApp 中配置:

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      // 初始路由
      initialRoute: '/',
      // 路由表
      routes: {
        '/': (context) => HomePage(),
        '/second': (context) => SecondPage(),
        '/third': (context) => ThirdPage(),
        '/detail': (context) => DetailPage(),
        '/profile': (context) => ProfilePage(),
        '/settings': (context) => SettingsPage(),
      },
    );
  }
}

5. 使用命名路由

基础跳转:

// 跳转
Navigator.pushNamed(context, '/second');

// 返回
Navigator.pop(context);

// 替换当前路由
Navigator.pushReplacementNamed(context, '/home');

// 清空路由栈并跳转
Navigator.pushNamedAndRemoveUntil(
  context,
  '/home',
  (route) => false,
);

完整示例:

class HomePage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: ListView(
        children: [
          ListTile(
            leading: Icon(Icons.person),
            title: Text('个人中心'),
            trailing: Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.pushNamed(context, '/profile');
            },
          ),
          ListTile(
            leading: Icon(Icons.settings),
            title: Text('设置'),
            trailing: Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.pushNamed(context, '/settings');
            },
          ),
          ListTile(
            leading: Icon(Icons.info),
            title: Text('详情'),
            trailing: Icon(Icons.arrow_forward_ios),
            onTap: () {
              Navigator.pushNamed(context, '/detail');
            },
          ),
        ],
      ),
    );
  }
}

三、路由传参

6. 构造函数传参

方式一:直接传参(基础路由):

// 跳转并传参
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => DetailPage(
      id: 123,
      title: '商品详情',
      price: 99.99,
    ),
  ),
);

// 接收参数
class DetailPage extends StatelessWidget {
  final int id;
  final String title;
  final double price;

  const DetailPage({
    required this.id,
    required this.title,
    required this.price,
  });

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('ID: $id', style: TextStyle(fontSize: 18)),
            SizedBox(height: 10),
            Text('标题: $title', style: TextStyle(fontSize: 18)),
            SizedBox(height: 10),
            Text('价格: ¥$price', style: TextStyle(fontSize: 24, color: Colors.red)),
          ],
        ),
      ),
    );
  }
}

7. 命名路由传参

使用 arguments:

// 跳转并传参
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {
    'id': 123,
    'title': '商品详情',
    'price': 99.99,
  },
);

// 接收参数
class DetailPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // 获取传递的参数
    final args = ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
    final id = args['id'];
    final title = args['title'];
    final price = args['price'];

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('ID: $id'),
            Text('标题: $title'),
            Text('价格: ¥$price'),
          ],
        ),
      ),
    );
  }
}

封装参数模型:

// 定义参数模型
class ProductDetailArgs {
  final int id;
  final String title;
  final double price;

  ProductDetailArgs({
    required this.id,
    required this.title,
    required this.price,
  });
}

// 跳转并传参
Navigator.pushNamed(
  context,
  '/detail',
  arguments: ProductDetailArgs(
    id: 123,
    title: '商品详情',
    price: 99.99,
  ),
);

// 接收参数
class DetailPage extends StatelessWidget {
  
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments as ProductDetailArgs;

    return Scaffold(
      appBar: AppBar(title: Text(args.title)),
      body: Column(
        children: [
          Text('ID: ${args.id}'),
          Text('价格: ¥${args.price}'),
        ],
      ),
    );
  }
}

四、路由拦截

8. onGenerateRoute

定义:
动态生成路由,可以实现路由拦截、权限验证等功能。

基础用法:

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: (settings) {
        // 打印路由信息
        print('路由名称: ${settings.name}');
        print('路由参数: ${settings.arguments}');

        // 根据路由名称返回对应页面
        switch (settings.name) {
          case '/':
            return MaterialPageRoute(builder: (context) => HomePage());
          case '/second':
            return MaterialPageRoute(builder: (context) => SecondPage());
          case '/detail':
            return MaterialPageRoute(builder: (context) => DetailPage());
          default:
            return MaterialPageRoute(builder: (context) => NotFoundPage());
        }
      },
    );
  }
}

9. 路由守卫(登录验证)

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: (settings) {
        // 检查登录状态
        bool isLoggedIn = checkLoginStatus();
        
        // 需要登录的路由列表
        List<String> protectedRoutes = ['/profile', '/cart', '/orders'];
        
        // 如果访问受保护的路由且未登录,跳转到登录页
        if (protectedRoutes.contains(settings.name) && !isLoggedIn) {
          return MaterialPageRoute(
            builder: (context) => LoginPage(
              redirectTo: settings.name,  // 登录后跳转的页面
            ),
          );
        }
        
        // 路由分发
        switch (settings.name) {
          case '/':
            return MaterialPageRoute(builder: (context) => HomePage());
          case '/profile':
            return MaterialPageRoute(builder: (context) => ProfilePage());
          case '/cart':
            return MaterialPageRoute(builder: (context) => CartPage());
          case '/orders':
            return MaterialPageRoute(builder: (context) => OrdersPage());
          case '/login':
            return MaterialPageRoute(builder: (context) => LoginPage());
          default:
            return MaterialPageRoute(builder: (context) => NotFoundPage());
        }
      },
    );
  }
  
  bool checkLoginStatus() {
    // 检查登录状态的逻辑
    // 可以从 SharedPreferences 或其他状态管理中获取
    return false;
  }
}

// 登录页
class LoginPage extends StatelessWidget {
  final String? redirectTo;

  const LoginPage({this.redirectTo});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('登录')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // 登录成功后
            if (redirectTo != null) {
              // 跳转到原来要访问的页面
              Navigator.pushReplacementNamed(context, redirectTo!);
            } else {
              // 跳转到首页
              Navigator.pushReplacementNamed(context, '/');
            }
          },
          child: Text('登录'),
        ),
      ),
    );
  }
}

10. 路由守卫封装

class RouteGuard {
  // 路由生成器
  static Route<dynamic>? generateRoute(RouteSettings settings) {
    // 获取登录状态
    bool isLoggedIn = _checkLoginStatus();
    
    // 需要登录的路由
    List<String> protectedRoutes = ['/profile', '/cart', '/orders', '/favorites'];
    
    // 需要管理员权限的路由
    List<String> adminRoutes = ['/admin', '/users', '/settings'];
    
    // 登录验证
    if (protectedRoutes.contains(settings.name) && !isLoggedIn) {
      return MaterialPageRoute(
        builder: (context) => LoginPage(redirectTo: settings.name),
      );
    }
    
    // 管理员权限验证
    if (adminRoutes.contains(settings.name)) {
      if (!isLoggedIn) {
        return MaterialPageRoute(
          builder: (context) => LoginPage(redirectTo: settings.name),
        );
      }
      if (!_isAdmin()) {
        return MaterialPageRoute(
          builder: (context) => ForbiddenPage(),
        );
      }
    }
    
    // 路由分发
    return _getRoute(settings);
  }
  
  // 检查登录状态
  static bool _checkLoginStatus() {
    // 从存储中获取登录状态
    return false;
  }
  
  // 检查管理员权限
  static bool _isAdmin() {
    // 从存储中获取用户角色
    return false;
  }
  
  // 获取路由
  static Route<dynamic>? _getRoute(RouteSettings settings) {
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (context) => HomePage());
      case '/profile':
        return MaterialPageRoute(builder: (context) => ProfilePage());
      case '/cart':
        return MaterialPageRoute(builder: (context) => CartPage());
      case '/orders':
        return MaterialPageRoute(builder: (context) => OrdersPage());
      case '/admin':
        return MaterialPageRoute(builder: (context) => AdminPage());
      case '/login':
        return MaterialPageRoute(builder: (context) => LoginPage());
      default:
        return MaterialPageRoute(builder: (context) => NotFoundPage());
    }
  }
}

// 在 MaterialApp 中使用
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      onGenerateRoute: RouteGuard.generateRoute,
    );
  }
}

五、页面转场动画

11. 自定义转场动画

// 淡入淡出动画
class FadeRoute extends PageRouteBuilder {
  final Widget page;
  
  FadeRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return FadeTransition(
              opacity: animation,
              child: child,
            );
          },
        );
}

// 使用
Navigator.push(
  context,
  FadeRoute(page: SecondPage()),
);

// 缩放动画
class ScaleRoute extends PageRouteBuilder {
  final Widget page;
  
  ScaleRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return ScaleTransition(
              scale: Tween<double>(begin: 0.0, end: 1.0).animate(
                CurvedAnimation(parent: animation, curve: Curves.easeInOut),
              ),
              child: child,
            );
          },
        );
}

// 滑动动画
class SlideRoute extends PageRouteBuilder {
  final Widget page;
  
  SlideRoute({required this.page})
      : super(
          pageBuilder: (context, animation, secondaryAnimation) => page,
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            var begin = Offset(1.0, 0.0);
            var end = Offset.zero;
            var curve = Curves.ease;
            var tween = Tween(begin: begin, end: end).chain(
              CurveTween(curve: curve),
            );
            
            return SlideTransition(
              position: animation.drive(tween),
              child: child,
            );
          },
        );
}

五、返回拦截

1. PopScope(Flutter 3.12+)

定义:
拦截返回操作,防止用户误触返回按钮。

基础用法:

class EditPage extends StatefulWidget {
  
  _EditPageState createState() => _EditPageState();
}

class _EditPageState extends State<EditPage> {
  bool _hasUnsavedChanges = false;

  
  Widget build(BuildContext context) {
    return PopScope(
      canPop: !_hasUnsavedChanges,  // false时拦截返回
      onPopInvoked: (bool didPop) {
        if (didPop) {
          return;
        }
        // 显示确认对话框
        _showExitConfirmDialog();
      },
      child: Scaffold(
        appBar: AppBar(title: Text('编辑页面')),
        body: Column(
          children: [
            TextField(
              onChanged: (value) {
                setState(() {
                  _hasUnsavedChanges = value.isNotEmpty;
                });
              },
              decoration: InputDecoration(hintText: '输入内容'),
            ),
            Text(
              _hasUnsavedChanges ? '有未保存的更改' : '无更改',
              style: TextStyle(color: Colors.red),
            ),
          ],
        ),
      ),
    );
  }

  void _showExitConfirmDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('退出确认'),
        content: Text('您有未保存的更改,确定要退出吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);  // 关闭对话框
              Navigator.pop(context);  // 退出页面
            },
            child: Text('确定'),
          ),
        ],
      ),
    );
  }
}

2. WillPopScope(旧版本兼容)

定义:
Flutter 3.12 之前使用的返回拦截组件。

基础用法:

WillPopScope(
  onWillPop: () async {
    // 返回 true 允许返回,false 拦截返回
    return await _showExitDialog(context) ?? false;
  },
  child: Scaffold(
    appBar: AppBar(title: Text('页面')),
    body: Center(child: Text('内容')),
  ),
)

Future<bool?> _showExitDialog(BuildContext context) {
  return showDialog<bool>(
    context: context,
    builder: (context) => AlertDialog(
      title: Text('确认退出'),
      content: Text('确定要退出吗?'),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: Text('取消'),
        ),
        TextButton(
          onPressed: () => Navigator.pop(context, true),
          child: Text('确定'),
        ),
      ],
    ),
  );
}

3. 双击退出应用

定义:
在首页双击返回键退出应用,防止误触。

完整示例:

class HomePage extends StatefulWidget {
  
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  DateTime? _lastPressedTime;

  
  Widget build(BuildContext context) {
    return PopScope(
      canPop: false,
      onPopInvoked: (bool didPop) {
        if (didPop) {
          return;
        }
        _exitApp();
      },
      child: Scaffold(
        appBar: AppBar(title: Text('首页')),
        body: Center(child: Text('双击返回键退出应用')),
      ),
    );
  }

  void _exitApp() {
    if (_lastPressedTime == null ||
        DateTime.now().difference(_lastPressedTime!) > Duration(seconds: 2)) {
      // 两次点击间隔超过2秒,重新记录时间
      _lastPressedTime = DateTime.now();
      
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text('再按一次退出应用'),
          duration: Duration(seconds: 2),
        ),
      );
    } else {
      // 退出应用
      SystemNavigator.pop();
    }
  }
}

WillPopScope 版本(旧):

WillPopScope(
  onWillPop: () async {
    if (_lastPressedTime == null ||
        DateTime.now().difference(_lastPressedTime!) > Duration(seconds: 2)) {
      _lastPressedTime = DateTime.now();
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('再按一次退出应用')),
      );
      return false;
    }
    return true;
  },
  child: Scaffold(...),
)

4. 表单保护示例

定义:
表单有未保存数据时拦截返回。

class FormPage extends StatefulWidget {
  
  _FormPageState createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {
  final _formKey = GlobalKey<FormState>();
  bool _isModified = false;
  
  String _name = '';
  String _email = '';

  
  Widget build(BuildContext context) {
    return PopScope(
      canPop: !_isModified,
      onPopInvoked: (bool didPop) async {
        if (didPop) {
          return;
        }
        
        final shouldPop = await showDialog<bool>(
          context: context,
          builder: (context) => AlertDialog(
            title: Text('未保存的更改'),
            content: Text('您有未保存的更改,确定要离开吗?'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(context, false),
                child: Text('继续编辑'),
              ),
              TextButton(
                onPressed: () => Navigator.pop(context, true),
                child: Text('放弃更改'),
              ),
            ],
          ),
        );
        
        if (shouldPop == true && context.mounted) {
          Navigator.pop(context);
        }
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text('表单编辑'),
          actions: [
            IconButton(
              icon: Icon(Icons.save),
              onPressed: _saveForm,
            ),
          ],
        ),
        body: Form(
          key: _formKey,
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Column(
              children: [
                TextFormField(
                  decoration: InputDecoration(labelText: '姓名'),
                  onChanged: (value) {
                    setState(() {
                      _name = value;
                      _isModified = true;
                    });
                  },
                ),
                TextFormField(
                  decoration: InputDecoration(labelText: '邮箱'),
                  onChanged: (value) {
                    setState(() {
                      _email = value;
                      _isModified = true;
                    });
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  void _saveForm() {
    if (_formKey.currentState!.validate()) {
      // 保存数据
      print('保存:姓名=$_name, 邮箱=$_email');
      
      setState(() {
        _isModified = false;
      });
      
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('保存成功')),
      );
    }
  }
}

5. 版本兼容处理

同时支持新旧版本:

import 'package:flutter/material.dart';

class CompatibleBackInterceptor extends StatelessWidget {
  final Widget child;
  final Future<bool> Function() onWillPop;

  const CompatibleBackInterceptor({
    required this.child,
    required this.onWillPop,
  });

  
  Widget build(BuildContext context) {
    // Flutter 3.12+ 使用 PopScope
    // 旧版本使用 WillPopScope
    return PopScope(
      canPop: false,
      onPopInvoked: (bool didPop) async {
        if (didPop) {
          return;
        }
        if (await onWillPop()) {
          if (context.mounted) {
            Navigator.pop(context);
          }
        }
      },
      child: child,
    );
  }
}

// 使用
CompatibleBackInterceptor(
  onWillPop: () async {
    return await _showExitDialog() ?? false;
  },
  child: Scaffold(...),
)

总结

路由方式对比

特性基础路由命名路由
使用方式Navigator.pushNavigator.pushNamed
传参方式构造函数arguments
路由管理分散集中
代码维护较困难简单
适用场景简单应用中大型应用

最佳实践

  1. 中大型应用:使用命名路由 + onGenerateRoute
  2. 小型应用:使用基础路由即可
  3. 需要权限控制:使用路由守卫
  4. 传递复杂参数:封装参数模型
  5. 页面转场:自定义 PageRouteBuilder
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值