Question #239EasyState Management

Difference between future/ stream to emit the state in BLOC ?

#state#bloc#future#stream

Answer

Overview

In

text
flutter_bloc
, there are two key ways to emit state from a BLoC handler — using
text
async*
+
text
yield
(Stream-based, older API) or
text
async
+
text
emit()
(Future-based, modern API). Modern BLoC uses
text
emit()
.


Modern API — async + emit() (Recommended)

Since

text
flutter_bloc 7.2+
, handlers use
text
Future<void>
with
text
Emitter<State>
:

dart
class LoginBloc extends Bloc<LoginEvent, LoginState> {
  LoginBloc() : super(LoginInitial()) {
    on<LoginSubmitted>(_onLoginSubmitted);
  }

  // Future-based handler
  Future<void> _onLoginSubmitted(
    LoginSubmitted event,
    Emitter<LoginState> emit,
  ) async {
    emit(LoginLoading());

    try {
      final user = await authRepo.signIn(event.email, event.password);
      emit(LoginSuccess(user));
    } catch (e) {
      emit(LoginFailure(e.toString()));
    }
  }
}

Stream-based emit (for streaming data sources)

When your data source IS a stream (e.g., Firestore, MQTT), use

text
emit.forEach
or
text
emit.onEach
:

dart
// Listening to a Stream from Firestore
Future<void> _onWatchUsers(
  WatchUsersEvent event,
  Emitter<UsersState> emit,
) async {
  await emit.forEach<List<User>>(
    userRepository.watchUsers(), // Returns Stream<List<User>>
    onData: (users) => UsersLoaded(users),
    onError: (error, stackTrace) => UsersError(error.toString()),
  );
}
dart
// Using emit.onEach for side effects without state mapping
await emit.onEach<int>(
  timerStream,
  onData: (tick) => emit(TimerTick(tick)),
  onError: (e, _) => emit(TimerError()),
);

Key Differences

Feature
text
emit()
(Future)
text
emit.forEach
(Stream)
Use caseSingle async operation (API call)Continuous stream (Firestore, WS)
CancellationCancelled if new event firesAutomatically managed
Error handlingtry/catch
text
onError
callback
testabilityEasyRequires stream testing
BLoC version7.2+7.2+

Old API (Pre-7.2) — mapEventToState with yield

dart
// ❌ Old style — deprecated in newer flutter_bloc versions

Stream<LoginState> mapEventToState(LoginEvent event) async* {
  if (event is LoginSubmitted) {
    yield LoginLoading();
    try {
      final user = await authRepo.signIn(event.email, event.password);
      yield LoginSuccess(user);
    } catch (e) {
      yield LoginFailure(e.toString());
    }
  }
}

Real-World Pattern — Mixed

dart
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
  DashboardBloc() : super(DashboardInitial()) {
    on<LoadDashboard>(_onLoad);
    on<WatchNotifications>(_onWatchNotifications);
  }

  // One-time API call → use emit() directly
  Future<void> _onLoad(LoadDashboard event, Emitter<DashboardState> emit) async {
    emit(DashboardLoading());
    final data = await apiService.fetchDashboard();
    emit(DashboardLoaded(data));
  }

  // Ongoing stream → use emit.forEach
  Future<void> _onWatchNotifications(
    WatchNotifications event,
    Emitter<DashboardState> emit,
  ) async {
    await emit.forEach(
      notificationService.stream,
      onData: (notifications) => NotificationsUpdated(notifications),
    );
  }
}

Rule: Use

text
emit()
directly for one-shot operations (API calls, DB reads). Use
text
emit.forEach
/
text
emit.onEach
when your data source is a continuous
text
Stream
.