如何在 Dart 中对 Textfield onChange 进行去抖动?
How to debounce Textfield onChange in Dart?
我正在尝试开发一个 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);
}
}
您可以使用 rxdart 包创建一个使用流的 Observable,然后根据您的要求对其进行去抖动。我认为这 link 可以帮助您入门。
实施
导入依赖项:
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,
// ...
)
使用 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
});
}
您可以使用 Timer
制作 Debouncer
class
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);
}
}
声明它
final _debouncer = Debouncer(milliseconds: 500);
并触发它
onTextChange(String text) {
_debouncer.run(() => print(text));
}
正如其他人所建议的那样,实施自定义去抖器 class 并不难。您还可以使用 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.
的作者
这是我的解决方案
subject = new PublishSubject<String>();
subject.stream
.debounceTime(Duration(milliseconds: 300))
.where((value) => value.isNotEmpty && value.toString().length > 1)
.distinct()
.listen(_search);
效用函数怎么样:
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可以改进。
这是我对抢先去抖的两分钱。
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);
}
}
}
看看EasyDebounce。
EasyDebounce.debounce(
'my-debouncer', // <-- An ID for this particular debouncer
Duration(milliseconds: 500), // <-- The debounce duration
() => myMethod() // <-- The target method
);
我喜欢 Dart 的 Callable Classes 去抖 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();
}
}
用法很简单 - 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');
});
},
);
}
这个解决方案适用于我使用 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);
}
我正在尝试开发一个 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);
}
}
您可以使用 rxdart 包创建一个使用流的 Observable,然后根据您的要求对其进行去抖动。我认为这 link 可以帮助您入门。
实施
导入依赖项:
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,
// ...
)
使用 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
});
}
您可以使用 Timer
Debouncer
class
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);
}
}
声明它
final _debouncer = Debouncer(milliseconds: 500);
并触发它
onTextChange(String text) {
_debouncer.run(() => print(text));
}
正如其他人所建议的那样,实施自定义去抖器 class 并不难。您还可以使用 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.
的作者这是我的解决方案
subject = new PublishSubject<String>();
subject.stream
.debounceTime(Duration(milliseconds: 300))
.where((value) => value.isNotEmpty && value.toString().length > 1)
.distinct()
.listen(_search);
效用函数怎么样:
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可以改进。
这是我对抢先去抖的两分钱。
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);
}
}
}
看看EasyDebounce。
EasyDebounce.debounce(
'my-debouncer', // <-- An ID for this particular debouncer
Duration(milliseconds: 500), // <-- The debounce duration
() => myMethod() // <-- The target method
);
我喜欢 Dart 的 Callable Classes 去抖 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();
}
}
用法很简单 - 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');
});
},
);
}
这个解决方案适用于我使用 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);
}