[Flutter 基础] - 路由与导航-Navigator, Router

257 阅读4分钟

路由和导航可以说是一个项目中最重要的部分之一。越复杂的项目,路由管理就显得越重要。同样越复杂的业务场景,对路由的使用要求也越高。Flutter原生的路由管理就提供了非常丰富的功能,灵活使用可以帮助我们高效的完成不同的业务场景开发。


一、核心概念

  1. Navigator:管理页面(Route)的组件,通过维护一个 路由栈 实现页面跳转。
  2. Route:表示一个页面或视图,Flutter 提供了多种 Route 类型(如 MaterialPageRouteCupertinoPageRoute)。
  3. 路由栈(Route Stack):Navigator 通过 push()pop() 等方法操作路由栈,实现页面导航。

二、基本用法

1. 直接导航(匿名路由)

通过 Navigator.push()(打开新页面,入栈) 和 Navigator.pop()(返回上一页,出栈) 实现页面跳转:

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

// 返回上一页(出栈)
Navigator.pop(context);

2. 命名路由

MaterialApp 中配置路由表,通过名称跳转:

return MaterialApp(
  title: 'Flutter Demo',
  initialRoute: "/",
  routes: {
   // '/': (context) => MyHomePage(), 注意:这里不能配置App的跟页面,因为和home 参数重复了。
    '/textFieldDemo': (context) => TextFieldDemo(),
  },
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
  ),
  home: const MyHomePage(title: 'Flutter Demo Home Page'),
);

// 通过名称跳转
Navigator.pushNamed(context, '/textFieldDemo');

去掉home属性的写法

return MaterialApp(
  title: 'Flutter Demo',
  initialRoute: "/",
  routes: {
    '/': (context) => MyHomePage(title: 'title'),//因为MyHomePage是这个页面的根页面。
    '/textFieldDemo': (context) => TextFieldDemo(),
  },
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
  )
);

// 通过名称跳转
Navigator.pushNamed(context, '/textFieldDemo');

3. 路由传参

  • 接受title参数的页面
class SecondPage extends StatelessWidget {
  final String? title;

  const SecondPage({super.key, @required this.title});

  @override
  Widget build(BuildContext context) {
    final args = ModalRoute.of(context)!.settings.arguments;
    var id, name;
    if (args != null && args is Map<String, dynamic>) {
      id = args['ID'];
      name = args['name'];
    }
    return Scaffold(
      appBar: AppBar(title: Text(title ?? 'demo')),
      body: Center(
        child: Column(
          children: [
            Text('second page'),
            if (id != null) Text('ID:$id') else Text('No ID'),
            if (name != null) Text('name:$name') else Text('No name'),
          ],
        ),
      ),
    );
  }
}
  • 直接传参(匿名路由):

    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => SecondPage(title: '传递的标题'),
      ),
    );
    
  • 命名路由传参

     // 跳转时传参
    Navigator.pushNamed(
     context,
     '/second',
     arguments: {'ID': 'this is id', 'name': 'this is name'},
    ),
    

4. 自定义路由过渡动画

通过 PageRouteBuilder 实现自定义动画:

20250506.gif

Navigator.push(
  context,
  PageRouteBuilder(
    transitionDuration: Duration(milliseconds: 500),
    pageBuilder: (_, animation, secondaryAnimation) => SecondPage(title: 'Fade Transition'),
    transitionsBuilder: (_, animation, secondaryAnimation, child) {
      return FadeTransition(
        opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
        child: child,
      );
    },
  ),
);

5. 保持页面状态

使用 AutomaticKeepAliveClientMixin 保留页面状态(如滚动位置):

class KeepAlivePage extends StatefulWidget {
  @override
  _KeepAlivePageState createState() => _KeepAlivePageState();
}

class _KeepAlivePageState extends State<KeepAlivePage> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true; // 标记需要保留状态

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return Scaffold(
      appBar: AppBar(title: Text('Keep Alive Page')),
      body: Center(child: Text('This page state is preserved')),
    );
  }
}

6. 路由监听与拦截

  • 全局路由监听

可以针对一些特殊的页面,比如权限配置,A/B测试等去做监听处理。

class MyNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route? previousRoute) {
  // 这里可以根据用户状态去判断进入不一样的页面。
      if (route.settings.name == '/a') {
          // 如果目标页面是/a,则判断用户是否是Admin,是则进入/a页面,否则重定向到/b
          if(user.isAdmin()) {
              print('进入A页面: ${route.settings.name}');
              Future.delayed(Duration.zero, () {
                  Navigator.of(navigator!.context).pushReplacementNamed('/a');
              });
           } else {
              print('进入B页面: ${route.settings.name}')
              Future.delayed(Duration.zero, () {
                  Navigator.of(navigator!.context).pushReplacementNamed('/b');
              });
          }
      }
  }
}

  @override
  void didPush(Route route, Route? previousRoute) {
    const allowedPages = user.getAllowedConfig();
    if(!allowedPages.contains(route.settings.name)) { //如果需要跳转到页面不在列表,则返回
      Navigator.of(navigator!.context).pop();
    }
  }

    // 在 MaterialApp 中注册
    MaterialApp(
      navigatorObservers: [
        MyNavigatorObserver(),
        MyPermissionManagerObserver(),
      ]
    );
  • 拦截手机物理返回按钮
    WillPopScope(
      onWillPop: () async {
        if (shouldPreventBack) {
          showExitConfirmDialog(); // 显示确认对话框
          return false; // 阻止返回
        }
        return true; // 允许返回
      },
      child: Scaffold(...),
    );
    

进阶技巧

1. 替换当前页面

使用 pushReplacement() 替换当前页面(不保留在栈中):

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

2. 返回到指定页面

使用 popUntil() 返回到某个特定页面:

Navigator.popUntil(context, ModalRoute.withName('/home'));

3. 动态路由生成

通过 onGenerateRoute 动态生成路由:

MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == '/detail') {
      final args = settings.arguments as DetailArgs;
      return MaterialPageRoute(
        builder: (context) => DetailPage(args: args),
      );
    }
    return null;
  },
);

最佳实践

  1. 命名路由 vs 匿名路由

    • 小型应用:优先使用匿名路由,代码简洁。
    • 中大型应用:使用命名路由,便于维护和统一管理。
  2. 路由栈管理

    • 避免路由栈过深(建议不超过 5 层)。
    • 使用 Navigator.popUntil() 清理不必要的路由。
  3. 性能优化

    • 对频繁切换的页面使用 AutomaticKeepAliveClientMixin
    • 监控路由栈内存占用,防止内存泄漏。
  4. 错误处理

    • onGenerateRoute 中处理未知路由:
      onGenerateRoute: (settings) {
        if (settings.name == '/404') {
          return MaterialPageRoute(builder: (context) => NotFoundPage());
        }
        return null;
      },
      

完整示例代码

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => HomePage(),
        '/second': (context) => SecondPage(),
      },
      onGenerateRoute: (settings) {
        if (settings.name == '/detail') {
          final args = settings.arguments as DetailArgs;
          return MaterialPageRoute(
            builder: (context) => DetailPage(args: args),
          );
        }
        return null;
      },
      navigatorObservers: [MyNavigatorObserver()],
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('首页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/second');
          },
          child: Text('跳转到第二页'),
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('第二页')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(
              context,
              '/detail',
              arguments: DetailArgs(id: 1, name: 'Detail Page'),
            );
          },
          child: Text('跳转到详情页'),
        ),
      ),
    );
  }
}

class DetailPage extends StatelessWidget {
  final DetailArgs args;

  DetailPage({required this.args});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(args.name)),
      body: Center(child: Text('ID: ${args.id}')),
    );
  }
}

@freezed
class DetailArgs with _$DetailArgs {
  factory DetailArgs({required int id, required String name}) = _DetailArgs;
}

class MyNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route route, Route? previousRoute) {
    print('进入页面: ${route.settings.name}');
  }
}

总结

  • 基础用法push()/pop() 实现简单导航,named routes 管理复杂应用。
  • 高级功能:自定义动画、嵌套导航、状态保持、路由监听。
  • 最佳实践:合理规划路由栈,优化性能,处理异常场景。

Navigator的基础功能非常丰富,通过灵活运用 Navigator,可以降低复杂项目的路由管理成本,同时也可以让用户有一个更好的体验。