异步防抖节流工具
Go to file
DESKTOP-I3JPKHK\wy 647baea3e7 文档补充提交 2025-12-12 17:15:34 +08:00
lib 补充提交 2025-12-12 17:11:42 +08:00
test 补充提交 2025-12-12 17:11:42 +08:00
.gitignore 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00
.metadata 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00
CHANGELOG.md 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00
LICENSE 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00
README.md 文档补充提交 2025-12-12 17:15:34 +08:00
TEST_REPORT.md 补充提交 2025-12-12 17:11:42 +08:00
analysis_options.yaml 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00
pubspec.yaml 提交异步防抖或者节流操作 2025-12-12 15:05:19 +08:00

README.md

yx_async_throttle_flutter

一个健壮的 Flutter 异步任务防抖Debounce与节流Throttle工具类。

它完美结合了 时间策略Throttle/Debounce异步任务锁Async Task Locking旨在解决复杂场景下如弱网环境或用户快速连点的重复执行问题。

功能特性

  • 双重保护机制:
    • 时间策略: 防止短时间内快速重复点击(基于 easy_debounce
    • 异步锁: 即使时间间隔已过,如果上一个异步任务(如网络请求)尚未完成,也会阻止新任务执行。
  • 节流模式 (Throttle - 默认): 立即执行,并在指定时间内忽略后续调用。非常适合“提交”、“登录”等按钮。
  • 防抖模式 (Debounce): 延迟执行,直到停止调用一段时间后才执行。非常适合“搜索框”、“实时输入校验”。
  • 内存安全: 在防抖模式下正确管理 Completer,防止因任务被取消导致的 await 永久挂起和内存泄漏。
  • 简单易用: 单例调用API 简洁。

快速开始 (Getting Started)

在你的 pubspec.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或者用户手速很快该函数也能确保在上一个请求完成时间间隔满足之前,不会重复执行。

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

PopupMenuItem(
  onTap: () {
    AsyncThrottle.instance.executeSafe(
      'withdraw_task',
      controller.withdrawTask,
      onError: (e, s) {
        // 这里做日志/Toast/上报,不影响 UI
      },
    );
  },
  child: ...,
)

2. 防抖模式 (搜索或输入)

适用于搜索框、实时输入联想等场景。只有当用户停止输入指定时间后,才会触发逻辑。

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. 检查执行状态

你可以随时检查某个任务是否正在执行中:

// 检查指定 tagId 是否有任务正在执行
bool isRunning = AsyncThrottle.instance.isExecuting('submit_order_btn');

if (isRunning) {
  print('订单正在提交中,请稍候...');
}

4. 清除锁 (可选)

在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。

AsyncThrottle.instance.clearAllLocks();

⚠️ 注意: clearAllLocks() 仅清除异步锁状态,不会影响 EasyThrottle/EasyDebounce 的内部时间窗口。


API 参考

execute 方法

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 方法

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 方法

bool isExecuting(String tagId)

检查指定 tagId 的任务是否正在执行中。

clearAllLocks 方法

void clearAllLocks()

强制清除所有异步锁和等待中的 Completer。


核心优势:为什么需要异步锁?

普通的 Throttle 库通常只根据 时间 来控制执行。

典型 Bug 场景: 设定节流时间为 500ms

  1. 用户点击“提交”。请求发出,但网络很慢,耗时 2000ms
  2. 600ms 时(此时节流时间已过),用户因焦急再次点击“提交”。
  3. 问题: 普通 Throttle 库会认为 500ms 已到,允许执行第二次请求,导致 重复下单

本库的解决方案: yx_async_throttle_flutter 会检查 isExecuting 状态。虽然时间已过但因为第一个请求2000ms还在运行第二次点击会被直接拦截从而完美防止重复提交。

💡 最佳实践: 如果在 onExecute 函数内部再添加 Loading 蒙层UI 阻断),配合本插件即可实现 “三层防护”UI 蒙层 + 时间策略 + 异步锁),彻底杜绝任何异常场景下的重复操作。


更多使用示例

表单多按钮场景

当页面有多个按钮时,使用不同的 tagId 确保它们互不影响:

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 状态管理

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;
        }
      },
    );
  }
}

列表项点击防重

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);
          },
        );
      },
    );
  },
)

实时输入验证(防抖)

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

    // ✅ 好
    'submit_order_page_main'
    'search_product_list'
    
    // ❌ 不好
    'btn1'
    'click'
    
  2. 在 onTap/onPressed 中使用 executeSafe

    // ✅ 推荐:不会影响 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 中调用:

    @override
    void dispose() {
      // 可选:清除该页面相关的锁
      // 注意:这会清除所有锁,请谨慎使用
      // AsyncThrottle.instance.clearAllLocks();
      super.dispose();
    }
    
  4. 异常处理execute 会传播异常,executeSafe 会吞掉异常并调用 onError


与其他方案的对比

特性 yx_async_throttle 普通 Throttle 手动锁变量
时间策略
异步锁
弱网保护
内存安全 ⚠️ 需自行处理
防抖模式
代码简洁

测试覆盖

本库已通过 75 项 全面的单元测试,覆盖:

  • 单例模式
  • 节流/防抖核心功能
  • 异常处理与传播
  • 并发场景100+ 次快速调用)
  • 内存安全(无泄漏)
  • 重入防护(无死锁)
  • 边界条件(极端参数)
  • 真实业务场景模拟

详细测试报告请参阅 TEST_REPORT.md


常见问题 (FAQ)

Q: 为什么我的第二次点击没有执行?

A: 这正是本库的设计目的。检查以下两点:

  1. 是否在 duration 时间窗口内?
  2. 上一次任务是否还在执行中?(使用 isExecuting 检查)

Q: 如何让同一按钮在不同页面独立工作?

A: 使用包含页面标识的 tagId

'submit_order_${widget.pageId}'

Q: executeSafe 和 execute 有什么区别?

A:

  • execute: 异常会向外抛出,适合可以 await 的场景
  • executeSafe: 异常被内部捕获,通过 onError 回调处理,适合 onTap 等无法 await 的场景

Q: clearAllLocks 后为什么还是不能立即执行?

A: clearAllLocks 只清除异步锁EasyThrottle 的时间窗口仍然有效。需要等待时间窗口过期,或使用不同的 tagId。


其他信息

本库底层的时间策略依赖于 easy_debounce

License

MIT License