flutter
/

Bloc Architecture: Scalable Project Structure for Flutter Apps

Last Sync: Today

On this page

15
0%
Intermediate
5 min read
Remaining
5 minleft

Click any section to jump β€” progress syncs automatically

flutterIntermediate

Bloc Architecture: Scalable Project Structure for Flutter Apps

πŸ“Œ Summary (Featured Snippet) – Bloc architecture in Flutter is a structured approach that separates UI, business logic, and data layers, enabling scalable, maintainable, and testable applications. This guide covers folder structure, repository pattern, dependency injection, and a complete authentication example.

πŸš€ Quick Start – If you just want a working folder structure and code example, jump to the Implementation Example section. For a full understanding of why and how to structure large Flutter apps, read the entire guide.

If your Flutter app is growing and becoming hard to manage, a proper architecture is essential. Bloc architecture helps you organize your project into scalable layers, making your code clean, testable, and easy to maintain. Unlike a simple setState approach, this pattern ensures that adding new features won't turn your codebase into a nightmare.

What is Bloc Architecture?

Bloc architecture refers to a structured way of organizing Flutter applications using the BLoC pattern (Business Logic Component). It goes beyond just using flutter_bloc – it defines how to separate concerns into distinct layers: presentation, business logic, and data. This leads to code that is testable, maintainable, and scalable, especially for large apps with multiple features. For a refresher on BLoC basics, check out our <a href='/flutter/bloc-introduction'>Bloc introduction</a>.

Data Flow in Bloc Architecture

This unidirectional flow ensures that each component has a single responsibility and that data changes are predictable. It also makes debugging easier because state changes are traceable.

Why a Layered Architecture?

  • Separation of concerns – UI is decoupled from business logic, which is decoupled from data sources.
  • Testability – Each layer can be unit‑tested independently.
  • Maintainability – Changes in one layer (e.g., swapping an API client) don’t affect others.
  • Scalability – New features follow the same pattern, reducing cognitive load.

Real-World Use Cases

  • Insurance form submission with validation – Handle complex multi‑step forms, validate fields, and submit to backend.
  • Login & authentication flow – Manage loading, success, error states, and token storage.
  • Fetching dropdown data from APIs – Load options from remote services with caching.
  • E‑commerce product catalog – Filtering, sorting, pagination, and offline support.

The Three Layers

Dependencies flow inward: Presentation β†’ Business Logic β†’ Data. The UI knows about the bloc, the bloc knows about repositories, repositories know about data sources. This follows the dependency inversion principle.

Recommended Folder Structure

TEXTRead-only
1
lib/
β”œβ”€β”€ main.dart
β”œβ”€β”€ app.dart                      # App entry point with MultiBlocProvider
β”œβ”€β”€ core/                         # Shared code across features
β”‚   β”œβ”€β”€ constants/
β”‚   β”œβ”€β”€ themes/
β”‚   β”œβ”€β”€ utils/
β”‚   └── widgets/
β”œβ”€β”€ features/                     # Feature-based modules
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ presentation/
β”‚   β”‚   β”‚   β”œβ”€β”€ pages/
β”‚   β”‚   β”‚   └── widgets/
β”‚   β”‚   β”œβ”€β”€ bloc/                 # Cubit/Bloc for this feature
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_bloc.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_event.dart
β”‚   β”‚   β”‚   └── auth_state.dart
β”‚   β”‚   β”œβ”€β”€ domain/               # Optional: use cases, repository interfaces
β”‚   β”‚   β”‚   β”œβ”€β”€ entities/
β”‚   β”‚   β”‚   β”œβ”€β”€ repositories/
β”‚   β”‚   β”‚   └── usecases/
β”‚   β”‚   └── data/                 # Repository implementations, data sources
β”‚   β”‚       β”œβ”€β”€ repositories/
β”‚   β”‚       └── datasources/
β”‚   └── home/
β”‚       β”œβ”€β”€ presentation/
β”‚       β”œβ”€β”€ bloc/
β”‚       └── ...
└── di/                           # Dependency injection setup (if using get_it)

Implementation Example: Authentication Feature

Let’s build an authentication feature following the layered architecture. We'll create a bloc, a repository, and a data source, and wire them together.

Start with an abstract repository interface (optional) and an implementation that uses a remote data source.

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

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  @override
  Future<String> login(String email, String password) async {
    // Simulate API call
    await Future.delayed(Duration(seconds: 1));
    if (email == 'test@example.com' && password == '123') {
      return 'fake-jwt-token';
    }
    throw Exception('Invalid credentials');
  }
}
DARTRead-only
1
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';
import '../datasources/auth_remote_datasource.dart';

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;

  AuthRepositoryImpl(this.remoteDataSource);

  @override
  Future<String> login(String email, String password) async {
    return await remoteDataSource.login(email, password);
  }
}

Define the repository interface and entities (plain Dart classes).

DARTRead-only
1
class User {
  final String id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}
DARTRead-only
1
abstract class AuthRepository {
  Future<String> login(String email, String password);
}
DARTRead-only
1
abstract class AuthEvent {}

class LoginRequested extends AuthEvent {
  final String email;
  final String password;
  LoginRequested(this.email, this.password);
}
DARTRead-only
1
abstract class AuthState {}

class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthSuccess extends AuthState {
  final String token;
  AuthSuccess(this.token);
}
class AuthFailure extends AuthState {
  final String message;
  AuthFailure(this.message);
}
DARTRead-only
1
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository repository;

  AuthBloc(this.repository) : super(AuthInitial()) {
    on<LoginRequested>(_onLoginRequested);
  }

  Future<void> _onLoginRequested(LoginRequested event, Emitter<AuthState> emit) async {
    emit(AuthLoading());
    try {
      final token = await repository.login(event.email, event.password);
      emit(AuthSuccess(token));
    } catch (e) {
      emit(AuthFailure(e.toString()));
    }
  }
}
DARTRead-only
1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/auth_bloc.dart';

class LoginPage extends StatelessWidget {
  final TextEditingController emailController = TextEditingController();
  final TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: BlocConsumer<AuthBloc, AuthState>(
          listener: (context, state) {
            if (state is AuthSuccess) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text('Logged in!')),
              );
              Navigator.pushReplacementNamed(context, '/home');
            }
            if (state is AuthFailure) {
              ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(content: Text(state.message)),
              );
            }
          },
          builder: (context, state) {
            if (state is AuthLoading) {
              return Center(child: CircularProgressIndicator());
            }
            return Column(
              children: [
                TextField(
                  controller: emailController,
                  decoration: InputDecoration(labelText: 'Email'),
                ),
                TextField(
                  controller: passwordController,
                  decoration: InputDecoration(labelText: 'Password'),
                  obscureText: true,
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: () {
                    context.read<AuthBloc>().add(
                      LoginRequested(
                        emailController.text,
                        passwordController.text,
                      ),
                    );
                  },
                  child: Text('Login'),
                ),
              ],
            );
          },
        ),
      ),
    );
  }
}

Dependency Injection

You can inject dependencies using BlocProvider and a service locator like get_it. Here’s how to set up the dependencies for the authentication feature. For more details, see <a href='/flutter/flutter-dependency-injection'>Flutter Dependency Injection</a>.

DARTRead-only
1
import 'package:get_it/get_it.dart';
import 'package:myapp/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:myapp/features/auth/data/repositories/auth_repository_impl.dart';
import 'package:myapp/features/auth/domain/repositories/auth_repository.dart';

final getIt = GetIt.instance;

void setup() {
  // Data sources
  getIt.registerLazySingleton<AuthRemoteDataSource>(
    () => AuthRemoteDataSourceImpl(),
  );

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

  // Blocs (factories because they depend on context)
  getIt.registerFactory<AuthBloc>(
    () => AuthBloc(getIt()),
  );
}

Then, in your main.dart, call setup() before runApp, and use BlocProvider to provide the bloc to the widget tree.

Bloc Architecture vs Clean Architecture

Both share similar ideas: separation of concerns, dependency inversion, and testability. Bloc architecture is a simplified adaptation for Flutter, focusing on the UI–Bloc–Repository flow. Clean Architecture adds an explicit domain layer with use cases and entities, and enforces strict dependency rules. For most Flutter apps, the structure presented here strikes the right balance between scalability and simplicity.

Performance Tips

  • Use Equatable – Avoids unnecessary rebuilds by ensuring states are only considered different when values change.
  • Split large blocs into smaller feature blocs – Keeps each bloc focused and reduces rebuild scope.
  • Use BlocSelector for granular rebuilds – Rebuild only the part of the UI that depends on a specific piece of state.
  • Avoid heavy logic inside UI layer – Keep business logic and data transformations in the bloc or use cases.

When NOT to Use Bloc Architecture

  • Small apps with simple state – Over‑engineering can slow down development.
  • Prototypes or quick demos – A simpler state management (like setState or Provider) is sufficient.
  • When Cubit alone is sufficient – For many features, a plain Cubit without full layered architecture is enough.

Best Practices

  • Use BlocProvider at the highest level needed – Provide blocs where they are required, not necessarily globally.
  • Keep blocs focused – Each bloc should handle one specific feature (e.g., LoginBloc, not UserBloc that does everything).
  • Use Equatable for states and events – Avoids unnecessary rebuilds and makes state comparison easy.
  • Avoid business logic in UI – Let blocs handle it; UI only dispatches events.
  • Use BlocListener for side effects – Navigation, snackbars, etc., not in BlocBuilder.
  • Consider use cases for complex business logic – They keep blocs thin and testable.

Common Mistakes

  • ❌ Putting API calls directly in bloc – Hard to test and swap data sources. βœ… Use repositories.
  • ❌ Creating BlocProvider inside build – Recreates the bloc on every rebuild. βœ… Use BlocProvider.value or define outside build.
  • ❌ Mixing presentation and business logic in one file – Violates separation. βœ… Keep layers in separate folders.
  • ❌ Not handling loading/error states – Users see no feedback. βœ… Always define and emit loading/error states.
  • ❌ Over‑engineering with unnecessary layers – For tiny apps, a simpler structure is fine.

Next Steps

Before diving into architecture, make sure you understand <a href='/flutter/bloc-introduction'>Bloc basics</a>. Then explore <a href='/flutter/bloc-testing'>testing blocs</a> and <a href='/flutter/bloc-cubit'>Cubit vs Bloc</a>.

Conclusion

A well‑structured Bloc architecture makes your Flutter apps scalable, testable, and maintainable. By separating concerns into presentation, business logic, and data layers, you ensure that each part can evolve independently. Start with a simple folder structure and gradually adopt more advanced patterns like use cases as your app grows.

Test Your Knowledge

Q1
of 3

What is the main purpose of the data layer in Bloc architecture?

A
Display UI
B
Handle business logic
C
Fetch and persist data from sources
D
Emit states
Q2
of 3

Which widget is used to listen for state changes without rebuilding the UI?

A
BlocBuilder
B
BlocProvider
C
BlocListener
D
BlocConsumer
Q3
of 3

Why would you use a repository pattern with Bloc?

A
To make UI look better
B
To separate data fetching logic from business logic
C
To reduce code size
D
To create widgets

Frequently Asked Questions

Do I need to use the domain layer in Flutter Bloc architecture?

No. For small projects, you can skip the domain layer and have the bloc directly depend on repository implementations. For larger projects with complex business logic, the domain layer adds clarity and testability.

How do I share a bloc between multiple screens in Flutter?

Provide the bloc at a higher level in the widget tree using BlocProvider (e.g., in the parent widget). Then all child widgets can access it via context.read<MyBloc>().

Should I use `get_it` or `BlocProvider` for dependency injection with Bloc?

Both can work together. Use get_it for non‑bloc dependencies (repositories, services) and BlocProvider for blocs. This keeps the dependency graph clean and testable.

How do I handle navigation based on state changes in Bloc?

Use BlocListener to listen for specific states (e.g., AuthSuccess) and perform navigation. Avoid using Navigator directly inside the bloc.

What's the difference between Bloc architecture and Clean Architecture?

Bloc architecture is a simplified version of Clean Architecture for Flutter. Clean Architecture adds an explicit domain layer with use cases and entities, and enforces stricter dependency inversion. The structure in this guide is a practical adaptation.

Previous

bloc introduction

Next

bloc installation

Related Content

Need help?

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