
Discover how CubitSignalMixin and BlocSignalMixin allow any existing Flutter controller, domain repository, or enterprise class to gain full reactive state container capabilities without occupying its single inheritance slot.
Every Dart and Flutter developer eventually runs headfirst into a fundamental language constraint: single inheritance.
In Dart, a class can extend only one superclass.
In greenfield tutorials, this is rarely an issue because classes start from a clean slate. But in real-world Flutter engineering, domain repositories, controllers, and services frequently already belong to an established inheritance hierarchy:
TextEditingController (which itself extends ValueNotifier<TextEditingValue>).ChangeNotifier or AnimationController.BaseRepository<T>, EntityStore, or microservices client.Historically, if you wanted that class to also be a Cubit or BLoC, you were out of luck. You could not write:
// ❌ Impossible in Dart (Multiple Inheritance is forbidden):
class SearchController extends TextEditingController, CubitSignal<SearchState> { ... }
This forced developers into a frustrating dilemma:
_cubit reference, requiring tedious method forwarding and manual synchronization.TextEditingController for the input widget and a separate SearchCubit for the state—forcing you to wire listeners between them in initState/dispose.With bloc_signals 1.2.0, that single inheritance wall has been completely demolished.
We have introduced CubitSignalMixin and BlocSignalMixin, enabling any class in an existing inheritance hierarchy to become a first-class, 0ms reactive BlocSignalBase container with zero wrapper boilerplate.
Because BlocSignal has a lean, highly disciplined API surface, mixing it into arbitrary classes introduces zero namespace pollution.
┌────────────────────────────────────────────────────────────────────────┐
│ BlocSignal Mixin Architecture │
├────────────────────────────────┬───────────────────────────────────────┤
│ Mixin │ Capabilities Added │
├────────────────────────────────┼───────────────────────────────────────┤
│ CubitSignalMixin<StateType> │ state, stateValue, emit(newState), │
│ │ equals(), createEffect(), close() │
├────────────────────────────────┼───────────────────────────────────────┤
│ BlocSignalMixin<Event, State> │ on<E>(), concurrency transformers │
│ │ (restartable, droppable), add(event) │
└────────────────────────────────┴───────────────────────────────────────┘
CubitSignalMixin<StateType>CubitSignalMixin implements BlocSignalBase<StateType>. All you do is mix it in and invoke initCubitSignal(initialState: ...) in your constructor:
class UserProfileRepository extends BaseRepository
with CubitSignalMixin<UserProfileState> {
UserProfileRepository(super.apiClient) {
initCubitSignal(initialState: const UserProfileInitial());
}
Future<void> fetchProfile(String userId) async {
emit(const UserProfileLoading());
try {
final profile = await apiClient.getProfile(userId);
emit(UserProfileLoaded(profile));
} catch (error, stackTrace) {
emit(UserProfileError(error.toString()));
}
}
}
BlocSignalMixin<Event, StateType>When you need full event-driven state machines with concurrency transformers (restartable(), droppable(), sequential()), mix in both CubitSignalMixin and BlocSignalMixin:
class OrderService extends BaseService
with CubitSignalMixin<OrderState>, BlocSignalMixin<OrderEvent, OrderState> {
OrderService(super.networkClient) {
initCubitSignal(initialState: const OrderInitial());
on<SubmitOrder>((event, emit) async {
emit(const OrderSubmitting());
final result = await networkClient.postOrder(event.order);
emit(OrderSuccess(result.orderId));
}, transformer: droppable()); // Discards duplicate taps while in flight!
}
}
TextEditingControllerLet us look at a practical scenario where this pattern shines: a live product search input.
In traditional Flutter architectures, building a debounced search input requires:
TextEditingController in widget state.SearchBloc or SearchCubit.initState that forwards controller.text into bloc.add(SearchQueryChanged(text)).dispose().With BlocSignalMixin, your TextEditingController IS the debounced BLoC:
sealed class SearchEvent {
const SearchEvent();
}
final class QueryChanged extends SearchEvent {
const QueryChanged(this.query);
final String query;
}
sealed class SearchState {
const SearchState();
}
final class SearchInitial extends SearchState {
const SearchInitial();
}
final class SearchLoading extends SearchState {
const SearchLoading();
}
final class SearchSuccess extends SearchState {
const SearchSuccess(this.results);
final List<Product> results;
}
final class SearchError extends SearchState {
const SearchError(this.message);
final String message;
}
/// A standard Flutter TextEditingController with built-in BLoC reactivity!
class SearchTextEditingController extends TextEditingController
with
CubitSignalMixin<SearchState>,
BlocSignalMixin<SearchEvent, SearchState> {
SearchTextEditingController(this._api) {
initCubitSignal(initialState: const SearchInitial());
// ⚡ Built-in restartable concurrency transformer automatically cancels
// previous in-flight queries when new text is entered!
on<QueryChanged>((event, emit) async {
final query = event.query.trim();
if (query.isEmpty) {
emit(const SearchInitial());
return;
}
emit(const SearchLoading());
try {
final products = await _api.search(query);
emit(SearchSuccess(products));
} catch (error) {
emit(SearchError(error.toString()));
}
}, transformer: restartable());
// 🎯 Forward controller text mutations straight into the event pipeline
addListener(() => add(QueryChanged(text)));
}
final SearchApiClient _api;
@override
void dispose() {
close(); // Closes signal subscriptions and cancels pending async transformers
super.dispose();
}
}
Because SearchTextEditingController extends TextEditingController AND implements BlocSignalBase<SearchState>, you pass it directly to TextField and read it directly with BlocSignalBuilder:
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key, required this.api});
final SearchApiClient api;
@override
State<SearchScreen> createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
late final SearchTextEditingController _searchController;
@override
void initState() {
super.initState();
_searchController = SearchTextEditingController(widget.api);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: const InputDecoration(
hintText: 'Search products...',
border: InputBorder.none,
),
),
),
body: BlocSignalBuilder<SearchTextEditingController, SearchState>(
bloc: _searchController,
builder: (context, state) => switch (state) {
SearchInitial() => const Center(
child: Text('Type a query to search products.'),
),
SearchLoading() => const Center(
child: CircularProgressIndicator(),
),
SearchSuccess(:final results) when results.isEmpty => const Center(
child: Text('No products found.'),
),
SearchSuccess(:final results) => ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) => ListTile(
title: Text(results[index].name),
subtitle: Text('\$${results[index].price}'),
),
),
SearchError(:final message) => Center(
child: Text('Error: $message', style: const TextStyle(color: Colors.red)),
),
},
),
);
}
}
Look at that simplicity:
TextField(controller: ...).One of our guiding principles in BlocSignal is avoiding parallel, divergent abstractions.
In bloc_signals 1.2.0, CubitSignal and BlocSignal themselves compose CubitSignalMixin and BlocSignalMixin as their single source of truth:
abstract class CubitSignal<StateType> extends BlocSignalBase<StateType>
with CubitSignalMixin<StateType> {
CubitSignal({
required StateType initialState,
bool Function(StateType, StateType)? equals,
SignalOptions<StateType>? options,
}) {
initCubitSignal(
initialState: initialState,
equals: equals,
options: options,
);
}
}
abstract class BlocSignal<Event, StateType> extends BlocSignalBase<StateType>
with CubitSignalMixin<StateType>, BlocSignalMixin<Event, StateType> {
BlocSignal({
required StateType initialState,
bool Function(StateType, StateType)? equals,
SignalOptions<StateType>? options,
}) {
initCubitSignal(
initialState: initialState,
equals: equals,
options: options,
);
}
}
Because CubitSignalMixin implements BlocSignalBase<StateType>, any class mixing it in is 100% polymorphic with the entire ecosystem:
BlocSignalProvider: Provide your mixed-in class directly with $O(1)$ lookup.context.select: Fine-grained rebuilds on state sub-properties (context.select<SearchTextEditingController, int>((c) => c.stateValue.results.length)).blocSignalTest: Declarative unit testing with zero mocking.bloc_signals_riverpod: Convert mixed-in classes to Riverpod providers via .toProvider().bloc_signals_hydrate: Add synchronous Frame-1 persistence by adding with HydratedMixin.bloc_signals_replay: Add undo/redo change history by adding with ReplayMixin.DevTools Extension: Automatically monitored in the DevTools timeline and instance tree.CubitSignalMixin and BlocSignalMixin are available now in bloc_signals 1.2.0:
dependencies:
bloc_signals: ^1.2.0
bloc_signals_flutter: ^1.2.1
Or install via terminal:
dart pub add bloc_signals
flutter pub add bloc_signals_flutter
Check out the interactive documentation, live demo visualizer, and architectural decision matrix at blocsignal.dev.
Have you run into Dart's single inheritance constraint when building custom Flutter controllers or enterprise repositories? How do you currently bridge TextEditingController or ChangeNotifier into your state management layer?
Drop your thoughts, questions, and feedback in the comments below!
googleappsscriptBreaking the Limits of GAS with Direct Cloud-to-Cloud Streaming in Persistent Linux...
flutterDiscover why Flutter state management is no longer an all-or-nothing choice. Explore how BlocSignal, Classic BLoC, and Riverpod now operate as first-class bidirectional peers at the Grand Central State Terminal.
kubernetesLearn how to troubleshoot and audit GKE Vertical Pod Autoscaler actions with structured decision logs in Cloud Logging.
kubernetesLearn how GKE VerticalPodAutoscaler (VPA) CPU Startup Boost cuts JVM cold starts and eliminates ongoing CPU waste using in-place Pod resizing.
aiIf you've built a website with AI recently, there is a good chance it looks familiar. Maybe you have...
gemmaThis article is about running a hand-written Gemma 4 port in pure JAX on three different...
Workflows from the Neura Market marketplace related to this Stable Diffusion resource