context_free_router 0.0.1 copy "context_free_router: ^0.0.1" to clipboard
context_free_router: ^0.0.1 copied to clipboard

retracted

No Context Router

无 BuildContext 的 Flutter 路由解决方案:ContextFreeRouter 插件详解 #

1. 插件概述 #

在 Flutter 应用开发中,路由管理是一个核心问题。传统的 Flutter 导航方法通常依赖于 BuildContext,这意味着你需要在 Widget 树中获取上下文才能执行导航操作。这种依赖带来了一系列限制,尤其是当你需要在非 UI 层(如业务逻辑层或数据层)进行导航时。

ContextFreeRouter 插件正是为解决这一问题而设计的。它提供了一种不依赖 BuildContext 的路由解决方案,让你可以在应用的任何位置进行导航操作,无论是在 UI 组件内部还是在业务逻辑层。

主要优势 #

  • 全局访问:在应用的任何位置都可以进行导航,无需传递 BuildContext
  • 关注点分离:将导航逻辑与 UI 逻辑分离,使代码更加清晰
  • 拦截器机制:提供强大的拦截器系统,可以在路由切换前执行自定义逻辑
  • 监控能力:内置路由监控功能,可以跟踪路由变化和相关数据
  • 单例模式:采用单例设计,确保全局只有一个路由实例

2. 核心功能详解 #

拦截器机制 #

ContextFreeRouter 的一个核心特性是其强大的拦截器机制。拦截器允许你在路由切换前执行自定义逻辑,例如身份验证、权限检查等。

拦截器接口

所有拦截器都需要实现 RouteInterceptor 接口:

abstract class RouteInterceptor {
  /// 拦截器优先级,数值越高优先级越高
  int get priority => 0;

  /// 在导航前调用,返回 InterceptorResult 控制导航流程
  FutureOr<InterceptorResult> onIntercept(InterceptorContext context);
}

拦截器可以返回三种结果:

  • InterceptorResult.proceed:继续导航流程
  • InterceptorResult.redirect:重定向到另一个路由
  • InterceptorResult.cancel:取消导航

身份验证拦截器示例

以下是一个身份验证拦截器的实现,它会检查用户是否已登录,如果未登录则重定向到登录页面:

class AuthInterceptor implements RouteInterceptor {
  final List<String> protectedRoutes;
  final String loginRoute;
  final FutureOr<bool> Function() isAuthenticatedCallback;
  
  @override
  int get priority => 100; // 高优先级,确保先执行
  
  AuthInterceptor({
    required this.protectedRoutes,
    required this.loginRoute,
    required this.isAuthenticatedCallback,
  });
  
  @override
  FutureOr<InterceptorResult> onIntercept(InterceptorContext context) async {
    final to = context.to;
    
    // 检查路由是否受保护
    if (protectedRoutes.any((route) => to.path.startsWith(route))) {
      // 检查用户是否已认证
      final isAuthenticated = await isAuthenticatedCallback();
      
      if (!isAuthenticated) {
        // 重定向到登录页,并传递返回路径
        context.redirect(
          loginRoute,
          params: {'returnTo': to.path},
        );
        return InterceptorResult.redirect;
      }
    }
    
    return InterceptorResult.proceed;
  }
}

权限检查拦截器示例

以下是一个权限检查拦截器,它会检查用户是否具有访问特定路由所需的权限:

class PermissionInterceptor implements RouteInterceptor {
  final Map<String, List<String>> routePermissions;
  final String unauthorizedRoute;
  final FutureOr<PermissionResult> Function(List<String> permissions) checkPermissionsCallback;
  
  @override
  int get priority => 90; // 在身份验证拦截器之后运行
  
  PermissionInterceptor({
    required this.routePermissions,
    required this.unauthorizedRoute,
    required this.checkPermissionsCallback,
  });
  
  @override
  FutureOr<InterceptorResult> onIntercept(InterceptorContext context) async {
    final to = context.to;
    
    // 检查路由是否需要权限
    final requiredPermissions = _getRequiredPermissions(to.path);
    
    if (requiredPermissions.isNotEmpty) {
      // 检查用户是否具有所需权限
      final result = await checkPermissionsCallback(requiredPermissions);
      
      if (result == PermissionResult.denied) {
        // 重定向到未授权页面
        context.redirect(unauthorizedRoute);
        return InterceptorResult.redirect;
      }
    }
    
    return InterceptorResult.proceed;
  }
  
  List<String> _getRequiredPermissions(String path) {
    for (final entry in routePermissions.entries) {
      if (path.startsWith(entry.key)) {
        return entry.value;
      }
    }
    return [];
  }
}

添加拦截器 #

你可以在应用初始化时添加拦截器:

final router = ContextFreeRouter.instance as ContextFreeRouterImpl;

// 添加身份验证拦截器
router.addInterceptor(AuthInterceptor(
  protectedRoutes: ['/dashboard', '/profile'],
  loginRoute: '/login',
  isAuthenticatedCallback: () async {
    // 检查用户是否已认证的逻辑
    return await AuthService.isLoggedIn();
  },
));

// 添加权限检查拦截器
router.addInterceptor(PermissionInterceptor(
  routePermissions: {
    '/profile': ['view_profile'],
    '/admin': ['admin_access'],
  },
  unauthorizedRoute: '/unauthorized',
  checkPermissionsCallback: (permissions) async {
    // 检查用户是否具有所需权限的逻辑
    return await PermissionService.checkPermissions(permissions);
  },
));

3. 路由注册与使用 #

路由注册 #

在使用 ContextFreeRouter 之前,你需要先注册路由。路由注册通过 register 方法完成,该方法接受一个路径和一个构建器函数:

void main() {
  final router = ContextFreeRouter.instance as ContextFreeRouterImpl;
  
  // 注册路由
  router.register('/', (config) => HomePage());
  router.register('/login', (config) => LoginPage(returnTo: config.params?['returnTo']));
  router.register('/dashboard', (config) => DashboardPage());
  router.register('/profile', (config) => ProfilePage());
  router.register('/unauthorized', (config) => UnauthorizedPage());
  
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'ContextFreeRouter Demo',
      navigatorKey: (ContextFreeRouter.instance).navigatorKey, // 重要:使用路由器的navigatorKey
      home: HomePage(),
    );
  }
}

参数传递 #

ContextFreeRouter 支持通过 paramsextra 参数传递数据:

// 导航到用户详情页,并传递用户ID
ContextFreeRouter.instance.navigateTo(
  '/user-details',
  params: {'userId': '123'},
  extra: {'showActions': true},
);

// 在目标页面中获取参数
class UserDetailsPage extends StatelessWidget {
  final String userId;
  final bool showActions;
  
  UserDetailsPage({
    required this.userId,
    this.showActions = false,
  });
  
  factory UserDetailsPage.fromConfig(RouteConfig config) {
    return UserDetailsPage(
      userId: config.params?['userId'] ?? '',
      showActions: (config.extra as Map?)?['showActions'] ?? false,
    );
  }
  
  @override
  Widget build(BuildContext context) {
    // 构建UI
  }
}

// 注册路由时使用工厂构造函数
router.register('/user-details', (config) => UserDetailsPage.fromConfig(config));

导航操作 #

ContextFreeRouter 提供了简单的导航 API:

// 导航到新页面
ContextFreeRouter.instance.navigateTo('/dashboard');

// 替换当前页面
ContextFreeRouter.instance.navigateTo('/login', replace: true);

// 返回上一页
ContextFreeRouter.instance.goBack();

// 返回上一页并传递结果
ContextFreeRouter.instance.goBack<bool>(true);

4. 测试用例分析 #

让我们分析提供的测试用例,了解每个页面的用途和场景。

登录页面 (/login) #

登录页面用于用户身份验证,它接受一个可选的 returnTo 参数,指定登录成功后要返回的页面:

router.register('/login', (config) => LoginPage(returnTo: config.params?['returnTo']));

登录页面的主要功能:

  • 提供用户名和密码输入字段
  • 验证用户输入
  • 登录成功后导航到指定页面或默认页面
  • 提供注册链接

测试场景:

  1. 直接访问登录页
  2. 从受保护页面重定向到登录页,登录成功后返回原页面
  3. 测试表单验证逻辑
  4. 测试登录失败的错误处理

仪表板页面 (/dashboard) #

仪表板页面是一个受保护的页面,需要用户登录才能访问:

router.register('/dashboard', (config) => DashboardPage());

仪表板页面的主要功能:

  • 显示用户数据概览
  • 提供导航到其他功能页面的入口
  • 包含抽屉菜单和操作按钮

测试场景:

  1. 已登录用户访问仪表板
  2. 未登录用户尝试访问仪表板(应被重定向到登录页)
  3. 测试从仪表板导航到其他页面
  4. 测试抽屉菜单的功能

个人资料页面 (/profile) #

个人资料页面是一个既需要身份验证又需要特定权限的页面:

router.register('/profile', (config) => ProfilePage());

个人资料页面的主要功能:

  • 显示用户个人信息
  • 允许编辑部分信息
  • 提供安全设置和密码修改入口
  • 包含退出登录功能

测试场景:

  1. 已登录且有权限的用户访问个人资料页
  2. 已登录但无权限的用户尝试访问(应被重定向到未授权页)
  3. 未登录用户尝试访问(应被重定向到登录页)
  4. 测试编辑功能和保存更改

未授权页面 (/unauthorized) #

未授权页面用于处理用户没有权限访问某些页面的情况:

router.register('/unauthorized', (config) => UnauthorizedPage());

未授权页面的主要功能:

  • 显示访问被拒绝的信息
  • 提供返回首页的选项
  • 提供联系管理员的选项
  • 提供切换账号的选项

测试场景:

  1. 用户尝试访问没有权限的页面时被重定向到此页面
  2. 测试各个导航按钮的功能

编写测试用例 #

以下是一个简单的测试用例,用于验证身份验证拦截器的功能:

void main() {
  testWidgets('Auth interceptor redirects to login page', (WidgetTester tester) async {
    // 设置测试环境
    final router = ContextFreeRouter.instance as ContextFreeRouterImpl;
    bool isAuthenticated = false;
    
    router.register('/', (config) => HomePage());
    router.register('/login', (config) => LoginPage(returnTo: config.params?['returnTo']));
    router.register('/dashboard', (config) => DashboardPage());
    
    router.addInterceptor(AuthInterceptor(
      protectedRoutes: ['/dashboard'],
      loginRoute: '/login',
      isAuthenticatedCallback: () async => isAuthenticated,
    ));
    
    // 渲染应用
    await tester.pumpWidget(MaterialApp(
      navigatorKey: router.navigatorKey,
      home: HomePage(),
    ));
    
    // 尝试导航到受保护页面
    ContextFreeRouter.instance.navigateTo('/dashboard');
    await tester.pumpAndSettle();
    
    // 验证是否被重定向到登录页
    expect(find.byType(LoginPage), findsOneWidget);
    expect(find.byType(DashboardPage), findsNothing);
    
    // 模拟登录成功
    isAuthenticated = true;
    
    // 再次尝试导航到受保护页面
    ContextFreeRouter.instance.navigateTo('/dashboard');
    await tester.pumpAndSettle();
    
    // 验证是否成功导航到仪表板
    expect(find.byType(DashboardPage), findsOneWidget);
    expect(find.byType(LoginPage), findsNothing);
  });
}

5. 监控功能介绍 #

ContextFreeRouter 提供了强大的路由监控功能,可以跟踪路由变化和相关数据。这对于分析用户行为、调试问题和集成分析工具非常有用。

监控接口 #

所有监控器都需要实现 RouteMonitor 接口:

abstract class RouteMonitor {
  /// 当路由成功导航时调用
  void onRouteChanged(RouteConfig from, RouteConfig to);
  
  /// 当路由导航被取消时调用
  void onRouteCancelled(RouteConfig from, RouteConfig to);
  
  /// 当路由导航被重定向时调用
  void onRouteRedirected(RouteConfig from, RouteConfig to, RouteConfig redirectTo);
  
  /// 当导航过程中发生错误时调用
  void onRouteError(RouteConfig from, RouteConfig to, Object error);
}

分析监控器示例 #

以下是一个用于分析的监控器实现:

class AnalyticsMonitor implements RouteMonitor {
  final void Function(String event, Map<String, dynamic> properties) trackEvent;
  
  AnalyticsMonitor({
    required this.trackEvent,
  });
  
  @override
  void onRouteChanged(RouteConfig from, RouteConfig to) {
    trackEvent('route_changed', {
      'from': from.path,
      'to': to.path,
      'params': to.params,
    });
  }
  
  @override
  void onRouteCancelled(RouteConfig from, RouteConfig to) {
    trackEvent('route_cancelled', {
      'from': from.path,
      'to': to.path,
    });
  }
  
  @override
  void onRouteRedirected(RouteConfig from, RouteConfig to, RouteConfig redirectTo) {
    trackEvent('route_redirected', {
      'from': from.path,
      'to': to.path,
      'redirectTo': redirectTo.path,
    });
  }
  
  @override
  void onRouteError(RouteConfig from, RouteConfig to, Object error) {
    trackEvent('route_error', {
      'from': from.path,
      'to': to.path,
      'error': error.toString(),
    });
  }
}

日志监控器示例 #

以下是一个简单的日志监控器,用于记录路由变化:

class LoggingMonitor implements RouteMonitor {
  final Logger logger;
  
  LoggingMonitor({
    required this.logger,
  });
  
  @override
  void onRouteChanged(RouteConfig from, RouteConfig to) {
    logger.info('Route changed from ${from.path} to ${to.path}');
  }
  
  @override
  void onRouteCancelled(RouteConfig from, RouteConfig to) {
    logger.warning('Route navigation cancelled from ${from.path} to ${to.path}');
  }
  
  @override
  void onRouteRedirected(RouteConfig from, RouteConfig to, RouteConfig redirectTo) {
    logger.info('Route redirected from ${from.path} to ${redirectTo.path} (original: ${to.path})');
  }
  
  @override
  void onRouteError(RouteConfig from, RouteConfig to, Object error) {
    logger.error('Route error from ${from.path} to ${to.path}: ${error.toString()}');
  }
}

添加监控器 #

你可以在应用初始化时添加监控器:

final router = ContextFreeRouter.instance as ContextFreeRouterImpl;

// 添加分析监控器
router.addMonitor(AnalyticsMonitor(
  trackEvent: (event, properties) {
    // 集成你的分析服务,如 Firebase Analytics
    FirebaseAnalytics.instance.logEvent(
      name: event,
      parameters: properties,
    );
  },
));

// 添加日志监控器
router.addMonitor(LoggingMonitor(
  logger: Logger(),
));

6. 总结与展望 #

总结 #

ContextFreeRouter 插件为 Flutter 应用提供了一种强大而灵活的路由解决方案,它的主要优点包括:

  1. 无 BuildContext 依赖:可以在应用的任何位置进行导航,无需传递上下文
  2. 强大的拦截器机制:支持身份验证、权限检查等自定义操作
  3. 全面的监控能力:可以跟踪路由变化和相关数据
  4. 简单的 API:提供直观的路由注册和导航方法
  5. 参数传递:支持通过 paramsextra 传递数据

这些特性使 ContextFreeRouter 特别适合以下场景:

  • 大型应用,需要在多个层级进行导航
  • 需要复杂权限控制的应用
  • 需要详细分析用户导航行为的应用
  • 采用清晰架构或 MVVM 模式的应用,需要在视图模型中进行导航

未来展望 #

ContextFreeRouter 插件还有很多可以改进和扩展的方向:

  1. 路由动画:添加自定义过渡动画支持,使页面切换更加流畅和美观
  2. 路径参数:实现类似 /users/:id 的路径参数支持,使路由更加灵活
  3. 嵌套导航:支持嵌套导航器,适用于底部标签栏和抽屉菜单等复杂导航结构
  4. 深层链接:增强对深层链接和外部 URL 的支持
  5. 路由缓存:实现路由页面缓存机制,提高性能和用户体验
  6. 声明式路由配置:提供更声明式的路由配置方式,使路由定义更加清晰
  7. 类型安全:增强类型安全,减少运行时错误

结语 #

ContextFreeRouter 插件通过解决 Flutter 导航中的 BuildContext 依赖问题,为开发者提供了更大的灵活性和更清晰的代码结构。它的拦截器机制和监控能力使复杂的导航逻辑变得简单而强大。

无论你是在开发一个简单的应用还是一个复杂的企业级项目,ContextFreeRouter 都能帮助你更好地管理应用的导航流程,提高开发效率和代码质量。

希望这篇文章能帮助你理解和使用 ContextFreeRouter 插件,如果你有任何问题或建议,欢迎在评论区留言或提交 issue 到我们的 GitHub 仓库。


参考资源