Building small demo applications in Flutter is straightforward. However, as an application grows to dozens of screens, complex API authentication, offline caching, and team collaboration, unstructured code quickly turns into an unmaintainable codebase.
This guide details a proven, production-grade Flutter Architecture combining Clean Architecture, BLoC (Business Logic Component), Repository Pattern, and GetIt Dependency Injection.
1. Architectural Layers & Separation of Concerns
+---------------------------------------------------------------+
| PRESENTATION LAYER |
| Flutter UI Widgets <---> BLoC / Cubit (State Management) |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| DOMAIN LAYER |
| Entities (Pure Data) <---> Use Cases (Business Rules) |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| DATA LAYER |
| Repositories Impl <---> Data Sources (Dio / Hive / Drift) |
+---------------------------------------------------------------+
2. Setting Up Dependency Injection with GetIt
Dependency Injection (DI) allows classes to receive their dependencies from an external container rather than instantiating them internally. This makes testing, mocking, and maintenance seamless.
flutter pub add get_it flutter_bloc dio fpdartDependency Injection Service Locator (injection_container.dart)
import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';
final sl = GetIt.instance;
Future<void> initServiceLocator() async {
// 1. External Libraries
sl.registerLazySingleton<Dio>(() => Dio(BaseOptions(
baseUrl: 'https://api.puneetdev.in/v1',
connectTimeout: const Duration(seconds: 10),
)));
// 2. Data Sources
sl.registerLazySingleton<UserRemoteDataSource>(
() => UserRemoteDataSourceImpl(dio: sl()));
// 3. Repositories
sl.registerLazySingleton<UserRepository>(
() => UserRepositoryImpl(remoteDataSource: sl()));
// 4. Use Cases
sl.registerLazySingleton(() => GetUserProfile(repository: sl()));
// 5. BLoCs / Cubits
sl.registerFactory(() => UserBloc(getUserProfile: sl()));
}3. Implementing the BLoC Pattern for State Management
The BLoC Pattern guarantees unidirectional data flow:
- Events enter the BLoC from the UI.
- States exit the BLoC to update the UI.
State Definition (user_state.dart)
part of 'user_bloc.dart';
abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final UserEntity user;
UserLoaded({required this.user});
}
class UserError extends UserState {
final String message;
UserError({required this.message});
}BLoC Logic (user_bloc.dart)
import 'package:flutter_bloc/flutter_bloc.dart';
class UserBloc extends Bloc<UserEvent, UserState> {
final GetUserProfile getUserProfile;
UserBloc({required this.getUserProfile}) : super(UserInitial()) {
on<FetchUserEvent>((event, emit) async {
emit(UserLoading());
final result = await getUserProfile.execute(event.userId);
result.fold(
(failure) => emit(UserError(message: failure.message)),
(user) => emit(UserLoaded(user: user)),
);
});
}
}4. UI Layer Integration with BlocBuilder
The UI listens to state changes and rebuilds reactively using BlocBuilder and BlocConsumer.
class UserProfileScreen extends StatelessWidget {
final String userId;
const UserProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => sl<UserBloc>()..add(FetchUserEvent(userId: userId)),
child: Scaffold(
appBar: AppBar(title: const Text('User Profile')),
body: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
return switch (state) {
UserInitial() || UserLoading() => const Center(
child: CircularProgressIndicator.adaptive(),
),
UserLoaded(:final user) => ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text('Name: ${user.name}', style: Theme.of(context).textTheme.headlineSmall),
Text('Email: ${user.email}'),
],
),
UserError(:final message) => Center(
child: Text('Error: $message', style: const TextStyle(color: Colors.red)),
),
_ => const SizedBox.shrink(),
};
},
),
),
);
}
}5. Summary & Key Architectural Takeaways
- Unidirectional Data Flow: BLoCs handle state logic; UI widgets only render state and dispatch events.
- Explicit Dependency Injection: Use
GetItto inject mock repositories during unit testing. - Decoupled Business Rules: Business rules in Use Cases remain 100% independent of UI widgets or API clients.