| 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. |