flutter
/

BLoC Feature-Based Structure: Organizing Large Flutter Apps

Last Sync: Today

On this page

8
0%
Intermediate
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterIntermediate

BLoC Feature-Based Structure: Organizing Large Flutter Apps

As your Flutter app grows, organizing code by type (e.g., all blocs in one folder, all pages in another) quickly becomes unmanageable. Feature-based structure – grouping all code related to a specific feature together – is a proven approach for scalability. Combined with BLoC, it keeps your codebase maintainable, testable, and easy to navigate. This guide shows you how to structure your BLoC apps by feature.

What is Feature-Based Structure?

In a feature-based structure, you organize code by features (e.g., authentication, products, cart) rather than by technical layers (e.g., blocs, models, views). Each feature contains everything it needs: UI, business logic, data models, and repositories. This approach has several advantages:

  • Cohesion – Related code is kept together.
  • Scalability – Adding a new feature doesn't clutter existing ones.
  • Team collaboration – Different teams can work on separate features without merge conflicts.
  • Lazy loading – Features can be loaded on demand.
  • Testability – Features can be tested in isolation.

Recommended Folder Structure

TEXTRead-only
1
lib/
├── main.dart
├── app.dart                     # App entry with MultiBlocProvider
├── core/                        # Shared code across features
│   ├── constants/
│   ├── utils/
│   ├── widgets/
│   ├── errors/
│   └── di/                      # Dependency injection (get_it)
├── features/
│   ├── auth/
│   │   ├── presentation/
│   │   │   ├── pages/
│   │   │   ├── widgets/
│   │   │   └── bloc/            # AuthBloc (events, states, bloc)
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   ├── repositories/
│   │   │   └── usecases/
│   │   └── data/
│   │       ├── repositories/
│   │       ├── datasources/
│   │       └── models/
│   ├── product/
│   │   ├── presentation/
│   │   ├── domain/
│   │   └── data/
│   └── cart/
│       ├── presentation/
│       ├── domain/
│       └── data/
└── routes/                      # Optional: if using go_router
    └── app_router.dart

Feature Example: Authentication

Let’s look at the auth feature in detail. Each subfolder has a specific responsibility:

  • pages/ – Screens (LoginPage, RegisterPage).
  • widgets/ – Reusable UI components (LoginForm, SocialButtons).
  • bloc/ – AuthBloc (events, states, bloc).
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/login.dart';
import '../../domain/usecases/register.dart';

// Events
abstract class AuthEvent {}
class LoginEvent extends AuthEvent {
  final String email;
  final String password;
  LoginEvent(this.email, this.password);
}
class RegisterEvent extends AuthEvent { ... }

// States
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
  final User user;
  AuthSuccess(this.user);
}
class AuthFailure extends AuthState {
  final String message;
  AuthFailure(this.message);
}

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final Login loginUseCase;
  final Register registerUseCase;

  AuthBloc(this.loginUseCase, this.registerUseCase) : super(AuthInitial()) {
    on<LoginEvent>(_onLogin);
    on<RegisterEvent>(_onRegister);
  }

  Future<void> _onLogin(LoginEvent event, Emitter<AuthState> emit) async {
    emit(AuthLoading());
    final result = await loginUseCase(LoginParams(event.email, event.password));
    result.fold(
      (failure) => emit(AuthFailure(failure.message)),
      (user) => emit(AuthSuccess(user)),
    );
  }
}
DARTRead-only
1
class User {
  final String id;
  final String name;
  final String email;
  const User({required this.id, required this.name, required this.email});
}
DARTRead-only
1
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/user.dart';

abstract class AuthRepository {
  Future<Either<Failure, User>> login(String email, String password);
  Future<Either<Failure, User>> register(String email, String password, String name);
}
DARTRead-only
1
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/user.dart';
import '../repositories/auth_repository.dart';

class Login {
  final AuthRepository repository;
  Login(this.repository);

  Future<Either<Failure, User>> call(LoginParams params) async {
    return await repository.login(params.email, params.password);
  }
}

class LoginParams {
  final String email;
  final String password;
  LoginParams(this.email, this.password);
}
DARTRead-only
1
abstract class AuthRemoteDataSource {
  Future<UserModel> login(String email, String password);
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final Dio dio;
  AuthRemoteDataSourceImpl(this.dio);

  @override
  Future<UserModel> login(String email, String password) async {
    final response = await dio.post('/login', data: {'email': email, 'password': password});
    return UserModel.fromJson(response.data);
  }
}
DARTRead-only
1
import '../../domain/entities/user.dart';

class UserModel extends User {
  UserModel({required super.id, required super.name, required super.email});

  factory UserModel.fromJson(Map<String, dynamic> json) => UserModel(
    id: json['id'],
    name: json['name'],
    email: json['email'],
  );
}
DARTRead-only
1
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_datasource.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;
  AuthRepositoryImpl(this.remoteDataSource);

  @override
  Future<Either<Failure, User>> login(String email, String password) async {
    try {
      final userModel = await remoteDataSource.login(email, password);
      return Right(userModel);
    } on DioException catch (e) {
      return Left(ServerFailure(e.message ?? 'Server error'));
    }
  }
}

Dependency Injection Across Features

Use get_it to register dependencies at the app level. This allows you to wire up features and share instances (like Dio) across features. You can register each feature's dependencies in its own init function and call them in main.

DARTRead-only
1
import 'package:get_it/get_it.dart';
import '../features/auth/data/datasources/auth_remote_datasource.dart';
import '../features/auth/data/repositories/auth_repository_impl.dart';
import '../features/auth/domain/repositories/auth_repository.dart';
import '../features/auth/domain/usecases/login.dart';
import '../features/auth/presentation/bloc/auth_bloc.dart';

final sl = GetIt.instance;

Future<void> initAuth() async {
  // Data sources
  sl.registerLazySingleton<AuthRemoteDataSource>(() => AuthRemoteDataSourceImpl(sl()));

  // Repositories
  sl.registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl(sl()));

  // Use cases
  sl.registerLazySingleton(() => Login(sl()));

  // Blocs
  sl.registerFactory(() => AuthBloc(sl(), sl()));
}

void initCore() {
  sl.registerLazySingleton(() => Dio(BaseOptions(baseUrl: 'https://api.example.com')));
  // ... other shared dependencies
}

// In main.dart
void main() async {
  initCore();
  await initAuth();
  runApp(MyApp());
}

Routing with Features

For navigation, you can use go_router with feature-based routes. Each feature can export its own GoRoute definition, and you compose them in a central router. This keeps routing modular.

DARTRead-only
1
import 'package:go_router/go_router.dart';
import 'pages/login_page.dart';
import 'pages/register_page.dart';

List<GoRoute> authRoutes = [
  GoRoute(
    path: '/login',
    builder: (context, state) => const LoginPage(),
  ),
  GoRoute(
    path: '/register',
    builder: (context, state) => const RegisterPage(),
  ),
];
DARTRead-only
1
import 'package:go_router/go_router.dart';
import '../features/auth/presentation/routes/auth_routes.dart';
import '../features/product/presentation/routes/product_routes.dart';

final router = GoRouter(
  initialLocation: '/',
  routes: [
    ...authRoutes,
    ...productRoutes,
    // other feature routes
  ],
);

Best Practices

  • Keep features independent – Avoid importing one feature into another. Use shared core services for communication.
  • Use a core folder for truly shared code – Utils, constants, widgets, and error handling that multiple features need.
  • Define clear boundaries – Each feature should have a single responsibility (e.g., authentication, product management).
  • Use dependency injection – Don't instantiate dependencies inside features; inject them to keep code testable.
  • Export feature's public API – If you plan to split features into packages later, create a public API for each feature (e.g., auth_api.dart).
  • Keep folders shallow – Avoid nesting more than 3‑4 levels deep.

Common Mistakes

  • ❌ Creating circular dependencies between features – Feature A imports Feature B and vice versa. ✅ Use shared services or events to communicate.
  • ❌ Putting all code in one giant feature – Defeats the purpose. ✅ Split into smaller, focused features.
  • ❌ Mixing UI and business logic across features – Makes testing hard. ✅ Keep each feature self‑contained.
  • ❌ Hardcoding imports with deep relative paths – Hard to refactor. ✅ Use package: imports (after structuring your project).
  • ❌ Forgetting to register dependencies per feature – May cause missing dependencies at runtime. ✅ Call each feature's init function in main.

Conclusion

A feature-based structure combined with BLoC gives you a scalable, maintainable architecture for large Flutter apps. By keeping each feature self‑contained and using dependency injection, you enable parallel development, easier testing, and a clear separation of concerns. Start with a simple folder layout and evolve it as your app grows.

Test Your Knowledge

Q1
of 3

What is the main benefit of organizing code by feature instead of by layer?

A
It reduces code duplication
B
It keeps related code together, improving scalability and team collaboration
C
It makes the app run faster
D
It automatically handles dependency injection
Q2
of 3

Where should shared widgets like a custom button be placed in a feature-based structure?

A
Inside each feature that uses it
B
In the `core/widgets` folder
C
Inside the `presentation` folder of a random feature
D
In a separate package
Q3
of 3

Which of the following is a common mistake when using feature-based structure?

A
Using dependency injection
B
Creating circular dependencies between features
C
Using GoRouter for navigation
D
Separating UI and business logic

Frequently Asked Questions

Should I use Clean Architecture layers inside each feature?

It depends on the complexity. For medium to large apps, using presentation/domain/data layers inside each feature is recommended. For simple features, you can keep it simpler (e.g., just presentation and data).

How do I share a bloc across multiple features?

If a bloc is truly global (e.g., AuthBloc), you can register it in the core and provide it at the app level using MultiBlocProvider. Then any feature can access it via context.read<AuthBloc>().

Can I use this structure with Cubit instead of Bloc?

Yes, the folder structure is independent of whether you use Bloc or Cubit. You would place Cubits in the presentation/cubit folder.

How do I handle feature communication (e.g., when a user logs out, all features should clear data)?

Use a global AuthBloc that emits LoggedOut state. Other blocs can listen to that state via BlocListener or by subscribing to the AuthBloc stream in their onInit.

Is there a recommended tool for generating feature modules?

You can use very_good_cli (which creates a feature-based structure) or create your own templates. The get_cli also supports modular generation, though it's GetX-specific.

Previous

bloc usecase layer

Next

bloc modular architecture

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.