如何去除 Dart 中变化的文本字段?

我正在尝试开发一个 TextField,它可以在数据库发生变化时更新数据。它似乎工作,但我需要防止 onChange 事件触发多次。

在 JS 中,我会使用 loash _ deounce () ,但是在 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);
    

}
}
57690 次浏览

You can use rxdart package to create an Observable using a stream then debounce it as per your requirements. I think this link would help you get started.

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

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

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

As others have suggested, implementing a custom debouncer class is not that difficult. You can also use a Flutter plugin, such as EasyDebounce.

In your case you'd use it like this:

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

Full disclosure: I'm the author of EasyDebounce.

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

What about a utility function like:

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

Then:

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

In the future with variable arguments it could be improved.

Here's my two cents with preemptive debouncing.

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

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

I like Dart's Callable Classes for my debounce class:

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

Usage is simple - 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"

For your specific example, it's important to dispose the timer, in case it makes use of your TextController after it's been disposed:

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

This solution works for me using 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);
}