diff --git a/CANCELTOKEN_USAGE_GUIDE.md b/CANCELTOKEN_USAGE_GUIDE.md new file mode 100644 index 0000000..05e2d96 --- /dev/null +++ b/CANCELTOKEN_USAGE_GUIDE.md @@ -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> getUser({ + @Query('id') int? id, +}); +``` + +**启用后 / After:** + +```dart +Future> 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 { + final _cancelToken = CancelToken(); + + @override + void dispose() { + _cancelToken.cancel('页面已关闭'); + super.dispose(); + } + + Future 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 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()` 会取消所有使用它的请求 diff --git a/CHANGELOG.md b/CHANGELOG.md index b80e7a0..7120885 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 getUsers({ + @Query('page') int? page, + @CancelRequest() CancelToken? cancelToken, +}); +``` + +--- + ## [3.2.1] - 2026-01-12 ### 🐛 修复 diff --git a/CHANGELOG_CANCELTOKEN.md b/CHANGELOG_CANCELTOKEN.md new file mode 100644 index 0000000..c247b7e --- /dev/null +++ b/CHANGELOG_CANCELTOKEN.md @@ -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) diff --git a/generator_config.template.yaml b/generator_config.template.yaml index 0d15a34..27ebd93 100644 --- a/generator_config.template.yaml +++ b/generator_config.template.yaml @@ -115,6 +115,15 @@ generation: # 方法命名 method_naming: "camelCase" # camelCase, snake_case + # CancelToken 支持(可选) + # 启用后,为每个 API 方法添加可选的 CancelToken 参数,用于取消请求 + # 生成的方法签名示例: + # Future getUser({ + # @Query('id') int? id, + # @CancelRequest() CancelToken? cancelToken, // <- 自动添加 + # }); + cancel_token_support: false # 默认: false + # 数据模型配置 models: enabled: true diff --git a/lib/core/config_repository.dart b/lib/core/config_repository.dart index 8645041..2600a17 100644 --- a/lib/core/config_repository.dart +++ b/lib/core/config_repository.dart @@ -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?; + final api = generation?['api'] as Map?; + return api?['cancel_token_support'] as bool? ?? false; + } + /// 获取额外的包导入列表 List get packageImports { final imports = _config['imports'] as Map?; diff --git a/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart index 89fb2f5..d6f541f 100644 --- a/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart +++ b/lib/pipeline/generate/impl/retrofit_api/api_parameters.dart @@ -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; } diff --git a/pubspec.yaml b/pubspec.yaml index 60cac1e..94d46f5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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' diff --git a/test/cancel_token_support_test.dart b/test/cancel_token_support_test.dart new file mode 100644 index 0000000..f33244b --- /dev/null +++ b/test/cancel_token_support_test.dart @@ -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({ + 'generation': { + 'api': {}, + }, + }); + + expect(config.cancelTokenSupport, isFalse); + }); + + test('cancelTokenSupport getter 处理缺少 api 配置', () { + final config = ConfigRepository({ + 'generation': {}, + }); + + expect(config.cancelTokenSupport, isFalse); + }); + }); + }); +}