Flutter 路由导航完全指南

引言

路由导航是任何移动应用的核心功能之一。Flutter 提供了强大而灵活的路由系统,支持多种导航方式。本文将深入探讨 Flutter 路由导航的各种技巧和最佳实践。

基础导航

Navigator.push

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondPage()),
);

Navigator.pop

Navigator.pop(context);

高级技巧一:命名路由

注册命名路由

void main() {
  runApp(MaterialApp(
    initialRoute: '/',
    routes: {
      '/': (context) => HomePage(),
      '/detail': (context) => DetailPage(),
      '/settings': (context) => SettingsPage(),
    },
  ));
}

使用命名路由导航

Navigator.pushNamed(context, '/detail');
Navigator.pushReplacementNamed(context, '/settings');
Navigator.popUntil(context, ModalRoute.withName('/'));

高级技巧二:路由传参

基础参数传递

// 传递参数
Navigator.pushNamed(
  context,
  '/detail',
  arguments: {'id': 1, 'name': 'Flutter'},
);

// 接收参数
final args = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>;
final id = args['id'];
final name = args['name'];

类型安全的参数传递

class DetailArguments {
  final int id;
  final String name;
  
  DetailArguments({required this.id, required this.name});
}

// 传递
Navigator.pushNamed(
  context,
  '/detail',
  arguments: DetailArguments(id: 1, name: 'Flutter'),
);

// 接收
final args = ModalRoute.of(context)?.settings.arguments as DetailArguments;

高级技巧三:路由动画

自定义页面过渡动画

Navigator.push(
  context,
  PageRouteBuilder(
    pageBuilder: (context, animation, secondaryAnimation) => DetailPage(),
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      const begin = Offset(1.0, 0.0);
      const end = Offset.zero;
      const curve = Curves.ease;
      
      var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
      
      return SlideTransition(
        position: animation.drive(tween),
        child: child,
      );
    },
  ),
);

使用 Hero 动画

// 源页面
Hero(
  tag: 'imageHero',
  child: Image.network('https://example.com/image.jpg'),
)

// 目标页面
Hero(
  tag: 'imageHero',
  child: Image.network('https://example.com/image.jpg'),
)

高级技巧四:嵌套导航

使用 Navigator 嵌套

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Navigator(
        initialRoute: 'home',
        onGenerateRoute: (settings) {
          WidgetBuilder builder;
          switch (settings.name) {
            case 'home':
              builder = (context) => HomeContent();
              break;
            case 'profile':
              builder = (context) => ProfilePage();
              break;
            default:
              throw Exception('Unknown route');
          }
          return MaterialPageRoute(builder: builder, settings: settings);
        },
      ),
    );
  }
}

高级技巧五:路由守卫

使用 WillPopScope

WillPopScope(
  onWillPop: () async {
    // 询问用户是否确认退出
    final shouldPop = await showDialog(
      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('确定'),
          ),
        ],
      ),
    );
    return shouldPop ?? false;
  },
  child: Scaffold(...),
)

高级技巧六:使用 GetX 路由

基本导航

// 跳转到新页面
Get.to(DetailPage());

// 跳转到新页面并移除之前的页面
Get.off(DetailPage());

// 跳转到新页面并移除所有之前的页面
Get.offAll(HomePage());

// 返回上一页
Get.back();

带参数导航

// 传递参数
Get.to(DetailPage(), arguments: {'id': 1, 'name': 'Flutter'});

// 在目标页面接收参数
final args = Get.arguments as Map<String, dynamic>;

路由别名

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/',
    getPages: [
      GetPage(name: '/', page: () => HomePage()),
      GetPage(name: '/detail', page: () => DetailPage()),
      GetPage(name: '/settings', page: () => SettingsPage()),
    ],
  ));
}

// 使用别名导航
Get.toNamed('/detail');
Get.toNamed('/detail?id=1&name=Flutter');

实战案例:路由管理服务

class RouteService {
  static const String home = '/';
  static const String detail = '/detail';
  static const String settings = '/settings';
  static const String login = '/login';
  
  static void goHome() => Get.offAllNamed(home);
  static void goDetail({required int id}) => Get.toNamed('$detail?id=$id');
  static void goSettings() => Get.toNamed(settings);
  static void goLogin() => Get.offAllNamed(login);
}

// 使用
RouteService.goDetail(id: 1);

实战案例:认证路由守卫

class AuthMiddleware extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    final isLoggedIn = Get.find<AuthService>().isLoggedIn;
    if (!isLoggedIn && route != RouteService.login) {
      return RouteSettings(name: RouteService.login);
    }
    return null;
  }
}

// 注册中间件
GetPage(
  name: RouteService.settings,
  page: () => SettingsPage(),
  middlewares: [AuthMiddleware()],
)

实战案例:页面过渡动画

GetPage(
  name: '/detail',
  page: () => DetailPage(),
  transition: Transition.fade,
  transitionDuration: Duration(milliseconds: 500),
)

// 自定义过渡
GetPage(
  name: '/detail',
  page: () => DetailPage(),
  customTransition: CustomTransition(
    transitionBuilder: (context, animation, secondaryAnimation, child) {
      return RotationTransition(
        turns: animation,
        child: child,
      );
    },
  ),
)

常见问题与解决方案

Q1:如何处理路由栈?

A:使用 Navigator.popUntilGet.offAll

Navigator.popUntil(context, ModalRoute.withName('/'));
Get.offAll(HomePage());

Q2:如何获取路由参数?

A:使用 ModalRoute.of(context)?.settings.argumentsGet.arguments

final args = ModalRoute.of(context)?.settings.arguments;
final args = Get.arguments;

Q3:如何实现深度链接?

A:配置 Android 和 iOS 的 URL Scheme:

<!-- Android -->
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="myapp" />
</intent-filter>

最佳实践

1. 使用命名路由

// 错误:直接使用 MaterialPageRoute
Navigator.push(context, MaterialPageRoute(builder: (context) => DetailPage()));

// 正确:使用命名路由
Navigator.pushNamed(context, '/detail');

2. 封装路由服务

class Routes {
  static const String home = '/';
  static const String detail = '/detail';
}

3. 使用路由守卫保护页面

GetPage(
  name: '/profile',
  page: () => ProfilePage(),
  middlewares: [AuthMiddleware()],
)

4. 统一过渡动画

GetMaterialApp(
  defaultTransition: Transition.cupertino,
)

总结

路由导航是 Flutter 应用的核心功能。通过本文的学习,你应该能够:

  1. 掌握基本的导航方法
  2. 使用命名路由和参数传递
  3. 实现自定义页面过渡动画
  4. 使用路由守卫保护页面
  5. 利用 GetX 简化导航操作

选择合适的路由方案可以让应用更加清晰和可维护。

更多推荐