This isn’t your typical 2-minutes read. Unless you’re prepared and focused, this isn’t for you yet! ⚡️
Our Goal
Present and explain the most appealing, scalable, and well-structured architecture for Flutter apps. 🌟
Who Should Read This?
Everyone. 👨💻👩💻
What Do We Call It?
Clean Architecture (MVVM with BLoC)
Let’s Dive In 🌊
Apps grow and need to scale. The best way to do so is separation of concerns. That’s why you need to truly understand how to separate logic in your structure.
Adding/removing features, collaborating on the same functionality — all of that requires a solid structure, beginning with… Features.
Each feature folder represents a specific functionality or flow in your app.
🗂 Clean Architecture Layers
Our structure consists of three main folders:
- Data
- Domain
- Presentation
Goals:
- Clearly separate concerns between Data, Domain, and Presentation layers, reducing interdependencies.
- Ensure each layer operates independently and respects the Single Responsibility Principle (SLP — look it up! 👀).
- Provide a clear and scalable structure for app architecture.
👁 Visual Representation
At the end, we should follow (and completely understand) the following graph:
💡 Simple enough? Well, we didn’t really dive in yet…
🌈 Presentation Layer
The easiest and most well-known part. It contains:
- BLoC/Controllers (State Management)
- Widgets (Screens, Stateless items, etc.)
Purpose of the Presentation Layer
Handles the app’s visual components by displaying data from the BLoC/Controller and processing user inputs by dispatching events back to BLoC.
The BLoC/Controller bridges the UI and domain layer, managing UI state, processing user actions through use cases, and retrieving data.
📈 How It Works
- Upon UI interaction, an event is dispatched, triggering the BLoC.
- BLoC calls and awaits UseCase functionality.
- Upon success, UseCase returns data (usually wrapped in an Entity).
- BLoC emits states, triggering the BlocBuilder/Listener/Consumer in the UI.
- UI rebuilds based on emitted states (e.g., update UI, navigate, show errors, etc.).
Basically, the work of UI layer starts with user interactions, ends with calling UseCases, and await results or errors.
🎮 Domain Layer — The Juicy Part 🤭
The Domain Layer contains business logic and remains entirely independent, meaning it does not rely on external libraries, databases, APIs, or frameworks. This isolation ensures business rules remain pure, reusable, and testable. (Cool, right? 🚀)
💼 Contains:
- Use Cases: Encapsulate application-specific logic, handling single operations within the system. They bridge domain rules with external interactions, processing data and coordinating repositories and entities.
- Entities: Core business objects defining key elements of the application’s domain. Immutable and contain only essential attributes, ensuring consistency.
- Repository (Abstraction): Interfaces defining operations expected from data sources, ensuring decoupling from actual implementations.
📈 How It Works
- BLoC calls UseCase.
- Each UseCase depends on one Repository and calls its functions.
- Repository performs data layer operations and returns results.
- UseCase returns data or throws errors depending on repository results.
Pro Tips 🚀
How do you know your Domain Layer was designed correctly?
- It has zero dependencies on Data or Presentation layers.
- It does not depend on Flutter-related libraries.
- Test it: Delete your Data and Presentation folders. If no IDE errors appear, your Domain Layer is truly independent. 🎉
📚 Data Layer
Handles multiple data sources (APIs, memory cache, local storage, databases) and ensures clean, structured data for the Domain Layer.
💼 Contains:
- Repositories: Define data access behavior, implement domain layer interfaces, and manage data retrieval.
- Data Sources: Interact with external systems (APIs, databases) or internal storage (cache).
- DTOs (Data Transfer Objects): Convert external data formats to domain models while keeping them separate from business logic.
- Mappers: Transform DTOs into Entities and vice versa, ensuring data consistency.
📈 How It Works
- Repository functions get called from UseCases.
- Repository maps data, converting from Entity to Request Objects.
- Request Objects (JSON) are sent to Data Sources.
- Repository decides which Data Source to use.
- No direct interaction between Data Sources; Repository handles all necessary calls.
- Repository maps results from Data Sources (DTOs) to Entities.
- Returns Entities (or mapped Exceptions) to UseCases.
Final Folder Structure
Wait, what are Exceptions, Requests, Responses? 🤔
- Exceptions: Sealed classes representing all exceptions used in a feature.
- Requests, Responses: DTO objects for communication in the Data Layer, mapped by the Repository.
🏆 Why Use This Architecture?
Separation of Concerns
- Each layer has a clear role, making the codebase organized and manageable.
Scalability & Maintainability
- New features can be added without affecting existing code.
- Large teams can work on different layers independently.
Testability
- Unit tests can be written separately for each layer.
- Business logic (Use Cases) can be tested without UI dependencies.
Reusability
- UseCases can be reused across multiple ViewModels/BLoCs.
- The same Repository can be used in different parts of the app.
Flexible Data Sources
- Switch between API, local database, or cache without affecting UI.
- Better State Management
- BLoC ensures structured state management, making UI reactive and efficient.
📖 Examples?
Sure! But let’s do it in another article… Here’s the link to the next one:
The Boring Flutter Architecture — Coding Example
See you in the next one. Cheers 🎉