flutter_bloc_one_shot 0.1.0
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 #
Flutter widgets for bloc_one_shot. Provides SideEffectListener and SideEffectConsumer that mirror the BlocListener/BlocConsumer API you already know.
For test utilities (
blocEffectTest), seebloc_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),listenWhentakes 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.