Project Structure
Feature-Driven Architecture
Section titled “Feature-Driven Architecture”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.
Why Feature-Driven Architecture?
Section titled “Why Feature-Driven Architecture?”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.
Getting Started
Section titled “Getting Started”- Create a new Flutter project:
Terminal window flutter create my_app - Set up the base folder structure:
Directorylib
- features
- shared
- core
- main.dart
- Define your app’s features and start coding!
Suggested Folder Structure
Section titled “Suggested Folder Structure”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 Diagram
Section titled “Architecture Flow Diagram”┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐│ 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.
Example Feature: User Profile
Section titled “Example Feature: User Profile”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
Layer Breakdown
Section titled “Layer Breakdown”Domain Layer
Section titled “Domain Layer”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
Domain Layer Code Examples
Section titled “Domain Layer Code Examples”// features/user_profile/domain/models/user_model.dartclass 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, }; }}
// features/user_profile/domain/repository/user_repository.dartclass UserRepository {final UserApiProvider apiProvider;
UserRepository(this.apiProvider);
Future<UserModel> getUser() async { final userData = await apiProvider.fetchUserProfile(); return UserModel.fromJson(userData);}}
Logic Layer
Section titled “Logic Layer”The logic layer manages state and business operations:
- Cubits (ViewModel Layer): Manage state transitions efficiently using the BLoC pattern
- States: Immutable state classes that represent different application states (e.g.,
UserLoaded
,UserError
)
Logic Layer Code Examples
Section titled “Logic Layer Code Examples”// features/user_profile/logic/states/user_state.dartabstract class UserState {}
class UserInitial extends UserState {}class UserLoading extends UserState {}class UserLoaded extends UserState {final UserModel user;UserLoaded(this.user);}class UserError extends UserState {final String message;UserError(this.message);}
// features/user_profile/logic/cubits/user_cubit.dartclass UserCubit extends Cubit<UserState> {final UserRepository repository;UserCubit(this.repository) : super(UserInitial());
Future<void> fetchUserProfile() async { emit(UserLoading()); try { final user = await repository.getUser(); emit(UserLoaded(user)); } catch (e) { emit(UserError(e.toString())); }}}
Presentation Layer
Section titled “Presentation Layer”The presentation layer handles all UI components and user interactions:
- Screens: Full-page UIs that represent complete application views (e.g.,
UserProfileScreen
) - Fragments: Reusable screen sections that can be composed into larger views (e.g.,
ProfileHeaderFragment
) - Widgets: Atomic UI components that provide specific functionality (e.g.,
UserAvatar
)
Presentation Layer Code Examples
Section titled “Presentation Layer Code Examples”// features/user_profile/presentation/screens/user_profile_screen.dartclass UserProfileScreen extends StatelessWidget {@overrideWidget build(BuildContext context) { return BlocProvider( create: (_) => getIt<UserCubit>()..fetchUserProfile(), child: Scaffold( appBar: AppBar(title: Text('User Profile')), body: BlocBuilder<UserCubit, UserState>( builder: (context, state) { if (state is UserLoading) { return Center(child: CircularProgressIndicator()); } else if (state is UserLoaded) { return ProfileContent(user: state.user); } return Center(child: Text('Select a user')); }, ), ), );}}
// features/user_profile/presentation/widgets/user_avatar.dartclass UserAvatar extends StatelessWidget {final String avatarUrl;
const UserAvatar({Key? key, required this.avatarUrl}) : super(key: key);
@overrideWidget build(BuildContext context) { return Container( width: 80, height: 80, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Theme.of(context).primaryColor), ), child: ClipOval( child: Image.network( avatarUrl, fit: BoxFit.cover, errorBuilder: (_, __, ___) => Icon(Icons.person), ), ), );}}
Applying the MVVM Pattern with Cubits
Section titled “Applying the MVVM Pattern with Cubits”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.
Performance Considerations
Section titled “Performance Considerations”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.
Migrating Existing Projects
Section titled “Migrating Existing Projects”-
Identify Features List distinct functionalities in your app.
-
Create Structure Set up the base folder structure alongside existing code.
-
Gradual Migration Move one feature at a time, starting with less complex ones.
-
Refactor State Management Gradually introduce Cubits for state management.
-
Update Imports Fix import paths throughout the codebase.
-
Test Thoroughly After each migration step, run tests to ensure functionality.
Best Practices
Section titled “Best Practices”Dependency Injection
Section titled “Dependency Injection”Keep dependencies managed efficiently using a service locator like GetIt. This ensures loose coupling between components and makes testing easier by allowing dependency mocking.
Modularity
Section titled “Modularity”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.
Testing Strategy
Section titled “Testing Strategy”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.
See Also
Section titled “See Also”- State Management Fundamentals – Learn how to manage complex states effectively
- Feature-Driven Development in Flutter – Comprehensive guide to feature-first Flutter architecture
- Flutter Official Documentation – Stay updated with the latest Flutter best practices