SuperQubit
A hierarchical state management library for Flutter with SuperQubit and Qubit support.
Features
- ✅ Minimal Dependencies - Uses only Flutter SDK and the
nestedpackage - ✅ Hierarchical State Management - One SuperQubit manages multiple Qubits
- ✅ Flexible Event Handling - Both parent and child can handle events
- ✅ Cross-Qubit Communication - Easy communication between Qubits via
dispatchandlistenTo - ✅ Event Propagation Control - Use flags to control when parent/child handlers execute
- ✅ Type-Safe - Full generic type support
- ✅ Stream-Based - Native Dart streams for state changes
- ✅ DevTools Integration - Inspect Qubit hierarchy, events, and states directly in Flutter DevTools
- ✅ Middleware Support - Intercept events, transitions, and errors
Why This Exists
As Flutter applications grow, managing multiple related pieces of state becomes complex. A single feature often requires multiple states working together - for example, a shopping cart needs loading state, items state, and filter state all coordinated under one domain.
Traditional approaches require you to:
- Scatter related states across multiple providers in the widget tree
- Manually coordinate state updates between separate managers
- Write boilerplate to enable communication between sibling state managers
- Nest multiple providers creating deep widget hierarchies
SuperQubit solves these challenges by introducing a hierarchical layer above individual state managers (Qubits). It allows you to:
- Group multiple states under a single feature domain (one SuperQubit manages many Qubits)
- Coordinate automatically with built-in communication via
dispatchandlistenTo - Handle events flexibly at both parent and child levels with propagation control
- Use one provider for an entire feature instead of multiple nested providers
This makes it ideal for complex features like shopping carts, multi-step forms, or dashboards where multiple states need to work together seamlessly as a cohesive unit.
Installation
Add to your pubspec.yaml:
dependencies:
super_qubit: ^0.1.0
Then run:
flutter pub get
Quick Start
1. Define States and Events
// States
class LoadState {
final bool isLoading;
const LoadState({this.isLoading = false});
}
class CartItemsState {
final List<Item> items;
const CartItemsState({this.items = const []});
}
// Events
class LoadEvent {}
class AddItemEvent {
final Item item;
AddItemEvent(this.item);
}
2. Create Qubits
import 'package:super_qubit/super_qubit.dart';
// Define Event base class
abstract class LoadEventBase {}
class LoadEvent extends LoadEventBase {}
abstract class CartItemsEventBase {}
class AddItemEvent extends CartItemsEventBase {
final Item item;
AddItemEvent(this.item);
}
class LoadQubit extends Qubit<LoadEventBase, LoadState> {
LoadQubit() : super(const LoadState()) {
on<LoadEvent>((event, emit) async {
emit(const LoadState(isLoading: true));
// Fetch data...
emit(const LoadState(isLoading: false));
});
}
}
class CartItemsQubit extends Qubit<CartItemsEventBase, CartItemsState> {
CartItemsQubit() : super(const CartItemsState()) {
on<AddItemEvent>((event, emit) {
final newItems = [...state.items, event.item];
emit(CartItemsState(items: newItems));
});
}
}
3. Create SuperQubit
class CartSuperQubit extends SuperQubit {
@override
void init() {
// Cross-Qubit communication
listenTo<CartItemsQubit>((state) {
if (state.items.isEmpty) {
dispatch<LoadQubit, LoadEvent>(LoadEvent());
}
});
// Parent-level event handler
on<CartItemsQubit, AddItemEvent>((event, emit) async {
// Handle at parent level (e.g., analytics)
// Child handler will also execute if not using ignoreWhenParentDefines
});
}
// Convenience getters
LoadQubit get load => getQubit<LoadQubit>();
CartItemsQubit get items => getQubit<CartItemsQubit>();
}
4. Provide to Widget Tree
void main() {
runApp(
SuperQubitProvider<CartSuperQubit>(
superQubit: CartSuperQubit(),
qubits: [
LoadQubit(),
CartItemsQubit(),
],
child: MyApp(),
),
);
}
5. Use in Widgets
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Option 1: Using context extensions
final cart = context.readSuper<CartSuperQubit>();
return StreamBuilder<CartItemsState>(
stream: cart.items.stream,
builder: (context, snapshot) {
final state = snapshot.data ?? const CartItemsState();
return Column(
children: [
Text('Items: ${state.items.length}'),
ElevatedButton(
onPressed: () {
cart.items.add(AddItemEvent(Item('New Item')));
},
child: const Text('Add Item'),
),
],
);
},
);
}
}
// Option 2: Using QubitBuilder (recommended)
class MyWidgetWithBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QubitBuilder<CartItemsQubit, CartItemsState>(
superQubitType: CartSuperQubit,
builder: (context, state) {
return Column(
children: [
Text('Items: ${state.items.length}'),
ElevatedButton(
onPressed: () {
context
.read<CartSuperQubit, CartItemsQubit>()
.add(AddItemEvent(Item('New Item')));
},
child: const Text('Add Item'),
),
],
);
},
);
}
}
Core Concepts
Event Propagation Flags
Control when parent and child handlers execute:
ignoreWhenParentDefines()
Use in child Qubit to skip execution if parent has a handler:
class LoadQubit extends Qubit<LoadEventBase, LoadState> {
LoadQubit() : super(const LoadState()) {
on<LoadEvent>((event, emit) {
// Only runs if parent doesn't handle it
}, ignoreWhenParentDefines());
}
}
ignoreWhenChildDefines<T>()
Use in parent SuperQubit to skip execution if child has a handler:
class CartSuperQubit extends SuperQubit {
@override
void init() {
on<LoadQubit, LoadEvent>((event, emit) async {
// Only runs if LoadQubit doesn't handle it
}, ignoreWhenChildDefines<LoadQubit>());
}
}
Default Behavior (No Flag)
Both parent and child execute in sequence:
- Child handler executes first
- Parent handler executes after
Cross-Qubit Communication
dispatch<T, E>(event)
Send an event to a specific child Qubit:
dispatch<LoadQubit, LoadEvent>(LoadEvent());
listenTo<T>(callback)
Listen to state changes from a child Qubit:
listenTo<CartItemsQubit>((state) {
if (state.items.isEmpty) {
dispatch<LoadQubit, LoadEvent>(LoadEvent());
}
});
Internal Communication
You can call listenTo or dispatch directly from a Child Qubit constructor. These calls are automatically queued and executed once the Qubit is registered with its parent SuperQubit.
class ProfileQubit extends Qubit<UserEvent, ProfileState> {
ProfileQubit() : super(const ProfileState()) {
// Safely listen to sibling Qubits in the constructor
listenTo<AuthQubit>((authState) {
if (!authState.isLoggedIn) {
add(ClearProfileEvent());
}
});
// Safely dispatch to sibling Qubits in the constructor
dispatch<LoadQubit, RefreshEvent>(RefreshEvent());
}
}
Event Transformers
Control how events are processed (e.g., debouncing search or throttling clicks).
on<SearchEvent>(
(event, emit) async {
// ... search logic ...
},
config: EventHandlerConfig(
transformer: Transformers.debounce(const Duration(milliseconds: 300)),
),
);
Built-in Transformers:
Transformers.sequential(): Processes events one by one (default).Transformers.concurrent(): Processes multiple events at the same time.Transformers.restartable(): Cancels previous execution and starts the newest one.Transformers.droppable(): Ignores new events while an event is being processed.Transformers.debounce(Duration): Waits for a quiet period before processing.Transformers.throttle(Duration): Processes the first event and ignores others for a period.
Accessing Child Qubits
// Get Qubit instance
final loadQubit = superQubit.getQubit<LoadQubit>();
// Get current state
final loadState = superQubit.getState<LoadQubit, LoadState>();
// Or use convenience getters
LoadQubit get load => getQubit<LoadQubit>();
Middleware
SuperQubit supports middleware for intercepting and monitoring events, transitions, errors, and lifecycle events. This is useful for logging, analytics, error reporting, etc.
Creating Middleware
Extend QubitMiddleware and override methods you want to intercept:
class LoggingMiddleware extends QubitMiddleware {
@override
void onEvent(BaseQubit qubit, dynamic event) {
print('Event: ${qubit.runtimeType} -> $event');
}
@override
void onTransition(BaseQubit qubit, Transition transition) {
print('Transition: ${transition.currentState} -> ${transition.nextState}');
}
@override
void onError(BaseQubit qubit, Object error, StackTrace stackTrace) {
print('Error: $error');
}
}
Global Middleware
Apply to all Qubits in the app.
Method 1: Widget-based (Recommended)
Use SuperQubitScope to provide middleware to the widget tree.
SuperQubitScope(
globalMiddleware: [LoggingMiddleware()],
child: MaterialApp(home: MyApp()),
)
Method 2: Programmatic
Register directly via SuperQubitConfig.
void main() {
SuperQubitConfig.middleware.add(LoggingMiddleware());
runApp(MyApp());
}
Scoped Middleware
Apply to a specific SuperQubit and its children.
Method 1: Widget-based (Recommended)
Use SuperQubitProvider to inject middleware.
SuperQubitProvider<CartSuperQubit>(
superQubit: CartSuperQubit(),
middleware: [AnalyticsMiddleware()],
qubits: [CartItemsQubit()],
child: CartPage(),
)
Method 2: Programmatic
Register directly on the SuperQubit instance.
class CartSuperQubit extends SuperQubit {
CartSuperQubit() {
middleware.add(AnalyticsMiddleware());
}
}
DevTools Integration
SuperQubit comes with a custom DevTools extension to help you debug your application. It allows you to:
- Inspect Hierarchy: View all registered SuperQubits and their child Qubits.
- Track Events: Real-time log of all events dispatched to specific Qubits.
- Monitor State: View state changes as they happen.
Enabling DevTools
Calls to SuperQubitDevTools.enable() should be made in your main() function. It is recommended to wrap this in a kDebugMode check to ensure it only runs during development.
import 'package:flutter/foundation.dart';
import 'package:super_qubit/super_qubit.dart';
void main() {
if (kDebugMode) {
SuperQubitDevTools.enable();
}
runApp(const MyApp());
}
Once enabled, open Flutter DevTools and look for the "SuperQubit" tab.
Example App
See the example/ directory for a complete shopping cart application demonstrating:
- ✅ Multiple Qubits (Load, CartItems, CartFilter)
- ✅ SuperQubit orchestration
- ✅ Cross-Qubit communication
- ✅ Parent and child event handlers
- ✅ Event propagation flags
- ✅ UI integration with StreamBuilder
Run the example:
cd example
flutter pub get
flutter run -d chrome # or macos, ios, android
API Reference
Qubit<Event, State>
Base class for state management.
Type Parameters:
Event- Base type for events this Qubit handlesState- Type of state this Qubit manages
Methods:
on<E extends Event>(handler, {config})- Register event handleradd(event)- Add event to be processedclose()- Close and clean up
Properties:
state- Current statestream- Stream of state changesisClosed- Whether Qubit is closed
SuperQubit
Container for managing multiple child Qubits.
Methods:
registerQubits(qubits)- Register child Qubitson<ChildQubit, Event>(handler, [config])- Register parent event handlerdispatch<T, E>(event)- Dispatch event to child QubitlistenTo<T>(callback)- Listen to child state changesgetQubit<T>()- Get child Qubit by typegetState<T, S>()- Get child Qubit stateclose()- Close SuperQubit and all children
SuperQubitProvider
Widget that provides SuperQubit to descendants.
Parameters:
superQubit- SuperQubit instancequbits- List of child Qubitschild- Widget tree
Static Methods:
SuperQubitProvider.of<T>(context)- Get SuperQubit from context
MultiSuperQubitProvider
Widget that provides multiple SuperQubits to descendants.
Convenience widget built on the nested package that nests multiple SuperQubitProviders cleanly without deep indentation.
Example:
MultiSuperQubitProvider(
providers: [
SuperQubitProvider<CartSuperQubit>(
superQubit: CartSuperQubit(),
qubits: [LoadQubit(), CartItemsQubit()],
),
SuperQubitProvider<UserSuperQubit>(
superQubit: UserSuperQubit(),
qubits: [AuthQubit(), ProfileQubit()],
),
SuperQubitProvider<SettingsSuperQubit>(
superQubit: SettingsSuperQubit(),
qubits: [ThemeQubit()],
),
],
child: MyApp(),
)
Parameters:
providers- List of SuperQubitProvider instanceschild- Widget tree
Context Extensions
For SuperQubit access:
context.readSuper<T>()- Get SuperQubit without listeningcontext.watchSuper<T>()- Get SuperQubit and listen
For child Qubit access:
context.read<T, Q>()- Get child Qubit without listeningcontext.watch<T, Q>()- Get child Qubit and listencontext.watchState<T, Q, S>()- Watch specific child Qubit statecontext.select<T, Q, S, R>(selector)- Select specific value from state
License
MIT License
Contributing
Contributions welcome! Please open an issue or PR.