bloc_after_effect 0.1.0
bloc_after_effect: ^0.1.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.
example/lib/main.dart
import 'package:bloc_after_effect/bloc_after_effect.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// --- Events ---
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class SavePressed extends CounterEvent {}
// --- State ---
class CounterState {
const CounterState({this.count = 0, this.isSaving = false});
final int count;
final bool isSaving;
CounterState copyWith({int? count, bool? isSaving}) => CounterState(
count: count ?? this.count,
isSaving: isSaving ?? this.isSaving,
);
}
// --- Effects ---
abstract class CounterEffect {}
class ShowSavedSnackBar extends CounterEffect {
ShowSavedSnackBar(this.count);
final int count;
}
class ShowErrorDialog extends CounterEffect {
ShowErrorDialog(this.message);
final String message;
}
// --- Bloc ---
class CounterBloc
extends EffectBloc<CounterEvent, CounterState, CounterEffect> {
CounterBloc() : super(const CounterState()) {
on<Increment>((event, emit) {
emit(state.copyWith(count: state.count + 1));
});
on<SavePressed>((event, emit) async {
emit(state.copyWith(isSaving: true));
// Simulate async save
await Future<void>.delayed(const Duration(milliseconds: 500));
emit(state.copyWith(isSaving: false));
if (state.count > 10) {
emitEffect(ShowErrorDialog('Count too high to save!'));
} else {
emitEffect(ShowSavedSnackBar(state.count));
}
});
}
}
// --- App ---
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'EffectBloc Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: BlocProvider(
create: (_) => CounterBloc(),
child: const CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return BlocEffectListener<CounterBloc, CounterEffect>(
listener: (context, effect) {
if (effect is ShowSavedSnackBar) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Saved! Count was ${effect.count}')),
);
} else if (effect is ShowErrorDialog) {
showDialog<void>(
context: context,
builder: (_) => AlertDialog(
title: const Text('Error'),
content: Text(effect.message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
},
child: Scaffold(
appBar: AppBar(title: const Text('EffectBloc Example')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'${state.count}',
style: Theme.of(context).textTheme.displayLarge,
),
const SizedBox(height: 16),
if (state.isSaving)
const CircularProgressIndicator()
else
FilledButton(
onPressed: () =>
context.read<CounterBloc>().add(SavePressed()),
child: const Text('Save'),
),
],
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: const Icon(Icons.add),
),
),
);
}
}