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

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

flutter_bloc_one_shot #

pub package License: MIT

Flutter widgets for bloc_one_shot. Provides SideEffectListener and SideEffectConsumer that mirror the BlocListener/BlocConsumer API you already know.

For test utilities (blocEffectTest), see bloc_one_shot_test.

Installation #

dependencies:
  flutter_bloc_one_shot: ^0.1.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)

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
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
150
points
172
downloads

Publisher

verified publisheraltumstack.com

Weekly Downloads

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

Homepage
Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

bloc_one_shot, flutter, flutter_bloc

More

Packages that depend on flutter_bloc_one_shot