Advanced BLoC Architecture for Production Flutter Apps
As Flutter applications grow, state management becomes difficult when UI code, API calls, business rules, and persistence are mixed together.
BLoC provides a structured way to separate these responsibilities.
A production architecture can look like:
UI
|
BLoC / Cubit
|
Use Case
|
Repository
|
Data Source
|
REST API / Database / Local Storage
Feature-first project structure
A scalable project can use:
lib/
core/
error/
network/
storage/
features/
authentication/
data/
domain/
presentation/
profile/
data/
domain/
presentation/
chat/
data/
domain/
presentation/
This keeps related code together.
Events and states
A login feature might define:
sealed class LoginEvent {}
class LoginSubmitted extends LoginEvent {
final String email;
final String password;
LoginSubmitted({
required this.email,
required this.password,
});
}
States:
sealed class LoginState {}
class LoginInitial extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState {
final String message;
LoginFailure(this.message);
}
BLoC
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final LoginUseCase login;
LoginBloc(this.login) : super(LoginInitial()) {
on<LoginSubmitted>(_onLoginSubmitted);
}
Future<void> _onLoginSubmitted(
LoginSubmitted event,
Emitter<LoginState> emit,
) async {
emit(LoginLoading());
try {
await login(
email: event.email,
password: event.password,
);
emit(LoginSuccess());
} catch (e) {
emit(LoginFailure(e.toString()));
}
}
}
The widget does not need to know how authentication works.
Repository pattern
abstract class AuthRepository {
Future<void> login({
required String email,
required String password,
});
}
Implementation:
class AuthRepositoryImpl implements AuthRepository {
final AuthRemoteDataSource remote;
AuthRepositoryImpl(this.remote);
@override
Future<void> login({
required String email,
required String password,
}) {
return remote.login(
email: email,
password: password,
);
}
}
This abstraction makes testing easier.
Avoid contradictory state flags
Avoid large combinations such as:
isLoading
hasError
isLoggedIn
hasData
because they can produce invalid combinations.
Prefer explicit states or immutable state objects.
Managing concurrency
For search or API operations, event transformers can control concurrency.
Conceptually:
User types:
a
ab
abc
abcd
Instead of sending four expensive requests,
cancel obsolete requests when appropriate.
Choose the correct behavior for each feature:
- sequential
- restartable
- droppable
- concurrent
Dependency injection
Inject dependencies instead of creating them inside BLoCs:
BlocProvider(
create: (_) => LoginBloc(loginUseCase),
child: LoginPage(),
)
This improves testability and separation of concerns.
Testing BLoCs
Test the state transition:
Event
β
BLoC
β
Expected states
For example:
LoginSubmitted
-> LoginLoading
-> LoginSuccess
and:
LoginSubmitted
-> LoginLoading
-> LoginFailure
Production responsibility matrix
| Layer | Responsibility |
|---|---|
| UI | Rendering and interaction |
| BLoC | State transitions |
| Use Case | Business rules |
| Repository | Data abstraction |
| Data Source | API/database communication |
| Model | Data representation |
Conclusion
Advanced BLoC architecture is not about adding more files. It is about making responsibilities explicit.
A good production architecture should make it easy to answer:
Where does this business rule belong?
If the answer is clear, the codebase becomes easier to maintain, test, and extend.
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)