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:
- Merges data from multiple repositories
- Is exceedingly complex
- 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
- Lower layers stay independent: Data doesn't depend on Domain or UI; Domain doesn't depend on UI.
- Unidirectional data flow: Data -> ViewModel -> View.
- Events flow opposite: View -> ViewModel -> Repository -> Service, or View -> ViewModel -> Use-case -> Repository -> Service when a Use-case is justified.
- No direct service calls from UI: Views and ViewModels don't call Services directly.
- 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)