flutter_bloc_effects 0.2.0 copy "flutter_bloc_effects: ^0.2.0" to clipboard
flutter_bloc_effects: ^0.2.0 copied to clipboard

A lightweight companion to flutter_bloc that adds effect streams, listeners, and helpers for one-off UI side effects like navigation, snackbars, and dialogs.

flutter_bloc_effects #

Side‑effect handling on top of flutter_bloc, with dedicated effect streams and listeners.

flutter_bloc_effects adds a simple pattern for modeling one‑off UI side effects (navigation, snackbars, dialogs, toasts, etc.) on top of flutter_bloc.
It gives you:

  • BlocEffectEmitter mixin: add an effects stream and emitEffect to any Bloc or Cubit.
  • BlocEffectListener widget: listen to the effects stream and run UI callbacks (similar to BlocListener, but for effects).
  • MultiBlocEffectListener widget: compose multiple BlocEffectListeners without deep nesting.

Installation #

Add the package to your pubspec.yaml:

dependencies:
  flutter_bloc_effects: ^0.2.0

  flutter_bloc: ^9.1.1
  provider: ^6.1.5+1

Then import it:

import 'package:flutter_bloc_effects/flutter_bloc_effects.dart';

Motivation #

BlocListener is great for reacting to state changes, but some things are really events that should happen once:

  • Navigate to another page
  • Show a SnackBar / Dialog
  • Show a one‑time toast / banner

If you put those in your state, you risk re‑triggering them on rebuilds, restores, or when states are re‑emitted.
This package separates those into an effect stream that the UI can listen to exactly once.


Usage #

1. Add BlocEffectEmitter to your bloc or cubit #

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_effects/flutter_bloc_effects.dart';

sealed class LoginEvent {}
class LoginSubmitted extends LoginEvent {
  LoginSubmitted(this.username, this.password);
  final String username;
  final String password;
}

sealed class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState {}

/// Define your effects (navigation, snackbars, dialogs, etc.)
sealed class LoginEffect {}
class ShowLoginError extends LoginEffect {
  ShowLoginError(this.message);
  final String message;
}
class NavigateToHome extends LoginEffect {
  NavigateToHome(this.username);
  final String username;
}

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

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

    try {
      // TODO: perform login
      emit(LoginSuccess());
      emitEffect(NavigateToHome(event.username));
    } catch (e) {
      emit(LoginFailure());
      emitEffect(ShowLoginError('Login failed'));
    }
  }
}

The mixin:

  • Adds an effects stream: Stream<LoginEffect> get effects
  • Exposes emitEffect(LoginEffect effect) to push one‑off UI effects.

Using with Cubit

You can also use BlocEffectEmitter with Cubit:

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_bloc_effects/flutter_bloc_effects.dart';

class CounterState {
  const CounterState(this.count);
  final int count;
}

sealed class CounterEffect {}
class ShowMessage extends CounterEffect {
  ShowMessage(this.message);
  final String message;
}

class CounterCubit extends Cubit<CounterState>
    with BlocEffectEmitter<CounterState, CounterEffect> {
  CounterCubit() : super(const CounterState(0));

  void increment() {
    emit(CounterState(state.count + 1));
    
    // Emit an effect when count reaches 10
    if (state.count == 10) {
      emitEffect(ShowMessage('Congratulations! You reached 10!'));
    }
  }

  void decrement() {
    if (state.count > 0) {
      emit(CounterState(state.count - 1));
    } else {
      emitEffect(ShowMessage('Cannot go below 0'));
    }
  }
}

2. Listen to effects with BlocEffectListener #

class LoginPage extends StatelessWidget {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => LoginBloc(),
      child: BlocEffectListener<LoginBloc, LoginEffect>(
        listener: (context, effect) {
          switch (effect) {
            case ShowLoginError(:final message):
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text(message)),
              );
            case NavigateToHome(:final username):
              Navigator.of(context).pushReplacementNamed('/home');
              // username is available if needed
          }
        },
        child: const _LoginView(),
      ),
    );
  }
}

With listenWhen

You can filter which effects trigger the listener:

BlocEffectListener<LoginBloc, LoginEffect>(
  listenWhen: (effect) => effect is ShowLoginError,
  listener: (context, effect) {
    if (effect case ShowLoginError(:final message)) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(message)),
      );
    }
  },
  child: const _LoginView(),
);

3. Use MultiBlocEffectListener for multiple blocs #

Instead of deeply nesting multiple BlocEffectListeners:

BlocEffectListener<BlocA, BlocAEffect>(
  listener: (context, effect) {},
  child: BlocEffectListener<BlocB, BlocBEffect>(
    listener: (context, effect) {},
    child: BlocEffectListener<BlocC, BlocCEffect>(
      listener: (context, effect) {},
      child: ChildA(),
    ),
  ),
);

You can write:

MultiBlocEffectListener(
  listeners: [
    BlocEffectListener<BlocA, BlocAEffect>(
      listener: (context, effect) {},
    ),
    BlocEffectListener<BlocB, BlocBEffect>(
      listener: (context, effect) {},
    ),
    BlocEffectListener<BlocC, BlocCEffect>(
      listener: (context, effect) {},
    ),
  ],
  child: ChildA(),
);

MultiBlocEffectListener is built on provider’s MultiProvider and just flattens the listeners into a tree.


API Reference #

BlocEffectEmitter<State, Effect> #

Mixin for Bloc<Event, State> or Cubit<State> that adds:

  • Stream<Effect> get effects: the effect stream.
  • void emitEffect(Effect effect): push a new effect.

BlocEffectListener<B extends StateStreamable, E> #

Widget that:

  • Subscribes to B.effects (where B mixes in BlocEffectEmitter<_, E>).

  • Calls:

    void Function(BuildContext context, E effect)
    

    for each effect (optionally filtered by listenWhen).

Options:

  • bloc (optional): provide a specific bloc instance; otherwise it uses BlocProvider / context.read<B>().
  • listenWhen (optional): bool Function(E effect) to filter which effects should be delivered.
  • child: the subtree that will be rendered.

MultiBlocEffectListener #

Construction:

MultiBlocEffectListener({
  required List<SingleChildWidget> listeners,
  required Widget child,
})
  • listeners: list of BlocEffectListener (or other SingleChildWidget-based listeners).
  • child: the subtree that will receive all effects.

Notes #

  • This package is designed to complement flutter_bloc, not replace any of its patterns.
  • Effects are intentionally decoupled from state to make side‑effects explicit and one‑time.

License #

This project is licensed under the terms specified in the LICENSE file.

3
likes
160
points
47
downloads

Publisher

verified publisherretozu.com

Weekly Downloads

A lightweight companion to flutter_bloc that adds effect streams, listeners, and helpers for one-off UI side effects like navigation, snackbars, and dialogs.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter, flutter_bloc, provider

More

Packages that depend on flutter_bloc_effects