Flutter如何在ApiService中获取BuildContext处理令牌过期登出
解决方案:ApiService中无需持有BuildContext实现令牌过期登出
ApiService作为独立的业务服务,不应该直接持有BuildContext——这会导致代码耦合,还可能引发内存泄漏或上下文无效的问题。你用BuildContextProvider没生效,是因为它没有正确挂载到Widget树中,无法获取有效上下文。以下是几种可行的解耦方案:
方案1:事件总线解耦(推荐)
通过全局事件总线发送"令牌过期"事件,在Widget层监听事件并执行UI操作,完全隔离业务层与UI层的依赖。
步骤实现:
- 定义事件类
class TokenExpiredEvent {}
- 创建全局事件总线(可使用第三方包
event_bus,或自己用StreamController实现)
// 使用event_bus包的方式 final authEventBus = EventBus(); // 自定义StreamController实现(无需第三方包) // final authEventBus = StreamController<TokenExpiredEvent>.broadcast();
- 修改拦截器代码,发送事件替代直接操作UI
// 替换AppInterceptors中原来的BuildContextProvider代码 if (dioException.type == DioExceptionType.badResponse && dioException.response?.statusCode == 400 && dioException.requestOptions.path.contains("refreshToken")) { Fimber.w("api_auth_service 400 unauth"); // 发送令牌过期事件 authEventBus.fire(TokenExpiredEvent()); UserRepository().logout(); }
- 在顶层Widget(如MainApp)中监听事件
class MainApp extends StatefulWidget { @override _MainAppState createState() => _MainAppState(); } class _MainAppState extends State<MainApp> { late StreamSubscription _tokenExpiredSubscription; @override void initState() { super.initState(); // 监听令牌过期事件 _tokenExpiredSubscription = authEventBus.on<TokenExpiredEvent>().listen((event) { context.read<LoginCubit>().logout(context); Device.showToast(context, "登录已过期,请重新登录。"); }); } @override void dispose() { _tokenExpiredSubscription.cancel(); // 自定义StreamController需关闭:authEventBus.close(); super.dispose(); } @override Widget build(BuildContext context) { return MaterialApp( // 你的应用配置 ); } }
方案2:全局导航键+回调
通过全局导航键获取上下文,配合顶层Widget注册的回调执行UI操作。
步骤实现:
- 定义全局导航键和回调
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); void Function(BuildContext)? onTokenExpired;
- 在MaterialApp中配置导航键并初始化回调
MaterialApp( navigatorKey: navigatorKey, builder: (context, child) { onTokenExpired = (context) { context.read<LoginCubit>().logout(context); Device.showToast(context, "登录已过期,请重新登录。"); }; return child!; }, // 其他配置 )
- 修改拦截器代码调用回调
if (dioException.type == DioExceptionType.badResponse && dioException.response?.statusCode == 400 && dioException.requestOptions.path.contains("refreshToken")) { Fimber.w("api_auth_service 400 unauth"); // 通过全局导航键获取上下文并执行回调 onTokenExpired?.call(navigatorKey.currentContext!); UserRepository().logout(); }
方案3:UI层捕获自定义异常
ApiService只负责抛出业务异常,将UI操作放到Bloc/ViewModel等UI逻辑层处理,上下文自然可用。
步骤实现:
- 定义自定义异常并在拦截器中抛出
class TokenExpiredException implements Exception {} // 修改AppInterceptors的onError逻辑 if (dioException.type == DioExceptionType.badResponse && dioException.response?.statusCode == 400 && dioException.requestOptions.path.contains("refreshToken")) { Fimber.w("api_auth_service 400 unauth"); UserRepository().logout(); // 抛出自定义异常 handler.reject(DioException( requestOptions: dioException.requestOptions, error: TokenExpiredException(), )); return; }
- 在UI层逻辑中捕获异常并处理
// 示例:在业务Cubit中调用API时捕获异常 try { await ApiService().request("/some/api"); } catch (e) { if (e is DioException && e.error is TokenExpiredException) { context.read<LoginCubit>().logout(context); Device.showToast(context, "登录已过期,请重新登录。"); } // 处理其他异常 }
为什么BuildContextProvider没生效?
你直接实例化BuildContextProvider()((context) => ...)的方式错误——这个Provider需要挂载到Widget树中,通过context.read或Provider.of获取,直接创建实例无法接入Widget树的上下文链,自然拿不到有效Context。
内容的提问来源于stack exchange,提问作者maxxxo
相关产品推荐
相关产品推荐

