flutter-clean-arch
Skill0 starsChanged 27 days agoMITView on GitHub
Opinionated Flutter architecture — Clean Architecture (data/domain/presentation), Cubit-first with Bloc only for global state, multi-state pattern (factory per state, no status enum), go_router with centralized Routes, manual DI via providers, repository with I interface + mandatory UseCase, mocktail + bloc_test. Use when creating features, blocs/cubits, routes, repositories or widgets in projects with these conventions.
2 other projects use a version of this.
Adopted by
Most recently taken up by
- gabuldev/flutter-skillstheir own version, first seen Sep 17, 2026
- duckyman-ai/agent-skillstheir own version, first seen May 18, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- gabuldev/flutter-skills★ 12same name
- duckyman-ai/agent-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository28 other files it gives its agents.
- ado-closeSkill
- ado-commentSkill
- ado-getSkill
- ado-refineSkill
- adrSkill
- approve-reviewSkill
- appSkill
- autoSkill
- challenge-specSkill
- completeSkill
- dart-clean-archSkill
- discoverySkill
- fixSkill
- implementSkill
- jira-refineSkill
- kickoffSkill
- linear-refineSkill
- orcamentoSkill
- planSkill
- post-reviewSkill
- pr-openSkill
- pr-statusSkill
- refineSkill
- reviewSkill
- specifySkill
- statusSkill
- tasksSkill
- verifySkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 6 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈6
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 58
No events — public methods on the cubit.
- Distinctive: line 80
Use Bloc only for app-wide cross-cutting features:
- Distinctive: line 167
Cubit/Bloc never calls Repository directly: Always via UseCase, even if it's pass-through.
- Distinctive: line 390
Check if the old version is still referenced.
- Distinctive: line 391
Don't mix logic between versions — duplicate if needed.
- Distinctive: line 394
After full migration, delete the old one and remove the suffix.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Flutter — Clean Architecture conventions
Opinionated Flutter architecture conventions for medium/large apps. Pair with dart-clean-arch for the language side.
Opinionated and adapted from production projects. Adopt, adapt, or ignore based on context.
Clean Architecture
Presentation → Domain ← Data- Presentation depends on Domain (entities, usecases).
- Data depends on Domain (implements contracts).
- Domain depends on nothing external (no Flutter, no Dio, no I/O libs).
Each feature/package has three layers: data/, domain/, presentation/.
lib/src/
├── data/
│ ├── apis/ # HTTP call wrappers
│ ├── datasources/ # Interfaces + implementations
│ ├── models/
│ │ ├── request/
│ │ └── response/
│ ├── repositories/ # Contract implementations
│ ├── errors/
│ │ ├── exceptions/
│ │ └── failures/
│ └── services/
├── domain/
│ ├── entities/ # Domain sealed classes
│ ├── repositories/ # Interfaces (with I prefix)
│ ├── usecases/ # One per operation
│ └── value_objects/
└── presentation/
├── bloc/ # Events + states + bloc class
├── cubits/ # State + cubit class
├── pages/ # Routed screens
├── views/ # Sub-screens / tabs
└── widgets/ # Reusable componentsState management: Cubit-first
Lib: flutter_bloc. Main rule: Cubit for almost everything, Bloc only for global/cross-cutting state.
Cubit (default for local features)
- 1:1 rule: one Cubit per feature/operation.
- Created via
BlocProviderat the page/feature level. - No events — public methods on the cubit.
class TasksCubit extends Cubit<TasksState> {
TasksCubit(this._useCase) : super(const TasksState.initial());
final IFetchTasksUseCase _useCase;
Future<void> fetchTasks() async {
emit(const TasksState.loading());
try {
final tasks = await _useCase.execute();
emit(TasksState.loaded(tasks: tasks));
} on AppException catch (e) {
emit(TasksState.error(message: e.toString()));
}
}
}Bloc (global state only)
Use Bloc only for app-wide cross-cutting features:
- Authentication, language, theme, connectivity, shared websocket, environment.
They live in lib/core/shared/ and are provided at the root level. They use events because they react to multiple sources (streams, lifecycle, other blocs).
Legacy code often uses Bloc for everything. When touching a local feature still using Bloc, migrate to Cubit.
Multi-state pattern (MANDATORY)
No status enum with a single state class. Each state is a separate factory in the sealed class.
@Freezed(map: FreezedMapOptions.none, when: FreezedWhenOptions.none)
sealed class TasksState with _$TasksState {
const factory TasksState.initial() = TasksInitial;
const factory TasksState.loading() = TasksLoading;
const factory TasksState.loaded({required List<Task> tasks}) = TasksLoaded;
const factory TasksState.error({String? message}) = TasksError;
}Why: each state carries only the data that makes sense in it. tasks only exists in TasksLoaded — no useless null check in TasksLoading.
UI consumption with pattern matching:
BlocBuilder<TasksCubit, TasksState>(
builder: (context, state) => switch (state) {
TasksInitial() || TasksLoading() => const LoadingIndicator(),
TasksLoaded(:final tasks) => TasksList(tasks: tasks),
TasksError(:final message) => ErrorView(message: message),
},
)Rules:
- Never
emit(state.copyWith(...))to switch states — emit the correct state directly. copyWithonly within the same factory (e.g., updating a field insideTasksLoaded).- No
enum TasksStatus { initial, loading, loaded, error }in a single field.
Layers: data → domain ← presentation
Page/Widget
↓
Cubit/Bloc ← calls UseCase
↓
UseCase ← orchestrates Repository
↓
IRepository (domain) — implemented by Repository (data)
↓
Datasource (data) — calls Api
↓
Api ← returns ApiResult<Model>Repository pattern
Domain defines interface with I prefix, data implements.
// domain/repositories/tasks_repository.dart
abstract class ITasksRepository {
Future<List<Task>> fetchTasks();
}
// data/repositories/tasks_repository.dart
class TasksRepository implements ITasksRepository {
TasksRepository(this._tasksApi, this._exceptionHandler);
final TasksApi _tasksApi;
final IExceptionHandler _exceptionHandler;
@override
Future<List<Task>> fetchTasks() async {
final result = await _tasksApi.fetchTasks();
return switch (result) {
_Success(:final data) => data.map((m) => m.toEntity()).toList(),
_Failure(:final exception, :final stackTrace) =>
throw _exceptionHandler.handle(exception, stackTrace),
};
}
}Mandatory UseCase
Cubit/Bloc never calls Repository directly. Always via UseCase, even if it's pass-through.
abstract class IFetchTasksUseCase {
Future<List<Task>> execute();
}
class FetchTasksUseCase implements IFetchTasksUseCase {
FetchTasksUseCase(this._repository);
final ITasksRepository _repository;
@override
Future<List<Task>> execute() => _repository.fetchTasks();
}Why: makes business logic easier to test in isolation, allows composing multiple repos, and adds an extension point without touching repo or cubit.
Dependency Injection: manual
No GetIt, no global Riverpod, no service locator. Everything is instantiated manually and injected via constructor.
Each package/feature exposes <name>_providers.dart:
class TasksProviders {
TasksProviders._();
static Future<List<RepositoryProvider<dynamic>>> resolve() async {
return [
RepositoryProvider<ITasksRepository>(
create: (ctx) => TasksRepository(
ctx.read<TasksApi>(),
ctx.read<IExceptionHandler>(),
),
),
RepositoryProvider<IFetchTasksUseCase>(
create: (ctx) => FetchTasksUseCase(ctx.read<ITasksRepository>()),
),
];
}
}main.dart aggregates all of them via Future.wait. Root app mounts MultiRepositoryProvider.
final repositories = (await Future.wait([
AuthProviders.resolve(),
TasksProviders.resolve(),
// ...
])).expand((e) => e).toList();
runApp(MyApp(repositories: repositories));Access in widget: context.read<IFetchTasksUseCase>().
Cubit/Bloc: created per-page via BlocProvider(create: (ctx) => TasksCubit(ctx.read<IFetchTasksUseCase>())).
Routing: go_router + centralized Routes
Lib: go_router. Single definition in lib/core/router/app_router.dart.
Route names centralized in a Routes class:
// lib/core/router/routes.dart
class Routes {
const Routes._();
static const home = 'home';
static const tasksList = 'tasks-list';
static const taskDetail = 'task-detail';
}On the page: route is derived from name, and name references Routes.
class TasksListPage extends StatefulWidget {
static const name = Routes.tasksList; // references Routes
static const route = '/$name'; // derived from name
}Never hardcode route strings on the page. Every new route: add to Routes first.
Complex parameters: JSON-stringified in state.extra.
context.push(TaskDetailPage.route, extra: jsonEncode(params.toJson()));
// builder:
final data = jsonDecode(state.extra! as String) as Map<String, dynamic>;
return TaskDetailPage(params: TaskParams.fromJson(data));Redirect: pure function computeRedirect() (testable in isolation).
Error handling
API → Repository → ExceptionHandler → throw AppException- 401 →
sessionExpiredstream → automatic logout. - 503 →
maintenancestream → UI shows maintenance screen. - Others → Cubit/Bloc catches and emits
state.error().
try {
final data = await _useCase.execute();
emit(State.loaded(data: data));
} on AppException catch (e) {
emit(State.error(message: e.toString()));
}Local cache
| Lib | When |
hive or isar | Structured with schema |
shared_preferences | Simple key-value (last email, onboarding flags) |
Pick one structured lib per project and stay consistent.
Localization
intl+ ARB files.- In monorepos: each package has its own
lib/l10n/and exposes aLocalizationsDelegate. - Root app lists all delegates in
localizationsDelegates.
Widgets
- Stateless predominantly with external Cubit/Bloc via
BlocBuilder/BlocListener. - No
flutter_hooks(optional — discardable opinion). - Shared widgets in
lib/core/shared/presentation/widgets/. - Assets via
flutter_gen:Assets.images.logo. - Theme via
BuildContextextension:context.colorPalette,context.spacing.
Tests
Stack: flutter_test + bloc_test + mocktail. Never mockito.
Structure mirrors lib/. Widget test helper in test/helpers/pump_app.dart.
class MockFetchTasksUseCase extends Mock implements IFetchTasksUseCase {}
void main() {
late MockFetchTasksUseCase mockUseCase;
late TasksCubit cubit;
setUp(() {
mockUseCase = MockFetchTasksUseCase();
cubit = TasksCubit(mockUseCase);
});
group('TasksCubit', () {
blocTest<TasksCubit, TasksState>(
'emits [loading, loaded] when fetchTasks succeeds',
build: () {
when(() => mockUseCase.execute()).thenAnswer((_) async => mockTasks);
return cubit;
},
act: (c) => c.fetchTasks(),
expect: () => [
const TasksState.loading(),
TasksState.loaded(tasks: mockTasks),
],
);
blocTest<TasksCubit, TasksState>(
'emits [loading, error] when fetchTasks fails',
build: () {
when(() => mockUseCase.execute()).thenThrow(AppException.network());
return cubit;
},
act: (c) => c.fetchTasks(),
expect: () => [
const TasksState.loading(),
isA<TasksError>(),
],
);
});
}registerFallbackValue for custom types in any() matchers. Mocks: class MockX extends Mock implements IX {}.
New package checklist
- Path dependency in root
pubspec.yaml. <pkg>_providers.dartwithstatic Future<List<RepositoryProvider>> resolve().resolve()added tomain.dartFuture.wait.<pkg>_router.dartwithstatic List<RouteBase> get routes(if the feature has routes).- ARB files in
lib/l10n/of the package + delegate registered in root app. flutter_genconfig in package'spubspec.yaml(if it uses assets).
Suggested tooling
# Full setup
flutter pub get && dart run build_runner build --delete-conflicting-outputs
# Format + analyze (pre-commit)
dart format . && dart analyze --fatal-infos --fatal-warnings
# Tests
flutter test
flutter test --coverage
# l10n generation
flutter gen-l10nIn monorepos, consider melos or a Makefile that scopes commands per package.
_v2 convention for gradual refactors
Features being refactored get the _v2 suffix. When touching:
- Check if the old version is still referenced.
- Don't mix logic between versions — duplicate if needed.
- New code always in
_v2.
After full migration, delete the old one and remove the suffix.
Feature creation checklist
- Folder with
data/,domain/,presentation/layers? - Cubit (not Bloc) if it's a local feature?
- Multi-state with factory per state (no status enum)?
- Repository with
Iinterface in domain, impl in data? - UseCase between Cubit and Repository (even if pass-through)?
- Model with
fromJson+fromEntity+toEntity? - Route added to
Routesandname/routederived on the page? - Provider exposed via
<pkg>_providers.dart(if cross-feature)? - ARBs in supported languages +
flutter gen-l10n? - Cubit test with
bloc_test+mocktail? - Pattern matching with
switchinBlocBuilder? - Build runner run, analyze passes, tests pass?
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

