Flutter conventions index
Skill zakariaf/Flutter-Skills/skills/flutter-conventions-index
The repo front-door for a Flutter/Dart app — the cross-cutting house rules (feature-first layered MVVM, immutable state with a single write path, Riverpod 3.x for state + DI, typed Result/Failure errors, dumb widgets, injected side effects, complexity limits) plus a routing table that sends each task to its deep-dive skill and a recommended feature build order. Use at the start of any Flutter/Dart work, before writing or reviewing a feature, when deciding which layer or package code belongs in, when unsure which skill governs a task (architecture, state, widgets, persistence, testing, i18n, design), or when onboarding to the conventions.From its SKILL.md
npx -y skills add zakariaf/Flutter-Skills --skill flutter-conventions-indexAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 25 days oldThe repository was created 25 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.0 KB, ~3.8k tokens by cl100k_base, as published. Nobody here has run it
Flutter Conventions — Index
The front door for this Flutter/Dart app. It states the cross-cutting house rules every task obeys, then routes each concern to a focused skill. For any non-trivial task: apply the rules here, then open the specialized skill for depth. This is the only skill that names every other skill in the library.
Assumes a single-package Flutter app by default. Monorepo / pub-workspace guidance is fenced inside the skills that own it (project-structure-and-packages, codegen-and-toolchain) — never required for a small app.
Non-negotiable rules
- Feature-first, layered, downward-only. Group by feature (a folder), then by layer: View → ViewModel → Repository → Service/data. Lower layers never import upward; the dependency graph is a strict DAG. WHY: an acyclic downward graph is the only structural guard against a big ball of mud. (
flutter-architecture,project-structure-and-packages) - Widgets are dumb. No business logic, data access, math, or formatting in a widget — it reads state and renders. WHY: logic in
build()is untestable and rebuilds unpredictably. (widget-composition) - One ViewModel per screen, over immutable state. A single
Notifier/AsyncNotifierowns private mutable state and exposes it as an immutable value with value equality; a transition assigns a new state, never mutates in place. WHY: immutable value + single owner makes every change diffable and testable. (state-management-riverpod) - Riverpod 3.x is state + DI. Modern
Notifier/AsyncNotifier/Future/Streamproviders only; providers are the DI container. Noget_it, nopackage:provider, no legacyStateProvider/StateNotifierProvider/ChangeNotifierProvider. WHY: one composition model, no second DI framework to reconcile. (state-management-riverpod,app-startup-and-bootstrap) - Single write path. A widget or ViewModel never mutates persisted state directly; every mutation is a repository method that persists first, then republishes via a stream. WHY: one durable, observable route means state is never half-written. (
state-management-riverpod,persistence-drift) - Derive, don't store; depend on abstractions. Compute derived values on read instead of caching a second copy; program against interfaces you inject, not concretes. WHY: a duplicated source of truth drifts out of sync; injected seams keep code testable. (
flutter-architecture,service-boundary-and-native) - Immutable models, typed errors. Domain and UI state are
freezed/sealed value types withcopyWithand value equality. Pure functions are total — they return uncertainty, never throw. Recoverable I/O returns a sealedResult<T, F extends Failure>, switched exhaustively; never a swallowedcatch (_) {}. WHY: the compiler enforces every case; failures carry a stable code, not a localized string. (error-handling-typed-results,dart3-idioms-and-coding-standards) - Side effects behind injected interfaces. Every platform/native effect (clock, notifications, share, analytics, storage) is a Dart interface behind a provider, overridden once at the composition root, faked in tests. Read "now" from an injected
Clock, neverDateTime.now(). WHY: a global side effect is a non-deterministic, untestable dependency. (service-boundary-and-native,value-objects-money-and-units) - Async is never silent.
awaiteverything or handle theFutureexplicitly; no fire-and-forget arrow callbacks. GuardBuildContext/mountedafter everyawait; dispose controllers, subscriptions, timers, and sinks. WHY: a droppedFutureswallows errors no lint catches. (async-safety) - Small units, extracted widget classes. Keep to the complexity-limit table owned by
dart3-idioms-and-coding-standards(method ≤30,build()≤80, file ≤300, positional params ≤3, logic nesting ≤3 — widget build trees may nest to ≤5 as the stated exception). ExtractconstStatelessWidgetclasses, never_buildX()methods;consteverywhere legal. WHY: small const subtrees rebuild less and read faster. (dart3-idioms-and-coding-standards,widget-composition,flutter-performance) - Names carry roles.
…Screen/…Notifier/…Repository/…Service/…Failure; Effective-Dart casing verbatim; full words with units in the name; file named after its primary declaration. WHY: a grep or a filename should reveal the layer. (naming-conventions) - RTL and a11y by construction. Use directional (
start/end) geometry only, never hardcoded left/right; every user string comes from an ARB viagen_l10n; label and role every semantic node; never clamp the text scaler or rely on color alone. WHY: correctness properties are cheap to build in and expensive to retrofit. (i18n-rtl-l10n,accessibility-as-code) - Test the shape of the code, not a fixed ratio. Pure core: fast, clock-injected
package:test+ property invariants. Everything else:flutter_test+mocktail,ProviderContainer.test()+overrideWith— fake the repository/services, not theNotifier. One acceptance test anchors the whole app. WHY: tests follow risk; the pure core carries the invariants. (testing-strategy,widget-golden-and-a11y-testing) - Strict lint is the floor; format is not negotiable.
dart formatclean anddart analyze --fatal-infosgreen before every PR, on a version-pinnedvery_good_analysisinclude withstrict-casts/strict-raw-types. WHY: a green analyzer is the cheapest correctness signal you have. (lint-and-style-config,dependency-hygiene)
Route to the right skill
| When you are… | Open |
|---|---|
| Orienting, unsure which skill governs the task | flutter-conventions-index (this) |
| Deciding layers, features-vs-packages, the dependency DAG | flutter-architecture |
Scaffolding packages, barrels, lib/ vs lib/src/, workspace layout | project-structure-and-packages |
Writing main(), error handlers, DI overrides, warm-up, app-lifecycle flush-on-background/resume | app-startup-and-bootstrap |
Writing a Notifier/AsyncNotifier, family/autoDispose, the single write path | state-management-riverpod |
Building or refactoring UI, writing build(), layout/insets | widget-composition |
| Configuring the app router, redirects/auth guards, deep links, nav shells, transitions, PopScope, 404 | navigation-and-routing |
Building a Form, sync/async field validation, focus traversal, keyboard actions, input formatters | forms-and-input |
Responsive breakpoints, large-screen/tablet/foldable master-detail, NavigationRail-vs-BottomNavigationBar by width | adaptive-layout |
| Choosing a Dart 3 construct (sealed, records, class modifiers) | dart3-idioms-and-coding-standards |
| Naming a class/file/variable/boolean | naming-conventions |
Configuring analysis_options.yaml, lint severity | lint-and-style-config |
| Fixing jank, narrowing rebuilds, lazy lists, off-isolate work | flutter-performance |
Writing /// doc comments on the public surface | dartdoc-conventions |
Writing async code, guarding context/mounted, disposal | async-safety |
Modeling errors, Result/Failure, never-lose-data flows | error-handling-typed-results |
| Writing unit/widget tests, setting up fakes | testing-strategy |
| Writing golden, layout, RTL, or accessibility widget tests | widget-golden-and-a11y-testing |
Adding a Drift table, DAO, or .watch stream | persistence-drift |
| Writing a forward-only schema migration | run-migration |
Setting up build_runner, build.yaml, generated-code policy | codegen-and-toolchain |
| Running codegen before analyze | run-codegen |
| Writing the GitHub Actions CI pipeline and gates | ci-pipeline-and-gates |
| Adding ARB strings, ICU plurals, RTL geometry | i18n-rtl-l10n |
Authoring Semantics, targets, traversal, text scaling | accessibility-as-code |
| Modeling money, units, dates in a pure core | value-objects-money-and-units |
| Building on-device reminders/notifications | local-notifications-scheduler |
Writing a CustomPainter + gesture hit-testing | custom-canvas-and-gestures |
| Wiring a native channel or platform side effect | service-boundary-and-native |
Editing pubspec/lockfile, auditing a new dependency | dependency-hygiene |
| Scaffolding a whole feature module end to end | scaffold-feature-module |
| Structuring tokens → theme → components (no aesthetic values) | design-system-structure |
| Running the once-per-app design/QA review before release | design-review-workflow |
Recommended order when building a feature
- Model + pure functions — immutable value types; total, clock-injected core →
dart3-idioms-and-coding-standards,value-objects-money-and-units - Data layer — repository = single source of truth / single write path; Drift table + DAO if persisted →
persistence-drift,error-handling-typed-results - Service seams — every side effect behind an injected interface →
service-boundary-and-native - ViewModel —
Notifier/AsyncNotifier,family/autoDispose, intent methods →state-management-riverpod - View + widgets — dumb
ConsumerWidget, smallconstwidget classes, forms, responsive layout, RTL-safe geometry →widget-composition,forms-and-input,adaptive-layout,i18n-rtl-l10n,accessibility-as-code - Wire DI + routing — providers overridden at the composition root; the feature route registers into the single
go_router→app-startup-and-bootstrap,navigation-and-routing,scaffold-feature-module - Docs + tests —
///on the public surface; core invariants + container/widget tests with fakes →dartdoc-conventions,testing-strategy,widget-golden-and-a11y-testing - Profile, then the CI gate — measure in profile mode; format/analyze/codegen/test green →
flutter-performance,codegen-and-toolchain,ci-pipeline-and-gates
Throughout: naming-conventions and lint-and-style-config keep every line honest.
Baseline project setup
# analysis_options.yaml
include: package:very_good_analysis/analysis_options.yaml # pin the version in pubspec
analyzer:
language:
strict-casts: true
strict-raw-types: true
# The PR gate (mirror in CI — see ci-pipeline-and-gates)
dart run build_runner build --delete-conflicting-outputs # if the app uses codegen
dart format --set-exit-if-changed .
dart analyze --fatal-infos --fatal-warnings
flutter test
The shape every feature repeats
// Repository = the SINGLE WRITE PATH: persist first, then observers re-emit.
class OrderRepository {
OrderRepository(this._db, this._clock);
final AppDatabase _db;
final Clock _clock;
/// The only route an order reaches storage. Returns a typed Result, never throws.
Future<Result<void, PersistFailure>> place(OrderDraft draft) async {
try {
await _db.transaction(() async {
await _db.orderDao.insert(draft.toRow(_clock.now())); // durable FIRST
});
return const Ok(null); // watchers over the DAO re-emit on commit
} on DriftException catch (e, s) {
return Err(PersistFailure.write(cause: e, stack: s));
}
}
}
// ViewModel = one StreamNotifier per screen; state tracks EVERY DAO emission.
// Riverpod 3.x: extend the unified base class, opt into autoDispose on the provider.
class OrderListNotifier extends StreamNotifier<List<Order>> {
@override
Stream<List<Order>> build() =>
ref.watch(orderRepositoryProvider).recent(); // re-emits on every commit
Future<void> place(OrderDraft draft) async {
final result = await ref.read(orderRepositoryProvider).place(draft);
switch (result) {
case Ok(): break; // the watched stream re-emits the new state
case Err(:final failure): state = AsyncError(failure, StackTrace.current);
}
}
}
final orderListProvider =
StreamNotifierProvider.autoDispose<OrderListNotifier, List<Order>>(
OrderListNotifier.new,
);
// View = dumb ConsumerWidget: reads one notifier, renders, no logic.
class OrderListScreen extends ConsumerWidget {
const OrderListScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ref.watch(orderListProvider).when(
loading: () => const _OrdersSkeleton(),
error: (e, _) => _RetryView(onRetry: () => ref.invalidate(orderListProvider)),
data: (orders) => _OrderListView(orders: orders),
);
}
}
Anti-patterns
- Data access or business logic inside a widget, a god-
build(), or_buildXxx()helper methods — untestable and rebuild-unfriendly. setStatefor app/shared state; a mutable model edited by the UI — breaks the single owner + immutable-state rule.get_it/package:provider/legacyStateProvider/StateNotifierProvider/ChangeNotifierProvider— a second DI/state framework to reconcile.- A widget or ViewModel writing a DAO directly, or "republish then persist" — bypasses the single write path and can expose half-written state.
DateTime.now()in domain code — a non-deterministic dependency; inject aClock.- A swallowed error (
catch (_) {}), an untyped exception leaking to the UI, or a fire-and-forgetFuture— silent failure no lint catches. - Hardcoded left/right, a raw digit string, or a user-facing literal in a widget — breaks RTL and localization by construction.
!/late/dynamicto dodge honest types; blanket// ignore; fightingdart format— hides the real defect.- Optimizing without profiling, or profiling in debug mode — measures the wrong thing.
Definition of done
- Code sits in the correct feature/layer; the dependency graph only points downward (rules 1, 6).
- The View is dumb; one
Notifier/AsyncNotifierowns immutable state; no legacy provider or extra DI container (rules 2–4). - Every mutation goes through a repository's single write path — persist, then republish (rule 5).
- Models immutable with value equality; pure functions total; recoverable I/O is
Result+ sealedFailure, exhaustively switched; no swallowed errors (rule 7). - Side effects are behind injected interfaces; "now" comes from a
Clock(rule 8). - Async is awaited/handled;
context/mountedguarded after awaits; controllers and subscriptions disposed (rule 9). - Units within limits; widget classes extracted;
constapplied; names carry role + units (rules 10–11). - RTL-safe geometry; strings in ARB; semantics labeled; text scaler not clamped (rule 12).
- Public surface has
///docs; core invariants + container/widget tests with fakes pass (rule 13). -
dart format+dart analyze --fatal-infosclean on a pinned lint include (rule 14). - The right specialized skill was consulted for the depth of the task.
Related skills
Every skill in this table is a sibling; open the one the routing table points to. Start with flutter-architecture for where code goes, state-management-riverpod for how state flows, and error-handling-typed-results for the Result/Failure spine.
References
- Effective Dart — https://dart.dev/effective-dart
- Flutter architecture recommendations — https://docs.flutter.dev/app-architecture
- Riverpod — https://riverpod.dev
- very_good_analysis — https://pub.dev/packages/very_good_analysis
- Flutter internationalization — https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most docs writing skills give in ~3.8k tokens
Counted across 1,637 of the 3,044 authors here whose files we hold, read 2026-08-07
- Announce the skill at startin 54 of 1637, across 26 files
- Convert legacy doc files before editingin 45 of 1637, across 7 files
- Predict questions readers might askin 42 of 1637, across 4 files
- Generate clarifying questions for initial contextin 42 of 1637, across 3 files
- Create document scaffold with placeholder textin 42 of 1637, across 3 files
- Brainstorm content options for each sectionin 42 of 1637, across 3 files
- Test the document with a fresh context-less instancein 42 of 1637, across 3 files
- Include exact file paths in every taskin 42 of 1637, across 15 files
- Ask interview questions one at a timein 42 of 1637, across 27 files
- Apply surgical edits during refinementin 41 of 1637, across 2 files
- Offer structured workflow or freeformin 40 of 1637, across 1 file
- Ask for document meta-contextin 40 of 1637, across 2 files
Said here and by no other author read
- group code by feature then layer downward only
- keep widgets free of business logic
- own screen state via one immutable notifier
- use Riverpod for state and dependency injection
- route every persisted mutation through the repository
- derive values on read instead of caching
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.