likes
comments
collection
share

Flutter 进阶:用 Function.apply 优雅的实现函数防抖 debounce

作者站长头像
站长
· 阅读数 2

一、需求来源

项目中遇到问题,当在极短的时间内多次点击某个按钮时,会造成一些异常情况: 多次请求、多次进入等问题。常见的处理方法:

  • 加 loading,缺点是麻烦且动不动小菊花体验不好;
  • 用 防抖函数将原方法包一层,在防抖回调中调用原方法,缺点是代码层级加深;
  • 可能得第三种

以上常用方法虽然可以解决问题,但是代码一点不优雅,然后就开始寻找(苦苦思索)最优雅的实现方式。突然本周灵光一闪,它就自己出来了,记录一下分享给大家。

二、使用示例

1、 VoidCallback 类型函数示例:

只需要在定义的 VoidCallback 函数后调用 debounce,对历史代码零入侵;

OutlinedButton(
  onPressed: testVoidCallback.debounce,
  child: NText("testVoidCallback.debounce")
),

...

void testVoidCallback() {
  final index = IntExt.random(max: 1000);///生产随机数
  ddlog("testVoidCallback index: $index");
}

1.1、实现一个授权判断扩展方法:

现在app都需要游客模式,登录和没登录执行不同的方法;

OutlinedButton(
    onPressed: testVoidCallback.auth(
      onAuth: (){
        return false;
      },
      onUnauth: (){
        ddlog("OutlinedButton - onUnauth");
      }
    ),
    child: NText("testVoidCallback.auth")
),

2、 ValueChanged 函数类型函数示例:

因为目前dart 中没有提供函数的参数获取,只能使用箭头函数将原有的 val 暴露出来(dart api 限制),比 VoidCallback 优雅程度差一点;

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: NSearchTextField(
    backgroundColor: Colors.black12,
    onChanged: (val) => onChanged.debounce(value: val),
  ),
)

...

void onChanged(val){
  final index = IntExt.random(max: 1000);
  ddlog("onChanged: $val, index: $index");
}

三、源码

1、debounce 实现;

/// 搜索框防抖
class Debounce {
  Debounce({
    this.delay = const Duration(milliseconds: 500),
  });

  Duration delay;

  Timer? _timer;

  call(VoidCallback callback) {
    _timer?.cancel();
    _timer = Timer(delay, callback);
  }

  bool get isRunning => _timer?.isActive ?? false;

  void cancel() => _timer?.cancel();
}

2、函数扩展


final _debounce = Debounce();

extension FunctionExt on Function{
  /// 同 Function.apply
  static apply(
      Function function,
      List<dynamic>? positionalArguments,
      [Map<String, dynamic>? namedArguments]
  ) {
    final arguments = namedArguments?.map((key, value) => MapEntry(Symbol(key), value));
    return Function.apply(function, positionalArguments, arguments);
  }

  ///效果同 Function.apply
  applyNew({
    List<dynamic>? positionalArguments,
    Map<String, dynamic>? namedArguments,
  }) {
    final arguments = namedArguments?.map((key, value) => MapEntry(Symbol(key), value));
    return Function.apply(this, positionalArguments, arguments);
  }

  /// 防抖
  debounce({
    Duration duration = const Duration(milliseconds: 500),
    List<dynamic>? positionalArguments,
    Map<String, dynamic>? namedArguments,
  }){
    _debounce.delay = duration;
    return _debounce(() {
      applyNew(positionalArguments: positionalArguments, namedArguments: namedArguments);
    });
  }
}


extension VoidCallbackExt on VoidCallback {

  /// 延迟执行
  Future delayed({
    Duration duration = const Duration(milliseconds: 500),
  }) => Future.delayed(duration, this);

  /// 防抖
  void debounce({
    Duration duration = const Duration(milliseconds: 500),
  }){
    _debounce.delay = duration;
    _debounce(() => this());
  }

  /// 认证
  ///
  /// onAuth 返回认证状态
  /// onUnauth 未认证回调
  auth({
    required bool Function() onAuth,
    VoidCallback? onUnauth,
  }) {
    final hadAuth = onAuth();
    if (!hadAuth) {
      return (){
        onUnauth?.call();
      };
    }
    return this;
  }
}


extension ValueChangedExt<T> on ValueChanged<T> {

  /// 防抖
  debounce({
    required T value,
    Duration duration = const Duration(milliseconds: 500),
  }){
    _debounce.delay = duration;
    _debounce(() => this.call(value));
  }
}

四、总结

1、Dart 中函数也是对象,不同的函数的类型也是不同的,不同的函数类型实现不同的功能,大家要对函数有更深层次的理解。不能简单的认为函数就是函数就完了。
  • 什么场景使用回调函数而不是其他?
  • 什么场景使用内部函数而不是其他?
  • 什么场景使用函数指针而不是其他?
  • 还有哪些能提高我们开发效率的函数理解吗?

无论任何编程语言,你对函数理解越深,你的冗余代码就越少。

2、Function.apply 是 flutter 中函数动态调用方法,核心是提前声明一个函数,然后通过 Function.apply 可以传递位置参数(数组)和名称参数(字典)
external static apply(Function function, List<dynamic>? positionalArguments,
    [Map<Symbol, dynamic>? namedArguments]);

参数介绍:

  • function 就是提前声明好的函数;
  • positionalArguments 就是位置参数,一般表现形式就是函数声明时没有用大括号括起来的参数;
  • namedArguments 就是名称参数,一般表现形式就是函数声明时用大括号括起来的参数;

官方示例:

void printWineDetails(int vintage, {String? country, String? name}) {
  print('Name: $name, Country: $country, Vintage: $vintage');
}

 void main() {
   Function.apply(
       printWineDetails, [2018], {#country: 'USA', #name: 'Dominus Estate'});
 }

注意点:

namedArguments 参数的key必须是 Symbol 类型;

final isEqual = Symbol("city") == #city;
ddlog("testVoidCallback isEqual: $isEqual");
// [log] 2024-04-21 12:33:04.265310 testVoidCallback isEqual: true
3、Function.apply 缺点是无法字符串(方法名)转方法,还达不到映射概念级别,只在动态调用函数级别,希望以后能支持。
4、其他

有些同学可能觉得 Function.apply 即麻烦还可能参数出错,但是高手从不怕自己的刀割破自己的手。当你脑中能装的复杂度越来越高时,其实也代表你处理问题的复杂度越来越高,能力越来越强。 Function.apply 有助于提升我们对于函数能力边界的理解。我们的目的不是漫无目的折腾,而是思维的扩展、能力和效率的提升。

github