You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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请求。

修改要点:

  1. 将全局cancelToken拆分为activeGetCancelToken(仅用于GET请求)和独立的临时token用于其他请求
  2. 在get方法中,先取消之前的活跃GET请求,再创建新token绑定当前请求
  3. 完善取消方法,供Tab切换时主动调用以取消当前GET请求
  4. 避免将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 10:59:50