yx_async_throttle_flutter/README.md

408 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<void> execute(
String tagId,
Future<void> Function() onExecute, {
Duration duration = const Duration(milliseconds: 300),
bool enableDebounce = false,
})
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
| `onExecute` | `Future<void> Function()` | 必填 | 要执行的异步函数 |
| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 |
| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 |
### `executeSafe` 方法
```dart
Future<void> executeSafe(
String tagId,
Future<void> Function() onExecute, {
Duration duration = const Duration(milliseconds: 300),
bool enableDebounce = false,
void Function(Object error, StackTrace stackTrace)? onError,
})
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
| `onExecute` | `Future<void> 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<void> 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