This article is about structuring Flutter app using BLoC pattern and how we avoided all the architectural issues we faced in previous projects.
Business Logic Component (BLoC) is a state management design pattern recommended for Flutter. It helps in managing the app state and acts as a bridge between a data source and the widgets that need the data. It receives data/events as a stream, handles any required business logic, then publishes results data streams to the interested widgets. To implement this pattern, we are using bloc and flutter_bloc plugins.
![]() |
---|
BLoC pattern |
The recommended approach of using flutter_bloc in Flutter projects is by having at least one bloc associated with each screen/feature - something like a viewmodel in MVVM architecture - At the start, everything seemed to work perfectly, however, when the app grew and became more complex, we started to have difficulties managing the app due to the bloc’s high dependency on each other especially for global/common blocs that their states/values might be needed in various parts of the application such as account expiry and connectivity status.
In addition, adding new states for blocs caused an unexpected behavior because there is no single source of truth and a change in one place requires visiting many many classes to ensure the newly added state does not affect other parts of the code.
In order to avoid the complexity and the difficulty to manage the app state and its business logic, we tried to find a way to achieve some kind of isolation between individual features/screen and global/common business logic, for example, feature A does not know about connectivity bloc and does not depend or listen to it, alternatively, the connectivity data will be provided to it.
The feature, however, will have its own widgets and business logic that are focused and only specific to that particular feature, hence, extracting it and reusing it in another project is possible and straightforward. That could only be achieved if the feature encapsulates its own data and business logic and has external data injected into it.
The solution is to;
- Separate global app state(s) from the app features and have the data provided to the feature when needed making it flexible and easier to test.
- Create a bloc manager in a single place that is responsible for handling bloc registration, fetching or disposing a registered bloc, and adding or removing state change listeners from a specific bloc.
- Use the bloc manager to listen to state changes in global/shared blocs and pass them to an state dispatcher.
- To have a single state dispatcher that is in charge of broadcasting states sent from the bloc manager to the active bloc expecting these states.
- Create a common/shared states interface that is used in different parts of the app, so they can be reused instead of code duplication.
The following diagrams show how the app is structured;
![]() |
---|
Proposed BLoC pattern implementation |
This section shows how to create a new bloc/cubit following the proposed architecture and use the bloc manager to coordinate and manage the blocs/cubits. In this example we will be using cubit;
1- Create a base states class which contains the states that are common in which a cubit class or other base states interfaces can implement. For example;
abstract class BaseStateListener {}
abstract class AuthStateListener implements BaseStateListener {
void onLogin(Authorize authorizedAccount);
void onLogout();
}
abstract class ConnectivityStateListener implements BaseStateListener {
void onDisconnect(DisconnectSource source);
void onConnect();
}
2- Create a cubit for a feature, let’s call it FeatureCubit
. The cubit class will implement both AuthStateListener
and ConnectivityStateListener
so it can expose the 4 methods in addition to any other feature-specific states. The type of the state FeatureCubit
is managing in this example, is Status with initial value as initial
;
enum Status {
initial,
loading,
disabled,
enabled,
loggedOut,
loggedIn,
}
The FeatureCubit
will expose the common/share onConnect
, onDisconnect
, onLogout
, onLogin
methods and a feature-specific loading
method;
import 'package:bloc/bloc.dart';
class FeatureCubit extends Cubit<Status> implements ConnectivityStateListener, AuthStateListener {
FeatureCubit() : super(Status.initial);
void loading() => emit(Status.loading);
@override
void onConnect() => emit(Status.enabled);
@override
void onDisconnect(DisconnectSource source) => emit(Status.disabled);
@override
void onLogin(Authorize authorizedAccount) => emit(Status.loggedIn);
@override
void onLogout() => emit(Status.loggedOut);
}
3- Cubits created should be registered with BlocManager
class. It is important to decide when a cubit should be registered/provided or not, you should register a bloc only when needed and should dispose of it if it is not needed anymore. For example, we have 2 global cubits in which their states/values are needed at every stage of the app’s lifecycle, named Connectivity and Account. These 2 important cubits should be registered early at the app’s lifecycle. For example;
void main() {
_registerBlocs();
runApp(CounterApp());
}
void _registerBlocs() {
BlocManager.instance.register(() => ConnectivityCubit());
BlocManager.instance.register(() => AccountCubit());
...
}
void _registerStateDispatchers() {
StateDispatcher(BlocManager.instance)
..register<ConnectivityCubit, ConnectionStateEmitter>(
(BaseBlocManager blocManager) => ConnectivityStateEmitter(blocManager),
)
..register<AccountCubit>(
(BaseBlocManager blocManager) => AccountStateEmitter(blocManager),
)
...;
}
As the snippet shows, we use BlocManager.instance.register()
to register a new cubit. In order to dispose of that cubit, BlocManager.instance.dispose()
could be called. So what does StateDispatcher(BlocManager.instance)
do?
As we mentioned earlier, bloc manager will handle listening to state changes in shared blocs in order to notify or broadcast the states to interested cubits that expect an update. And that’s exactly what StateDispatcher(BlocManager.instance)
do, it adds listeners to get state changes update and then will dispatch states to cubits, for example, to FeatureCubit
as shown below;
abstract class BaseStateEmitter<S extends BaseStateListener, B extends BlocBase<Object>> {
/// Initializes base state emitter.
BaseStateEmitter(this.blocManager) {
blocManager.registerStateEmitter(this);
}
/// Bloc manager instance.
final BaseBlocManager blocManager;
/// Handles states for state listener [S].
void handleStates({required S stateListener, required Object state});
/// Emits state to the listener.
void call({required BaseStateListener stateListener, Object? state}) {
if (stateListener is S) {
handleStates(
stateListener: stateListener,
state: state ?? blocManager.fetch<B>().state,
);
}
}
}
class ConnectionStateEmitter extends BaseStateEmitter<ConnectionStateListener, ConnectionCubit> {
/// Initializes connection state emitter.
ConnectionStateEmitter(BaseBlocManager blocManager) : super(blocManager);
@override
void handleStates({
required ConnectionStateListener stateListener,
required Object state,
}) {
if (state is ConnectionConnectedState) {
stateListener.onConnected();
} else if (state is ConnectionDisconnectedState) {
stateListener.onDisconnect();
} else if (state is ConnectionErrorState) {
stateListener.onConnectionError(state.error);
}
}
}
void register<B extends BlocBase<Object>, S extends BaseStateEmitter<BaseStateListener, B>>(
StateEmitterBuilder stateEmitterBuilder,
) {
stateEmitterBuilder(blocManager);
if (!blocManager.hasListener<B>(key)) {
blocManager.addListener<B>(
key: key,
handler: (Object state) => _dispatcher<S>(state),
);
}
}
The example shows a state is active or not coming from the global AccountCubit
, then the StateDispatcher
will broadcast login or logout states. _dispatcher
function looks for all registered blocs and sends the states to blocs implementing the BaseCubit
class.
void _dispatcher<S extends BaseStateEmitter<BaseStateListener, BlocBase<Object>>>(
Object state,
) =>
blocManager.repository.forEach(
(String key, BlocBase<Object> bloc) =>
blocManager.emitCoreStates<S>(bloc: bloc, state: state),
);
For example, bloc.onLogout();
will call void logout()
at FeatureCubit which in turns, emits (Status.loggedOut)
to update the UI or do an action. So how can we use FeatureCubit
in a widget or a screen?
4- Cubit is registered when needed, so we register FeatureCubit
when we navigate to FeaturePage,
class FeaturePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
BlocManager.instance.register<FeatureCubit>(() => FeatureCubit());
return FeatureView();
}
}
In order to use FeatureCubit
in FeatureView
, we should call fetch method from BlocManager
class as following;
final _featureCubit = BlocManager.instance.fetch<FeatureCubit>();
then use a BlocBuilder;
BlocBuilder<FeatureCubit, Status>(
cubit: _featureCubit,
builder: (context, state) => ... // Use the state.
);
The full class implementation looks as follows;
class FeatureView extends StatelessWidget {
final _featureCubit = BlocManager.instance.fetch<FeatureCubit>();
@override
Widget build(BuildContext context) =>
Builder(
builder: (BuildContext context) => BlocBuilder<FeatureCubit, Status>(
cubit: _featureCubit,
builder: (context, state) {
if (state == Status.enabled) {
// Enabled state logic.
} else if (state == Status.disabled) {
// Disabled state logic.
} else if (state == Status.loggedIn) {
// Logged in state logic.
} else if (state == Status.loggedOut) {
// Logged out state logic.
} else {
// Initial state logic.
}
},
),
);
}
To see a sample app of this implementation, please refer to this link.