feat: 添加 CancelToken 请求取消支持 (v3.3.0)

新增功能:
- 可通过 cancel_token_support 配置启用
- 自动为所有 API 方法添加 @CancelRequest() CancelToken 参数
- 完全向后兼容,默认禁用

修改文件:
- lib/core/config_repository.dart: 添加 cancelTokenSupport getter
- lib/pipeline/generate/impl/retrofit_api/api_parameters.dart: 参数生成逻辑
- generator_config.template.yaml: 配置项说明

新增文档:
- CANCELTOKEN_USAGE_GUIDE.md: 使用指南
- CHANGELOG_CANCELTOKEN.md: 功能变更详情

测试:
- test/cancel_token_support_test.dart: 10 个单元测试
This commit is contained in:
Max 2026-01-31 00:25:29 +08:00
parent 264ec39ca9
commit 52a242f392
8 changed files with 515 additions and 1 deletions

132
CANCELTOKEN_USAGE_GUIDE.md Normal file
View File

@ -0,0 +1,132 @@
# CancelToken 使用指南 / CancelToken Usage Guide
## 概述 / Overview
CancelToken 是 Dio 提供的请求取消机制。启用此功能后,生成的 API 方法将自动包含可选的 `CancelToken?` 参数,允许您在请求进行中取消它。
CancelToken is a request cancellation mechanism provided by Dio. When enabled, generated API methods will automatically include an optional `CancelToken?` parameter, allowing you to cancel requests while they are in progress.
---
## 配置 / Configuration
`generator_config.yaml` 中启用:
```yaml
generation:
api:
cancel_token_support: true # 启用 CancelToken 支持
```
---
## 生成效果 / Generated Code
**启用前 / Before:**
```dart
Future<BaseResult<User>> getUser({
@Query('id') int? id,
});
```
**启用后 / After:**
```dart
Future<BaseResult<User>> getUser({
@Query('id') int? id,
@CancelRequest() CancelToken? cancelToken,
});
```
---
## 使用示例 / Usage Examples
### 基本用法 / Basic Usage
```dart
final cancelToken = CancelToken();
// 发起请求 / Make request
final future = api.getUser(id: 1, cancelToken: cancelToken);
// 取消请求 / Cancel the request
cancelToken.cancel('用户取消操作');
try {
final result = await future;
} on DioException catch (e) {
if (CancelToken.isCancel(e)) {
print('请求已取消: ${e.message}');
}
}
```
### 页面离开时取消 / Cancel on Page Dispose
```dart
class MyPageState extends State<MyPage> {
final _cancelToken = CancelToken();
@override
void dispose() {
_cancelToken.cancel('页面已关闭');
super.dispose();
}
Future<void> loadData() async {
try {
final result = await api.getData(cancelToken: _cancelToken);
// 处理结果
} on DioException catch (e) {
if (!CancelToken.isCancel(e)) {
// 处理真正的错误
}
}
}
}
```
### GetX Controller 使用 / With GetX Controller
```dart
class MyController extends GetxController {
final _cancelToken = CancelToken();
@override
void onClose() {
_cancelToken.cancel('Controller 已关闭');
super.onClose();
}
Future<void> fetchData() async {
try {
final result = await api.getData(cancelToken: _cancelToken);
// 更新状态
} on DioException catch (e) {
if (!CancelToken.isCancel(e)) {
// 显示错误
}
}
}
}
```
---
## 最佳实践 / Best Practices
1. **每个请求使用独立 Token** - 如果需要单独取消某个请求
2. **页面级别共享 Token** - 页面离开时取消所有进行中的请求
3. **检查取消状态** - 使用 `CancelToken.isCancel(e)` 区分取消和真正的错误
4. **及时取消** - 避免不必要的网络请求和资源浪费
---
## 注意事项 / Notes
- CancelToken 来自 `package:dio/dio.dart`,无需额外导入
- `@CancelRequest()` 注解来自 `package:retrofit/retrofit.dart`
- 取消已完成的请求不会有任何效果
- 一个 CancelToken 可以用于多个请求,调用 `cancel()` 会取消所有使用它的请求

View File

@ -2,6 +2,44 @@
All notable changes to this project will be documented in this file.
## [3.3.0] - 2026-01-31
### 🎉 新特性
#### CancelToken 请求取消支持
- ✅ **可选配置**:在 `generator_config.yaml` 中通过 `generation.api.cancel_token_support: true` 启用
- ✅ **自动参数注入**:为每个生成的 API 方法自动添加 `@CancelRequest() CancelToken? cancelToken` 参数
- ✅ **完全可选**CancelToken 参数可选,不影响现有代码
- ✅ **Retrofit 兼容**:生成的代码与 retrofit_generator 完全兼容
### 📝 文档更新
- 新增 [**CancelToken 使用指南**](./CANCELTOKEN_USAGE_GUIDE.md) - 详细配置说明和使用示例
- 新增 [**CancelToken 变更日志**](./CHANGELOG_CANCELTOKEN.md) - 功能实现细节
### 🔧 技术细节
- 修改 `lib/core/config_repository.dart`:添加 `cancelTokenSupport` getter
- 修改 `lib/pipeline/generate/impl/retrofit_api/api_parameters.dart`:在 `_generateParameters()` 中添加 CancelToken 参数
- 更新 `generator_config.template.yaml`:添加 `cancel_token_support` 配置项说明
- 新增 `test/cancel_token_support_test.dart`10 个单元测试验证功能正确性
### 💡 使用示例
```yaml
# generator_config.yaml
generation:
api:
cancel_token_support: true
```
生成的代码:
```dart
Future<Result> getUsers({
@Query('page') int? page,
@CancelRequest() CancelToken? cancelToken,
});
```
---
## [3.2.1] - 2026-01-12
### 🐛 修复

41
CHANGELOG_CANCELTOKEN.md Normal file
View File

@ -0,0 +1,41 @@
# CancelToken 功能变更日志 / CancelToken Feature Changelog
## [3.3.0] - 2026-01-31
### 🎉 新特性 / New Features
#### CancelToken 支持
- ✅ **配置开关**:在 `generator_config.yaml` 中配置 `generation.api.cancel_token_support: true` 启用
- ✅ **自动添加参数**:启用后,所有 API 方法自动添加 `@CancelRequest() CancelToken? cancelToken` 参数
- ✅ **向后兼容**:默认关闭,不影响现有项目
### 📝 文档 / Documentation
- ✅ 新增 `CANCELTOKEN_USAGE_GUIDE.md` - 使用指南和最佳实践
- ✅ 更新 `generator_config.template.yaml` - 添加配置示例
### 🔧 技术细节 / Technical Details
**修改的文件:**
- `lib/core/config_repository.dart` - 添加 `cancelTokenSupport` 配置解析
- `lib/pipeline/generate/impl/retrofit_api/api_parameters.dart` - 在 `_generateParameters()` 中添加 CancelToken 参数生成逻辑
- `generator_config.template.yaml` - 添加配置项说明
**生成的参数格式:**
```dart
@CancelRequest() CancelToken? cancelToken
```
### 💡 使用场景 / Use Cases
1. **页面离开取消请求** - 避免内存泄漏和不必要的状态更新
2. **用户主动取消** - 如搜索框输入时取消之前的搜索请求
3. **超时控制** - 配合 Timer 实现自定义超时逻辑
4. **批量取消** - 一个 CancelToken 可用于多个请求
---
## 相关资源 / Related Resources
- [Dio CancelToken 文档](https://pub.dev/packages/dio#cancellation)
- [Retrofit CancelRequest 注解](https://pub.dev/packages/retrofit)
- [使用指南](./CANCELTOKEN_USAGE_GUIDE.md)

View File

@ -115,6 +115,15 @@ generation:
# 方法命名
method_naming: "camelCase" # camelCase, snake_case
# CancelToken 支持(可选)
# 启用后,为每个 API 方法添加可选的 CancelToken 参数,用于取消请求
# 生成的方法签名示例:
# Future<Result> getUser({
# @Query('id') int? id,
# @CancelRequest() CancelToken? cancelToken, // <- 自动添加
# });
cancel_token_support: false # 默认: false
# 数据模型配置
models:
enabled: true

View File

@ -432,6 +432,14 @@ class ConfigRepository {
return output?['split_by_tags'] as bool? ?? true;
}
/// CancelToken
/// API CancelToken
bool get cancelTokenSupport {
final generation = _config['generation'] as Map<String, dynamic>?;
final api = generation?['api'] as Map<String, dynamic>?;
return api?['cancel_token_support'] as bool? ?? false;
}
///
List<String> get packageImports {
final imports = _config['imports'] as Map<String, dynamic>?;

View File

@ -96,6 +96,20 @@ mixin RetrofitApiParameters {
);
}
// CancelToken
final config = ConfigRepository.loadSync();
if (config.cancelTokenSupport) {
parameters.add(
ApiMethodParameter(
name: 'cancelToken',
type: 'CancelToken?',
annotation: _g.useRetrofit ? '@CancelRequest()' : '',
required: false,
description: 'Cancel token for request cancellation',
),
);
}
return parameters;
}

View File

@ -1,7 +1,7 @@
name: swagger_generator_flutter
description: A powerful Swagger/OpenAPI code generator for Flutter projects with Dio + Retrofit support
version: 3.2.1
version: 3.3.0
environment:
sdk: '>=3.0.0 <4.0.0'

View File

@ -0,0 +1,272 @@
import 'package:swagger_generator_flutter/core/config_repository.dart';
import 'package:swagger_generator_flutter/core/models.dart';
import 'package:swagger_generator_flutter/pipeline/generate/apis.dart';
import 'package:test/test.dart';
void main() {
group('CancelToken Support Tests', () {
late SwaggerDocument simpleDocument;
setUp(() {
// 使
ConfigRepository.setCachedConfig(ConfigRepository({}));
simpleDocument = SwaggerDocument(
title: 'Test API',
version: '1.0.0',
description: 'A test API for request cancellation',
servers: [
const ApiServer(
url: 'https://api.example.com',
description: 'Test server',
),
],
paths: {
const ApiPathKey('/users', HttpMethod.get): const ApiPath(
path: '/users',
method: HttpMethod.get,
summary: 'Get users',
description: 'Get all users',
operationId: 'getUsers',
tags: ['users'],
parameters: [
ApiParameter(
name: 'page',
location: ParameterLocation.query,
required: false,
type: PropertyType.integer,
description: 'Page number',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'Success',
),
},
),
const ApiPathKey('/users/{id}', HttpMethod.get): const ApiPath(
path: '/users/{id}',
method: HttpMethod.get,
summary: 'Get user by ID',
description: 'Get a specific user',
operationId: 'getUserById',
tags: ['users'],
parameters: [
ApiParameter(
name: 'id',
location: ParameterLocation.path,
required: true,
type: PropertyType.integer,
description: 'User ID',
),
],
responses: {
'200': ApiResponse(
code: '200',
description: 'User found',
),
},
),
const ApiPathKey('/users', HttpMethod.post): const ApiPath(
path: '/users',
method: HttpMethod.post,
summary: 'Create user',
description: 'Create a new user',
operationId: 'createUser',
tags: ['users'],
parameters: [],
requestBody: ApiRequestBody(
description: 'User data',
required: true,
content: {
'application/json': ApiMediaType(
schema: {'type': 'object'},
),
},
),
responses: {
'201': ApiResponse(
code: '201',
description: 'User created',
),
},
),
},
models: {},
controllers: {},
);
});
tearDown(() {
//
ConfigRepository.setCachedConfig(ConfigRepository({}));
});
group('CancelToken 参数生成测试', () {
test('当 cancelTokenSupport 启用时,生成 CancelToken 参数', () {
// CancelToken
ConfigRepository.setCachedConfig(
ConfigRepository({
'generation': {
'api': {
'cancel_token_support': true,
},
},
}),
);
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
// CancelToken 使
expect(result, contains('CancelToken? cancelToken'));
expect(result, contains('@CancelRequest()'));
});
test('当 cancelTokenSupport 禁用时,不生成 CancelToken 参数', () {
// CancelToken
ConfigRepository.setCachedConfig(
ConfigRepository({
'generation': {
'api': {
'cancel_token_support': false,
},
},
}),
);
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
// CancelToken 使
expect(result, isNot(contains('CancelToken? cancelToken')));
expect(result, isNot(contains('@CancelRequest()')));
});
test('默认情况下不生成 CancelToken 参数', () {
// 使
ConfigRepository.setCachedConfig(ConfigRepository({}));
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
// CancelToken
expect(result, isNot(contains('CancelToken? cancelToken')));
expect(result, isNot(contains('@CancelRequest()')));
});
test('CancelToken 参数应该是可选的', () {
ConfigRepository.setCachedConfig(
ConfigRepository({
'generation': {
'api': {
'cancel_token_support': true,
},
},
}),
);
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
// CancelToken 使 ?
expect(result, contains('CancelToken?'));
//
expect(result, isNot(contains('required CancelToken')));
});
test('所有 API 方法都应包含 CancelToken 参数', () {
ConfigRepository.setCachedConfig(
ConfigRepository({
'generation': {
'api': {
'cancel_token_support': true,
},
},
}),
);
final generator = RetrofitApiGenerator(
className: 'TestApi',
splitByTags: false,
);
final result = generator.generateFromDocument(simpleDocument);
// CancelToken API
final cancelTokenCount =
'CancelToken? cancelToken'.allMatches(result).length;
// 3 API getUsers, getUserById, createUser
expect(cancelTokenCount, equals(3));
});
});
group('ConfigRepository CancelToken 配置测试', () {
test('cancelTokenSupport getter 正确解析 true', () {
final config = ConfigRepository({
'generation': {
'api': {
'cancel_token_support': true,
},
},
});
expect(config.cancelTokenSupport, isTrue);
});
test('cancelTokenSupport getter 正确解析 false', () {
final config = ConfigRepository({
'generation': {
'api': {
'cancel_token_support': false,
},
},
});
expect(config.cancelTokenSupport, isFalse);
});
test('cancelTokenSupport getter 默认为 false', () {
final config = ConfigRepository({});
expect(config.cancelTokenSupport, isFalse);
});
test('cancelTokenSupport getter 处理部分配置', () {
final config = ConfigRepository(<String, dynamic>{
'generation': <String, dynamic>{
'api': <String, dynamic>{},
},
});
expect(config.cancelTokenSupport, isFalse);
});
test('cancelTokenSupport getter 处理缺少 api 配置', () {
final config = ConfigRepository(<String, dynamic>{
'generation': <String, dynamic>{},
});
expect(config.cancelTokenSupport, isFalse);
});
});
});
}