Что такое debounce в RxDart и для чего он используется?

Ответ

Debounce (также debounceTime) — это оператор в RxDart, который откладывает эмиссию событий из потока до тех пор, пока не пройдет заданный интервал времени без новых событий. Это паттерн "подавления дребезга".

Основное применение в Flutter:

  • Поиск с автодополнением
  • Валидация форм
  • Обработка скролла
  • Отслеживание изменений размера окна

Пример поиска с debounce:

import 'package:rxdart/rxdart.dart';

class SearchBloc {
  final _searchController = BehaviorSubject<String>();

  // Поток с debounce 500ms
  Stream<String> get searchResults => _searchController.stream
      .debounceTime(Duration(milliseconds: 500))
      .distinct() // Игнорируем повторяющиеся значения
      .where((query) => query.length >= 3); // Минимум 3 символа

  void updateQuery(String query) {
    _searchController.add(query);
  }

  void dispose() {
    _searchController.close();
  }
}

// Использование в виджете:
StreamBuilder<String>(
  stream: searchBloc.searchResults,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return _buildResults(snapshot.data!);
    }
    return CircularProgressIndicator();
  },
)

Отличие от throttle:

  • Debounce: ждет паузу, затем эмитит последнее значение
  • Throttle: эмитит первое значение, затем игнорирует новые в течение интервала

Практический пример с TextField:

TextField(
  onChanged: (value) {
    // Без debounce: запрос на каждый символ
    // С debounce: запрос только после остановки ввода на 500ms
    searchBloc.updateQuery(value);
  },
  decoration: InputDecoration(
    hintText: 'Search...',
    suffixIcon: Icon(Icons.search),
  ),
)

Настройка:

// С кастомной логикой
stream.debounce((event) => 
  TimerStream(event, Duration(milliseconds: event.length * 100))
)

Ответ 18+ 🔞

А, debounce! Ну это же классика, ёпта! Представь себе: ты сидишь, печатаешь в поисковой строке "как заставить Flutter не тормозить", и с каждым нажатием клавиши у тебя летит запрос на сервер. После буквы "к" — запрос, после "ка" — ещё один, "как" — третий... Сервер уже охуел, у него волнение ебать, он думает: "Мужик, ты определись уже, что тебе надо, я не успеваю!" Вот именно от этого ада и спасает debounce.

По-простому: это такой оператор, который говорит потоку: "Слушай, дружище, не несись как угорелый. Подожди, пока пользователь перестанет тыкать в клавиши, и только тогда мы отправим последний, самый полный запрос. А все промежуточные — нахуй не нужны".

Где это втыкать, чтобы жизнь мёдом не казалась:

  • Поиск с подсказками (автодополнение). Это прям его родной дом, хитрая жопа.
  • Проверка формы. Чтоб не орать на пользователя "НЕПРАВИЛЬНЫЙ EMAIL!!" после каждого первого символа. Пусть допечатает, тогда и проверим.
  • Скролл списка. Чтобы не пытаться подгружать новые данные, когда палец ещё не оторвался от экрана.
  • Изменение размера окна. Чтобы не пересчитывать layout триста раз в секунду, когда тащат за угол.

Смотри, как это выглядит в коде, чтоб было понятно:

import 'package:rxdart/rxdart.dart';

class SearchBloc {
  final _searchController = BehaviorSubject<String>();

  // Вот эта строка — магия. debounceTime говорит: "Жди 500 миллисекунд тишины".
  Stream<String> get searchResults => _searchController.stream
      .debounceTime(Duration(milliseconds: 500)) // <--- Волшебная палочка
      .distinct() // Игнорируем, если юзер ввёл "кот", стёр, и снова ввёл "кот"
      .where((query) => query.length >= 3); // Ищем только осмысленные запросы

  void updateQuery(String query) {
    _searchController.add(query); // Сюда летит каждый символ
  }

  void dispose() {
    _searchController.close();
  }
}

// А в виджете используем как обычно:
StreamBuilder<String>(
  stream: searchBloc.searchResults, // Сюда уже приходят "успокоенные" запросы
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return _buildResults(snapshot.data!); // Строим результат
    }
    return CircularProgressIndicator(); // Или крутимся
  },
)

Чем не throttle, спросишь? А я объясню, тут разница принципиальная, блядь.

  • Debounce (наш герой): Терпеливый, как удав. Сидит, ждёт паузу. Юзер печатает "к... о... т" — тишина 500 мс — только тогда debounce выплёвывает "кот". Если юзер продолжает печатать "котэ", таймер сбрасывается, и ждём снова.
  • Throttle: Скорострел. Получил первое значение "к" — сразу отправил. А потом следующие 500 мс тупо игнорирует всё, что в него летит. "о" — проигнорировано, "т" — проигнорировано. Через 500 мс, если пришло "котэ", отправит уже его.

Как прикрутить к полю ввода, чтоб работало:

TextField(
  onChanged: (value) {
    // Раньше тут был пиздец: запрос на КАЖДЫЙ символ. Сервер в ахуе.
    // Теперь с debounce в bloc'е — запрос полетит только когда юзер остановится.
    searchBloc.updateQuery(value);
  },
  decoration: InputDecoration(
    hintText: 'Search...',
    suffixIcon: Icon(Icons.search),
  ),
)

А если хочется извратиться? Пожалуйста, можно и кастомную логику запилить:

stream.debounce((event) => 
  // Например, делаем задержку зависящей от длины строки. Длинный запрос — больше времени на обдумывание.
  TimerStream(event, Duration(milliseconds: event.length * 100))
)

Короче, debounce — это как умный приятель, который держит тебя за руку и говорит: "Расслабься, чувак, не дёргайся. Подожди секунду, всё само устаканится". Без него в современном UI — просто пидарас шерстяной, доверия ебать ноль к интерфейсу.