Flutter中页面拦截器的实现方法

427 阅读5分钟

在 Flutter 中,页面拦截器(路由拦截)可以通过多种方式实现,以下是主要的几种方法:

1. 使用 NavigatorObserver(导航观察者)

这是 Flutter 内置的标准方法,通过监听导航事件实现拦截。

class AuthNavigatorObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    // 页面被推入时调用
    super.didPush(route, previousRoute);
    _checkAuth(route);
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    // 页面被弹出时调用
    super.didPop(route, previousRoute);
  }

  void _checkAuth(Route<dynamic> route) {
    if (route.settings.name == '/profile' && !UserService.isLoggedIn) {
      // 重定向到登录页
      Navigator.pushReplacementNamed(route.navigator!.context, '/login');
    }
  }
}

// 在 MaterialApp 中使用
MaterialApp(
  navigatorObservers: [AuthNavigatorObserver()],
  // 其他配置...
)

2. 使用 GetX 中间件(推荐用于 GetX 项目)

如果您使用 GetX 状态管理库,可以使用其内置的中间件系统。

class AuthMiddleware extends GetMiddleware {
  @override
  int? priority = 1; // 优先级,数字越小优先级越高
  
  @override
  RouteSettings? redirect(String? route) {
    // 检查用户是否登录
    if (!AuthService.isLoggedIn && route != '/login') {
      return RouteSettings(name: '/login');
    }
    return null;
  }
}

// 在路由配置中使用
GetPage(
  name: '/profile',
  page: () => ProfilePage(),
  middlewares: [AuthMiddleware()],
),

个人理解

middlewares: [AuthMiddleware()] 中间件可以设置多个,他们的执行顺序按照他们各自的priority优先级来;
返回的RouteSettings(name: '/login')必须是在
GetPage(
  name: '/login',
  page: () => OtherPageView(),
),中配置的
一旦某个中间件返回了重定向(RouteSettings),后续所有中间件都将不会执行。

前两种方式的性能差异:

GetX 的中间件(拦截器):只有你在路由配置里设置了 middlewares,对应页面跳转时才会执行这些中间件。如果没设置中间件,页面跳转就不会有拦截和额外逻辑,性能不会有影响。

Flutter 的 NavigatorObserver:这是 Flutter 原生的导航观察者,只要你在 MaterialApp 或 GetMaterialApp 里注册了,它会监听所有页面的 push、pop、replace 等导航事件。
它的回调(如 didPush、didPop)会在每次页面跳转时执行。

性能影响说明:

NavigatorObserver 本身只是事件监听,回调方法很轻量,通常不会影响性能。
只有你在这些回调里写了复杂或耗时的逻辑,才可能拖慢页面跳转。
一般用于埋点、统计、日志、权限等场景,合理使用不会有明显性能问题。
总结:

GetX 中间件只在你设置时才执行,不设置就没有影响。
NavigatorObserver 只要注册就会监听所有导航事件,但本身很轻量,性能影响极小,主要看你回调里写了什么。

3. 使用 onGenerateRoute 拦截

在 MaterialApp 的 onGenerateRoute 回调中实现路由拦截。

MaterialApp(
  onGenerateRoute: (RouteSettings settings) {
    // 检查权限
    if (settings.name == '/admin' && !UserService.isAdmin) {
      return MaterialPageRoute(builder: (_) => UnauthorizedPage());
    }
    
    // 正常路由
    switch (settings.name) {
      case '/':
        return MaterialPageRoute(builder: (_) => HomePage());
      case '/profile':
        return MaterialPageRoute(builder: (_) => ProfilePage());
      default:
        return MaterialPageRoute(builder: (_) => NotFoundPage());
    }
  },
)

1与3的差异:

NavigatorObserver 和 onGenerateRoute 在拦截导航行为上有根本性的区别:它们处于导航过程的不同阶段,因此拥有不同的能力和职责。
onGenerateRoute 是 路由生成器:它在路由被创建之前拦截,决定要创建什么。
NavigatorObserver 是 路由观察者:它在路由被推送或弹出之后拦截,用于知道发生了什么。

核心区别对比表

特性onGenerateRouteNavigatorObserver
拦截时机路由创建之前路由已经执行之后
角色生成者/决策者观察者/记录者
主要能力创建、替换、重定向路由。可以决定不创建目标路由,而是创建另一个(如登录页)。监听、记录、分析导航事件。无法改变当前正在发生的导航行为。
访问 Context可以,因为它是一个构建页面的函数,自然能拿到 context不可以,观察者方法里没有 context 参数。
典型用例身份验证守卫:用户未登录时,访问个人中心页面的请求被重定向到登录页。行为分析:记录用户浏览了哪些页面(Firebase Analytics等)。
修改导航可以强干预,直接返回一个不同的 Route 对象。不能直接干预当前的 push/pop 操作。但可以通过监听结果触发新的导航(这可能导致体验不佳)。
依赖获取可通过 ModalRoute.of(context) 获取路由参数,可通过 Provider 等访问全局状态。无法直接通过 context 获取依赖,通常需要直接访问全局状态容器(如 Get.put() 的实例、全局的 bloc 等)。

4. 使用 RouteAware 混入

通过混入 RouteAware 类并配合 NavigatorObserver 实现页面级别的拦截。

class AuthPage extends StatefulWidget {
  @override
  _AuthPageState createState() => _AuthPageState();
}

class _AuthPageState extends State<AuthPage> with RouteAware {
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    RouteObserver.of(context).subscribe(this, ModalRoute.of(context)!);
  }

  @override
  void didPush() {
    // 页面被推入时
    _checkAuth();
  }

  @override
  void dispose() {
    RouteObserver.of(context).unsubscribe(this);
    super.dispose();
  }

  void _checkAuth() {
    if (!UserService.isLoggedIn) {
      Navigator.pushReplacementNamed(context, '/login');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 页面内容...
    );
  }
}

// 在 MaterialApp 中注册观察者
MaterialApp(
  navigatorObservers: [RouteObserver<PageRoute>()],
)

5. 使用第三方路由库

许多第三方路由库提供了更强大的拦截功能:

AutoRoute

@RoutePage()
class ProfilePage extends StatelessWidget {
  // 页面内容...
}

// 使用守卫
class AuthGuard extends AutoRouteGuard {
  @override
  void onNavigation(NavigationResolver resolver, StackRouter router) {
    if (UserService.isLoggedIn) {
      resolver.next(true);
    } else {
      router.push(LoginRoute());
      resolver.next(false);
    }
  }
}

Fluro

// 定义处理程序
var profileHandler = Handler(
  handlerFunc: (context, params) {
    if (!UserService.isLoggedIn) {
      return LoginPage();
    }
    return ProfilePage();
  },
);

// 配置路由
void defineRoutes(FluroRouter router) {
  router.define('/profile', handler: profileHandler);
}

6. 使用 BLoC/Cubit 模式结合导航

结合状态管理实现更复杂的拦截逻辑。

// 在 BLoC 中控制导航
class NavigationCubit extends Cubit<NavigationState> {
  final AuthCubit authCubit;
  StreamSubscription? _authSubscription;

  NavigationCubit({required this.authCubit}) : super(NavigationInitial()) {
    // 监听认证状态变化
    _authSubscription = authCubit.stream.listen((authState) {
      if (authState is Unauthenticated) {
        // 重定向到登录页
        emit(NavigationRedirect(route: '/login'));
      }
    });
  }

  @override
  Future<void> close() {
    _authSubscription?.cancel();
    return super.close();
  }
}

// 在 UI 中监听导航状态
BlocListener<NavigationCubit, NavigationState>(
  listener: (context, state) {
    if (state is NavigationRedirect) {
      Navigator.pushReplacementNamed(context, state.route);
    }
  },
  child: // 页面内容...
)

选择建议

  1. 简单项目:使用 onGenerateRouteNavigatorObserver
  2. GetX 项目:使用 GetX 中间件
  3. 复杂路由需求:考虑 AutoRoute Fluro
  4. 状态驱动导航:使用 BLoC/Cubit 模式

根据您的项目规模和需求选择最适合的方法。对于大多数应用,GetX 中间件或 NavigatorObserver 提供了良好的平衡 between 简单性和功能性。