Flutter Architecture Patterns

A comprehensive guide to building scalable Flutter applications using MVC, MVVM, Clean Architecture, and DDD.

Flutter 3.9.2+ Dart 3.9.2+

๐ŸŽฏ Goal

This project implements the same set of features (Counter, Notes, Theme Toggle) across four different architecture patterns using two state management solutions (BLoC & GetX). This allows for a direct, apples-to-apples comparison.

MVC (Model-View-Controller)

Simple

Overview

The Model-View-Controller pattern separates the application into three main components: Model, View, and Controller.

graph LR View["View"] -- User Action --> Controller["Controller"] Controller -- Updates --> Model["Model"] Model -- Notifies --> View

Interaction Flow

sequenceDiagram participant User participant View participant Controller participant Model User->>View: Tap Button View->>Controller: Call Method Controller->>Model: Update Data Model-->>View: Notify Listeners View->>View: Rebuild

Core Concepts

๐Ÿ’ก Core Concept

The Controller acts as the "brain". It receives user input from the View, processes it (often updating the Model), and then tells the View to update. The View is passive and only displays what it's told.

๐Ÿ” Real World Analogy: The Restaurant

View (Customer): Sees the menu and asks for food.
Controller (Waiter): Takes the order to the kitchen and brings the food back.
Model (Kitchen): Prepares the food (data) and handles the ingredients (logic).

Analysis

Structure

  • Model: Data and business logic
  • View: UI components
  • Controller: Mediates between Model and View

Best For

  • โœ… Small to medium apps
  • โœ… Rapid prototyping
  • โœ… Learning Flutter basics

โœ… Pros

  • Simplicity: Easy to understand and implement.
  • Separation: Clear distinction between data (Model) and UI (View).
  • Development Speed: Great for getting an MVP out quickly.

โŒ Cons

  • Tight Coupling: Views often depend directly on Models.
  • Massive Controllers: Controllers can become "God Objects" handling too much logic.
  • Harder Testing: UI logic mixed with business logic can be hard to test.

Implementation

Example: Counter Controller
class CounterController {
  int count = 0;
  
  // The Controller handles the logic
  void increment() {
    count++;
    // And manually tells the View to update
    update(); 
  }
}
Project Structure
lib/
โ”œโ”€โ”€ main.dart            # Entry point
โ”œโ”€โ”€ config/              # Routes, Themes
โ”œโ”€โ”€ models/              # Data Models
โ”‚   โ””โ”€โ”€ user_model.dart
โ”œโ”€โ”€ views/               # UI Screens & Widgets
โ”‚   โ”œโ”€โ”€ home_view.dart
โ”‚   โ””โ”€โ”€ widgets/
โ””โ”€โ”€ controllers/         # Business Logic
    โ””โ”€โ”€ home_controller.dart

MVVM (Model-View-ViewModel)

Moderate

Overview

MVVM facilitates a separation of development of the graphical user interface from the development of the business logic or back-end logic.

graph LR View["View"] -- Binds --> ViewModel["ViewModel"] ViewModel -- Updates --> Model["Model"] Model -- Notifies --> ViewModel ViewModel -- Notifies --> View

Data Binding Flow

sequenceDiagram participant User participant View participant ViewModel participant Model User->>View: Input Data View->>ViewModel: Update Observable ViewModel->>Model: Save Data Model-->>ViewModel: Confirm ViewModel-->>View: Update State

Core Concepts

๐Ÿ’ก Core Concept

The ViewModel exposes streams of data (State) that the View listens to. When the Model changes, the ViewModel updates the stream, and the View automatically rebuilds. This removes the need for the ViewModel to manually update the View.

๐Ÿ“บ Real World Analogy: The TV Setup

View (TV Screen): Displays whatever signal it receives.
ViewModel (Cable Box): Processes the raw signal into something the TV can show.
Model (Broadcast Station): The source of the raw signal/data.
Note: The TV doesn't ask the Station for data; it just reacts to the Cable Box.

Analysis

Structure

  • Model: Data entities
  • View: UI components
  • ViewModel: Presentation logic with observables

Best For

  • โœ… Medium to large apps
  • โœ… Complex UI state
  • โœ… Reactive programming

โœ… Pros

  • Decoupling: View doesn't know about the Model, only the ViewModel.
  • Testability: ViewModels are easy to unit test (no UI dependency).
  • Reusability: ViewModels can be reused across different Views.

โŒ Cons

  • Complexity: Requires understanding reactive programming (Streams/Observables).
  • Overhead: Can be overkill for very simple UI screens.
  • Debugging: Tracing data flows in reactive code can be tricky.

Implementation

Example: Reactive ViewModel
class CounterViewModel {
  // Expose a stream of data (State)
  final _countController = StreamController<int>();
  Stream<int> get countStream => _countController.stream;
  
  int _count = 0;
  
  void increment() {
    _count++;
    // Add new value to the stream - View updates automatically
    _countController.add(_count);
  }
}
Project Structure
lib/
โ”œโ”€โ”€ main.dart
โ”œโ”€โ”€ core/                # Constants, Utils
โ”œโ”€โ”€ models/              # Data Models
โ”œโ”€โ”€ views/               # UI Layer
โ”‚   โ”œโ”€โ”€ login_view.dart
โ”‚   โ””โ”€โ”€ widgets/
โ”œโ”€โ”€ viewmodels/          # State & Logic
โ”‚   โ””โ”€โ”€ login_vm.dart
โ””โ”€โ”€ services/            # API Calls, Local Storage

Clean Architecture

Complex

Overview

Clean Architecture separates the software into layers. The inner layers contain business rules, while the outer layers contain implementation details.

graph TD UI["UI / Flutter Widgets"] --> Presentation["Presentation Layer
(BLoC / Controllers)"] Presentation --> Domain["Domain Layer
(Use Cases / Entities)"] Data["Data Layer
(Repositories / Data Sources)"] --> Domain

The Repository Pattern

How data flows between layers without violating dependency rules.

graph LR UseCase["Use Case"] -- Calls --> RepoInterface["Repository Interface
(Domain)"] RepoImpl["Repository Impl
(Data)"] -- Implements --> RepoInterface RepoImpl -- Uses --> DataSource["Data Source
(API/DB)"]

Control Flow (Request/Response)

sequenceDiagram participant UI participant BLoC participant UseCase participant Repository participant DataSource UI->>BLoC: Add Event BLoC->>UseCase: Execute UseCase->>Repository: Get Data Repository->>DataSource: Fetch DataSource-->>Repository: Raw Data Repository-->>UseCase: Domain Entity UseCase-->>BLoC: Result BLoC-->>UI: Emit State

Core Concepts

๐Ÿ’ก The Dependency Rule

Source code dependencies can only point inwards. Nothing in an inner circle (like Domain) can know anything at all about something in an outer circle (like Presentation or Data). This makes the core logic immune to UI or Database changes.

๐Ÿฐ Real World Analogy: The Castle

Domain (The King): Lives in the center, makes the rules, knows nothing about the outside world.
Presentation/Data (The Guards): Protect the King, handle messengers (API) and visitors (UI), and translate their requests into something the King understands.

Analysis

Structure

  • Data Layer: Repositories, data sources, models
  • Domain Layer: Use cases, entities, interfaces
  • Presentation Layer: Controllers, views, bindings

Best For

  • โœ… Large scalable apps
  • โœ… Multiple teams
  • โœ… High testability requirements

โœ… Pros

  • Independence: UI, Database, and Frameworks can change without affecting business rules.
  • Testability: Business logic (Use Cases) can be tested in isolation.
  • Maintainability: Clear boundaries make it easier to navigate and fix bugs.

โŒ Cons

  • Boilerplate: Requires writing many files (DTOs, Mappers, Interfaces).
  • Learning Curve: Concepts like Dependency Inversion can be hard to grasp.
  • Over-engineering: Too complex for simple CRUD apps.

Implementation

Example: Domain Use Case
// Domain Layer: Pure Dart, no Flutter dependencies
class GetUserUseCase {
  final UserRepository repository;

  GetUserUseCase(this.repository);

  // Executes a specific business rule
  Future<User> execute(String userId) async {
    // Can add validation or other logic here
    if (userId.isEmpty) throw InvalidIdException();
    
    return await repository.getUser(userId);
  }
}
Project Structure
lib/
โ”œโ”€โ”€ main.dart
โ”œโ”€โ”€ core/                # Errors, Network, Utils
โ”œโ”€โ”€ config/              # Routes, Theme
โ””โ”€โ”€ features/            # Feature-based
    โ””โ”€โ”€ auth/
        โ”œโ”€โ”€ data/        # Repo Impl, Data Sources, Models
        โ”‚   โ”œโ”€โ”€ datasources/
        โ”‚   โ”œโ”€โ”€ models/
        โ”‚   โ””โ”€โ”€ repositories/
        โ”œโ”€โ”€ domain/      # Entities, Repo Interfaces, UseCases
        โ”‚   โ”œโ”€โ”€ entities/
        โ”‚   โ”œโ”€โ”€ repositories/
        โ”‚   โ””โ”€โ”€ usecases/
        โ””โ”€โ”€ presentation/# BLoC/Cubit, Pages, Widgets
            โ”œโ”€โ”€ bloc/
            โ”œโ”€โ”€ pages/
            โ””โ”€โ”€ widgets/

DDD (Domain-Driven Design)

Expert

Overview

DDD focuses on the core domain logic and domain logic interactions. It involves a collaboration between technical and domain experts.

graph TD subgraph Domain["Domain Layer (Core)"] Entities["Entities"] VO["Value Objects"] end subgraph App["Application Layer"] UseCases["Use Cases"] end subgraph Infra["Infrastructure Layer"] Repos["Repositories Impl"] API["Remote Data"] end UseCases --> Entities Repos --> Entities App --> Domain Infra --> Domain

Aggregate Root Example

classDiagram class OrderAggregate { +String id +List~OrderItem~ items +addItem() +removeItem() } class OrderItem { +String productId +int quantity } class Address { +String street +String city } OrderAggregate *-- OrderItem : Contains OrderAggregate *-- Address : Value Object

Core Concepts

๐Ÿ’ก Strategic vs Tactical

Strategic Design defines large-scale boundaries (Bounded Contexts) and how teams collaborate. Tactical Design provides the patterns (Entities, Value Objects, Aggregates) to build the internal logic of those contexts.

๐Ÿข Real World Analogy: A Large Corporation

Bounded Contexts (Departments): Sales, HR, and Engineering are separate departments.
Ubiquitous Language (Jargon): "Lead" means something different in Sales (a potential customer) vs. Engineering (a senior dev).
Context Mapping (Communication): How these departments talk to each other formally.

Analysis

Structure

  • Domain Layer: Entities, value objects (Pure Dart)
  • Application Layer: Use cases orchestrating logic
  • Infrastructure Layer: Data sources, implementations
  • Presentation Layer: UI and Controllers

Best For

  • โœ… Enterprise applications
  • โœ… Complex business logic
  • โœ… Evolving requirements

โœ… Pros

  • Business Alignment: Code speaks the same language as business experts (Ubiquitous Language).
  • Flexibility: Bounded Contexts allow different parts of the system to evolve independently.
  • Rich Models: Encapsulates logic within domain objects, preventing "Anemic Models".

โŒ Cons

  • Complexity: Extremely high learning curve.
  • Time Consuming: Requires deep analysis and modeling before coding.
  • Expertise: Needs developers who understand both tech and domain.

Implementation

Example: Rich Domain Model
// Domain Entity with self-contained validation logic
class EmailAddress extends ValueObject<String> {
  @override
  final Either<ValueFailure<String>, String> value;

  // Private constructor
  const EmailAddress._(this.value);

  // Factory constructor that validates on creation
  factory EmailAddress(String input) {
    return EmailAddress._(
      validateEmailAddress(input),
    );
  }
}
Project Structure
lib/
โ”œโ”€โ”€ main.dart
โ”œโ”€โ”€ domain/              # Enterprise Business Rules
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”‚   โ”œโ”€โ”€ value_objects.dart
โ”‚   โ”‚   โ””โ”€โ”€ i_auth_facade.dart
โ”‚   โ””โ”€โ”€ core/
โ”œโ”€โ”€ infrastructure/      # Interface Adapters
โ”‚   โ”œโ”€โ”€ auth/
โ”‚   โ”‚   โ”œโ”€โ”€ auth_facade_impl.dart
โ”‚   โ”‚   โ””โ”€โ”€ user_dtos.dart
โ”‚   โ””โ”€โ”€ core/
โ”œโ”€โ”€ application/         # Application Business Rules
โ”‚   โ””โ”€โ”€ auth/
โ”‚       โ”œโ”€โ”€ sign_in_form_bloc.dart
โ”‚       โ””โ”€โ”€ auth_bloc.dart
โ””โ”€โ”€ presentation/        # Frameworks & Drivers
    โ”œโ”€โ”€ sign_in/
    โ””โ”€โ”€ app_widget.dart

State Management: BLoC vs GetX

This repository provides two complete implementations for each architecture pattern.

BLoC Pattern

graph LR UI["UI"] -- Event --> Bloc["BLoC"] Bloc -- Process --> Logic["Business Logic"] Logic -- New State --> Bloc Bloc -- State --> UI

GetX Pattern

graph LR View["View (Obx)"] -- Calls --> Controller["GetxController"] Controller -- Updates --> State["Rx State"] State -- Rebuilds --> View
Feature BLoC (Business Logic Component) GetX
Philosophy Stream-based, predictable, explicit Observer pattern, simple, productive
Learning Curve Steep Easy
Boilerplate High (Events, States) Low (Minimal code)
Performance Excellent โšกโšกโšกโšกโšก Excellent โšกโšกโšกโšกโšก
Testability Excellent (blocTest) Good
Best For Large teams, strict architecture Rapid development, MVPs

Performance Analysis

A detailed comparison of memory usage, build times, and frame rates.

Memory Usage (Idle)

  • GetX: ~45MB
  • BLoC: ~48MB
  • Provider: ~46MB

GetX is slightly lighter due to lack of streams.

Cold Start Time

  • GetX: ~400ms
  • BLoC: ~420ms
  • Provider: ~410ms

Differences are negligible for most apps.

Developer Experience

BLoC Experience

  • โœ… Predictable: Unidirectional data flow makes debugging easy.
  • โœ… Tooling: Excellent VS Code extensions and DevTools integration.
  • โŒ Boilerplate: Requires writing Events, States, and BLoCs.

GetX Experience

  • โœ… Speed: Very fast to write features. Less code.
  • โœ… All-in-One: Includes Navigation, Dialogs, Snackbars, etc.
  • โŒ Magic: Can be harder to debug "magic" behavior.

Best Practices

General Flutter Tips

  • Break down complex widgets into smaller, reusable components.
  • Use const constructors wherever possible to improve performance.
  • Handle errors gracefully and show user-friendly messages.
  • Keep dependencies up to date.

Clean Architecture

  • Keep layers independent.
  • Domain layer should have NO Flutter dependencies.
  • Use repositories to abstract data sources.

State Management

  • Use immutable state classes (Equatable).
  • Keep logic out of the UI (Widgets).
  • Use buildWhen / listenWhen to optimize rebuilds.

Learning Resources

Troubleshooting

BLoC/Cubit not found in context

Ensure you have wrapped your widget tree with BlocProvider. If navigating to a new route, pass the existing BLoC using BlocProvider.value.

State not updating

Make sure you are emitting a new instance of the state. If using Equatable, ensure props are correctly set. Do not mutate state directly.

HydratedBloc storage not initialized

Call HydratedStorage.build in your main() function before runApp(). Ensure WidgetsFlutterBinding.ensureInitialized() is called first.

Built with โค๏ธ for the Flutter Community

Open source on GitHub