【问题标题】:How to debounce Textfield onChange in Dart?如何在 Dart 中对 Textfield onChange 进行去抖动?
【发布时间】:2019-01-18 08:44:23
【问题描述】:

我正在尝试开发一个 TextField,它可以在 Firestore 数据库中的数据发生更改时更新它们。它似乎有效,但我需要防止 onChange 事件多次触发。

在 JS 中我会使用 lodash _debounce() 但在 Dart 中我不知道该怎么做。我读过一些去抖动库,但我不知道它们是如何工作的。

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

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

【问题讨论】:

    标签: flutter dart textfield onchange debouncing


    【解决方案1】:

    您可以使用 rxdart 包使用流创建 Observable,然后根据您的要求对其进行去抖动。我认为link 可以帮助您入门。

    【讨论】:

    • 感谢 Bhanu,我已经了解如何对流进行去抖动,但是如何获得与我的小部件事件相关的流?
    【解决方案2】:

    实施

    导入依赖:

    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,
            // ...
        )
    

    【讨论】:

    • 在 dispose 中取消定时器是怎么回事?
    • 很好,@SoufianeGhzal。更新了示例。
    • @SoufianeGhzal 是的,这是完美的fine to call cancel on a stopped timer,最好在您的小部件处置中调用它或将您的计时器代码包装在if (!mounted) 条件中
    • @JannieTheunissen 对于其他任何人,您可能还想将_debounce.cancel(); 更新为_debounce?.cancel();,以防计时器从未创建,这样您在弹出路线时就不会出现异常。跨度>
    • @MarcoFregoso 好点!示例已更新。我只将它添加到析构函数中的实例中,如果没有执行任何搜索,则 Timer 句柄可以为空。
    【解决方案3】:

    使用 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
      });
    }
    

    【讨论】:

    • 完美方法
    【解决方案4】:

    您可以使用Timer 制作Debouncer

    import 'package:flutter/foundation.dart';
    import 'dart:async';
    
    class Debouncer {
      final int milliseconds;
      VoidCallback action;
      Timer _timer;
    
      Debouncer({ this.milliseconds });
    
      run(VoidCallback action) {
        if (_timer != null) {
          _timer.cancel();
        }
    
        _timer = Timer(Duration(milliseconds: milliseconds), action);
      }
    }
    

    声明

    final _debouncer = Debouncer(milliseconds: 500);
    

    并触发它

    onTextChange(String text) {
      _debouncer.run(() => print(text));
    }
    

    【讨论】:

    • 我在此要点中对您的答案进行了一些修改:gist.github.com/venkatd/7125882a8e86d80000ea4c2da2c2a8ad。 - 放弃了对 Flutter 的依赖,因此它可以在纯 Dart 中使用(不需要 VoidCallback) - 不使用动作实例 var - 使用 timer?.cancel() 简写 - 换成 Duration 类型以支持以毫秒为单位传递
    • 别忘了配置定时器
    【解决方案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:
    【解决方案6】:

    这是我的解决方案

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

    【讨论】:

    • 如果使用 Flutter,那么 subject 是您的小部件中的一个字段。以上来自答案的代码需要转到initState()_search 函数将处理您的去抖动搜索查询,而您在TextField 中的onChange 回调将需要调用subject.add(string)
    【解决方案7】:

    像这样的实用函数呢:

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

    将来variable arguments 可以改进。

    【讨论】:

      【解决方案8】:

      这是我的两分钱抢先去抖动。

      import 'dart:async';
      
      /// Used to debounce function call.
      /// That means [runnable] function will be called at most once per [delay].
      class Debouncer {
        int _lastTime;
        Timer _timer;
        Duration delay;
      
        Debouncer(this.delay)
            : _lastTime = DateTime.now().millisecondsSinceEpoch;
        
        run(Function runnable) {
          _timer?.cancel();
      
          final current = DateTime.now().millisecondsSinceEpoch;
          final delta = current - _lastTime;
      
          // If elapsed time is bigger than [delayInMs] threshold -
          // call function immediately.
          if (delta > delay.inMilliseconds) {
            _lastTime = current;
            runnable();
          } else {
            // Elapsed time is less then [delayInMs] threshold -
            // setup the timer
            _timer = Timer(delay, runnable);
          }
        }
      }
      

      【讨论】:

        【解决方案9】:

        看看EasyDebounce

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

        【讨论】:

          【解决方案10】:

          我喜欢 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();
            }
          }
          

          用法很简单——example on 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');
                });
              },
            );
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-10-24
            • 2021-02-25
            • 2018-09-13
            • 2021-02-11
            • 2015-06-24
            • 2017-02-03
            • 2018-08-20
            • 1970-01-01
            相关资源
            最近更新 更多