Skip to content

Project Structure

Feature-Driven Architecture (FDA) structures Flutter projects around self-contained features, each handling its UI, business logic, and state management. This ensures a modular, scalable, and maintainable codebase.

Feature-driven architecture provides several key benefits for Flutter development:

  • Modularity: Features are self-contained and reusable across different parts of the application
  • Scalability: New features can be added without disrupting existing functionality
  • Separation of Concerns: Clear division between UI, logic, and data layers prevents code mixing
  • Improved Collaboration: Teams can work on features independently without conflicts
  • Easier Testing & Maintenance: Isolated features simplify debugging and unit testing
  • Faster Onboarding: Self-contained features make it easier for new developers to understand the codebase

This structure helps developers build maintainable and efficient Flutter applications that can grow with business requirements.

  1. Create a new Flutter project:
    Terminal window
    flutter create my_app
  2. Set up the base folder structure:
    • Directorylib
      • features
      • shared
      • core
      • main.dart
  3. Define your app’s features and start coding!

This folder structure organizes features into distinct layers, ensuring a clean separation of concerns:

  • Directoryfeatures
    • feature_1
    • Directoryfeature_2
      • Directorydomain (Data Layer - Models, Providers, Repositories)
        • models # Data Classes
        • providers # Handles API/database interactions
        • repository # Business logic interacting with models & providers
      • Directorylogic (Business Logic Layer - ViewModel equivalent)
        • cubits # Handles state changes
        • states # Defines different states
      • Directorypresentation (UI Layer - View equivalent)
        • screens # Full-screen widgets
        • fragments # Partial UI components
        • widgets # Reusable UI elements
  • shared # Code shared between multiple features
  • core # Core functionality, constants, helpers, and routing

Architecture Flow
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Presentation │ │ Logic │ │ Domain │
│ Layer │◄────┤ Layer │◄────┤ Layer │
│ (UI/View) │ │ (ViewModel) │ │ (Data/Models) │
└───────────────────┘ └───────────────────┘ └───────────────────┘
▲ │ ▲
│ ▼ │
│ ┌───────────────────┐ │
└──────────────────┤ State Updates ├──────────────┘
│ (Cubit to View) │
└───────────────────┘

Data flows from the Domain Layer (API/DB) through the Logic Layer (state processing) to the Presentation Layer (UI). User interactions in the UI trigger actions in the Logic Layer, which may fetch or update data through the Domain Layer.

  • Directoryfeatures
    • Directoryuser_profile
      • Directorydomain
        • Directorymodels
          • user_model.dart
        • Directoryproviders
          • user_api_provider.dart
        • Directoryrepository
          • user_repository.dart
      • Directorylogic
        • Directorycubits
          • user_cubit.dart
        • Directorystates
          • user_state.dart
      • Directorypresentation
        • Directoryscreens
          • user_profile_screen.dart
        • Directoryfragments
          • profile_header_fragment.dart
        • Directorywidgets
          • user_avatar.dart

The domain layer handles data and business logic:

  • Models: Define data structures and entities (e.g., UserModel)
  • Providers: Handle API/database communication and external data sources
  • Repositories: Bridge between providers and business logic, abstracting data access
User Model
// features/user_profile/domain/models/user_model.dart
class UserModel {
final String id;
final String name;
final String email;
UserModel({required this.id, required this.name, required this.email});
factory UserModel.fromJson(Map<String, dynamic> json) {
return UserModel(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
User Repository
// features/user_profile/domain/repository/user_repository.dart
class UserRepository {
final UserApiProvider apiProvider;
UserRepository(this.apiProvider);
Future<UserModel> getUser() async {
final userData = await apiProvider.fetchUserProfile();
return UserModel.fromJson(userData);
}
}

The MVVM (Model-View-ViewModel) pattern separates concerns, ensuring better maintainability and testability. Cubits (from the BLoC pattern) handle state management efficiently by maintaining immutable states.

ViewModel Layer: Implemented via Cubits that handle state transitions and business logic. Cubits serve as the bridge between the UI and data layers, processing user actions and emitting appropriate states.

View Layer: Widgets observe Cubit states and react accordingly to state changes. This reactive approach ensures the UI automatically updates when data changes, maintaining consistency across the application.

This architecture impacts performance in several key areas:

Memory Usage: Feature separation prevents unnecessary loading of unused components. Each feature loads independently, reducing initial memory footprint and allowing for better resource management.

Build Time: Cubits rebuild only the specific widgets that depend on changed states, avoiding full UI rebuilds. This selective rebuilding improves application responsiveness and reduces unnecessary computations.

Tree Shaking: Properly structured code allows the Dart compiler to optimize away unused code during build time, resulting in smaller app bundles and faster load times.

Load Time: Consider lazy-loading features that aren’t needed during initial app startup. This approach reduces initial bundle size and improves time-to-interactive metrics.

  1. Identify Features List distinct functionalities in your app.

  2. Create Structure Set up the base folder structure alongside existing code.

  3. Gradual Migration Move one feature at a time, starting with less complex ones.

  4. Refactor State Management Gradually introduce Cubits for state management.

  5. Update Imports Fix import paths throughout the codebase.

  6. Test Thoroughly After each migration step, run tests to ensure functionality.

Keep dependencies managed efficiently using a service locator like GetIt. This ensures loose coupling between components and makes testing easier by allowing dependency mocking.

Keep features self-contained and avoid unnecessary dependencies between features. Each feature should be able to function independently, with shared functionality placed in the shared folder.

Structure your code to support comprehensive testing at all layers. Unit test your Cubits and repositories, widget test your UI components, and integration test complete user flows.

  1. State Management Fundamentals – Learn how to manage complex states effectively
  2. Feature-Driven Development in Flutter – Comprehensive guide to feature-first Flutter architecture
  3. Flutter Official Documentation – Stay updated with the latest Flutter best practices