How to debounce Textfield onChange in Dart?
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
);
DxW
Updated on July 05, 2022Comments
-
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 almost 6 yearsThanks Bhanu, I have understood how to debounce a stream, but how do i get the one related to my widget event?
-
Vikas Jangra over 4 yearsI 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 about 4 yearswhat's about canceling timer in dispose?
-
Jannie Theunissen about 4 yearsGood catch, @SoufianeGhzal. Updated the example.
-
gsouf about 4 yearsIs 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 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 almost 4 yearsIf using Flutter, then
subject
is a field in your widget. This code above from the answer needs to go toinitState()
, the_search
function will handle your debounced search query, and youronChange
callback inTextField
will need to callsubject.add(string)
. -
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 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 over 3 yearsalso don't forget to dispose the timer
-
anztrax over 3 yearsthanks magnus, u're library help me alot thanks :+1:
-
mike over 3 yearsWhy don't you just pass an
_onSearchChanged
method to the TextField(onChanged
:) property instead of hooking it via listeners? -
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 over 3 yearsSure! dartpad.dev/bb246c0789163369cd146357183ae5b9 Lmk if that works for you, thanks!
-
Jannie Theunissen over 3 yearsOkay yes! That is better. Thanks @mike Let me tweak the recipe.
-
mike over 3 yearsFinal 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 aSave
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 over 3 yearsThanks @mike well spotted.
-
Rajesh about 3 yearsPerfect approach
-
Jolzal over 2 yearshow to dispose this