【问题标题】:How to debounce search suggestions in flutter's SearchPage Widget?如何在颤振搜索页面小部件中消除搜索建议?
【发布时间】:2019-03-25 16:24:46
【问题描述】:

我需要使用默认 Flutter 的 SearchPage 获得 Google Places 搜索建议,每当用户开始输入时,我需要提供自动完成建议,并且我使用 FutureBuilder 异步实现这一点,现在的问题是我需要在 500 毫秒或更长时间内消除搜索请求的分派,而不是在用户仍在键入时浪费大量请求

总结一下我到目前为止所做的事情:

1) 在我的小部件中,我调用了

showSearch(context: context, delegate: _delegate);

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-我需要限制/去抖动搜索直到 xxx 毫秒过去

我有一个想法,是否有一种简单的方法可以将 FutureBuilder 转换为 Stream 并使用 Stream 构建器(我在一些文章中读到它支持去抖动)?

**我知道有一些第 3 方 AutoComplete 小部件(如 TypeAhead)正在这样做(从头开始),但我现在不想使用它。

【问题讨论】:

  • 您的问题解决了吗?我想实现与上述相同的功能,但无法做到..

标签: dart flutter


【解决方案1】:

更新:我为此制作了一个适用于回调、期货和/或流的包。 https://pub.dartlang.org/packages/debounce_throttle。使用它可以简化下面描述的两种方法,尤其是基于流的方法,因为不需要引入新的类。这是一个 dartpad 示例 https://dartpad.dartlang.org/e4e9c07dc320ec400a59827fff66bb49

至少有两种方法可以做到这一点,一种是基于Future 的方法,另一种是基于Stream 的方法。自从内置去抖动功能以来,使用 Streams 也出现了类似的问题,但让我们看看这两种方法。

面向未来的方法

Futures 本身是不可取消的,但他们使用的底层Timers 是。这是一个实现基本去抖动功能的简单类,使用回调而不是 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));
  }  
}

然后使用它(兼容 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()); 
}

现在要将回调转换为 Future,请使用 Completer。这是一个消除对 Google API 的 List&lt;Suggestion&gt; 调用的示例。

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

基于流的方法

由于相关数据来自 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();
  }
}

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

从您的示例中不清楚 query 字符串的来源,但要完成连接,您可以调用 controller.query.add(newQuery) 并由 StreamBuilder 处理其余部分。

结论

由于您使用的 API 产生 Futures,因此使用该方法似乎更简单一些。缺点是 Debouncer 类的开销,并添加了一个 Completer 以将其绑定到 FutureBuilder。

流方法很流行,但也包含相当多的开销。如果您不熟悉,正确创建和处理流可能会很棘手。

【讨论】:

  • 感谢@jacob,一旦我从假期回来,我会尝试一下您的解决方案,如果它有效,则将其标记为正确,通常听起来不错,现在谈到“查询”,它是委托上设置的属性来自创建 SearchPage 小部件的颤振团队,它重新呈现当前在 TextField 中的文本,我认为这是一个糟糕的设计,而不是显式发送它以及调用以获取建议的方法,他们只是决定保持它一个班级的水平,除了这种方式,确实还有很多其他的方法。
  • 是的,我在避免其他工作的同时做得有点过火了。我发现最好的答案是将其转换为流,这真的让我很烦恼。无论如何,干杯!
  • 再次感谢@Jacob Phillips,请更新答案以表明我必须在每次方法完成时重新创建一个完成器,否则它会引发异常,即使我在 Rx 中使用了 Completer,我也没有不要马上把它和那个联系起来,我花了一段时间才明白发生了什么。
  • 好的,明天再做。该软件包有助于缓解该问题,因为它在内部重新创建了完成程序。它的链接显示了一个例子,我只是没有改变答案。
  • @Luca 只需使用来自simple_observableDebouncer
【解决方案2】:

这是另一个答案的简单替代方案。

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

我认为这大概就是你应该这样做的方式。

以下是使用去抖动器代替流的一些想法。

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

或者使用 StreamTransformer。

Stream<List<Suggestion>> get suggestions => 
  debouncer.values.transform(StreamTransformer.fromHandlers(
    handleData: (value, sink) => sink.add(GooglePlaces.getInstance()
      .getAutocompleteSuggestions(value))));

【讨论】:

  • 这个答案似乎有点过时了。你的 simple_observable 包不再有 Debouncer 类
  • 是的,我把它移到了一个名为debounce_throttle的包中
  • 我尝试使用您的 Future 代码,但它仍然多次执行。这不应该只执行一次吗?例如2 秒。我快速输入 3 个数字,它不会立即执行,仅在 2 秒后才执行,但仍会运行函数 3x。
  • 这很奇怪,你能在 repo 上打开一个问题吗? pub.dev/packages/debounce_throttle
  • @JacobPhillips 是的 chitgoks 是对的,它被称为 3x
【解决方案3】:

我只是这样做了,不需要库:

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

【讨论】:

  • 但是如何将此代码与Widget buildResults(BuildContext context)Widget buildSuggestions(BuildContext context) 集成
【解决方案4】:

定时器可用于去抖动搜索输入。

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

每当向文本框添加新输入时,该函数都会取消前一个计时器并启动一个新计时器。搜索功能只有在 2 秒不活动后才会启动

【讨论】:

  • 但是如何将此代码与Widget buildResults(BuildContext context)Widget buildSuggestions(BuildContext context) 集成
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-02
  • 2018-10-02
  • 1970-01-01
  • 2017-12-22
  • 2014-01-10
相关资源
最近更新 更多