skills/flutter-architecture/references/layers.md

6.2 KiB

Architecture Layers

Detailed guide to layer responsibilities and interactions in Flutter apps.

Layer Overview

┌─────────────────────────────────────┐
│           UI Layer                  │
│  ┌────────────┐  ┌─────────────┐ │
│  │   Views    │  │ ViewModels  │ │
│  └────────────┘  └─────────────┘ │
└─────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────┐
│      Domain Layer (Optional)        │
│       ┌─────────────────────┐      │
│       │     Use-cases       │      │
│       └─────────────────────┘      │
└─────────────────────────────────────┘
                  ↓
┌─────────────────────────────────────┐
│          Data Layer                │
│  ┌────────────┐  ┌─────────────┐ │
│  │Repositories │  │  Services   │ │
│  └────────────┘  └─────────────┘ │
└─────────────────────────────────────┘

UI Layer

Responsibility: Interact with user, display data, receive input.

Views

What they do:

  • Compose widgets to present data
  • Handle user interactions (taps, form inputs)
  • Pass events to ViewModel
  • Re-render when ViewModel state changes

What they don't do:

  • Business logic
  • Data transformation
  • State persistence
  • API calls

Logic allowed in Views:

  • Simple if-statements for conditional rendering
  • Animation logic
  • Layout logic (responsive design, orientation)
  • Simple routing

ViewModels

What they do:

  • Transform repository data into UI state
  • Maintain UI state (survives configuration changes)
  • Expose commands for user actions
  • Aggregate data from multiple repositories

What they don't do:

  • Direct UI manipulation
  • Platform API calls
  • File I/O
  • Network requests directly (use repositories)

State management:

  • Use ChangeNotifier for simple apps
  • Consider Provider, Riverpod, or Bloc for complex apps
  • Expose state as Streams or ChangeNotifiers
  • Call Repositories directly for simple flows; introduce Use-cases only when business logic is complex, reused, or spans multiple Repositories.

Domain Layer (Optional)

Purpose: Abstract complex business logic from ViewModels.

When to add Domain Layer

Add when ViewModels have logic that:

  1. Merges data from multiple repositories
  2. Is exceedingly complex
  3. Will be reused by different ViewModels

Use-cases

What they do:

  • Take data from repositories
  • Transform for UI layer consumption
  • Encapsulate reusable business logic

Relationships:

  • Use-cases depend on Repositories
  • Use-cases and Repositories: many-to-many
  • ViewModels may depend on Use-cases for complex operations and on Repositories directly for simple operations.

Pros:

  • Avoid code duplication in ViewModels
  • Improve testability by separating complex logic
  • Improve code readability in ViewModels

Cons:

  • Increases complexity
  • Additional mocks for testing
  • Adds boilerplate code

Recommendation: Add use-cases only when needed. Don't force all data access through use-cases for simple operations.

Data Layer

Responsibility: Handle business data and logic.

Repositories

What they do:

  • Single source of truth for data types
  • Poll data from Services
  • Transform raw data into domain models
  • Handle business logic:
    • Caching
    • Error handling
    • Retry logic
    • Data refresh (polling, user-triggered)

What they don't do:

  • Direct UI rendering
  • Business logic better suited for Domain layer
  • Direct knowledge of other repositories

Output:

  • Domain models (data classes tailored for app needs)
  • Exposed as Streams (real-time) or Futures (one-time)

Relationships:

  • Many-to-many with ViewModels
  • Many-to-many with Services
  • Should not be aware of each other

Services

What they do:

  • Wrap external data sources
  • Expose async response objects (Future, Stream)
  • Isolate data loading
  • One service per data source

What they don't do:

  • Hold state (stateless)
  • Business logic
  • Data transformation
  • Caching

Examples:

  • Platform APIs (iOS/Android native)
  • REST/GraphQL API clients
  • Local file access
  • Database access
  • Platform plugins (geolocation, camera, etc.)

Relationships:

  • Many-to-many with Repositories
  • Can be shared across the app

Layer Communication Rules

  1. Lower layers stay independent: Data doesn't depend on Domain or UI; Domain doesn't depend on UI.
  2. Unidirectional data flow: Data -> ViewModel -> View.
  3. Events flow opposite: View -> ViewModel -> Repository -> Service, or View -> ViewModel -> Use-case -> Repository -> Service when a Use-case is justified.
  4. No direct service calls from UI: Views and ViewModels don't call Services directly.
  5. No forced Domain layer: Skipping Use-cases is valid for simple logic.

When to Use Which Layer

Small CRUD apps:

  • UI Layer (Views + ViewModels)
  • Data Layer (Repositories + Services)
  • Skip Domain Layer

Apps with complex business logic:

  • UI Layer
  • Domain Layer (for complex, reusable logic)
  • Data Layer
  • ViewModels may use Repositories directly for simple operations, Use-cases for complex

Enterprise apps with complex rules:

  • All three layers
  • Consider enforcing Use-cases for all data access

Testing Strategy

UI Layer Tests:

  • Widget tests for Views
  • Unit tests for ViewModels (mock Repositories)

Domain Layer Tests:

  • Unit tests for Use-cases (mock Repositories)

Data Layer Tests:

  • Unit tests for Repositories (mock Services)
  • Integration tests for Services (actual APIs/databases)