DEV Community

vmodal_ai
vmodal_ai

Posted on

Clean Architecture in Flutter: A Production-Ready Folder Structure Guide with State Management

As Flutter applications grow, managing code becomes challenging. A project that starts with a few screens can quickly become difficult to maintain when you add authentication, APIs, databases, state management, and multiple developers.

This is where Clean Architecture helps.

In this tutorial, we will learn:

  • What Clean Architecture means
  • Why Flutter projects need it
  • The recommended folder structure
  • How Presentation, Domain, and Data layers work together
  • How to organize a production Flutter application

What is Clean Architecture?

Clean Architecture is a software design pattern that separates an application into independent layers.

The main goal is:

Business logic should not depend on UI, frameworks, or external services.

Instead of mixing everything together:

Screen → API → Database
Enter fullscreen mode Exit fullscreen mode

we separate responsibilities:

Presentation Layer
        ↓
Domain Layer
        ↓
Data Layer
Enter fullscreen mode Exit fullscreen mode

Each layer has a specific role.


Why Use Clean Architecture in Flutter?

A simple Flutter project often starts like this:

lib/

├── screens/
├── widgets/
├── models/
└── services/
Enter fullscreen mode Exit fullscreen mode

This works for small apps, but problems appear as the application grows:

  • UI code contains business logic
  • API changes require many modifications
  • Testing becomes difficult
  • Code becomes hard to reuse
  • Features become tightly connected

Clean Architecture solves these problems by creating clear boundaries.


Clean Architecture Layers in Flutter

A production Flutter application usually contains three main layers:

lib/

├── presentation/
├── domain/
└── data/
Enter fullscreen mode Exit fullscreen mode

Let's understand each layer.


1. Presentation Layer

The presentation layer is responsible for everything related to the user interface.

It contains:

  • Screens
  • Widgets
  • State management
  • UI events
  • User interactions

Example structure:

presentation/

├── pages/
│   ├── login_page.dart
│   └── home_page.dart
│
├── widgets/
│   └── custom_button.dart
│
└── bloc/
    ├── auth_bloc.dart
    ├── auth_event.dart
    └── auth_state.dart
Enter fullscreen mode Exit fullscreen mode

The UI should not directly call APIs.

Avoid:

class LoginPage {

 Future<void> login(){

   api.login();

 }

}
Enter fullscreen mode Exit fullscreen mode

The UI should communicate with the domain layer instead.


2. Domain Layer

The domain layer contains the application's business rules.

This is the core of your application.

It should not depend on:

  • Flutter
  • Firebase
  • REST APIs
  • Databases

It contains:

  • Entities
  • Use Cases
  • Repository interfaces

Structure:

domain/

├── entities/
│   └── user.dart
│
├── repositories/
│   └── auth_repository.dart
│
└── usecases/
    └── login_user.dart
Enter fullscreen mode Exit fullscreen mode

Entities

Entities represent business objects.

Example:

class User {

  final String id;
  final String email;

  User({
    required this.id,
    required this.email,
  });

}
Enter fullscreen mode Exit fullscreen mode

The entity only contains business data.

No API code.
No Firebase code.
No JSON conversion.


Use Cases

A use case represents a user action.

Examples:

  • Login user
  • Get profile
  • Update settings
  • Upload image

Example:

class LoginUser {

 final AuthRepository repository;

 LoginUser(this.repository);


 Future<User> call(
    String email,
    String password
 ){

   return repository.login(
      email,
      password
   );

 }

}
Enter fullscreen mode Exit fullscreen mode

The use case does not know where the data comes from.

It only communicates with the repository.


3. Data Layer

The data layer handles external sources.

Examples:

  • REST API
  • Firebase
  • SQLite
  • Local storage

Structure:

data/

├── models/
│   └── user_model.dart
│
├── repositories/
│   └── auth_repository_impl.dart
│
└── datasources/

    ├── remote/
    │   └── auth_api.dart
    │
    └── local/
        └── storage.dart
Enter fullscreen mode Exit fullscreen mode

Repository Pattern

Repositories create a connection between the domain and data layers.

The flow looks like this:

Use Case

   ↓

Repository Interface

   ↓

Repository Implementation

   ↓

API / Database
Enter fullscreen mode Exit fullscreen mode

Repository Interface

Located in the domain layer:

abstract class AuthRepository {

 Future<User> login(
    String email,
    String password
 );

}
Enter fullscreen mode Exit fullscreen mode

The domain layer only defines what should happen.


Repository Implementation

Located in the data layer:

class AuthRepositoryImpl 
implements AuthRepository {


 final AuthRemoteDataSource remote;


 AuthRepositoryImpl(
    this.remote
 );


 @override
 Future<User> login(
    String email,
    String password
 ) async {


   return await remote.login(
      email,
      password
   );

 }

}
Enter fullscreen mode Exit fullscreen mode

The data layer decides how data is fetched.


Production Flutter Folder Structure

For large applications, feature-based architecture works best.

Example:

lib/

├── core/
│
│   ├── constants/
│   ├── errors/
│   ├── network/
│   └── utils/
│
├── features/
│
│   ├── authentication/
│   │
│   │   ├── data/
│   │   │
│   │   ├── domain/
│   │   │
│   │   └── presentation/
│   │
│   │
│   ├── profile/
│   │
│   └── home/
│
└── main.dart
Enter fullscreen mode Exit fullscreen mode

Each feature owns its:

  • Screens
  • Business logic
  • API integration
  • Models

This makes the project easier to scale.


Dependency Rule

The most important Clean Architecture rule:

Presentation
      |
      ↓
Domain
      |
      ↓
Data
Enter fullscreen mode Exit fullscreen mode

The domain layer should never import Flutter packages.

Avoid:

import 'package:flutter/material.dart';
Enter fullscreen mode Exit fullscreen mode

inside your domain folder.

Your business logic should remain independent.


State Management with Clean Architecture

Clean Architecture works well with:

  • BLoC
  • Cubit
  • Riverpod
  • Provider

Example flow:

Login Button

      ↓

AuthBloc

      ↓

LoginUser UseCase

      ↓

AuthRepository

      ↓

Firebase API
Enter fullscreen mode Exit fullscreen mode

Each component has a clear responsibility.


Dependency Injection

Large applications usually use dependency injection.

Popular packages:

  • get_it
  • injectable

Example:

final getIt = GetIt.instance;


void setupDependencies(){

 getIt.registerLazySingleton(
    () => AuthRepositoryImpl()
 );

}
Enter fullscreen mode Exit fullscreen mode

Dependency injection makes code easier to test and maintain.


Benefits of Clean Architecture

Using Clean Architecture gives you:

✅ Better code organization
✅ Easier testing
✅ Independent business logic
✅ Easier feature development
✅ Better teamwork
✅ Easier maintenance


When Should You Use Clean Architecture?

Use it for:

✅ Enterprise applications
✅ Production apps
✅ Apps with multiple developers
✅ Long-term projects

Avoid over-engineering:

❌ Small demo apps
❌ Simple prototypes
❌ One-screen applications


Conclusion

Clean Architecture provides a scalable structure for Flutter applications.

The main principles are:

  • Separate UI from business logic
  • Keep domain independent
  • Use repositories as boundaries
  • Organize code by features
  • Keep dependencies pointing inward

A clean architecture setup may require more initial effort, but it saves significant time when your Flutter application grows.


Recommended Dev.to Tags

#flutter
#dart
#mobile
#architecture
#cleanarchitecture
Enter fullscreen mode Exit fullscreen mode

Top comments (0)