BLoC의 스트림 변환기 및 이벤트 디바운싱
동시성 변환기를 적용해 들어오는 이벤트를 제한하고 디바운스하며 순서대로 처리합니다.
BLoC의 스트림 변환기 및 이벤트 디바운싱은(는) CoddyKit의 무료 Flutter Mobile Development 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Flutter Mobile Development 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Event Concurrency Matters
In flutter_bloc, every call to add(event) pushes an event into an internal stream. By default each event handler runs concurrently as events arrive. For most events that is fine, but some sources fire far too often:
- Search bars emit a
TextChangedevent on every keystroke. - Scroll listeners emit dozens of
ScrolledToBottomevents per second. - Buttons can be tapped rapidly, firing duplicate
SubmitPressedevents.
Firing a network request per keystroke wastes bandwidth and can show stale results. This lesson shows how to throttle, debounce, and sequence these events using stream transformers in BLoC.
The transformer Parameter
The modern on<Event> API accepts an optional transformer argument. A transformer is an EventTransformer<E> — a function that receives the incoming Stream<E> of events and a mapper, and returns a transformed stream.
By supplying a transformer you control how events of that type are processed: debounced, throttled, dropped, or run one-at-a-time. The signature is:
typedef EventTransformer<Event> = Stream<Event> Function(Stream<Event> events, EventMapper<Event> mapper);
You rarely write transformers by hand — the bloc_concurrency package provides the common ones.
// Registering a handler with a custom transformer
on<SearchTermChanged>(
_onSearchTermChanged,
transformer: (events, mapper) => events
.debounceTime(const Duration(milliseconds: 300))
.switchMap(mapper),
);Debounce: Wait for the Pause
Debouncing ignores events until a quiet period elapses. If the user is still typing, we keep resetting the timer; only when they pause for, say, 300ms do we process the latest event.
This is the right choice for a search box: you want to query the API once the user stops typing, not on every keystroke. Debounce drops all intermediate events and keeps only the final one in each burst.
- Reduces network calls dramatically.
- Trades a little latency (the debounce delay) for efficiency.
Debounce in Plain Dart
Before wiring it into BLoC, here is the debounce idea expressed as a runnable Dart program. We simulate keystrokes arriving with varying gaps and only print the term once typing pauses for 300ms.
The RxDart operator does this for you, but seeing the timer logic clarifies what debounce means.
import 'dart:async';
void main() async {
Timer? debounce;
String? pending;
final done = Completer<void>();
void onChanged(String term) {
pending = term;
debounce?.cancel();
debounce = Timer(const Duration(milliseconds: 300), () {
print('search: $pending');
if (pending == 'flutter') done.complete();
});
}
// Fast burst, then a pause, then more typing.
onChanged('f');
await Future.delayed(const Duration(milliseconds: 50));
onChanged('fl');
await Future.delayed(const Duration(milliseconds: 50));
onChanged('flu');
await Future.delayed(const Duration(milliseconds: 400)); // pause -> fires
onChanged('flutter');
await done.future;
}Throttle: One Per Window
Throttling lets the first event through, then ignores further events for a fixed window. Unlike debounce, throttle does not wait for a pause — it emits immediately and then rate-limits.
This suits infinite scroll and rapid button taps: you want to react to the first ScrolledToBottom right away, but ignore the storm of duplicates that follows during the same scroll gesture.
throttleTimewithtrailing: false= leading edge only (act now, then cool down).- Prevents duplicate page loads or double submissions.
// Infinite-scroll feed: react immediately, then cool down 500ms
on<FeedScrolledToEnd>(
_onScrolledToEnd,
transformer: (events, mapper) => events
.throttleTime(const Duration(milliseconds: 500))
.asyncExpand(mapper),
);bloc_concurrency Transformers
The official bloc_concurrency package ships four ready-made transformers that control how overlapping events are handled:
concurrent()— handlers run in parallel (the default).sequential()— events are queued and processed strictly one after another.droppable()— while a handler is running, new events of that type are discarded.restartable()— a new event cancels the in-flight handler and starts fresh.
These compose with timing operators: e.g. debounce first, then restartable() to cancel a stale search.
import 'package:bloc_concurrency/bloc_concurrency.dart';
// Submit button: ignore extra taps while the first submit is in flight
on<FormSubmitted>(_onSubmit, transformer: droppable());
// Saving steps that must run in order
on<StepSaved>(_onStepSaved, transformer: sequential());Combining Debounce with restartable
For a search BLoC the ideal recipe is debounce then restartable:
- Debounce the
TextChangedevents so you only query after the user pauses. - restartable() so that if a newer query arrives while the previous request is still loading, the stale request is cancelled and never overwrites fresh results.
Together they eliminate both wasted calls and out-of-order responses. You wrap bloc_concurrency's transformer with a small helper that applies debounceTime first.
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:rxdart/rxdart.dart';
EventTransformer<E> debounceRestartable<E>(Duration duration) {
return (events, mapper) =>
restartable<E>().call(events.debounceTime(duration), mapper);
}
// Usage inside a Bloc constructor:
on<SearchTermChanged>(
_onSearchTermChanged,
transformer: debounceRestartable(const Duration(milliseconds: 300)),
);A Full Search Bloc
Here is how the pieces fit together in a real SearchBloc. Note how the handler is async and can emit multiple states (loading, then results or error). Because the transformer is restartable, an outdated request stops emitting as soon as a newer term arrives.
class SearchBloc extends Bloc<SearchEvent, SearchState> {
final SearchRepository repo;
SearchBloc(this.repo) : super(const SearchState.initial()) {
on<SearchTermChanged>(
_onTermChanged,
transformer: debounceRestartable(const Duration(milliseconds: 300)),
);
}
Future<void> _onTermChanged(
SearchTermChanged event,
Emitter<SearchState> emit,
) async {
final term = event.term.trim();
if (term.isEmpty) {
emit(const SearchState.initial());
return;
}
emit(const SearchState.loading());
try {
final results = await repo.search(term);
emit(SearchState.success(results));
} catch (e) {
emit(SearchState.failure(e.toString()));
}
}
}Mapping Operators: switchMap vs asyncExpand
Inside a custom transformer you decide how the mapper is applied to each event:
switchMap(mapper)— cancels the previous inner stream when a new event arrives. Equivalent in spirit torestartable.exhaustMap(mapper)— ignores new events while one is active. Equivalent todroppable.asyncExpand(mapper)— runs sequentially; each event waits for the previous handler to finish. Equivalent tosequential.flatMap(mapper)— runs all concurrently. Equivalent toconcurrent.
Prefer the named bloc_concurrency transformers for clarity; reach for raw RxDart only when you must combine timing and mapping in one expression.
Throttle Sequence Demo in Dart
This runnable example models throttling without any framework. The first event in each 200ms window is processed; events arriving during the cooldown are dropped. Watch how only the leading events survive.
import 'dart:async';
void main() async {
DateTime? lastAccepted;
const window = Duration(milliseconds: 200);
final accepted = <int>[];
void onEvent(int id) {
final now = DateTime.now();
if (lastAccepted == null || now.difference(lastAccepted!) >= window) {
lastAccepted = now;
accepted.add(id);
}
}
// Fire 6 events; some land inside the cooldown window.
for (var i = 1; i <= 6; i++) {
onEvent(i);
await Future.delayed(const Duration(milliseconds: 90));
}
// Only leading-edge events per 200ms window are kept.
print('accepted: $accepted');
}Choosing the Right Strategy
Match the transformer to the user intent:
- Search / autocomplete: debounce + restartable — wait for the pause, cancel stale queries.
- Infinite scroll page load: throttle + droppable — load once, ignore the rest of the gesture.
- Form submit / payment: droppable — block duplicate submissions while one is in flight.
- Ordered writes (save steps, analytics): sequential — preserve order, no overlap.
Always add bloc_concurrency and rxdart to pubspec.yaml, and remember a transformer only affects the one event type it is registered on.
Quick Check
Test your understanding of the search-box scenario.
Recap
You learned how to control event concurrency in BLoC with stream transformers:
- The
on<Event>handler takes atransformerthat reshapes the incoming event stream. - Debounce waits for a pause (great for search); throttle acts on the leading edge then cools down (great for scroll and rapid taps).
bloc_concurrencyprovidesconcurrent,sequential,droppable, andrestartable, mirroring RxDart'sflatMap,asyncExpand,exhaustMap, andswitchMap.- The canonical search recipe is debounce then restartable; submits use droppable; ordered writes use sequential.
Pick the transformer that matches user intent, and remember it applies only to the event type it is registered on.
자주 묻는 질문
“BLoC의 스트림 변환기 및 이벤트 디바운싱” 강의는 무료인가요?
네 — “BLoC의 스트림 변환기 및 이벤트 디바운싱” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Flutter Mobile Development 강의 전체를 잠금 해제할 수 있습니다. Flutter Mobile Development 강의에는 총 4개의 강의가 포함되어 있습니다.
“BLoC의 스트림 변환기 및 이벤트 디바운싱”에서 뭘 배우나요?
동시성 변환기를 적용해 들어오는 이벤트를 제한하고 디바운스하며 순서대로 처리합니다. 브라우저에서 직접 실행하는 실습 코드로 Flutter Mobile Development을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Flutter Mobile Development을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Flutter Mobile Development은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“BLoC의 스트림 변환기 및 이벤트 디바운싱” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Flutter Mobile Development 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Flutter Mobile Development 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 이벤트, 상태 및 Cubit과 Bloc 중 선택하기
- BLoC의 스트림 변환기 및 이벤트 디바운싱
- HydratedBloc을 활용한 상태 저장
- bloc_test 및 Mocktail을 활용한 BLoC 테스트