bloc_small 2.2.0
bloc_small: ^2.2.0 copied to clipboard
An easy-to-use Flutter package offering a streamlined BLoC pattern implementation for intuitive and efficient state management in Flutter apps.
bloc_small #
A lightweight and simplified BLoC (Business Logic Component) library for Flutter, built on top of the flutter_bloc package. bloc_small simplifies state management, making it more intuitive while maintaining the core benefits of the BLoC pattern.
- Features
- Installation
- Core Concepts
- Basic Usage
- Using Cubit
- Auto Route Integration
- Advanced Usage
- Best Practices
- API Reference
- Contributing
- License
Features #
- Simplified BLoC pattern implementation using flutter_bloc
- Easy-to-use reactive programming with Dart streams
- Automatic resource management and disposal
- Integration with GetIt for dependency injection
- Support for loading states and error handling
- Streamlined state updates and event handling
- Built-in support for asynchronous operations
- Seamless integration with freezed for immutable state and event classes
- Enhanced
ReactiveSubjectwith powerful stream transformation methods rxdart - Optional integration with auto_route for type-safe navigation, including:
- Platform-adaptive transitions
- Deep linking support
- Nested navigation
- Compile-time route verification
- Clean and consistent navigation API
Installation #
Add bloc_small to your pubspec.yaml file:
dependencies:
bloc_small:
injectable:
freezed:
dev_dependencies:
injectable_generator:
build_runner:
freezed_annotation:
Then run:
flutter pub run build_runner build --delete-conflicting-outputs
Note: Remember to run the build runner command every time you make changes to files that use Freezed or Injectable annotations. This generates the necessary code for your BLoCs, events, and states.
Core Concepts #
| Class | Description | Base Class | Purpose |
|---|---|---|---|
MainBloc |
Foundation for BLoC pattern implementation | MainBlocDelegate |
Handles events and emits states |
MainCubit |
Simplified state management alternative | MainCubitDelegate |
Direct state mutations without events |
MainBlocEvent |
Base class for all events | - | Triggers state changes in BLoCs |
MainBlocState |
Base class for all states | - | Represents application state |
CommonBloc |
Global functionality manager | - | Manages loading states and common features |
BLoC Pattern:
@injectable
class CounterBloc extends MainBloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState.initial()) {
on<Increment>(_onIncrement);
}
}
Cubit Pattern:
@injectable
class CounterCubit extends MainCubit<CounterState> {
CounterCubit() : super(const CounterState.initial());
void increment() => emit(state.copyWith(count: state.count + 1));
}
Basic Usage #
1. Set up Dependency Injection #
Use GetIt and Injectable for dependency injection:
@InjectableInit()
void configureInjectionApp() {
// Step 1: Register core dependencies from package bloc_small
getIt.registerCore();
// Step 2: Register your app dependencies
getIt.init();
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
configureInjectionApp(); // Initialize both core and app dependencies
runApp(MyApp());
}
Important: The
RegisterModuleclass with theCommonBlocsingleton is essential. If you don't include this Dependency Injection setup, your app will encounter errors. TheCommonBlocis used internally bybloc_smallfor managing common functionalities like loading states across your app.
Make sure to call configureInjectionApp() before running your app
2. Define your BLoC #
@injectable
class CountBloc extends MainBloc<CountEvent, CountState> {
CountBloc() : super(const CountState.initial()) {
on<Increment>(_onIncrementCounter);
on<Decrement>(_onDecrementCounter);
}
Future<void> _onIncrementCounter(Increment event, Emitter<CountState> emit) async {
await blocCatch(actions: () async {
await Future.delayed(Duration(seconds: 2));
emit(state.copyWith(count: state.count + 1));
});
}
void _onDecrementCounter(Decrement event, Emitter<CountState> emit) {
if (state.count > 0) emit(state.copyWith(count: state.count - 1));
}
}
3. Define Events and States with Freezed #
abstract class CountEvent extends MainBlocEvent {
const CountEvent._();
}
@freezed
class Increment extends CountEvent with _$Increment {
const factory Increment() = _Increment;
}
@freezed
class Decrement extends CountEvent with _$Decrement {
const factory Decrement() = _Decrement;
}
@freezed
class CountState extends MainBlocState with $CountState {
const factory CountState.initial({@Default(0) int count}) = Initial;
}
4. Create a StatefulWidget with BaseBlocPageState #
class MyHomePage extends StatefulWidget {
MyHomePage({required this.title});
final String title;
@override
MyHomePageState createState() => _MyHomePageState();
}
class MyHomePageState extends BaseBlocPageState<MyHomePage, CountBloc> {
@override
Widget buildPage(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => bloc.add(Increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => bloc.add(Decrement()),
tooltip: 'decrement',
child: Icon(Icons.remove),
),
],
),
);
}
}
Using Cubit (Alternative Approach) #
If you prefer a simpler approach without events, you can use Cubit instead of BLoC:
1. Define your Cubit #
@injectable
class CounterCubit extends MainCubit<CounterState> {
CounterCubit() : super(const CounterState());
Future<void> increment() async {
await cubitCatch(
actions: () async {
await Future.delayed(Duration(seconds: 1));
emit(state.copyWith(count: state.count + 1));
},
keyLoading: 'increment',
);
}
void decrement() {
if (state.count > 0) {
emit(state.copyWith(count: state.count - 1));
}
}
}
2. Define Cubit State with Freezed #
@freezed
class CountState extends MainBlocState with $CountState {
const factory CountState.initial({@Default(0) int count}) = Initial;
}
3. Create a StatefulWidget with BaseCubitPageState #
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends BaseCubitPageState<CounterPage, CountCubit> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
loadingKey: 'increment',
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountCubit, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => bloc.increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 16),
FloatingActionButton(
onPressed: () => bloc.decrement(),
child: const Icon(Icons.remove),
),
],
),
),
);
}
}
| Feature | BLoC | Cubit |
|---|---|---|
| Event Handling | Uses events | Direct method calls |
| Base Class | MainBloc |
MainCubit |
| Widget State | BaseBlocPageState |
BaseCubitPageState |
| Complexity | More boilerplate | Simpler implementation |
| Use Case | Complex state logic | Simple state changes |
Using StatelessWidget #
bloc_small also supports StatelessWidget with similar functionality to StatefulWidget implementations.
1. Using BLoC with StatelessWidget #
class MyHomePage extends BaseBlocPage<CountBloc> {
const MyHomePage({super.key});
@override
Widget buildPage(BuildContext context, CountBloc bloc) {
return buildLoadingOverlay(
context,
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(const Increment()),
child: const Icon(Icons.add),
),
),
);
}
}
2. Using Cubit with StatelessWidget #
class CounterPage extends BaseCubitPage<CountCubit> {
const CounterPage({super.key});
@override
Widget buildPage(BuildContext context, CountCubit cubit) {
return buildLoadingOverlay(
context,
child: Scaffold(
appBar: AppBar(title: const Text('Counter Example')),
body: Center(
child: BlocBuilder<CountCubit, CountState>(
builder: (context, state) {
return Text(
'${state.count}',
style: const TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => cubit.increment(),
child: const Icon(Icons.add),
),
),
);
}
}
Key Features of StatelessWidget Implementation #
| Feature | Description |
|---|---|
| Base Classes | BaseBlocPage and BaseCubitPage |
| DI Support | Automatic dependency injection |
| Loading Management | Built-in loading overlay support |
| Navigation | Integrated navigation capabilities |
| State Management | Full BLoC/Cubit pattern support |
When to Use StatelessWidget vs StatefulWidget #
| Use Case | Widget Type |
|---|---|
| Simple UI without local state | StatelessWidget |
| Complex UI with local state | StatefulWidget |
| Performance-critical screens | StatelessWidget |
| Screens with lifecycle needs | StatefulWidget |
If you want to use Auto Route Integration #
- Add auto_route to your dependencies:
dependencies:
auto_route:
dev_dependencies:
auto_route_generator:
- Create your router:
@AutoRouterConfig()
@LazySingleton()
class AppRouter extends BaseAppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: HomeRoute.page, initial: true),
AutoRoute(page: SettingsRoute.page),
];
}
- Register Router in Dependency Injection
Register your router during app initialization:
void configureInjectionApp() {
// Register AppRouter (recommended)
getIt.registerAppRouter<AppRouter>(AppRouter(), enableNavigationLogs: true);
// Register other dependencies
getIt.registerCore();
getIt.init();
}
- Setup MaterialApp
Configure your MaterialApp to use auto_route:
class MyApp extends StatelessWidget {
final _router = getIt<AppRouter>();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _router.config(),
// ... other MaterialApp properties
);
}
}
- Navigation
Use the provided AppNavigator for consistent navigation across your app:
class MyWidget extends StatelessWidget {
final navigator = getIt.getNavigator();
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
Use the bloc and cubit provided AppNavigator:
class _MyWidgetState extends BaseBlocPageState<MyWidget, MyWidgetBloc> {
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
class _MyWidgetState extends BaseCubitPageState<MyWidget, MyWidgetCubit> {
void _onNavigate() {
navigator?.push(const HomeRoute());
}
}
// Basic navigation
navigator?.push(const HomeRoute());
// Navigation with parameters
navigator?.push(UserRoute(userId: 123));
Best Practices
- Always register AppRouter in your DI setup
- Use the type-safe methods provided by AppNavigator
- Handle potential initialization errors
- Consider creating a navigation service class for complex apps
Features
- Type-safe routing
- Automatic route generation
- Platform-adaptive transitions
- Deep linking support
- Nested navigation
- Integration with dependency injection
Benefits
- Compile-time route verification
- Clean and consistent navigation API
- Reduced boilerplate code
- Better development experience
- Easy integration with bloc_small package
For more complex navigation scenarios and detailed documentation, refer to the auto_route documentation.
Note: While you can use any navigation solution, this package is optimized to work with auto_route. The integration between auto_route and this package provides
If you choose a different navigation solution, you'll need to implement your own navigation registration strategy.
Advanced Usage #
Handling Loading States #
bloc_small provides a convenient way to manage loading states and display loading indicators using the CommonBloc and the buildLoadingOverlay method.
Using buildLoadingOverlay
When using BaseBlocPageState, you can easily add a loading overlay to your entire page:
class MyHomePageState extends BaseBlocPageState<MyHomePage, CountBloc> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
child: Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
BlocBuilder<CountBloc, CountState>(
builder: (context, state) {
return Text('${state.count}');
},
)
],
),
),
floatingActionButton: Wrap(
spacing: 5,
children: [
FloatingActionButton(
onPressed: () => bloc.add(Increment()),
tooltip: 'Increment',
child: Icon(Icons.add),
),
FloatingActionButton(
onPressed: () => bloc.add(Decrement()),
tooltip: 'decrement',
child: Icon(Icons.remove),
),
],
),
),
);
}
}
The buildLoadingOverlay method wraps your page content and automatically displays a loading indicator when the loading state is active.
Customizing the Loading Overlay
You can customize the loading overlay by providing a loadingWidget and specifying a loadingKey:
buildLoadingOverlay(
child: YourPageContent(),
loadingWidget: YourCustomLoadingWidget(),
loadingKey:'customLoadingKey'
)
Activating the Loading State
To show or hide the loading overlay, use the showLoading and hideLoading methods in your BLoC:
class YourBloc extends MainBloc<YourEvent, YourState> {
Future<void> someAsyncOperation() async {
showLoading(); // or showLoading(key: 'customLoadingKey');
try {
// Perform async operation
} finally {
hideLoading(); // or hideLoading(key: 'customLoadingKey');
}
}
}
This approach provides a clean and consistent way to handle loading states across your application, with the flexibility to use global or component-specific loading indicators.
Error Handling #
Use the blocCatch method in your BLoC to handle errors:
await blocCatch(
actions: () async {
// Your async logic here
throw Exception('Something went wrong');
},
onError: (error) {
// Handle the error
print('Error occurred: $error');
}
);
ReactiveSubject #
Using ReactiveSubject #
ReactiveSubject is a powerful stream controller that combines the functionality of BehaviorSubject with additional reactive operators.
Core Features #
- Value Management
// Create with initial value
final subject = ReactiveSubject<int>(initialValue: 0);
// Create broadcast subject
final broadcast = ReactiveSubject<int>.broadcast(initialValue: 0);
// Add values
subject.add(1);
// Get current value
print(subject.value);
// Check if closed
print(subject.isClosed);
// Dispose when done
subject.dispose();
Stream Transformations #
- map - Transform each value
final celsius = ReactiveSubject<double>();
final fahrenheit = celsius.map((c) => c * 9/5 + 32);
- where - Filter values
final numbers = ReactiveSubject<int>();
final evenNumbers = numbers.where((n) => n % 2 == 0);
- switchMap - Switch to new stream
final searchQuery = ReactiveSubject<String>();
final results = searchQuery.switchMap((query) =>
performSearch(query)); // Cancels previous search
- debounceTime - Delay emissions
final input = ReactiveSubject<String>();
final debouncedInput = input.debounceTime(Duration(milliseconds: 300));
- throttleTime - Rate limit emissions
final clicks = ReactiveSubject<void>();
final throttledClicks = clicks.throttleTime(Duration(seconds: 1));
- distinct - Emit only when value changes
final values = ReactiveSubject<String>();
final distinctValues = values.distinct();
Advanced Operations #
- withLatestFrom - Combine with another stream
final main = ReactiveSubject<int>();
final other = ReactiveSubject<String>();
final combined = main.withLatestFrom(other,
(int a, String b) => '$a-$b');
- startWith - Begin with a value
final subject = ReactiveSubject<int>();
final withDefault = subject.startWith(0);
- scan - Accumulate values
final prices = ReactiveSubject<double>();
final total = prices.scan<double>(
0.0,
(sum, price, _) => sum + price,
);
- doOnData/doOnError - Side effects
final subject = ReactiveSubject<int>();
final withLogging = subject
.doOnData((value) => print('Emitted: $value'))
.doOnError((error, _) => print('Error: $error'));
Static Operators #
- combineLatest - Combine multiple subjects
final subject1 = ReactiveSubject<int>();
final subject2 = ReactiveSubject<String>();
final combined = ReactiveSubject.combineLatest([subject1, subject2]);
- merge - Merge multiple subjects
final subject1 = ReactiveSubject<int>();
final subject2 = ReactiveSubject<int>();
final merged = ReactiveSubject.merge([subject1, subject2]);
Practical Example in BLoC #
Here's how you might use ReactiveSubject within a BLoC to manage state:
class SearchBloc extends MainBloc<SearchEvent, SearchState> {
final ReactiveSubject<String> _searchQuery = ReactiveSubject<String>();
late final ReactiveSubject<List<String>> _searchResults;
SearchBloc() : super(const SearchState.initial()) {
_searchResults = _searchQuery
.debounceTime(Duration(milliseconds: 100))
.doOnData((query) {
showLoading(key: 'search');
})
.switchMap((query) => _performSearch(query))
.doOnData((query) => hideLoading(key: 'search'));
_searchResults.stream.listen((results) {
add(UpdateResults(results));
});
on<UpdateQuery>(_onUpdateQuery);
on<UpdateResults>(_onUpdateResults);
on<SearchError>(_onSearchError);
}
Future<void> _onUpdateQuery(
UpdateQuery event, Emitter<SearchState> emit) async {
await blocCatch(
keyLoading: 'search',
actions: () async {
await Future.delayed(Duration(seconds: 2));
_searchQuery.add(event.query);
});
}
void _onUpdateResults(UpdateResults event, Emitter<SearchState> emit) {
emit(SearchState.loaded(event.results));
}
void _onSearchError(SearchError event, Emitter<SearchState> emit) {
emit(SearchState.error(event.message));
}
Stream<List<String>> _performSearch(String query) {
final resultSubject = ReactiveSubject<List<String>>();
Future.delayed(Duration(seconds: 1)).then((_) {
if (query.isEmpty) {
resultSubject.add([]);
} else {
resultSubject.add(['Result 1 for "$query"', 'Result 2 for "$query"']);
}
}).catchError((error) {
add(SearchError(error.toString()));
});
return resultSubject.stream;
}
@override
Future<void> close() {
_searchQuery.dispose();
_searchResults.dispose();
return super.close();
}
}
Error Handling #
// Add error
subject.addError('Something went wrong');
// Handle errors in stream
subject.stream.listen(
(data) => print('Data: $data'),
onError: (error) => print('Error: $error'),
);
// Using fromFutureWithError
final subject = ReactiveSubject.fromFutureWithError(
Future.delayed(Duration(seconds: 1)),
onError: (error) => print('Error: $error'),
onFinally: () => print('Completed'),
timeout: Duration(seconds: 5),
);
Best Practices #
- Always dispose subjects when no longer needed
- Use broadcast subjects for multiple listeners
- Consider memory implications with large datasets
- Handle errors appropriately
- Use meaningful variable names
- Document complex transformations
- Consider using timeouts for async operations
Best Practices #
1. State Management #
- Keep states immutable using Freezed
- Use meaningful state classes
- Avoid storing complex objects in state
2. Event Handling #
- Keep events simple and focused
- Use meaningful event names
- Document complex event flows
3. Error Handling #
- Always use blocCatch for async operations
- Implement proper error recovery
- Log errors appropriately
4. Testing #
- Test BLoCs in isolation
- Mock dependencies
- Test error scenarios
- Verify state transitions
5. Architecture #
- Follow single responsibility principle
- Keep BLoCs focused and small
- Use dependency injection
- Implement proper separation of concerns
API Reference #
MainBloc #
MainBloc(initialState): Constructor for creating a new BLoC.blocCatch({required Future<void> Function() actions, Function(dynamic)? onError}): Wrapper for handling errors in async operations.showLoading({String key = 'global'}): Shows a loading indicator.hideLoading({String key = 'global'}): Hides the loading indicator.
MainBlocState #
Base class for all states in your BLoCs.
MainBlocEvent #
Base class for all events in your BLoCs.
CommonBloc #
add(SetComponentLoading): Set loading state for a component.state.isLoading(String key): Check if a component is in loading state.
ReactiveSubject #
ReactiveSubject<T> is a wrapper around RxDart's BehaviorSubject or PublishSubject, providing a simpler API for reactive programming in Dart.
Constructors #
ReactiveSubject({T? initialValue}): Creates a newReactiveSubject(wrapsBehaviorSubject).ReactiveSubject.broadcast({T? initialValue}): Creates a new broadcastReactiveSubject(wrapsPublishSubject).
Properties #
T value: Gets the current value of the subject.Stream<T> stream: Gets the stream of values emitted by the subject.Sink<T> sink: Gets the sink for adding values to the subject.bool isClosed: Indicates whether the subject is closed.
Methods #
void add(T value): Adds a new value to the subject.void addError(Object error, [StackTrace? stackTrace]): Adds an error to the subject.void dispose(): Disposes of the subject.
Transformation Methods #
ReactiveSubject<R> map<R>(R Function(T event) mapper): Transforms each item emitted by applying a function.ReactiveSubject<T> where(bool Function(T event) test): Filters items based on a predicate.ReactiveSubject<R> switchMap<R>(Stream<R> Function(T event) mapper): Switches to a new stream when a new item is emitted.ReactiveSubject<T> debounceTime(Duration duration): Emits items only after a specified duration has passed without another emission.ReactiveSubject<T> throttleTime(Duration duration): Emits the first item in specified intervals.ReactiveSubject<T> distinct([bool Function(T previous, T next)? equals]): Emits items that are distinct from their predecessors.ReactiveSubject<T> startWith(T startValue): Prepends a given value to the subject.ReactiveSubject<R> scan<R>(R initialValue, R Function(R accumulated, T current, int index) accumulator): Accumulates items using a function.ReactiveSubject<R> withLatestFrom<S, R>(ReactiveSubject<S> other, R Function(T event, S latestFromOther) combiner): Combines items with the latest from another subject.ReactiveSubject<T> doOnData(void Function(T event) onData): Performs a side-effect action for each data event emitted.ReactiveSubject<T> doOnError(void Function(Object error, StackTrace stackTrace) onError): Performs a side-effect action for each error event emitted.
Static Methods #
static ReactiveSubject<List<T>> combineLatest<T>(List<ReactiveSubject<T>> subjects): Combines the latest values of multiple subjects.static ReactiveSubject<T> merge<T>(List<ReactiveSubject<T>> subjects): Merges multiple subjects into one.
Contributing #
We welcome contributions! Here's how you can help:
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please make sure to:
- Update tests as appropriate
- Update documentation
- Follow the existing coding style
- Add examples for new features
License #
This project is licensed under the MIT License - see the LICENSE file for details.