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
ReactiveSubject
with 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
RegisterModule
class with theCommonBloc
singleton is essential. If you don't include this Dependency Injection setup, your app will encounter errors. TheCommonBloc
is used internally bybloc_small
for 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.initial());
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) {
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) {
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');
}
);
Error Handling with BlocErrorHandlerMixin
bloc_small
provides a mixin for standardized error handling and logging:
@injectable
class CountBloc extends MainBloc<CountEvent, CountState> with BlocErrorHandlerMixin {
CountBloc() : super(const CountState.initial()) {
on<Increment>(_onIncrement);
}
Future<void> _onIncrement(Increment event, Emitter<CountState> emit) async {
await blocCatch(
actions: () async {
// Your async logic that might throw
if (state.count > 5) {
throw ValidationException('Count cannot exceed 5');
}
emit(state.copyWith(count: state.count + 1));
},
onError: handleError, // Uses the mixin's error handler
);
}
}
The mixin provides:
- Automatic error logging with stack traces
- Built-in support for common exceptions (NetworkException, ValidationException, TimeoutException)
- Automatic loading state cleanup
- Helper method for error messages
You can get error messages without state emission:
String message = getErrorMessage(error); // Returns user-friendly error message
For custom error handling, override the handleError method:
@override
Future<void> handleError(Object error, StackTrace stackTrace) async {
// Always call super to maintain logging
super.handleError(error, stackTrace);
// Add your custom error handling here
if (error is CustomException) {
// Handle custom exception
}
}
Lifecycle Management
bloc_small provides lifecycle hooks to manage state and resources based on widget lifecycle events.
Using Lifecycle Hooks in BLoC
@injectable
class CounterBloc extends MainBloc<CounterEvent, CounterState> {
Timer? _timer;
CounterBloc() : super(const CounterState.initial()) {
on<StartTimer>(_onStartTimer);
}
@override
void onDependenciesChanged() {
// Called when dependencies change (e.g., Theme, Locale)
add(const CounterEvent.checkDependencies());
}
@override
void onDeactivate() {
// Called when widget is temporarily removed
_timer?.cancel();
}
Future<void> _onStartTimer(StartTimer event, Emitter<CounterState> emit) async {
_timer = Timer.periodic(Duration(seconds: 1), (_) {
add(const CounterEvent.increment());
});
}
}
Implementation in Widget
class CounterPage extends StatefulWidget {
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends BaseBlocPageState<CounterPage, CounterBloc> {
@override
Widget buildPage(BuildContext context) {
return buildLoadingOverlay(
child: Scaffold(
body: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) => Text('Count: ${state.count}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => bloc.add(const StartTimer()),
child: Icon(Icons.play_arrow),
),
),
);
}
}
The lifecycle hooks are automatically managed by the base classes and provide:
- Automatic resource cleanup
- State synchronization with system changes
- Proper handling of widget lifecycle events
- Memory leak prevention
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.
Libraries
- base/base_app_router
- base/base_bloc_page
- base/base_bloc_page_state
- base/base_cubit_page
- base/base_cubit_state
- base/base_page_delegate
- base/base_page_stateless_delegate
- bloc/common/common_bloc
- bloc/core/base_delegate
- bloc/core/bloc/main_bloc
- bloc/core/cubit/main_cubit
- bloc/core/di/di
- bloc/core/error/bloc_error_handler
- bloc/core/error/cubit_error_handler
- bloc/core/error/error_state
- bloc/core/error/exceptions
- bloc/core/main_bloc_event
- bloc/core/main_bloc_state
- bloc_small
- constant/default_loading
- extensions/bloc_context_extension
- utils/reactive_subject
- widgets/loading_indicator