Async/Await Debouncer – Flutter

Debouncer

import 'dart:async';

class Debouncer<Return> {

  Duration debounceDuration;
  
  Map<DateTime, Completer> _completers = {};
  Timer? _timer;

  Debouncer(this.debounceDuration);

  Future<Return?> run(Future<Return> Function() toRun) async {
    _timer?.cancel();
    _completers.entries
        .map((e) => e.value)
        .forEach((completer) {
          completer.completeError(_CancelException());
        });
    print('❕ Cancelling previous completer and timer…');

    final now = DateTime.now();
    _completers[now] = Completer();
    _timer = Timer(debounceDuration, () {
      _completers[now]?.complete();
    });

    try {
      await _completers[now]?.future;
      _completers.remove(now);
      print('???? Will execute toRun…');
      return await toRun();
    } catch (exception) {
      print('❌ Cancelled!');
      _completers.remove(now);
      return null;
    }
  }

}

class _CancelException implements Exception {
  const _CancelException() : super();
}

Utilisation

final _debouncer = Debouncer<GeocodingResponse>(Duration(milliseconds: 330));

// This can get called many times in quick succession,
// but it will only actually execute the query after 330 milliseconds have elapsed since the last call.
final response = await _debouncer.run(() {
  return network.geocode(query);
});

S
❕ Cancelling previous completer and timer…
Se
❕ Cancelling previous completer and timer…
❌ Cancelled!
Sea
❕ Cancelling previous completer and timer…
❌ Cancelled!
Seat
❕ Cancelling previous completer and timer…
❌ Cancelled!
Seatt
❕ Cancelling previous completer and timer…
❌ Cancelled!
Seattl
❕ Cancelling previous completer and timer…
❌ Cancelled!
Seattle
❕ Cancelling previous completer and timer…
❌ Cancelled!
✅ Will execute toRun…

Contexte

J’ai commencé à apprendre Flutter récemment dans le cadre d’un nouveau projet. En cherchant comment faire un champ de recherche pour un lieu, je me suis rendu compte que Flutter n’avait pas de solution simple pour le « Debouncing ».

Le « Debouncing » c’est une technique qui permet de retarder l’exécution d’un code qui est amené à être invoqué plusieurs fois.

Comme vous pouvez le voir plus haut, un champ de texte effectuant une requête réseau est souvent un bon candidat pour l’utilisation du debouncing. L’utilisateur rentre les caractères un par un dans le champ de texte, et si l’on fait une requête réseau à chaque insertion, on se retrouve avec beaucoup de requêtes réseaux inutiles qui entraîneront un coût côté serveur. C’est pourquoi on préfère n’exécuter la requête réseau qu’une fois que l’on est sûr que ça devient intéressant pour l’utilisateur. On décide donc d’un délai (en l’occurrence 330 millisecondes), et on n’appelle la requête qu’une fois ce délai expiré sans qu’un autre caractère ait été inséré.

Flutter a un excellent widget Autocomplete. Ce widget permet d’afficher des résultats à mesure que l’utilisateur tape dans le textfield, et il est parfait pour mon usage. Toutefois, ce widget a une spécificité : il fonctionne avec async/await.

Sans rentrer dans les détails, il faut savoir que du code asynchrone utilisant cette technique aime bien communiquer avec du code qui est lui aussi écrit avec async/await. Certaines solutions, notamment à base de callbacks ne vont pas fonctionner ou ne seront pas élégantes. Je suis très fan de async/await qui apporte une syntaxe plus lisible et compréhensible à un problème compliqué. Mais toutes les solutions de Debouncing que j’ai pu trouver sur internet fonctionnaient avec des callbacks, étaient trop compliquées, ou avaient des features limitées.

Par exemple, dans la documentation du widget Autocomplete, il y a un exemple d’utilisation supportant le debouncing. Mais le code fourni est à la fois compliqué et plutôt rigide. On ne peut pas facilement exécuter ce que l’on veut à la fin du debouncing.

Je veux tout de même ouvrir une parenthèse pour dire que je trouve ça absolument formidable que cette doc propose ce genre d’exemples. J’ai rarement vu des documentations aussi utiles, et autant axée sur « qu’est-ce que je peux faire de ça ». La plupart se contentent de décrire les possibilités sans réellement décrire des cas d’usage courants, ce que cette doc et plus généralement la doc de Flutter fait à merveille. ????

La solution que je propose a l’avantage d’être simple, flexible et de fonctionner avec async/await. J’espère qu’elle vous sera utile !