Imported from gaurishankar007/Flutter-Clean-Architecture (
.agents/AGENTS.md). Install upstream withnpx skills add gaurishankar007/Flutter-Clean-Architecture --skill .agents. Copyright stays with the author.
Clean Architecture Flutter — Agent Rules (AGENTS.md)
These rules apply to every AI agent or assistant working in this workspace. Follow them strictly.
1. Project Identity
- App Name: Clean Architecture — a reference/template app demonstrating Feature-first Clean Architecture with SOLID principles.
- Architecture: Feature-first Clean Architecture (data/domain/presentation) using Flutter +
flutter_bloc(Cubits). - Platforms: Android, iOS, Web.
- Flavors:
development,staging,production. - Entry Points:
lib/main_dev.dart,lib/main_stg.dart,lib/main.dart.
2. Mandatory Rules (Never Violate)
- BEFORE performing any task, ALWAYS consult
README.md(Project Structure, State Management, Data Handling sections) anddocs/(testing.mdfor test conventions,solid_principles.mdfor the SOLID rationale) for established patterns. Only read what's needed to minimize context usage. - NEVER use relative imports. Always use package imports (enforced by the
always_use_package_importslint rule):// ✅ Correct import 'package:clean_architecture/core/clients/remote/http/http_client.dart'; // ❌ Wrong import '../../core/clients/remote/http/http_client.dart'; - NEVER write business logic inside pages or widgets. Business logic belongs exclusively in Cubits (
presentation/cubits/). This includes resolving a use case/repository directly from a widget — every flow needs a Cubit, even a trivial one, so it goes through the same state/message pattern as the rest of the app. - NEVER access
LocalStorageClient,HttpClient, or another feature's data source directly from a Cubit or widget. Go through the feature's own repository/use-case layer. - NEVER use
voidas the return type of anasyncmethod. UseFuture<void>(enforced by theavoid_void_asynclint). - NEVER show a toast/message or perform navigation from inside a Cubit. A Cubit only computes state; showing that state (via
ToastUtil) and reacting to it (navigating) is the UI layer's job. See "Messages & Navigation" below.
3. Code Style
This project uses leancode_lint (see analysis_options.yaml) plus a few extra rules.
- String literals: Use single quotes (
'). - Constants: Prefer
constconstructors and declarations wherever possible. - Control flow: Always put
if,for, andwhilebodies on a new line. - Constructors: Always sort constructors first in class declarations.
- Lambdas: Avoid unnecessary lambdas; prefer method tearoffs.
- Comments: Keep them short, straight to the point, and single-line wherever possible. Don't write multi-line prose explaining what the code already shows — only note the non-obvious "why" (an edge case, a workaround, a load-bearing ordering), and say it in as few words as it takes.
4. Architecture Rules
State Management
- All Cubits must extend
BaseCubit<T>(shared_ui/cubits/base/base_cubit.dart). - All Cubit states must extend
BaseState, usingStateStatus(initial/loading/loaded/noInternet/error). - UI layers must use
ToastUtilto present messages; do not useSnackBardirectly.
Messages & Navigation (UI-only)
Cubits never call ToastUtil or navigate directly — both are UI-layer concerns:
- A Cubit reports outcomes by setting
BaseState.message(aStateMessage?:SuccessMessage/ErrorMessage/WarningMessage). Derive it from aDataStatewithstateMessageFromDataState(dataState, {message: '...'}), or constructSuccessMessage/ErrorMessage/WarningMessagedirectly for a manual message. A state'scopyWith/constructor must never preserve the previousmessage(message: message, nevermessage ?? this.message) so a message is shown exactly once. - The page that provides the Cubit wires
useCubitMessageListener(cubit)(shared_ui/utils/cubit_message_listener.dart, aflutter_hookshook) to actually show that message viaToastUtil. Get the cubit instance directly (e.g. viauseMemoized) and pass it to bothBlocProvider.valueand the listener hook — seelogin_page.dart. - A Cubit method that used to navigate itself instead returns a result —
typically
Future<bool>(did it succeed), or anenumfor more than one outcome — and the calling widget awaits it and navigates viaNavigationUtil.I. SeeLoginCubit/login_button.dartandDashboardCubit/its drawer/bottom-nav/setting-page call sites for worked examples. StateMessagesubclasses must stayEquatable— this matters for testing (seedocs/testing.md); don't remove that if you touchbase_state.dart.
UI / Widget Composition
Two layers, each with one job — don't blur them:
- Base widgets (
shared_ui/ui/base/) — app-wide, zero domain knowledge, pure styling (BaseText,PrimaryButton/SecondaryButton,BaseTextField,BaseScaffold, etc.). Before adding a new base widget, check whether an existing one can take a variant/parameter instead of creating a near-duplicate sibling. - Feature-local widgets (
features/*/presentation/widgets/, or a page's ownwidgets/folder) — domain-aware, wire a Cubit's state into base widgets. Presentation only; see the business-logic rule above.
If a widget is reusable across features but still domain-aware, or a
feature-agnostic composed pattern (e.g. an empty-state placeholder) emerges,
give it its own home under shared_ui/ui/ (outside base/) rather than
duplicating it per feature.
Other rules:
- Text styling must go through
BaseText's factory constructors — never construct a rawTextStyleinline in a feature or base widget. - Extract widgets as classes, not private methods. A
StatefulWidgetwithWidget _buildFoo() => ...helper methods should instead beclass _Foo extends StatelessWidget. Private methods returning widgets don't get their ownElement/subtree, so Flutter can't skip rebuilding them independently and they loseconstopportunities. - Pages (
@RoutePage()files) should stay thin: lifecycle glue (initState/dispose) ifStatefulWidget, otherwise just composing already-extracted widgets. Push conditionals/styling down into extracted widgets rather than growing the page's ownbuild().
Dependency Injection
- Use
@LazySingleton,@injectable, or@singletonannotations. - Always run the build runner after adding or modifying any injectable class, repository implementation, or client:
dart run build_runner build
Data Layer
- Data models implement
DomainConvertible<R>(R toDomain(), seecore/data/models/domain_convertible.dart); repositories call.toDomain()on the fetched model to map it to the domain entity. - Always check connectivity via
InternetClient.isConnectedbefore making API calls, and pass it toRepositoryFetcher. - Use
ApiExecutorfor HTTP responses andRepositoryFetcher.fetchWithFallback(And Map)for offline-first data flows. - Always use
SuccessState.nilas the return value forFutureVoid-typed methods that succeed. NEVER useSuccessState(data: null)orSuccessState(data: true). - Always use
FutureVoid(notFutureBool) for a method whose result no caller actually reads (onlyhasData/errorTypematters to the caller). ReserveFutureBoolfor methods where the caller genuinely branches on the returnedbool(e.g.checkAuth()). - Import shared typedefs (
FutureData,FutureVoid,JsonMap, etc.) fromcore/types/types.dart— there is nocore/utils/type_defs.dart(removed).
5. Project Structure Rules
When creating new features or files, strictly follow this layout:
lib/features/<feature_name>/
├── data/
│ ├── data_sources/
│ ├── models/
│ └── repositories/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── use_cases/ # optional
└── presentation/
├── cubits/
├── pages/
└── widgets/
When to use Mason instead of writing manually:
- New feature →
mason make cubit_feature -c config.json - New page + Cubit within an existing feature →
mason make cubit_page -c config.json
6. Testing Rules
This project has a real test suite (test/, testing/, patrol_test/) —
see docs/testing.md before writing any tests. In short:
- Unit/widget tests live in
test/, mirroringlib/'s directory structure. - Mock with
mocktail(never mockito/generated mocks); shared mocks live intesting/mocks/, shared helpers intesting/helpers/. - Cubit tests use
bloc_test; widget tests usepatrol_finders(patrolWidgetTest), not the plainflutter_testfinders. - Integration tests live in
patrol_test/using the Patrol framework. - When testing a Cubit that follows the Messages & Navigation pattern above,
assert on
state.messageand the returned value/use-case calls — not on a mockedNavigationClientcall from inside the Cubit (that verification belongs in the page's widget test instead, since navigation now happens there). - If test coverage requirements are ambiguous, ask the user rather than inventing a convention.
7. Code Generation
Whenever routes or injectable registrations change, regenerate code:
dart run build_runner build
# or, while iterating:
flutter pub run build_runner watch
8. Available Custom Skills
The following agent skills are available in .agents/skills/:
codegen— Runsbuild_runnercode generation.scaffolding— Usesmasonto scaffold features and pages.api_integration— Automates integrating a new API endpoint through the data source → repository → domain layers.
.agents/workflows/release.md documents how a release actually reaches
testers via the Firebase App Distribution CI — consult it before proposing
or invoking any release step (see the CI/CD section below).
9. CI/CD
.github/actions/build-android/action.yml: composite action — sets up Java/Flutter, writes.envfrom secrets, runsflutter analyzeandflutter test, thenflutter build apk --release --flavor <flavor> --target <entrypoint>..github/workflows/firebase_app_distribution.yml: on push tostagingbuilds and distributes the staging APK; on push tomasterbuilds and distributes the production APK; both to Firebase App Distribution. Also runnable manually (workflow_dispatch) with custom release notes.- Keep the workflow's pinned Flutter version in sync with the version this
repo is actually developed against (check
flutter --versionlocally before bumping it).
10. Code Quality, Analysis & Linting
This project strictly follows the linting rules provided by the leancode_lint package (configured in analysis_options.yaml).
- Always ensure your code passes
flutter analyzewith zero issues, andflutter testpasses, after any code modification/creation in.dartfiles. - If you are unsure about a specific rule, refer to the leancode_lint documentation.
Verification
flutter analyze
flutter test
11. What NOT to Do
| ❌ Don't | ✅ Do Instead |
|---|---|
| Write business logic in pages/widgets | Put it in a Cubit |
| Use relative imports | Use package:clean_architecture/... imports |
| Hardcode API URLs | Use AppConfig via the flavor system |
Use SnackBar directly |
Use ToastUtil |
| Show a toast or navigate from a Cubit | Set BaseState.message / return a result and let the UI show it or navigate |
| Create feature files manually | Use mason make cubit_feature/cubit_page |
Use bare try/catch in data code |
Use ErrorHandlerProvider.I.execute/executeSafe* |
Use FutureBool when no caller reads the bool |
Use FutureVoid + SuccessState.nil |
Import core/utils/type_defs.dart |
Import core/types/types.dart |
Violate leancode_lint rules |
Fix all analysis issues |
| Skip analysis/tests after changes | Run flutter analyze and flutter test |
| Call a use case/repository from a widget | Route it through a Cubit |
Hardcode a TextStyle in a widget |
Use BaseText's factories |
Extract a widget as a private _buildX() method |
Extract it as its own widget class |