如何在Dart中防抖Textfield onChange事件?

120

我正在尝试开发一个文本字段,当它们更改时更新 Firestore 数据库中的数据。它似乎可以工作,但我需要防止 onChange 事件多次触发。

在 JS 中,我会使用 lodash 的 _debounce(),但在 Dart 中我不知道如何实现。我已经阅读了一些 debounce 库,但我无法弄清楚它们的工作原理。

这是我的代码,它只是一个测试,所以可能有些奇怪:

import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';


class ClientePage extends StatefulWidget {

  String idCliente;


  ClientePage(this.idCliente);

  @override
  _ClientePageState createState() => new _ClientePageState();

  
}

class _ClientePageState extends State<ClientePage> {

  TextEditingController nomeTextController = new TextEditingController();


  void initState() {
    super.initState();

    // Start listening to changes 
    nomeTextController.addListener(((){
        _updateNomeCliente(); // <- Prevent this function from run multiple times
    }));
  }


  _updateNomeCliente = (){

    print("Aggiorno nome cliente");
    Firestore.instance.collection('clienti').document(widget.idCliente).setData( {
      "nome" : nomeTextController.text
    }, merge: true);

  }



  @override
  Widget build(BuildContext context) {

    return new StreamBuilder<DocumentSnapshot>(
      stream: Firestore.instance.collection('clienti').document(widget.idCliente).snapshots(),
      builder: (BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
        if (!snapshot.hasData) return new Text('Loading...');

        nomeTextController.text = snapshot.data['nome'];


        return new DefaultTabController(
          length: 3,
          child: new Scaffold(
            body: new TabBarView(
              children: <Widget>[
                new Column(
                  children: <Widget>[
                    new Padding(
                      padding: new EdgeInsets.symmetric(
                        vertical : 20.00
                      ),
                      child: new Container(
                        child: new Row(
                          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                          children: <Widget>[
                            new Text(snapshot.data['cognome']),
                            new Text(snapshot.data['ragionesociale']),
                          ],
                        ),
                      ),
                    ),
                    new Expanded(
                      child: new Container(
                        decoration: new BoxDecoration(
                          borderRadius: BorderRadius.only(
                            topLeft: Radius.circular(20.00),
                            topRight: Radius.circular(20.00)
                          ),
                          color: Colors.brown,
                        ),
                        child: new ListView(
                          children: <Widget>[
                            new ListTile(
                              title: new TextField(
                                style: new TextStyle(
                                  color: Colors.white70
                                ),
                                controller: nomeTextController,
                                decoration: new InputDecoration(labelText: "Nome")
                              ),
                            )
                          ]
                        )
                      ),
                    )
                  ],
                ),
                new Text("La seconda pagina"),
                new Text("La terza pagina"),
              ]
            ),
            appBar: new AppBar(
              title: Text(snapshot.data['nome'] + ' oh ' + snapshot.data['cognome']),
              bottom: new TabBar(          
                tabs: <Widget>[
                  new Tab(text: "Informazioni"),  // 1st Tab
                  new Tab(text: "Schede cliente"), // 2nd Tab
                  new Tab(text: "Altro"), // 3rd Tab
                ],
              ),
            ),
          )
        );
        
      },
    );

    print("Il widget id è");
    print(widget.idCliente);
    
  }
}
14个回答

269

实现

导入依赖项:

import 'dart:async';

在您的小部件状态中声明一个计时器:

Timer? _debounce;

添加一个监听器方法:

_onSearchChanged(String query) {
    if (_debounce?.isActive ?? false) _debounce.cancel();
    _debounce = Timer(const Duration(milliseconds: 500), () {
        // do something with query
    });
    }

别忘了进行清理:

@override
void dispose() {
    _debounce?.cancel();
    super.dispose();
}

使用方法

在您的构建树中挂接onChanged事件:

child: TextField(
        onChanged: _onSearchChanged,
        // ...
    )

即使计时器已经完成,无条件取消它是否可以?从我的测试来看,它可以工作,只是想确保这是一个好的做法。 - gsouf
如果您不取消,您将保持默认行为,只有计时器中配置的延迟。 - Lucas Sabino
如果_onSearchChanged需要返回结果怎么办?我们实际上无法在Timer回调中将结果返回给它。 - undefined

111
你可以使用Timer来创建Debouncer类。
import 'dart:async';

import 'package:flutter/foundation.dart';

class Debouncer {
  final int milliseconds;
  Timer? _timer;

  Debouncer({required this.milliseconds});

  void run(VoidCallback action) {
    _timer?.cancel();
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }

  void dispose() {
    _timer?.cancel();
  }
}

声明它
final _debouncer = Debouncer(milliseconds: 500);

并触发它
onTextChange(String text) {
  _debouncer.run(() => print(text));
}

8
我对您在这个链接(https://gist.github.com/venkatd/7125882a8e86d80000ea4c2da2c2a8ad)中的回答进行了一些修改:
  • 去掉了对Flutter的依赖,现在可以在纯Dart中使用(不需要VoidCallback)
  • action实例变量未被使用
  • 使用了timer?.cancel()缩写形式
  • 改用Duration类型来传递毫秒数。
- Venkat D.
3
请不要忘记释放计时器。 - Ali80
1
如何处理这个? - Jolzal
@Jolzal 请查看此链接 https://gist.github.com/venkatd/7125882a8e86d80000ea4c2da2c2a8ad?permalink_comment_id=3934061#gistcomment-3934061 - MBH

34
使用rxdart库中的BehaviorSubject是一个不错的解决方案。它忽略了与上一个状态变化相隔小于X秒的变化。
final searchOnChange = new BehaviorSubject<String>();
...
TextField(onChanged: _search)
...

void _search(String queryString) {
  searchOnChange.add(queryString);
}   

void initState() {    
  searchOnChange.debounceTime(Duration(seconds: 1)).listen((queryString) { 
  >> request data from your API
  });
}

我认为这种方法会防抖流的发射,而不是防抖未来的函数调用。 - Mehrdad Shokri

13

我喜欢Dart的可调用类Callable Classes 用于我的去抖动(debounce)类:

import 'dart:async';

class Debounce {
  Duration delay;
  Timer? _timer;

  Debounce(
    this.delay,
  );

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

  dispose() {
    _timer?.cancel();
  }
}

使用方法很简单 - 在DartPad上查看示例

// 1 - Create a debounce instance
final Debounce _debounce = Debounce(Duration(milliseconds: 400));

// 2 - Use it
_debounce((){ print('First'); });
_debounce((){ print('Second'); });
_debounce((){ print('Third'); });

// ...after 400ms you'll see "Third"

针对您的具体示例,重要的是要处理计时器,以防止在处理后使用 TextController:
final TextEditingController _controller = TextEditingController();
final Debounce _debounce = Debounce(Duration(milliseconds: 400));

@override
void dispose() {
  _controller.dispose();
  _debounce.dispose();
  super.dispose();
}

@override
Widget build(BuildContext context) {
  return TextField(
    controller: _controller,
    onChanged: (String value) {
      _debounce((){
        print('Value is $value');
      });
    },
  );
}

8

这里是我的解决方案

 subject = new PublishSubject<String>();
      subject.stream
          .debounceTime(Duration(milliseconds: 300))
          .where((value) => value.isNotEmpty && value.toString().length > 1)
          .distinct()
          .listen(_search);

1
如果使用Flutter,那么subject是您的小部件中的一个字段。上面的代码需要放在initState()中,_search函数将处理您的去抖动搜索查询,并且您在TextField中的onChange回调需要调用subject.add(string) - antonone

7

请看EasyDebounce

EasyDebounce.debounce(
  'my-debouncer',                 // <-- An ID for this particular debouncer
   Duration(milliseconds: 500),    // <-- The debounce duration
  () => myMethod()                // <-- The target method
);

当 myMethod 返回一个值时,如何使用这个 when? - Herz3h

5

那么像这样的实用函数怎么样:

import 'dart:async';

Function debounce(Function func, int milliseconds) {
  Timer timer;
  return () { // or (arg) if you need an argument
    if (timer != null) {
      timer.cancel();
    }

    timer = Timer(Duration(milliseconds: milliseconds), func); // or () => func(arg) 
  };
}

然后:

var debouncedSearch = debounce(onSearchChanged, 250);
_searchQuery.addListener(debouncedSearch);

在未来,通过使用可变参数, 可能会得到改进。

5

正如其他人建议的那样,实现一个自定义的防抖类并不难。您还可以使用Flutter插件,例如EasyDebounce

在您的情况下,您将像这样使用它:

import 'package:easy_debounce/easy_debounce.dart';

...

// Start listening to changes 
nomeTextController.addListener(((){
    EasyDebounce.debounce(
        '_updatenomecliente',        // <-- An ID for this debounce operation
        Duration(milliseconds: 500), // <-- Adjust Duration to fit your needs
        () => _updateNomeCliente()
    ); 
}));

声明:本人是EasyDebounce的作者。


谢谢Magnus,你的库给我帮了很大的忙,感谢:+1: - anztrax
我有一个应该返回值的方法,但 debounce 只能返回 void,那么在这种情况下如何使用它? - Herz3h

2

您可以使用rxdart包通过流创建一个Observable,然后根据您的要求进行防抖处理。我认为这个链接会帮助您入门。


1
谢谢Bhanu,我已经理解了如何去抖动一个流,但是我该如何获取与我的小部件事件相关的那个呢? - DxW

1

这个解决方案对我来说很有用,使用RxDart实现。

final _search = TextEditingController(text: '');
  RxString searchText = ''.obs;

  @override
  void initState() {
    super.initState();
    _search.addListener(() {
      searchText.value = _search.text;
    });
    debounce(searchText, (_) {
      listenToSearch();
    }, time: const Duration(seconds: 1));
  }

  listenToSearch() {
    AlertsFilterModel filter = widget.filter;
    filter.searchText = _search.text;
    widget.sendAlertFilters(filter);
  }

1
易于应用,谢谢 - Charles Lim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接