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

Flutter widgets for bloc_one_shot. Provides SideEffectProvider, SideEffectListener, and SideEffectConsumer widgets that mirror BlocProvider/BlocListener/BlocConsumer API conventions.

flutter_bloc_one_shot #

pub package License: MIT

Flutter widgets for bloc_one_shot. Provides SideEffectProvider, SideEffectListener, SideEffectConsumer, and MultipleSideEffectListener that mirror the BlocProvider/BlocListener/BlocConsumer/MultiBlocListener API you already know.

For test utilities (blocEffectTest), see bloc_one_shot_test.

Installation #

dependencies:
  flutter_bloc_one_shot: ^0.2.0

This package re-exports bloc_one_shot, so you only need a single dependency for both core and widgets.

Quick Start #

1. Define effects and add the mixin #

// Effects — ephemeral, one-shot actions
sealed class LoginEffect {}
class NavigateToHome extends LoginEffect {}
class ShowErrorSnackbar extends LoginEffect {
  final String message;
  ShowErrorSnackbar(this.message);
}

// Cubit with SideEffectMixin
class LoginCubit extends Cubit<LoginState>
    with SideEffectMixin<LoginState, LoginEffect> {

  Future<void> login(String email, String password) async {
    emit(LoginLoading());
    try {
      await authRepo.login(email, password);
      emit(LoginSuccess());
      emitEffect(NavigateToHome());           // Side effect
    } catch (e) {
      emit(LoginInitial());
      emitEffect(ShowErrorSnackbar(e.toString()));
    }
  }
}

2. Use widgets in the UI #

See SideEffectListener and SideEffectConsumer below.

Widgets #

SideEffectListener #

Listens to side effects without rebuilding the widget tree. Use this for navigation, snackbars, dialogs, and other fire-and-forget actions.

SideEffectListener<LoginCubit, LoginEffect>(
  listener: (context, effect) {
    switch (effect) {
      case NavigateToHome():
        Navigator.of(context).pushReplacementNamed('/home');
      case ShowErrorSnackbar(:final message):
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(message)),
        );
    }
  },
  child: LoginForm(),
)

Parameters

Parameter Type Required Description
listener void Function(BuildContext, E) Yes Called once per effect
bloc B? No If omitted, resolved via context.read<B>()
listenWhen bool Function(E)? No Filter — listener only fires when this returns true
child Widget? No Child widget

Bloc resolution

If bloc is not provided, SideEffectListener resolves it from the widget tree using context.read<B>(). This means you can provide your Bloc via BlocProvider as usual:

BlocProvider(
  create: (_) => LoginCubit(),
  child: SideEffectListener<LoginCubit, LoginEffect>(
    listener: (context, effect) { /* ... */ },
    child: LoginForm(),
  ),
)

Filtering effects

Use listenWhen to filter which effects trigger the listener:

SideEffectListener<LoginCubit, LoginEffect>(
  listenWhen: (effect) => effect is NavigateToHome,
  listener: (context, effect) {
    // Only called for NavigateToHome effects
    Navigator.of(context).pushReplacementNamed('/home');
  },
  child: LoginForm(),
)

Note: Unlike BlocListener's condition which receives (previous, current), listenWhen takes a single effect. Effects are ephemeral — there is no "previous" effect.

Buffered delivery

Effects emitted before the widget subscribes (e.g. during Bloc initialization) are buffered and delivered when the listener mounts:

// Effect emitted before widget tree is built
final cubit = LoginCubit();
cubit.emitEffect(ShowErrorSnackbar('Session expired'));

// Later, when the widget mounts, it receives the buffered effect
SideEffectListener<LoginCubit, LoginEffect>(
  bloc: cubit,
  listener: (context, effect) {
    // 'Session expired' is delivered here
  },
  child: LoginForm(),
)

Re-subscription safety

When a widget unmounts (e.g. during navigation) and remounts later, any effects emitted during the gap are buffered and delivered to the new listener:

Widget mounts     →  effect A (delivered)  →  Widget unmounts
Widget remounts   →  effect B (was buffered, now delivered)  →  effect C (delivered live)

SideEffectProvider #

Combines BlocProvider and SideEffectListener into a single widget. Use this when you need to create (or provide) a Bloc and listen to its effects in one step.

SideEffectProvider<LoginCubit, LoginEffect>(
  create: (_) => LoginCubit(),
  listener: (context, effect) {
    switch (effect) {
      case NavigateToHome():
        Navigator.of(context).pushReplacementNamed('/home');
      case ShowErrorSnackbar(:final message):
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(message)),
        );
    }
  },
  child: LoginForm(),
)

This is equivalent to:

BlocProvider(
  create: (_) => LoginCubit(),
  child: SideEffectListener<LoginCubit, LoginEffect>(
    listener: (context, effect) { /* ... */ },
    child: LoginForm(),
  ),
)

.value constructor

Use SideEffectProvider.value to provide an existing Bloc instance (without closing it on dispose):

SideEffectProvider<LoginCubit, LoginEffect>.value(
  value: existingCubit,
  listener: (context, effect) { /* ... */ },
  child: LoginForm(),
)

Parameters

Parameter Type Required Description
create B Function(BuildContext) Yes* Creates the Bloc (*default constructor only)
value B Yes* Existing Bloc instance (*.value constructor only)
listener void Function(BuildContext, E) Yes Called once per effect
listenWhen bool Function(E)? No Filter — listener only fires when this returns true
lazy bool No Whether to lazily create the Bloc (default: true)
child Widget? No Child widget

MultipleSideEffectListener #

Merges multiple SideEffectListener widgets into a single widget tree, avoiding deeply nested listeners. Mirrors MultiBlocListener from flutter_bloc.

MultipleSideEffectListener(
  listeners: [
    SideEffectListener<AuthBloc, AuthEffect>(
      listener: (context, effect) {
        // Handle auth effects (e.g. session expired → navigate to login)
      },
    ),
    SideEffectListener<NotificationBloc, NotificationEffect>(
      listener: (context, effect) {
        // Handle notification effects (e.g. show snackbar)
      },
    ),
  ],
  child: HomePage(),
)

This is equivalent to nesting them manually:

// Without MultipleSideEffectListener (nesting hell)
SideEffectListener<AuthBloc, AuthEffect>(
  listener: (context, effect) { /* ... */ },
  child: SideEffectListener<NotificationBloc, NotificationEffect>(
    listener: (context, effect) { /* ... */ },
    child: HomePage(),
  ),
)

Parameters

Parameter Type Required Description
listeners List<SingleChildWidget> Yes List of SideEffectListener widgets
child Widget Yes Child widget rendered below all listeners

All SideEffectListener features (listenWhen, bloc, context resolution) work inside MultipleSideEffectListener.


SideEffectConsumer #

Combines BlocBuilder (for state) and SideEffectListener (for effects) in a single widget. Equivalent to nesting a SideEffectListener around a BlocBuilder.

SideEffectConsumer<LoginCubit, LoginState, LoginEffect>(
  builder: (context, state) {
    return switch (state) {
      LoginLoading() => const Center(child: CircularProgressIndicator()),
      LoginSuccess() => const Center(child: Text('Welcome!')),
      _ => LoginForm(),
    };
  },
  listener: (context, effect) {
    switch (effect) {
      case NavigateToHome():
        Navigator.of(context).pushReplacementNamed('/home');
      case ShowErrorSnackbar(:final message):
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(message)),
        );
    }
  },
)

Parameters

Parameter Type Required Description
builder Widget Function(BuildContext, S) Yes Builds UI from state
listener void Function(BuildContext, E) Yes Called once per effect
bloc B? No If omitted, resolved via context.read<B>()
buildWhen bool Function(S, S)? No State filter — same as BlocBuilder.buildWhen
listenWhen bool Function(E)? No Effect filter

Independent filters

buildWhen and listenWhen operate independently — you can filter state rebuilds without affecting effect delivery, and vice versa:

SideEffectConsumer<CounterCubit, int, CounterEffect>(
  // Only rebuild when count is even
  buildWhen: (previous, current) => current.isEven,
  // Only listen to overflow effects
  listenWhen: (effect) => effect is ShowOverflow,
  builder: (context, count) => Text('$count'),
  listener: (context, effect) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Counter overflow!')),
    );
  },
)

When to Use What #

Scenario Widget
React to effects only (navigation, snackbar) SideEffectListener
Create/provide a Bloc + listen to effects SideEffectProvider
React to effects from multiple blocs MultipleSideEffectListener
Build UI from state only BlocBuilder (from flutter_bloc)
Build UI from state + react to effects SideEffectConsumer
Listen to state changes (not effects) BlocListener (from flutter_bloc)

Global Effect Observer #

Set up a global observer at app startup to log, track, or report all effects:

void main() {
  EffectObserver.instance = AppEffectObserver();
  runApp(MyApp());
}

class AppEffectObserver extends EffectObserver {
  @override
  void onEffect(BlocBase bloc, Object? effect) {
    debugPrint('[Effect] ${bloc.runtimeType} -> $effect');
    // Also: send to analytics, attach as Sentry breadcrumb, etc.
  }
}

Full Example #

// main.dart
void main() {
  EffectObserver.instance = LoggingEffectObserver();
  runApp(
    MaterialApp(
      home: BlocProvider(
        create: (_) => LoginCubit(),
        child: const LoginPage(),
      ),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SideEffectConsumer<LoginCubit, LoginState, LoginEffect>(
        listener: (context, effect) {
          switch (effect) {
            case NavigateToHome():
              Navigator.of(context).pushReplacementNamed('/home');
            case ShowErrorSnackbar(:final message):
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text(message)),
              );
          }
        },
        builder: (context, state) {
          return switch (state) {
            LoginLoading() => const Center(
                child: CircularProgressIndicator(),
              ),
            _ => LoginForm(
                onSubmit: (email, password) {
                  context.read<LoginCubit>().login(email, password);
                },
              ),
          };
        },
      ),
    );
  }
}

License #

MIT — see LICENSE for details.

1
likes
140
points
172
downloads

Publisher

verified publisheraltumstack.com

Weekly Downloads

Flutter widgets for bloc_one_shot. Provides SideEffectProvider, SideEffectListener, and SideEffectConsumer widgets that mirror BlocProvider/BlocListener/BlocConsumer API conventions.

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

bloc_one_shot, flutter, flutter_bloc, provider

More

Packages that depend on flutter_bloc_one_shot