main
md 162 lines 6.2 KB
Rendered Raw
1 ---
2 name: flutter-apply-architecture-best-practices
3 description: Architects a Flutter application using the recommended layered approach (UI, Logic, Data). Use when structuring a new project or refactoring for scalability.
4 metadata:
5 model: models/gemini-3.1-pro-preview
6 last_modified: Tue, 21 Apr 2026 20:11:20 GMT
7 ---
8 # Architecting Flutter Applications
9
10 ## Contents
11 - [Architectural Layers](#architectural-layers)
12 - [Project Structure](#project-structure)
13 - [Workflow: Implementing a New Feature](#workflow-implementing-a-new-feature)
14 - [Examples](#examples)
15
16 ## Architectural Layers
17
18 Enforce strict Separation of Concerns by dividing the application into distinct layers. Never mix UI rendering with business logic or data fetching.
19
20 ### UI Layer (Presentation)
21 Implement the MVVM (Model-View-ViewModel) pattern to manage UI state and logic.
22 * **Views:** Write reusable, lean widgets. Restrict logic in Views to UI-specific operations (e.g., animations, layout constraints, simple routing). Pass all required data from the ViewModel.
23 * **ViewModels:** Manage UI state and handle user interactions. Extend `ChangeNotifier` (or use `Listenable`) to expose state. Expose immutable state snapshots to the View. Inject Repositories into ViewModels via the constructor.
24
25 ### Data Layer
26 Implement the Repository pattern to isolate data access logic and create a single source of truth.
27 * **Services:** Create stateless classes to wrap external APIs (HTTP clients, local databases, platform plugins). Return raw API models or `Result` wrappers.
28 * **Repositories:** Consume one or more Services. Transform raw API models into clean Domain Models. Handle caching, offline synchronization, and retry logic. Expose Domain Models to ViewModels.
29
30 ### Logic Layer (Domain - Optional)
31 * **Use Cases:** Implement this layer only if the application contains complex business logic that clutters the ViewModel, or if logic must be reused across multiple ViewModels. Extract this logic into dedicated Use Case (interactor) classes that sit between ViewModels and Repositories.
32
33 ## Project Structure
34
35 Organize the codebase using a hybrid approach: group UI components by feature, and group Data/Domain components by type.
36
37 ```text
38 lib/
39 ├── data/
40 │ ├── models/ # API models
41 │ ├── repositories/ # Repository implementations
42 │ └── services/ # API clients, local storage wrappers
43 ├── domain/
44 │ ├── models/ # Clean domain models
45 │ └── use_cases/ # Optional business logic classes
46 └── ui/
47 ├── core/ # Shared widgets, themes, typography
48 └── features/
49 └── [feature_name]/
50 ├── view_models/
51 └── views/
52 ```
53
54 ## Workflow: Implementing a New Feature
55
56 Follow this sequential workflow when adding a new feature to the application. Copy the checklist to track progress.
57
58 ### Task Progress
59 - [ ] **Step 1: Define Domain Models.** Create immutable data classes for the feature using `freezed` or `built_value`.
60 - [ ] **Step 2: Implement Services.** Create or update Service classes to handle external API communication.
61 - [ ] **Step 3: Implement Repositories.** Create the Repository to consume Services and return Domain Models.
62 - [ ] **Step 4: Apply Conditional Logic (Domain Layer).**
63 - *If the feature requires complex data transformation or cross-repository logic:* Create a Use Case class.
64 - *If the feature is a simple CRUD operation:* Skip to Step 5.
65 - [ ] **Step 5: Implement the ViewModel.** Create the ViewModel extending `ChangeNotifier`. Inject required Repositories/Use Cases. Expose immutable state and command methods.
66 - [ ] **Step 6: Implement the View.** Create the UI widget. Use `ListenableBuilder` or `AnimatedBuilder` to listen to ViewModel changes.
67 - [ ] **Step 7: Inject Dependencies.** Register the new Service, Repository, and ViewModel in the dependency injection container (e.g., `provider` or `get_it`).
68 - [ ] **Step 8: Run Validator.** Execute unit tests for the ViewModel and Repository.
69 - *Feedback Loop:* Run tests -> Review failures -> Fix logic -> Re-run until passing.
70
71 ## Examples
72
73 ### Data Layer: Service and Repository
74
75 ```dart
76 // 1. Service (Raw API interaction)
77 class ApiClient {
78 Future<UserApiModel> fetchUser(String id) async {
79 // HTTP GET implementation...
80 }
81 }
82
83 // 2. Repository (Single source of truth, returns Domain Model)
84 class UserRepository {
85 UserRepository({required ApiClient apiClient}) : _apiClient = apiClient;
86
87 final ApiClient _apiClient;
88 User? _cachedUser;
89
90 Future<User> getUser(String id) async {
91 if (_cachedUser != null) return _cachedUser!;
92
93 final apiModel = await _apiClient.fetchUser(id);
94 _cachedUser = User(id: apiModel.id, name: apiModel.fullName); // Transform to Domain Model
95 return _cachedUser!;
96 }
97 }
98 ```
99
100 ### UI Layer: ViewModel and View
101
102 ```dart
103 // 3. ViewModel (State management and presentation logic)
104 class ProfileViewModel extends ChangeNotifier {
105 ProfileViewModel({required UserRepository userRepository})
106 : _userRepository = userRepository;
107
108 final UserRepository _userRepository;
109
110 User? _user;
111 User? get user => _user;
112
113 bool _isLoading = false;
114 bool get isLoading => _isLoading;
115
116 Future<void> loadProfile(String id) async {
117 _isLoading = true;
118 notifyListeners();
119
120 try {
121 _user = await _userRepository.getUser(id);
122 } finally {
123 _isLoading = false;
124 notifyListeners();
125 }
126 }
127 }
128
129 // 4. View (Dumb UI component)
130 class ProfileView extends StatelessWidget {
131 const ProfileView({super.key, required this.viewModel});
132
133 final ProfileViewModel viewModel;
134
135 @override
136 Widget build(BuildContext context) {
137 return ListenableBuilder(
138 listenable: viewModel,
139 builder: (context, _) {
140 if (viewModel.isLoading) {
141 return const Center(child: CircularProgressIndicator());
142 }
143
144 final user = viewModel.user;
145 if (user == null) {
146 return const Center(child: Text('User not found'));
147 }
148
149 return Column(
150 children: [
151 Text(user.name),
152 ElevatedButton(
153 onPressed: () => viewModel.loadProfile(user.id),
154 child: const Text('Refresh'),
155 ),
156 ],
157 );
158 },
159 );
160 }
161 }
162 ```