# yx_async_throttle_flutter 一个健壮的 Flutter 异步任务防抖(Debounce)与节流(Throttle)工具类。 它完美结合了 **时间策略**(Throttle/Debounce)与 **异步任务锁**(Async Task Locking),旨在解决复杂场景下(如弱网环境或用户快速连点)的重复执行问题。 ## 功能特性 - **双重保护机制**: - **时间策略**: 防止短时间内快速重复点击(基于 easy_debounce)。 - **异步锁**: 即使时间间隔已过,如果上一个异步任务(如网络请求)尚未完成,也会阻止新任务执行。 - **节流模式 (Throttle - 默认)**: 立即执行,并在指定时间内忽略后续调用。非常适合“提交”、“登录”等按钮。 - **防抖模式 (Debounce)**: 延迟执行,直到停止调用一段时间后才执行。非常适合“搜索框”、“实时输入校验”。 - **内存安全**: 在防抖模式下正确管理 `Completer`,防止因任务被取消导致的 `await` 永久挂起和内存泄漏。 - **简单易用**: 单例调用,API 简洁。 ## 快速开始 (Getting Started) 在你的 `pubspec.yaml` 文件中添加依赖: ```yaml dependencies: yx_async_throttle_flutter: git: url: https://gitea.23544.com/wangyang/yx_async_throttle_flutter.git ref: 1.0.0 ``` ## 使用指南 (Usage) ### 1. 基础节流 (按钮防暴力点击) 适用于“提交订单”、“登录”、“保存”等场景。 即使网络很慢(请求耗时 > 500ms),或者用户手速很快,该函数也能确保在上一个请求完成**且**时间间隔满足之前,不会重复执行。 ```dart import 'package:yx_async_throttle_flutter/async_throttle.dart'; void onSubmit() async { // 使用唯一的 tagId 标识这个动作 await AsyncThrottle.instance.execute( 'submit_order_btn', () async { // 你的异步逻辑(例如 API 调用) print('正在提交订单...'); await Future.delayed(Duration(seconds: 2)); // 模拟耗时网络请求 print('订单提交完成!'); }, duration: Duration(milliseconds: 500), // 默认 300ms enableDebounce: false, // 默认 false (节流模式) ); } ``` ### 1.1 UI 回调最佳实践(不影响 UI / 不抛到全局) 像 `onTap` / `onPressed` / `PopupMenuItem.onTap` 这类回调通常**不能 await**(即使写成 async 也经常没人等待它)。 为了避免 `onExecute` 内部异常变成 **unhandled async error** 影响 UI,建议用 `executeSafe`: ```dart PopupMenuItem( onTap: () { AsyncThrottle.instance.executeSafe( 'withdraw_task', controller.withdrawTask, onError: (e, s) { // 这里做日志/Toast/上报,不影响 UI }, ); }, child: ..., ) ``` ### 2. 防抖模式 (搜索或输入) 适用于搜索框、实时输入联想等场景。只有当用户停止输入指定时间后,才会触发逻辑。 ```dart import 'package:yx_async_throttle_flutter/async_throttle.dart'; void onSearchChanged(String query) async { await AsyncThrottle.instance.execute( 'search_input', () async { // 只有当用户停止输入 500ms 后才会执行 print('搜索内容: $query'); await fetchSearchResults(query); }, duration: Duration(milliseconds: 500), enableDebounce: true, // 启用防抖模式 ); } ``` ### 3. 检查执行状态 你可以随时检查某个任务是否正在执行中: ```dart // 检查指定 tagId 是否有任务正在执行 bool isRunning = AsyncThrottle.instance.isExecuting('submit_order_btn'); if (isRunning) { print('订单正在提交中,请稍候...'); } ``` ### 4. 清除锁 (可选) 在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。 ```dart AsyncThrottle.instance.clearAllLocks(); ``` > ⚠️ **注意**: `clearAllLocks()` 仅清除异步锁状态,不会影响 EasyThrottle/EasyDebounce 的内部时间窗口。 --- ## API 参考 ### `execute` 方法 ```dart Future execute( String tagId, Future Function() onExecute, { Duration duration = const Duration(milliseconds: 300), bool enableDebounce = false, }) ``` | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `tagId` | `String` | 必填 | 任务的唯一标识符 | | `onExecute` | `Future Function()` | 必填 | 要执行的异步函数 | | `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 | | `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 | ### `executeSafe` 方法 ```dart Future executeSafe( String tagId, Future Function() onExecute, { Duration duration = const Duration(milliseconds: 300), bool enableDebounce = false, void Function(Object error, StackTrace stackTrace)? onError, }) ``` | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `tagId` | `String` | 必填 | 任务的唯一标识符 | | `onExecute` | `Future Function()` | 必填 | 要执行的异步函数 | | `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 | | `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 | | `onError` | `Function?` | `null` | 错误回调,用于日志/上报 | ### `isExecuting` 方法 ```dart bool isExecuting(String tagId) ``` 检查指定 `tagId` 的任务是否正在执行中。 ### `clearAllLocks` 方法 ```dart void clearAllLocks() ``` 强制清除所有异步锁和等待中的 Completer。 --- ## 核心优势:为什么需要异步锁? 普通的 `Throttle` 库通常只根据 **时间** 来控制执行。 **典型 Bug 场景:** 设定节流时间为 `500ms`。 1. 用户点击“提交”。请求发出,但网络很慢,耗时 `2000ms`。 2. 在 `600ms` 时(此时节流时间已过),用户因焦急再次点击“提交”。 3. **问题**: 普通 Throttle 库会认为 500ms 已到,允许执行第二次请求,导致 **重复下单**。 **本库的解决方案:** `yx_async_throttle_flutter` 会检查 `isExecuting` 状态。虽然时间已过,但因为第一个请求(2000ms)还在运行,第二次点击会被直接拦截,从而完美防止重复提交。 > **💡 最佳实践**: 如果在 `onExecute` 函数内部再添加 **Loading 蒙层**(UI 阻断),配合本插件即可实现 **“三层防护”**(UI 蒙层 + 时间策略 + 异步锁),彻底杜绝任何异常场景下的重复操作。 --- ## 更多使用示例 ### 表单多按钮场景 当页面有多个按钮时,使用不同的 `tagId` 确保它们互不影响: ```dart class OrderPage extends StatelessWidget { void onSave() { AsyncThrottle.instance.executeSafe('order_save', () async { await saveOrder(); }); } void onSubmit() { AsyncThrottle.instance.executeSafe('order_submit', () async { await submitOrder(); }); } void onCancel() { AsyncThrottle.instance.executeSafe('order_cancel', () async { await cancelOrder(); }); } } ``` ### 配合 GetX/Provider 状态管理 ```dart class OrderController extends GetxController { final isLoading = false.obs; Future submitOrder() async { await AsyncThrottle.instance.execute( 'submit_order', () async { isLoading.value = true; try { await api.submitOrder(); Get.snackbar('成功', '订单提交成功'); } finally { isLoading.value = false; } }, ); } } ``` ### 列表项点击防重 ```dart ListView.builder( itemBuilder: (context, index) { final item = items[index]; return ListTile( onTap: () { // 使用 item.id 作为唯一标识,确保不同列表项互不影响 AsyncThrottle.instance.executeSafe( 'list_item_${item.id}', () async { await navigateToDetail(item); }, ); }, ); }, ) ``` ### 实时输入验证(防抖) ```dart TextField( onChanged: (value) { AsyncThrottle.instance.execute( 'username_validation', () async { final isAvailable = await api.checkUsername(value); setState(() { usernameError = isAvailable ? null : '用户名已被占用'; }); }, enableDebounce: true, duration: const Duration(milliseconds: 500), ); }, ) ``` --- ## 注意事项与最佳实践 ### ✅ 推荐做法 1. **使用有意义的 tagId** ```dart // ✅ 好 'submit_order_page_main' 'search_product_list' // ❌ 不好 'btn1' 'click' ``` 2. **在 onTap/onPressed 中使用 executeSafe** ```dart // ✅ 推荐:不会影响 UI onTap: () { AsyncThrottle.instance.executeSafe('action', doSomething); } // ⚠️ 需要注意:异常可能变成 unhandled async error onTap: () async { await AsyncThrottle.instance.execute('action', doSomething); } ``` 3. **合理设置 duration** - 按钮点击:`300ms ~ 500ms` - 搜索防抖:`300ms ~ 800ms` - 实时验证:`500ms ~ 1000ms` ### ⚠️ 注意事项 1. **tagId 全局唯一**:不同功能使用不同的 tagId,避免意外阻塞 2. **单例模式**:`AsyncThrottle.instance` 是全局单例,状态跨页面共享 3. **页面销毁时的处理**:如果需要在页面销毁时取消未完成的任务,可以在 `dispose` 中调用: ```dart @override void dispose() { // 可选:清除该页面相关的锁 // 注意:这会清除所有锁,请谨慎使用 // AsyncThrottle.instance.clearAllLocks(); super.dispose(); } ``` 4. **异常处理**:`execute` 会传播异常,`executeSafe` 会吞掉异常并调用 `onError` --- ## 与其他方案的对比 | 特性 | yx_async_throttle | 普通 Throttle | 手动锁变量 | |------|-------------------|---------------|-----------| | 时间策略 | ✅ | ✅ | ❌ | | 异步锁 | ✅ | ❌ | ✅ | | 弱网保护 | ✅ | ❌ | ✅ | | 内存安全 | ✅ | ✅ | ⚠️ 需自行处理 | | 防抖模式 | ✅ | ✅ | ❌ | | 代码简洁 | ✅ | ✅ | ❌ | --- ## 测试覆盖 本库已通过 **75 项** 全面的单元测试,覆盖: - ✅ 单例模式 - ✅ 节流/防抖核心功能 - ✅ 异常处理与传播 - ✅ 并发场景(100+ 次快速调用) - ✅ 内存安全(无泄漏) - ✅ 重入防护(无死锁) - ✅ 边界条件(极端参数) - ✅ 真实业务场景模拟 详细测试报告请参阅 [TEST_REPORT.md](./TEST_REPORT.md)。 --- ## 常见问题 (FAQ) ### Q: 为什么我的第二次点击没有执行? A: 这正是本库的设计目的。检查以下两点: 1. 是否在 `duration` 时间窗口内? 2. 上一次任务是否还在执行中?(使用 `isExecuting` 检查) ### Q: 如何让同一按钮在不同页面独立工作? A: 使用包含页面标识的 tagId: ```dart 'submit_order_${widget.pageId}' ``` ### Q: executeSafe 和 execute 有什么区别? A: - `execute`: 异常会向外抛出,适合可以 await 的场景 - `executeSafe`: 异常被内部捕获,通过 onError 回调处理,适合 onTap 等无法 await 的场景 ### Q: clearAllLocks 后为什么还是不能立即执行? A: `clearAllLocks` 只清除异步锁,EasyThrottle 的时间窗口仍然有效。需要等待时间窗口过期,或使用不同的 tagId。 --- ## 其他信息 本库底层的时间策略依赖于 [easy_debounce](https://pub.dev/packages/easy_debounce)。 ## License MIT License