I need to have Google Places search suggestions using the default flutter's SearchPage, whenever the user starts typing I need to give autocomplete suggestions and I achieve this Asynchronously using FutureBuilder
, the problem now is that I need to debounce the dispatch of search requests for 500ms or more rather than wasting a lot of requests while the user is still typing
我需要让谷歌放置搜索建议使用默认扑翼的SearchPage,每当用户开始输入我需要给出自动补全建议,我使用FutureBuilder异步实现了这一点,现在的问题是,我需要取消分派的搜索请求500ms或更长时间,而不是浪费大量的请求,而用户仍在输入
To summarize what I've done so far:
总结一下我到目前为止所做的工作:
1) In my widget I call
1)在我的小部件中调用
showSearch(context: context, delegate: _delegate);
2) My delegate looks like this:
2)我的代理如下所示:
class _LocationSearchDelegate extends SearchDelegate<Suggestion> {
@override
List<Widget> buildActions(BuildContext context) {
return <Widget>[
IconButton(
tooltip: 'Clear',
icon: const Icon(Icons.clear),
onPressed: () {
query = '';
showSuggestions(context);
},
)
];
}
@override
Widget buildLeading(BuildContext context) => IconButton(
tooltip: 'Back',
icon: AnimatedIcon(
icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation,
),
onPressed: () {
close(context, null);
},
);
@override
Widget buildResults(BuildContext context) {
return FutureBuilder<List<Suggestion>>(
future: GooglePlaces.getInstance().getAutocompleteSuggestions(query),
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> suggestions) {
if (!suggestions.hasData) {
return Text('No results');
}
return buildLocationSuggestions(suggestions.data);
},
);
}
@override
Widget buildSuggestions(BuildContext context) {
return buildResults(context);
}
Widget buildLocationSuggestions(List<Suggestion> suggestions) {
return ListView.builder(
itemBuilder: (context, index) => ListTile(
leading: Icon(Icons.location_on),
title: Text(suggestions[index].text),
onTap: () {
showResults(context);
},
),
itemCount: suggestions.length,
);
}
}
3-I need to throttle / debounce searching until xxx Milliseconds have passed
3-我需要限制/取消搜索,直到xxx毫秒过去
I have 1 idea in mind, would there be an easy way to convert FutureBuilder to Stream and Use Stream builder, (which I read in some articles that it supports debouncing)?
我有一个想法,有没有一种简单的方法可以将FutureBuilder转换为Stream并使用Stream Builder(我在一些文章中读到了它支持去抖动)?
**I am aware that there are some 3rd party AutoComplete widgets like TypeAhead
that's doing that (from scratch) but I don't wanna use this at the moment.
**我知道有一些第三方自动完成小工具,比如Typehead,就是(从头开始)这样做的,但我现在不想用这个。
更多回答
Have you solved your problem? I want to achieve the same above functionality but is not able to do ..
你的问题解决了吗?我想实现相同的上述功能,但无法实现。
Update: I made a package for this that works with callbacks, futures, and/or streams. https://pub.dartlang.org/packages/debounce_throttle. Using it would simplify both of the approaches described below, especially the stream based approach as no new classes would need to be introduced. Here's a dartpad example https://dartpad.dartlang.org/e4e9c07dc320ec400a59827fff66bb49.
更新:我为此制作了一个与回调、期货和/或流一起工作的包。Https://pub.dartlang.org/packages/debounce_throttle.使用它将简化下面描述的两种方法,特别是基于流的方法,因为不需要引入新的类。下面是一个飞镖示例https://dartpad.dartlang.org/e4e9c07dc320ec400a59827fff66bb49.
There are at least two ways of doing this, a Future
based approach, and a Stream
based approach. Similar questions have gotten pointed towards using Streams since debouncing is built in, but let's look at both methods.
至少有两种方法可以做到这一点,一种是基于未来的方法,另一种是基于流的方法。类似的问题已经指向使用流,因为去抖动是内置的,但让我们看看这两种方法。
Future-based approach
面向未来的方法
Future
s themselves aren't cancelable, but the underlying Timer
s they use are. Here's a simple class that implements basic debounce functionality, using a callback instead of a Future.
期货本身是不可取消的,但它们使用的基础计时器是可以取消的。下面是一个简单的类,它使用回调而不是Future来实现基本的去抖动功能。
class Debouncer<T> {
Debouncer(this.duration, this.onValue);
final Duration duration;
void Function(T value) onValue;
T _value;
Timer _timer;
T get value => _value;
set value(T val) {
_value = val;
_timer?.cancel();
_timer = Timer(duration, () => onValue(_value));
}
}
Then to use it (DartPad compatible):
然后使用它(与DartPad兼容):
import 'dart:async';
void main() {
final debouncer = Debouncer<String>(Duration(milliseconds: 250), print);
debouncer.value = '';
final timer = Timer.periodic(Duration(milliseconds: 200), (_) {
debouncer.value += 'x';
});
/// prints "xxxxx" after 1250ms.
Future.delayed(Duration(milliseconds: 1000)).then((_) => timer.cancel());
}
Now to turn the callback into a Future, use a Completer
. Here's an example that debounces the List<Suggestion>
call to Google's API.
现在,要将回调变成未来,请使用补充器。下面的示例揭穿了对Google API的list
调用。
void main() {
final completer = Completer<List<Suggestion>>();
final debouncer = Debouncer<String>(Duration(milliseconds: 250), (value) async {
completer.complete(await GooglePlaces.getInstance().getAutocompleteSuggestions(value));
});
/// Using with a FutureBuilder.
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Suggestion>>(
future: completer.future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return Center(child: CircularProgressIndicator());
}
},
);
}
}
Stream-based approach
基于流的方法
Since the data in question arrives from a Future and not a Stream, we have to setup a class to handle query inputs and suggestion outputs. Luckily it handles debouncing the input stream naturally.
由于所讨论的数据来自Future而不是Stream,因此我们必须设置一个类来处理查询输入和建议输出。幸运的是,它可以自然地处理输入流的去抖动。
class SuggestionsController {
SuggestionsController(this.duration) {
_queryController.stream
.transform(DebounceStreamTransformer(duration))
.listen((query) async {
_suggestions.add(
await GooglePlaces.getInstance().getAutocompleteSuggestions(query));
});
}
final Duration duration;
final _queryController = StreamController<String>();
final _suggestions = BehaviorSubject<List<Suggestion>>();
Sink<String> get query => _queryController.sink;
Stream<List<Suggestion>> get suggestions => _suggestions.stream;
void dispose() {
_queryController.close();
_suggestions.close();
}
}
To use this controller class in Flutter, let's create a StatefulWidget that will manage the controller's state. This part includes the call to your function buildLocationSuggestions()
.
为了在Flutter中使用这个控制器类,让我们创建一个管理控制器状态的StatefulWidget。这部分包括对函数buildLocationSuggestions()的调用。
class SuggestionsWidget extends StatefulWidget {
_SuggestionsWidgetState createState() => _SuggestionsWidgetState();
}
class _SuggestionsWidgetState extends State<SuggestionsWidget> {
final duration = Duration(milliseconds: 250);
SuggestionsController controller;
@override
Widget build(BuildContext context) {
return StreamBuilder<List<Suggestion>>(
stream: controller.suggestions,
builder: (context, snapshot) {
if (snapshot.hasData) {
return buildLocationSuggestions(snapshot.data);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
} else {
return Center(child: CircularProgressIndicator());
}
},
);
}
@override
void initState() {
super.initState();
controller = SuggestionsController(duration);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void didUpdateWidget(SuggestionsWidget oldWidget) {
super.didUpdateWidget(oldWidget);
controller.dispose();
controller = SuggestionsController(duration);
}
}
It's not clear from your example where the query
string comes from, but to finish wiring this up, you would call controller.query.add(newQuery)
and StreamBuilder handles the rest.
从您的示例中不清楚查询字符串来自哪里,但要完成连接,您将调用Controler.query.add(NewQuery),StreamBuilder将处理其余部分。
Conclusion
结论
Since the API you're using yields Futures, it seems a little more straightforward to use that approach. The downside is the overhead of the Debouncer class and adding a Completer to tie it in to FutureBuilder.
由于您正在使用的API会产生期货收益,因此使用这种方法似乎更直接一些。缺点是Debouler类和添加Completer以将其绑定到FutureBuilder的开销。
The stream approach is popular, but includes a fair amount of overhead as well. Creating and disposing of the streams correctly can be tricky if you're not familiar.
流方法很流行,但也包括相当数量的开销。如果您不熟悉,那么正确地创建和处理流可能会很棘手。
I Simply did it this way no library required:
我只是这样做,不需要库:
void searchWithThrottle(String keyword, {int throttleTime}) {
_timer?.cancel();
if (keyword != previousKeyword && keyword.isNotEmpty) {
previousKeyword = keyword;
_timer = Timer.periodic(Duration(milliseconds: throttleTime ?? 350), (timer) {
print("Going to search with keyword : $keyword");
search(keyword);
_timer.cancel();
});
}
}
Here's a simple alternative to the other answer.
这是另一个答案的简单替代方案。
import 'package:debounce_throttle/debounce_throttle.dart';
final debouncer = Debouncer<String>(Duration(milliseconds: 250));
Future<List<Suggestion>> queryChanged(String query) async {
debouncer.value = query;
return GooglePlaces.getInstance().getAutocompleteSuggestions(await debouncer.nextValue)
}
@override
Widget buildResults(BuildContext context) {
return FutureBuilder<List<Suggestion>>(
future: queryChanged(query),
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> suggestions) {
if (!suggestions.hasData) {
return Text('No results');
}
return buildLocationSuggestions(suggestions.data);
},
);
}
That's roughly how you should be doing it I think.
我认为这大概就是你应该做的事情。
Here are a couple of ideas for using a stream instead, using the debouncer.
这里有几个想法,而不是使用流,使用去抖动。
void queryChanged(query) => debouncer.value = query;
Stream<List<Suggestion>> get suggestions async* {
while (true)
yield GooglePlaces.getInstance().getAutocompleteSuggestions(await debouncer.nexValue);
}
@override
Widget buildResults(BuildContext context) {
return StreamBuilder<List<Suggestion>>(
stream: suggestions,
builder: (BuildContext context, AsyncSnapshot<List<Suggestion>> suggestions) {
if (!suggestions.hasData) {
return Text('No results');
}
return buildLocationSuggestions(suggestions.data);
},
);
}
Or with a StreamTransformer.
或者使用StreamTransformer。
Stream<List<Suggestion>> get suggestions =>
debouncer.values.transform(StreamTransformer.fromHandlers(
handleData: (value, sink) => sink.add(GooglePlaces.getInstance()
.getAutocompleteSuggestions(value))));
I had trouble getting @Jacob Phillip's debounce_throttle package to work, as the code that worked for him back in 2018 no longer seems to work with the latest versions of Flutter / Dart. It waits for the debounce time to be reached but then executes all of the attempts at once after instead of just the last one.
我很难让@Jacob Phillip的DEBEAKE_THROTTLE程序包工作,因为早在2018年为他工作的代码似乎不再适用于最新版本的Ffltter/DART。它等待达到去抖动时间,但随后立即执行所有尝试,而不是仅执行最后一次尝试。
I was able to get it working with some modifications. I would have posted this as a comment on his answer, but it's too long.
经过一些修改,我能够让它工作起来。我本想把这篇文章作为对他的回答的评论,但它太长了。
final _debouncer = Debouncer<String>(const Duration(milliseconds: 500), initialValue: '');
late String userQuery;
Future<Iterable<LocationModel>> onSearchChanged(TextEditingValue textEditingValue) async {
// Debounce for half a second, so we don't make unnecessary api calls as the user types.
_debouncer.value = userQuery = textEditingValue.text;
await _debouncer.nextValue;
if (textEditingValue.text != userQuery) {
return const Iterable<LocationModel>.empty();
}
// Only retrieve location suggestions if the user typed at least 4 characters...
if (textEditingValue.text.length < _userQueryMinSuggestionsLength) {
return const Iterable<LocationModel>.empty();
}
// Get user's location.
var position = await _locationService.getPosition();
// Get location suggestions.
var propertyMode = await _userSettingsService.propertyMode;
try {
_locationSuggestions = await _placesService.getLocationSuggestionsAsync(
propertyMode, textEditingValue.text, position.latitude, position.longitude, LocationSearchType.property);
} catch (e) {
handleError(e, message: 'Error retrieving location suggestions');
}
notifyListeners();
return _locationSuggestions ?? [];
}
Timer can be used to debounce search input.
定时器可用于取消搜索输入。
Timer debounce;
void handleSearch(String value) {
if (debounce != null) debounce.cancel();
setState(() {
debounce = Timer(Duration(seconds: 2), () {
searchItems(value);
//call api or other search functions here
});
});
}
whenever a new input is added to the text box the function cancels previous timer and start a new one. The search function will be only initiated after 2 seconds of inactivity
每当向文本框添加新输入时,该函数都会取消先前的计时器并启动新的计时器。搜索功能仅在处于非活动状态2秒后启动
Adding the updated code for null safety in case anyone is looking for just copy paste. I know programmer can scroll two pages instead of just appending a '?' after the variables :)
为零安全添加了更新的代码,以防任何人只是在寻找复制粘贴。我知道程序员可以滚动两个页面,而不是只附加一个‘?’在变量后面:)
class Debouncer<T> {
Debouncer({required this.duration, required this.onValue});
final Duration duration;
void Function(T value) onValue;
T? _value;
Timer? _timer;
T? get value => _value;
set value(T? val) {
_value = val;
_timer?.cancel();
_timer = Timer(duration, () => onValue(_value));
}
}
更多回答
Thanks @jacob once I am back from vacation I will give your solution a try and marker it correct if it works, generally it sounds right, now speaking about "query" it's a property set on the delegate from the flutter team who created the SearchPage widget, it reoresents the text that's currently in the TextField, which I consider as a bad design, instead of explicitly sending it along with methods that are invoked to get suggestions, they just decided to keep it on a class level, there are indeed many other ways than doing it this way.
谢谢@Jacob一旦我度假回来,我会试一试你的解决方案,如果它有效的话,标记它是正确的,通常听起来是正确的,现在说到“Query”,它是创建SearchPage小部件的Ffltter团队的代表上设置的一个属性,它表示当前在Textfield中的文本,我认为这是一个糟糕的设计,而不是显式地将其与调用来获取建议的方法一起发送,他们只是决定将其保持在类级别,除了这种方式,确实还有许多其他方法。
Yeah I kinda overdid this one while avoiding other work. It really bugged me the best answer I found was to convert it to a stream. Anyway, cheers!
是的,我在逃避其他工作的同时,做得有点过头了。这真的让我很烦恼,我找到的最好的答案就是把它转换成一条流。不管怎样,干杯!
Thanks again @Jacob Phillips, Please update the answer to indicate that I have to recreate a completer every time the method completes, otherwise It throws an exception, even though I use Completer in Rx, I didn't associate it to that right away, it took me a while to understand what's going on.
再次感谢@Jacob Phillips,请更新答案以指示我必须在每次方法完成时重新创建完成器,否则它会抛出异常,即使我在Rx中使用Completer,我也没有立即将其与之关联,我花了一段时间才理解发生了什么。
Okay will do tomorrow. The package helps mitigate that issue since it recreates the completer internally. Its link shows an example, I just havent changed the answer.
好的,明天就可以了。该包有助于缓解该问题,因为它在内部重新创建了完成器。它的链接显示了一个例子,我只是没有改变答案。
@Luca Just use the Debouncer
class from simple_observable
@Luca只需使用SIMPLE_EATABLABLE中的Debouler类
But how to integrate this code with Widget buildResults(BuildContext context)
or Widget buildSuggestions(BuildContext context)
但是如何将这些代码与Widget BuildResults(BuildContext Context)或Widget BuildSuggestions(BuildContext Context)集成呢?
This answer seems to be a bit outdated. Your simple_observable package no longer has a Debouncer class
这个答案似乎有点过时了。您的Simple_Observable包不再有Debouler类
yeah i moved it to a package named debounce_throttle
是的,我把它移到了一个名为“去弹跳_节流”的包裹里。
i tried to use your Future code but it still executes it more than once. shouldnt this be executed only once? e.g. 2 seconds. i type fast 3 numbers , it doesnt execute right away, only after 2 seconds have elapsed but still runs the function 3x.
我试图使用您的Future代码,但它仍然多次执行它。这不是应该只执行一次吗?例如2秒。我输入了快速的3个数字,它没有立即执行,只在2秒后执行,但仍然运行函数3x。
@JacobPhillips yes chitgoks is right it is getting called 3x
@JacobPhillips是的,Chitgoks是对的,它被称为3x
This code not working as I expected, because it's still call a request multiple times based on total user character typing
这段代码没有像我预期的那样工作,因为它仍然根据用户的全部字符输入多次调用请求
But how to integrate this code with Widget buildResults(BuildContext context)
or Widget buildSuggestions(BuildContext context)
但是如何将这些代码与Widget BuildResults(BuildContext Context)或Widget BuildSuggestions(BuildContext Context)集成呢?
我是一名优秀的程序员,十分优秀!