Flutter Mobile Development · レッスン

BLoCのストリーム変換とイベントのデバウンス

並行性トランスフォーマーを適用し、受信イベントをスロットリング、デバウンス、順序制御します。

レッスン 2/413 ステップ

「BLoCのストリーム変換とイベントのデバウンス」はCoddyKit上の無料Flutter Mobile Developmentレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応の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 TextChanged event on every keystroke.
  • Scroll listeners emit dozens of ScrolledToBottom events per second.
  • Buttons can be tapped rapidly, firing duplicate SubmitPressed events.

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.

  • throttleTime with trailing: 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 TextChanged events 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 to restartable.
  • exhaustMap(mapper) — ignores new events while one is active. Equivalent to droppable.
  • asyncExpand(mapper) — runs sequentially; each event waits for the previous handler to finish. Equivalent to sequential.
  • flatMap(mapper) — runs all concurrently. Equivalent to concurrent.

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 a transformer that 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_concurrency provides concurrent, sequential, droppable, and restartable, mirroring RxDart's flatMap, asyncExpand, exhaustMap, and switchMap.
  • 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.

無料で開始

AI チューターと学ぶ Dart — 無料

ブラウザでリアルコードを書いて実行し、24/7 の AI チューターから瞬時にサポートを受け、ウェブまたはアプリで続きから学習できます。

コース
22
レッスン
88

よくある質問

「BLoCのストリーム変換とイベントのデバウンス」レッスンは無料ですか?

はい。「BLoCのストリーム変換とイベントのデバウンス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Flutter Mobile Developmentコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Flutter Mobile Developmentコースには全4レッスンが含まれています。

「BLoCのストリーム変換とイベントのデバウンス」で何を学びますか?

並行性トランスフォーマーを適用し、受信イベントをスロットリング、デバウンス、順序制御します。 ブラウザで直接実行するハンズオンコードでFlutter Mobile Developmentを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Flutter Mobile Developmentを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのFlutter Mobile Developmentは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「BLoCのストリーム変換とイベントのデバウンス」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このFlutter Mobile Developmentレッスンでコードを書いて実行できますか?

はい。すべてのFlutter Mobile Developmentレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. イベント、状態、CubitとBlocの選択
  2. BLoCのストリーム変換とイベントのデバウンス
  3. HydratedBlocによる状態の永続化
  4. bloc_testとMocktailによるBLoCのテスト
← Flutter Mobile Developmentに戻る