How to debounce Textfield onChange in Dart?

38,297

Solution 1

Implementation

Import dependencies:

import 'dart:async';

In your widget state declare a timer:

Timer? _debounce;

Add a listener method:

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

Don't forget to clean up:

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

Usage

In your build tree hook the onChanged event:

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

Solution 2

You can make Debouncer class using Timer

import 'package:flutter/foundation.dart';
import 'dart:async';

class Debouncer {
  final int milliseconds;
  Timer? _timer;

  Debouncer({required this.milliseconds});

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

Declare it

final _debouncer = Debouncer(milliseconds: 500);

and trigger it

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

Solution 3

Using BehaviorSubject from rxdart lib is a good solution. It ignores changes that happen within X seconds of the previous.

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

Solution 4

Here is my solution

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

Solution 5

Have a look at EasyDebounce.

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

DxW

Updated on July 05, 2022

Comments

  • DxW
    DxW almost 2 years

    I'm trying to develop a TextField that update the data on a Firestore database when they change. It seems to work but I need to prevent the onChange event to fire multiple times.

    In JS I would use lodash _debounce() but in Dart I don't know how to do it. I've read of some debounce libraries but I can't figure out how they work.

    That's my code, it's only a test so something may be strange:

    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);
        
      }
    }
    
  • DxW
    DxW almost 6 years
    Thanks Bhanu, I have understood how to debounce a stream, but how do i get the one related to my widget event?
  • Vikas Jangra
    Vikas Jangra over 4 years
    I made some modifications to your answer in this gist: gist.github.com/venkatd/7125882a8e86d80000ea4c2da2c2a8ad. - Dropped dependency on Flutter so it can be used in pure Dart (no need for VoidCallback) - action instance var isn't used - used timer?.cancel() shorthand - swapped to Duration type in favor of passing in milliseconds
  • gsouf
    gsouf about 4 years
    what's about canceling timer in dispose?
  • Jannie Theunissen
    Jannie Theunissen about 4 years
    Good catch, @SoufianeGhzal. Updated the example.
  • gsouf
    gsouf about 4 years
    Is it fine to cancel it unconditionally even if the timer is finished? From my testing it worked, just want to make sure it's a good practice.
  • Jannie Theunissen
    Jannie Theunissen about 4 years
    @SoufianeGhzal yes, it is perfectly fine to call cancel on a stopped timer and it is best practice to call it in your widget dispose or to a wrap your timer code in a if (!mounted) condition
  • antonone
    antonone almost 4 years
    If using Flutter, then subject is a field in your widget. This code above from the answer needs to go to initState(), the _search function will handle your debounced search query, and your onChange callback in TextField will need to call subject.add(string).
  • Marco Fregoso
    Marco Fregoso over 3 years
    @JannieTheunissen For anyone else, you might also want to update _debounce.cancel(); to _debounce?.cancel(); In case the timer is never created so you don't get an exception when you pop the Route.
  • Jannie Theunissen
    Jannie Theunissen over 3 years
    @MarcoFregoso good point! Example updated. I added it only to the instance in the destructor where the Timer handle can be null if no searches were performed.
  • Ali80
    Ali80 over 3 years
    also don't forget to dispose the timer
  • anztrax
    anztrax over 3 years
    thanks magnus, u're library help me alot thanks :+1:
  • mike
    mike over 3 years
    Why don't you just pass an _onSearchChanged method to the TextField(onChanged:) property instead of hooking it via listeners?
  • Jannie Theunissen
    Jannie Theunissen over 3 years
    @mike not quite sure how that would work. How about adding your solution so we can all see what you propose?
  • mike
    mike over 3 years
    Sure! dartpad.dev/bb246c0789163369cd146357183ae5b9 Lmk if that works for you, thanks!
  • Jannie Theunissen
    Jannie Theunissen over 3 years
    Okay yes! That is better. Thanks @mike Let me tweak the recipe.
  • mike
    mike over 3 years
    Final note, for this use case you don't need the TextEditingController; you can just work with the String query. You'd need the controller if you wanted to access the value in the TextField externally (ie, to store it with a Save button), which is not the case here. It could be useful in a more general case so that's why I left it in my gist. Cheers!
  • Jannie Theunissen
    Jannie Theunissen over 3 years
    Thanks @mike well spotted.
  • Rajesh
    Rajesh about 3 years
    Perfect approach
  • Jolzal
    Jolzal over 2 years
    how to dispose this