Flutter中使用Dio CancelToken取消正在进行的API请求
问题描述
我在TabBar中有两个标签页,点击每个标签页时会发送带有不同查询参数的请求。目前用户点击get_cars标签后,在数据返回前快速切换到get_drivers标签,会先显示get_cars的数据,再显示get_drivers的数据。我需要实现:当用户切换标签时,取消之前的请求,只发送当前标签的请求。仅针对GET请求进行处理,以下是我使用Dio包编写的代码:
import 'dart:convert'; import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:untitled/core/error/exceptions.dart'; import 'package:untitled/core/platform/network_urls.dart'; import 'package:untitled/routes/app_router.dart'; import 'package:untitled/routes/app_router.gr.dart'; import '../get_it/get_it.dart'; import '../local_storage/keys.dart'; import '../local_storage/local_storage.dart'; abstract class HttpRequests { Future<Response> get(String url); Future<Response> post(String url, {Object? body}); Future<Response> delete(String url); Future<Response> patch(String url, {Object? body}); void cancelRequests(); // Method to cancel requests } class HttpRequestsImp extends HttpRequests { final LocalStorage localStorage = sl<LocalStorage>(); late CancelToken cancelToken; late final Dio dio; HttpRequestsImp() { cancelToken = CancelToken(); dio = Dio(BaseOptions( baseUrl: BASE_API_URL, connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 10), contentType: Headers.jsonContentType, responseType: ResponseType.json, )) ..interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { final userCredentials = localStorage.read(USER_CREDENTIALS); if (userCredentials != null) { final userMapData = jsonDecode(userCredentials); options.headers['Authorization'] = 'Bearer ${userMapData["token"]["accessToken"]}'; } return handler.next(options); }, onResponse: (response, handler) { return handler.next( response); // continue with the response which will be returned }, onError: (DioException e, handler) async { if (e.type == DioExceptionType.cancel) { log('DioError: ${e.message}'); return; } if (e.response?.statusCode == 401) { try { final newAccessToken = await getNewAccessToken(); if (newAccessToken.isNotEmpty) { e.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; final retryResponse = await dio.request( e.requestOptions.path, options: Options( method: e.requestOptions.method, headers: e.requestOptions.headers, ), data: e.requestOptions.data, queryParameters: e.requestOptions.queryParameters, cancelToken: cancelToken, // Apply cancel token on retry ); return handler.resolve(retryResponse); } } catch (tokenError) { log('Token refresh failed: $tokenError'); final router = sl<AppRouter>(); router.push(const Login()); } } return handler.next(e); }, )); } Future<String> getNewAccessToken() async { try { final userCredentials = localStorage.read(USER_CREDENTIALS); final userMapData = jsonDecode(userCredentials); final response = await dio.post( '/auth/token/refresh', data: {'refreshToken': userMapData['token']['refreshToken']}, cancelToken: cancelToken, // Apply cancel token here as well ); if (response.statusCode != 200) { localStorage.delete(USER_CREDENTIALS); final router = sl<AppRouter>(); router.push(const Login()); return ""; } else { final data = response.data as Map<String, dynamic>; userMapData['token']['accessToken'] = data['payload']['accessToken']; userMapData['token']['refreshToken'] = data['payload']['refreshToken']; await localStorage.write(USER_CREDENTIALS, jsonEncode(userMapData)); return data['payload']['accessToken']; } } catch (e) { log('Failed to get new access token: $e'); localStorage.delete(USER_CREDENTIALS); final router = sl<AppRouter>(); router.push(const Login()); return ""; } } @override Future<Response> get(String url) async { cancelToken.cancel('cancelled'); print(cancelToken.isCancelled); cancelToken = CancelToken(); final response = await dio.get(url, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override Future<Response> post(String url, {Object? body}) async { final response = await dio.post(url, data: body, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override Future<Response> delete(String url) async { final response = await dio.delete(url, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override Future<Response> patch(String url, {Object? body}) async { final response = await dio.patch(url, data: body, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override void cancelRequests() {} }
解决方案
你的代码当前使用全局单一的CancelToken,会导致所有请求共享同一个token,切换标签时可能误取消非GET请求(比如token刷新的POST请求)。正确的做法是为GET请求单独维护活跃的token,确保仅取消未完成的GET请求。
修改要点:
- 将全局
cancelToken拆分为activeGetCancelToken(仅用于GET请求)和独立的临时token用于其他请求 - 在
get方法中,先取消之前的活跃GET请求,再创建新token绑定当前请求 - 完善取消方法,供Tab切换时主动调用以取消当前GET请求
- 避免将GET专用token绑定到POST/DELETE/PATCH等请求,防止误取消
修改后的完整代码:
import 'dart:convert'; import 'dart:developer'; import 'package:dio/dio.dart'; import 'package:untitled/core/error/exceptions.dart'; import 'package:untitled/core/platform/network_urls.dart'; import 'package:untitled/routes/app_router.dart'; import 'package:untitled/routes/app_router.gr.dart'; import '../get_it/get_it.dart'; import '../local_storage/keys.dart'; import '../local_storage/local_storage.dart'; abstract class HttpRequests { Future<Response> get(String url); Future<Response> post(String url, {Object? body}); Future<Response> delete(String url); Future<Response> patch(String url, {Object? body}); void cancelActiveGetRequest(); // 明确取消活跃的GET请求 } class HttpRequestsImp extends HttpRequests { final LocalStorage localStorage = sl<LocalStorage>(); CancelToken? activeGetCancelToken; // 仅用于GET请求的活跃token late final Dio dio; HttpRequestsImp() { dio = Dio(BaseOptions( baseUrl: BASE_API_URL, connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 10), contentType: Headers.jsonContentType, responseType: ResponseType.json, )) ..interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { final userCredentials = localStorage.read(USER_CREDENTIALS); if (userCredentials != null) { final userMapData = jsonDecode(userCredentials); options.headers['Authorization'] = 'Bearer ${userMapData["token"]["accessToken"]}'; } return handler.next(options); }, onResponse: (response, handler) { return handler.next(response); }, onError: (DioException e, handler) async { if (e.type == DioExceptionType.cancel) { log('DioError: 请求已取消 - ${e.message}'); return; // 取消请求直接终止流程,不往下传递错误 } if (e.response?.statusCode == 401) { try { final newAccessToken = await getNewAccessToken(); if (newAccessToken.isNotEmpty) { e.requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; // 重试请求时,根据原请求方法决定是否绑定GET专用token final retryCancelToken = e.requestOptions.method == 'GET' ? CancelToken() : null; final retryResponse = await dio.request( e.requestOptions.path, options: Options( method: e.requestOptions.method, headers: e.requestOptions.headers, ), data: e.requestOptions.data, queryParameters: e.requestOptions.queryParameters, cancelToken: retryCancelToken, ); return handler.resolve(retryResponse); } } catch (tokenError) { log('Token刷新失败: $tokenError'); final router = sl<AppRouter>(); router.push(const Login()); } } return handler.next(e); }, )); } Future<String> getNewAccessToken() async { try { final userCredentials = localStorage.read(USER_CREDENTIALS); final userMapData = jsonDecode(userCredentials); // 刷新token的POST请求不绑定GET专用token,避免被误取消 final response = await dio.post( '/auth/token/refresh', data: {'refreshToken': userMapData['token']['refreshToken']}, ); if (response.statusCode != 200) { localStorage.delete(USER_CREDENTIALS); final router = sl<AppRouter>(); router.push(const Login()); return ""; } else { final data = response.data as Map<String, dynamic>; userMapData['token']['accessToken'] = data['payload']['accessToken']; userMapData['token']['refreshToken'] = data['payload']['refreshToken']; await localStorage.write(USER_CREDENTIALS, jsonEncode(userMapData)); return data['payload']['accessToken']; } } catch (e) { log('获取新AccessToken失败: $e'); localStorage.delete(USER_CREDENTIALS); final router = sl<AppRouter>(); router.push(const Login()); return ""; } } @override Future<Response> get(String url) async { // 先取消之前未完成的GET请求 cancelActiveGetRequest(); // 创建新的token绑定当前GET请求 activeGetCancelToken = CancelToken(); try { final response = await dio.get(url, cancelToken: activeGetCancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } on DioException catch (e) { if (e.type != DioExceptionType.cancel) { // 非取消类型的错误才抛出 throw e; } rethrow; // 取消错误直接抛出,上层可以捕获后忽略 } } @override Future<Response> post(String url, {Object? body}) async { // POST请求使用独立的临时token,不共享GET的活跃token final cancelToken = CancelToken(); final response = await dio.post(url, data: body, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override Future<Response> delete(String url) async { final cancelToken = CancelToken(); final response = await dio.delete(url, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override Future<Response> patch(String url, {Object? body}) async { final cancelToken = CancelToken(); final response = await dio.patch(url, data: body, cancelToken: cancelToken); if (response.statusCode! >= 500) { throw ServerException(response.data); } return response; } @override void cancelActiveGetRequest() { if (activeGetCancelToken != null && !activeGetCancelToken!.isCancelled) { activeGetCancelToken!.cancel('用户切换标签,取消之前的GET请求'); log('已取消活跃的GET请求'); } } }
使用说明
在TabBar的切换回调中(比如onTap),调用httpRequests.cancelActiveGetRequest()即可取消之前未完成的GET请求,确保只有当前标签的请求会返回数据:
// 假设你已注入HttpRequests实例 final httpRequests = sl<HttpRequests>(); TabBar( onTap: (index) { // 切换标签时取消之前的GET请求 httpRequests.cancelActiveGetRequest(); // 然后发送当前标签的请求 if (index == 0) { fetchCars(); } else { fetchDrivers(); } }, tabs: const [ Tab(text: '车辆'), Tab(text: '司机'), ], )
内容的提问来源于stack exchange,提问作者Mustafa Majid
相关产品推荐
相关产品推荐

