bloc_plus 0.2.1
bloc_plus: ^0.2.1 copied to clipboard
Extensions for flutter_bloc focused on ergonomics and safety.
bloc_plus #
bloc_plus extends flutter_bloc with ergonomic widgets, reusable policies,
cooperative async helpers, and explicit effect handling primitives.
Features #
BlocBuilderWithBloc,BlocListenerWithBloc,BlocConsumerWithBloc,BlocSelectorWithBloc,BlocConsumerWithEffects- BuildContext extensions:
readOrNull<B>()watchOrNull<B>()selectOrNull<B, S, T>(selector)withBloc<B, R>(fn)
- Reusable policies:
- Rebuild:
distinct,onChange,onChangeBy,whenRebuild,always,never - Listen:
distinctListen,onChangeListen,onChangeListenBy,whenListen,alwaysListen,neverListen - Composition:
and,or,not
- Rebuild:
- Async safety:
SafeEmitMixinCancellationTokenRestartableTaskRestartableTasksMixin
- Effects:
HasEffectsEffectListenerMultiEffectListenereffectWhenfiltering
Recent delivery notes are tracked in
docs/library_improvement_plan.md.
Getting started #
Add dependency:
dependencies:
bloc_plus: ^0.2.1
Run the example app:
cd example
flutter pub get
flutter run
Usage #
UI widgets with bloc in callback #
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilderWithBloc<CounterCubit, int>(
builder: (context, bloc, state) {
return Text('$state');
},
);
}
}
Null-safe context access #
void tryIncrement(BuildContext context) {
final counterCubit = context.readOrNull<CounterCubit>();
counterCubit?.increment();
}
Combined state and effects #
class CounterView extends StatelessWidget {
const CounterView({super.key});
@override
Widget build(BuildContext context) {
return BlocConsumerWithEffects<CounterCubit, CounterState, String>(
effectWhen: (effect) => effect.startsWith('snack:'),
listener: (context, bloc, state) {},
onEffect: (context, bloc, effect) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(effect)),
);
},
builder: (context, bloc, state) {
return Text('${state.count}');
},
);
}
}
Policies #
final evenOnlyPolicy = distinct<MyState>().and(
whenRebuild<MyState>((previous, current) => current.count.isEven),
);
final listPolicy = onChangeBy<MyState, List<int>>(
(state) => state.items,
equals: _listEquals,
);
Async safety #
class SearchCubit extends Cubit<SearchState>
with SafeEmitMixin<SearchState>, RestartableTasksMixin<SearchState> {
SearchCubit() : super(const SearchState());
Future<void> loadPreview(String query) async {
safeEmit(state.copyWith(isLoading: true));
final result = await runLatest<String>('preview', () async {
return repository.fetchPreview(query);
});
if (result == null) return;
safeEmit(state.copyWith(isLoading: false, result: result));
}
}
Effects #
MultiEffectListener(
listeners: [
EffectListener<AuthCubit, AuthState, String>(
effectWhen: (effect) => effect.startsWith('snack:'),
onEffect: (context, effect) {},
),
EffectListener<AuthCubit, AuthState, String>(
effectWhen: (effect) => effect.startsWith('dialog:'),
onEffect: (context, effect) {},
),
],
child: const AuthView(),
)