main
md 196 lines 11.4 KB
Rendered Raw
1 ---
2 name: flutter-feature-based-architecture
3 description: Design, implement, refactor, and review Flutter apps that use feature-based vertical slices, handwritten Dart models/providers, componentized StatelessWidget UI, Riverpod state management, and a shared core layer. Use when adding Flutter features, organizing lib/, moving shared code, wiring repositories/providers/routes, avoiding code generation, avoiding private UI build helper methods, or checking architecture consistency in a Riverpod Flutter project.
4 ---
5
6 # Flutter Feature-Based Architecture
7
8 Use this skill to keep Flutter code organized by product capability instead of broad technical layers. Prefer vertical feature slices under `lib/features/`, keep stable cross-feature code in `lib/core/`, use Riverpod as the state boundary between UI and data access, write models/providers by hand, and split UI into small `StatelessWidget` components.
9
10 ## First Pass
11
12 Before editing a project:
13
14 1. Inspect `pubspec.yaml`, `lib/`, route setup, serialization conventions, and existing Riverpod style.
15 2. Preserve coherent local conventions, especially folder names such as `repository/` vs `repositories/`, `components/` vs `widgets/`, and handwritten provider organization.
16 3. Prefer the smallest architecture change that solves the request.
17 4. State any dependency assumptions if Riverpod, routing, or serialization helpers are missing.
18 5. Run the project-standard checks after changes, usually `flutter analyze` and relevant tests.
19
20 ## Target Shape
21
22 Use this default shape for new projects or features unless the existing project already has a consistent equivalent:
23
24 ```text
25 lib/
26 ├── app/ # Optional app composition: shell, router, bootstrap
27 ├── core/ # Shared, cross-cutting concerns
28 │ ├── components/ # Reusable presentational widgets
29 │ ├── constants/ # App-wide constants and non-secret config
30 │ ├── extensions/ # Shared Dart extensions
31 │ ├── locales/ # Localization setup and handwritten keys
32 │ ├── logger/ # App-wide logging wrapper
33 │ ├── models/ # Cross-feature models only
34 │ ├── providers/ # App-wide Riverpod providers
35 │ ├── repositories/ # Shared repositories and infrastructure services
36 │ ├── routes/ # Route config if the app has no app/ layer
37 │ ├── theme/ # ThemeData, text styles, component themes
38 │ └── utils/ # General-purpose helpers
39 ├── features/
40 │ └── orders/
41 │ ├── data/
42 │ │ ├── models/
43 │ │ └── repositories/
44 │ ├── providers/
45 │ └── views/
46 │ └── components/
47 └── main.dart
48 ```
49
50 ### Core Layer
51
52 Move code to `core/` only when it is genuinely app-wide or shared by multiple features. Keep feature-specific models, providers, repository methods, and UI components inside the owning feature until there is a stable reason to extract them.
53
54 Place composition code carefully:
55
56 - Let `main.dart`, `app/`, or `core/routes/` import feature entry pages to wire navigation.
57 - Do not let one feature import another feature directly.
58 - Move shared contracts, route constants, data types, or UI primitives to `core/` when two features need them.
59
60 ## Feature Layer
61
62 Use this internal structure for each feature:
63
64 ```text
65 features/<feature_name>/
66 ├── data/
67 │ ├── models/ # Feature-specific data/domain models
68 │ └── repositories/ # API, database, cache, and persistence access
69 ├── providers/ # Riverpod state, controllers, and query providers
70 └── views/
71 ├── <feature>_page.dart # Page/screen widgets
72 └── components/ # Feature-only StatelessWidget components
73 ```
74
75 Add optional folders only when they carry real weight:
76
77 | Folder | Use for |
78 | --- | --- |
79 | `data/datasources/` | Remote/local data source adapters when repositories would become too large |
80 | `data/dto/` | Transport-specific objects that should not leak into UI state |
81 | `services/` | Stateless feature services such as validators, schedulers, or mappers |
82 | `utils/` | Helpers that are private to one feature |
83
84 ## Dependency Rules
85
86 - Import from `core/` and the current feature only.
87 - Do not import from sibling features. Extract shared code to `core/` instead.
88 - Keep views lean. Views handle layout, gestures, animation, navigation calls, and local-only UI concerns.
89 - Keep business rules, async work, persistence, and mutation orchestration in Riverpod notifiers/controllers or repositories.
90 - Keep repositories free of widget concerns. They should expose clear async APIs and translate data-source errors into app-level failures where the project has a failure type.
91 - Use `setState` only for strictly local ephemeral UI state. Do not use it for business data or cross-widget state.
92 - Do not split UI with private helper methods such as `_buildHeader`, `_buildBody`, `_buildList`, or `_buildItem`.
93 - Extract UI sections into named `StatelessWidget` classes under `views/components/` instead.
94
95 ## Handwritten Dart Only
96
97 Do not introduce code generation for models, providers, routes, or serialization. Avoid `freezed`, `json_serializable`, `riverpod_annotation`, `build_runner`, generated `part` files, `.g.dart`, and `.freezed.dart` files unless the user explicitly asks for generated code.
98
99 When a project already contains generated files:
100
101 - Do not delete or rewrite existing generated architecture as part of an unrelated feature.
102 - Do not expand the generated surface for new work.
103 - Interact with existing generated APIs only when needed for compatibility.
104 - Ask before changing generator inputs if the task cannot be completed cleanly with handwritten Dart.
105
106 For handwritten models and state classes:
107
108 - Use immutable classes with `final` fields and `const` constructors where possible.
109 - Add `factory Model.fromJson(Map<String, dynamic> json)`, `Map<String, dynamic> toJson()`, and `copyWith()`.
110 - Add `==` and `hashCode` manually when value equality affects provider rebuilds, tests, collections, or state comparison.
111 - Keep parsing and serialization defensive enough for the app's data source.
112
113 ## Riverpod Defaults
114
115 Prefer the project's existing Riverpod style when it is internally consistent. For new handwritten Riverpod code, prefer current Riverpod primitives:
116
117 | Need | Default |
118 | --- | --- |
119 | Repository/service dependency | `Provider<T>` |
120 | Simple local filter/toggle/query text | `StateProvider<T>` |
121 | Read-only one-shot async query | `FutureProvider<T>` |
122 | Read-only realtime data | `StreamProvider<T>` |
123 | Synchronous controller with mutable state | `NotifierProvider<Notifier, State>` |
124 | Async controller with loading/error/data state | `AsyncNotifierProvider<Notifier, State>` |
125 | Parameterized provider | `.family` |
126 | Screen-scoped state | `.autoDispose` |
127
128 Use `StateNotifierProvider` only when the project already standardizes on `StateNotifier` or dependency constraints require it.
129
130 ### Provider Organization
131
132 In a feature's provider file, use this order:
133
134 1. Repository/service providers.
135 2. Simple filter or UI-state providers.
136 3. Query providers or notifier classes.
137 4. Provider declarations for controllers/notifiers.
138
139 Split a provider file when it becomes hard to scan, not just because a second provider exists. Prefer names that read naturally at call sites: `orderRepositoryProvider`, `orderSearchQueryProvider`, `orderListProvider`, `orderControllerProvider`.
140
141 ### Async UI Rules
142
143 - Use `AsyncValue.when`, pattern matching, or project-standard helpers to render loading/error/data states.
144 - Use `ref.watch()` for reactive state in `build`.
145 - Use `ref.read()` for event handlers and one-shot actions.
146 - Use `ref.listen()` for side effects such as snackbars, dialogs, and redirects.
147 - Do not start network fetches, mutations, navigation, or provider invalidation from `build`, `itemBuilder`, or other repeatedly-called render callbacks.
148
149 ## UI Composition
150
151 Use page widgets as thin state connectors. Prefer `ConsumerWidget` pages when Riverpod state is needed, and use `ConsumerStatefulWidget` only for unavoidable local lifecycle concerns such as controllers, focus nodes, or animations. Extracted UI sections should be `StatelessWidget` components by default.
152
153 Follow these rules:
154
155 - Keep each required widget `build` override focused on composing named widgets.
156 - Do not create private methods that return widgets for page sections.
157 - Move page sections, cards, list rows, empty states, form sections, buttons groups, and reusable layout pieces into `views/components/`.
158 - Pass plain data and callbacks into components instead of reading providers inside every component.
159 - Use `ConsumerWidget` in a component only when passing data/callbacks would create worse coupling or excessive plumbing.
160 - Promote a feature component to `core/components/` only after it is reused by multiple features.
161
162 ## Feature Implementation Workflow
163
164 When adding a feature:
165
166 1. Create `lib/features/<feature_name>/` with `data/models/`, `data/repositories/`, `providers/`, and `views/`.
167 2. Define handwritten models with `fromJson`, `toJson`, `copyWith`, and manual equality when needed.
168 3. Implement the repository. Inject shared clients from `core/providers/` or `core/repositories/`; do not instantiate global dependencies deep in views.
169 4. Add Riverpod providers. Start with repository providers, then query/controller providers.
170 5. Implement views as thin `ConsumerWidget` shells, using `ConsumerStatefulWidget` only for unavoidable local lifecycle state, then extract UI sections into `StatelessWidget` files under `views/components/`.
171 6. Wire navigation in the existing composition layer.
172 7. Extract shared code to `core/` only after confirming it is cross-feature or app-wide.
173 8. Run formatting, static analysis, and focused tests.
174
175 ## Naming
176
177 | Item | Convention | Example |
178 | --- | --- | --- |
179 | Feature directory | `snake_case` | `order_history/` |
180 | Model file | `snake_case` | `order_model.dart` |
181 | Repository file | `<feature>_repository.dart` | `orders_repository.dart` |
182 | Provider file | `<feature>_providers.dart` | `orders_providers.dart` |
183 | Page file | `<feature>_page.dart` or specific screen name | `orders_page.dart`, `order_details_page.dart` |
184 | Component file | `<component_name>.dart` | `orders_list.dart`, `order_summary_card.dart` |
185 | Component class | `PascalCase` widget name | `OrdersList`, `OrderSummaryCard` |
186 | Notifier/controller class | `PascalCase` + `Notifier` or `Controller` | `OrdersNotifier`, `OrderFormController` |
187 | State class | `PascalCase` + `State` | `OrdersState`, `OrderFormState` |
188 | Provider variable | `camelCase` + `Provider` | `ordersProvider`, `orderRepositoryProvider` |
189 | Barrel file | Directory summary name | `models.dart`, `components.dart` |
190
191 ## Reference Files
192
193 Load reference files only when useful for the current task:
194
195 - Read [references/handwritten-feature-example.md](references/handwritten-feature-example.md) when implementing a concrete feature or when a full handwritten model, repository, provider, page, and component example would reduce ambiguity.
196 - Read [references/pagination-state-notifier-example.md](references/pagination-state-notifier-example.md) when implementing page-number pagination in a project that already uses `StateNotifierProvider`, or when the user asks for an indexed pagination controller pattern.