bloc_after_effect 0.3.0
bloc_after_effect: ^0.3.0 copied to clipboard
An extension of Bloc that adds a one-shot UI side-effect stream alongside state, for navigation, dialogs, snackbars, and other fire-and-forget actions.
bloc_after_effect #
An extension of bloc that adds a one-shot UI side-effect stream alongside state — for navigation, dialogs, snackbars, and other fire-and-forget UI actions that don't belong in State.
- Bloc decides what happened:
emitEffect(ShowErrorDialog('Save failed')) - UI decides how to show it:
showDialog(context: context, ...)
Install #
flutter pub add bloc_after_effect
Usage #
Define your effects, extend EffectBloc, and emit effects from event handlers:
abstract class ProfileEffect {}
class ShowErrorSnackBar extends ProfileEffect {
ShowErrorSnackBar(this.message);
final String message;
}
class ProfileBloc extends EffectBloc<ProfileEvent, ProfileState, ProfileEffect> {
ProfileBloc() : super(ProfileState.initial()) {
on<SavePressed>((event, emit) async {
try {
await repo.save();
emitEffect(ShowSuccessSnackBar('Saved'));
} catch (_) {
emitEffect(ShowErrorSnackBar('Save failed'));
}
});
}
}
Widgets #
BlocEffectListener #
Subscribes to EffectBloc.effects and calls a callback for each effect. Does not build any UI — just wraps a child.
BlocEffectListener<ProfileBloc, ProfileEffect>(
listener: (context, effect) {
if (effect is ShowErrorSnackBar) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(effect.message)),
);
} else if (effect is NavigateToEdit) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => EditPage(userId: effect.userId)),
);
}
},
child: ProfilePageBody(),
)
Parameters:
listener(required) —void Function(BuildContext, E)callback per effectchild(required) — child widgetbloc(optional) — if omitted, resolved viacontext.read<B>()
BlocEffectBuilder #
Combines BlocBuilder with an effect listener. Rebuilds the UI from state and reacts to effects. This is the main widget for most use cases.
BlocEffectBuilder<ProfileBloc, ProfileState, ProfileEffect>(
effectListener: (context, effect) {
if (effect is NavigateToEdit) Navigator.of(context).push(...);
},
buildWhen: (prev, curr) => prev.profile != curr.profile,
builder: (context, state) => ProfileView(state: state),
)
Parameters:
builder(required) —Widget Function(BuildContext, S)builds UI from stateeffectListener(required) —void Function(BuildContext, E)callback per effectbloc(optional) — if omitted, resolved viacontext.read<B>()buildWhen(optional) — filter for rebuilds
BlocEffectConsumer (migration only) #
Not recommended for new code. Use
BlocEffectBuilderinstead.
A migration helper that combines BlocBuilder, an optional state listener, and an optional effect listener into a single widget. Use it when gradually transitioning from flutter_bloc's BlocConsumer to effects — it lets you keep the existing state listener while you move one-shot side-effects into proper Effects one callback at a time.
BlocEffectConsumer<ProfileBloc, ProfileState, ProfileEffect>(
effectListener: (context, effect) {
if (effect is NavigateToEdit) Navigator.of(context).push(...);
},
listener: (context, state) { // optional
if (state.error != null) logger.error(state.error);
},
listenWhen: (prev, curr) => prev.error != curr.error, // optional
buildWhen: (prev, curr) => prev.profile != curr.profile, // optional
builder: (context, state) => ProfileView(state: state),
)
Parameters:
builder(required) —Widget Function(BuildContext, S)builds UI from statelistener(optional) —void Function(BuildContext, S)state change callbackeffectListener(optional) —void Function(BuildContext, E)callback per effectbloc(optional) — if omitted, resolved viacontext.read<B>()buildWhen(optional) — filter for rebuildslistenWhen(optional) — filter for state listener
Migration from flutter_bloc #
From BlocBuilder #
If you only need to add effect handling to an existing BlocBuilder, use BlocEffectBuilder:
// Before
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => CounterView(state: state),
)
// After
BlocEffectBuilder<CounterBloc, CounterState, CounterEffect>(
effectListener: (context, effect) { /* handle effects */ },
builder: (context, state) => CounterView(state: state),
)
From BlocListener + BlocBuilder #
Replace the pair with a single BlocEffectBuilder and move one-shot side-effects from the state listener into effectListener:
// Before
BlocListener<CounterBloc, CounterState>(
listener: (context, state) {
if (state.saved) ScaffoldMessenger.of(context).showSnackBar(...);
},
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => CounterView(state: state),
),
)
// After — side-effects are now proper Effects, not state
BlocEffectBuilder<CounterBloc, CounterState, CounterEffect>(
effectListener: (context, effect) {
if (effect is ShowSavedSnackBar) {
ScaffoldMessenger.of(context).showSnackBar(...);
}
},
builder: (context, state) => CounterView(state: state),
)
From BlocConsumer (gradual migration) #
If you still need a state listener during migration, temporarily use BlocEffectConsumer. Once all one-shot side-effects have been moved to Effects, replace it with BlocEffectBuilder:
// Before
BlocEffectListener<CounterBloc, CounterEffect>(
listener: (context, effect) { /* show snackbar */ },
child: BlocConsumer<CounterBloc, CounterState>(
listener: (context, state) { /* log */ },
builder: (context, state) => CounterView(state: state),
),
)
// After
BlocEffectConsumer<CounterBloc, CounterState, CounterEffect>(
effectListener: (context, effect) { /* show snackbar */ },
listener: (context, state) { /* log */ },
builder: (context, state) => CounterView(state: state),
)
Migration steps:
- Replace
BlocConsumer<B, S>withBlocEffectConsumer<B, S, E>(add the Effect type parameter). - Unwrap the surrounding
BlocEffectListener— move itslistenerintoeffectListener. buildWhen/listenWhencarry over unchanged.
State vs Effect #
| State | Effect | |
|---|---|---|
| Stored | Yes, in the Bloc | No, fire-and-forget |
| Replayed on rebuild | Yes | No |
| Examples | isLoading, items, error |
showDialog, navigate, showSnackBar |
If the UI must render it on every build — it's State. If the UI must do it once — it's an Effect.
The effects stream is a broadcast stream: effects emitted with no active listener are dropped.
Documentation & example #
- Full documentation: doc/bloc_after_effect.md
- Runnable example: example/
License #
MIT — see LICENSE.