Claude Cursor Skill

flutter-best-practices

Use when writing, reviewing, refactoring, or planning Flutter/Dart code — screens, features, project structure, state management, folders, widgets, cubits/blocs, repositories, services, or tests.

LLM Mart · 0 points · 15 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download evanca-flutter-ai-rules-skills_flutter-best-practices-7d226d8.zip · 16 KB
Part of evanca/flutter-ai-rules — 37 skills

Install

skills CLI npx skills add https://github.com/evanca/flutter-ai-rules/tree/main/skills/flutter-best-practices
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install evanca-flutter-ai-rules@llmmart
Git git clone https://github.com/evanca/flutter-ai-rules.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole evanca/flutter-ai-rules collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Flutter Best Practices

Standards for building maintainable Flutter apps, distilled from the official Flutter architecture guide and LeanCode's experience shipping 40+ Flutter projects (including a 30-developer banking app). Apply these when writing new code; when touching existing code, prefer consistency with the surrounding codebase and raise conflicts with these standards rather than silently rewriting.

How to use this skill

Read the reference file that matches the task. Read more than one when tasks overlap (e.g. a new feature touches both structure and coding style).

Task Read
Design layers, decide where logic lives, MVVM, repositories/services references/architecture-layers.md
Create/organize a feature, folder structure, state management wiring references/feature-structure.md
Write or review Dart/Flutter code, widgets, tests, lints references/dart-coding-practices.md
Multi-team/multi-package apps, monorepo, navigation, localization, API contracts, E2E tests references/enterprise-scale.md
App localization setup, reusable UI package string ownership, language picker visibility references/localization-package-boundaries.md

For a quick task (small widget fix, one-line review comment), the core rules below may be enough on their own.

Core rules (always apply)

Architecture

  1. Separate UI from data. Two broad layers: UI (views + view models / cubits) and Data (repositories + services). Dependencies point one way: View → ViewModel → Repository → Service. Lower layers never import upper layers. Repositories never depend on each other.
  2. Views hold no business logic. Widgets may contain show/hide conditionals, animation, layout, and simple routing logic — nothing that transforms or decides about data. All data logic lives in the view model (or cubit/bloc), which has no access to BuildContext.
  3. Organize by feature, not by layer. Everything a feature needs — state management, widgets, models — lives under one feature directory. Don't create top-level blocs/, widgets/, models/ buckets that scatter a feature across the tree.
  4. State is immutable and explicit. Model UI state as a sealed/union type (initial / inProgress / failure / ready) so every case is handled exhaustively. One-off effects (snackbars, navigation) are events, not state.
  5. Add layers only when they pay for themselves. Start with view-model → API client. Introduce a repository when you need caching, offline, or merging sources. Introduce a use case only when logic merges multiple repositories, is genuinely complex, or is reused by several view models.

Coding

  1. Prefer intent-revealing widgets over Container. Use Padding, SizedBox, ColoredBox, DecoratedBox, Center — they are const-able and self-describing. Container is fine only when combining several properties at once.
  2. Use modern Dart. Pattern matching (if (x case final v?)), switch expressions with exhaustiveness, records and destructuring, collection if/for/spreads instead of .add() loops, expression bodies for pass-through async functions (no redundant async/await).
  3. Prefix sliver-returning widgets with Sliver so misuse in the wrong scroll context is caught at a glance.
  4. Tests tell a story. Use expressive matchers (isEmpty, throwsA, isA, completion) and minimize dependencies — plain Text/SizedBox over design-system widgets in test fixtures. Test cubits/view models in isolation from the widget tree.
  5. Every // ignore: gets a reason on the same or preceding line. Log errors with dedicated error/stackTrace parameters, never string interpolation.

Workflow checklists

Adding a new feature/screen

  1. Read references/feature-structure.md and mirror the existing project's conventions for the feature directory.
  2. Define the state as a union type first; then the cubit/view model; then the widgets. Constructor-inject dependencies; scope them to the feature's widget subtree.
  3. Data comes in through a repository or typed API client — never fetched inside a widget.
  4. Add unit tests for the cubit/view model logic before wiring UI details.

Reviewing Flutter code

Check, in order of importance:

  1. Logic in the right layer (rule 1–2)? Any BuildContext in business logic?
  2. State modeled as immutable union types, all cases handled?
  3. Feature self-contained, or does it reach into another feature's internals?
  4. Widget choices (rule 6), modern Dart (rule 7), sliver naming (rule 8)?
  5. Tests present for logic, readable, minimal dependencies?
  6. Unexplained // ignore:, string-interpolated error logs, deprecated patterns still spreading?

Starting a new project

  1. Read references/architecture-layers.md for the layer blueprint and references/feature-structure.md for the folder skeleton.
  2. If more than ~2 teams or clearly separable domains are involved, read references/enterprise-scale.md and consider a Melos monorepo with one package per domain from day one.
  3. Set up strict lints early (leancode_lint or equivalent + custom rules) — retrofitting is far more expensive.

Package palette

Defaults that these standards assume (swap for project-local equivalents when the codebase already uses something else):

  • State: bloc (Cubit) + freezed for union-type states; bloc_presentation for one-off UI events
  • DI: provider scoped to widget subtrees (accepting its lack of compile-time safety as the lesser evil)
  • Boilerplate reduction: flutter_hooks
  • Monorepo: melos
  • Localization: flutter_localizations + intl with .arb files
  • Lints: leancode_lint
  • E2E/UI tests: patrol

The official Flutter guide is state-management-agnostic (MVVM with ChangeNotifier works too); what matters is the layer separation, not the package. See the reference files for rationale and trade-offs.

Files (flutter-ai-rules)
  • references
    • architecture-layers.md 5.6 KB
      # Architecture Layers (MVVM)
      
      Based on the official Flutter app architecture guide
      (https://docs.flutter.dev/app-architecture/guide). This is the layer
      blueprint: what each component is responsible for, what it must not do, and
      how components relate. Folder placement of these pieces is covered in
      `feature-structure.md`.
      
      ## The two mandatory layers
      
      Separation of concerns is the foundational principle. Every app splits into:
      
      1. **UI layer** — interaction with the user (views + view models)
      2. **Data layer** — business data and logic (repositories + services)
      
      A third **domain layer** (use cases) is optional — see below.
      
      ## Dependency flow
      
      ```
      View → ViewModel → [Use Case (optional)] → Repository → Service
      ```
      
      - Flow is strictly unidirectional. Lower layers never depend on upper layers.
      - A service never imports a repository; a repository never imports a view
        model; nothing in the data layer knows Flutter widgets exist.
      - **Repositories never depend on each other.** If two repositories need
        coordination, that logic belongs in a use case or view model above them.
      
      | Relationship | Cardinality |
      |---|---|
      | View ↔ ViewModel | one-to-one (per feature) |
      | ViewModel ↔ Repository | many-to-many |
      | Repository ↔ Service | many-to-many |
      | Repository ↔ Repository | none — never |
      
      ## UI layer
      
      ### Views
      
      Widget classes that render UI. They display data given to them by the view
      model and forward user gestures to view-model commands.
      
      Allowed logic in a view — and *only* this:
      
      - simple `if` statements to show/hide widgets based on flags
      - animation logic
      - layout logic (screen size, orientation, responsive breakpoints)
      - simple routing logic
      
      Anything that filters, sorts, aggregates, validates, or otherwise *decides
      about data* is business logic and belongs in the view model. If you find
      yourself writing a `.where(...)` or a date-formatting branch inside `build`,
      move it.
      
      ### View models
      
      Expose exactly the data a view needs, in presentation-ready form.
      
      Responsibilities:
      
      1. **Retrieve and transform** — pull domain models from repositories; filter,
         sort, aggregate into UI state.
      2. **Maintain UI state** — selection flags, carousel position, form validity,
         loading/error status. State survives configuration changes because it
         lives here, not in the widget.
      3. **Expose commands** — member functions (named after the Command pattern)
         that views call from gesture handlers. The view never talks to a
         repository directly.
      
      View models are plain Dart: no `BuildContext`, no widget imports. That is
      what makes them unit-testable without the rendering pipeline. (A Cubit, as
      used in `feature-structure.md`, is a view model in this sense.)
      
      Granularity tip from the official guide: a view/view-model pair doesn't have
      to be a full screen. A `LoginView` + `LoginViewModel` is a screen; a
      `LogoutButton` + `LogoutViewModel` is a reusable component that can appear in
      many places. Give any sufficiently complex, reusable component its own pair.
      
      ## Data layer
      
      ### Repositories
      
      The **single source of truth** for a type of model data.
      
      Responsibilities:
      
      - poll raw data from one or more services
      - transform raw responses into **domain models** consumable by view models
      - own the business logic around data: caching, error handling, retry logic,
        refresh logic
      
      Repositories also manage app-wide session state that multiple features share:
      the active user session, in-memory caches, transient settings. Multiple view
      models depend on the same repository instance (via DI) and observe changes
      reactively through exposed streams or methods.
      
      ### Services
      
      The lowest layer. A service wraps one external data source and exposes async
      objects — nothing more.
      
      - **Stateless.** A service holds no state, ever. State belongs in
        repositories.
      - One service class per data source: a REST API, a platform channel
        (iOS/Android API), local files, a database.
      - If a "service" starts caching or retrying, it has become a repository —
        rename it or move that logic up.
      
      ## Optional domain layer: use cases / interactors
      
      A use case sits between view models and repositories and abstracts complex
      interaction logic.
      
      Add a use case only when the logic meets **at least one** of:
      
      1. it merges data from multiple repositories
      2. it is exceedingly complex
      3. it will be reused by several different view models
      
      Trade-offs to weigh:
      
      - Pros: removes duplication from view models, isolates complex logic for
        testing, keeps view models readable.
      - Cons: more architectural complexity, more mocks in tests, more boilerplate.
      
      **Add use cases incrementally** — when you notice duplication or complexity,
      not preemptively for every data access. Most features never need one. When
      present: use cases depend on repositories (many-to-many), and view models may
      depend on both use cases and repositories directly.
      
      ## Why this pays off
      
      - View-model logic is testable without widgets; services and repositories are
        testable in isolation.
      - UI state survives configuration changes (rotation) because it lives outside
        the widget.
      - Swapping a data source touches one service; swapping caching strategy
        touches one repository; the UI layer doesn't notice.
      
      ## Interaction with feature-based structure
      
      LeanCode's feature-based approach (see `feature-structure.md`) starts leaner:
      a Cubit may call a typed API client directly, and a repository is introduced
      per-feature only when caching/offline demands it. That is compatible with
      this guide — it's the "add layers when they pay for themselves" principle.
      The invariants that must hold from day one are: no business logic in widgets,
      no `BuildContext` in business logic, unidirectional dependencies.
      
    • dart-coding-practices.md 5.5 KB
      # Dart & Flutter Coding Practices
      
      Based on LeanCode's Flutter coding best practices
      (https://leancode.co/blog/flutter-coding-best-practices). These are
      line-level habits: how to write the code once the architecture has decided
      where it goes.
      
      ## Contents
      
      1. [Null safety & pattern matching](#null-safety--pattern-matching)
      2. [Collections](#collections)
      3. [Control flow](#control-flow)
      4. [Async](#async)
      5. [Widgets](#widgets)
      6. [Testing](#testing)
      7. [Documentation & logging](#documentation--logging)
      8. [Linting](#linting)
      
      ## Null safety & pattern matching
      
      **Check-and-bind in one step.** Prefer `case` patterns over null checks
      followed by `!`:
      
      ```dart
      // Avoid — the ! silently breaks if the check above is refactored away
      if (userData != null) {
        greet(userData!.name);
      }
      
      // Prefer — compiler-verified: user is non-null and bound in one step
      if (userData case final user?) {
        greet(user.name);
      }
      ```
      
      **Destructure instead of repeated field access:**
      
      ```dart
      final Point(:x, :y) = point;      // objects
      final (lat, lng) = coordinates;   // records
      final [first, second, ...] = items; // lists (rest pattern)
      ```
      
      ## Collections
      
      **Build lists declaratively — the same way you build widget trees.** Use
      collection `if`, collection `for`, spreads, and null-aware elements instead
      of imperative `.add()`/`.addAll()`:
      
      ```dart
      // Avoid
      final messages = <Widget>[];
      messages.add(welcomeMessage);
      if (showPromo) messages.addAll(promoBanners);
      
      // Prefer
      final messages = [
        welcomeMessage,
        ?optionalMessage,           // null-aware element
        if (showPromo) ...promoBanners,
        for (final item in news) NewsTile(item),
      ];
      ```
      
      ## Control flow
      
      **Switch expressions over if/else chains.** Pattern matching does the type
      checking and destructuring, and the compiler enforces exhaustiveness — add a
      new subtype and every unhandled switch becomes a compile error:
      
      ```dart
      final label = switch (state) {
        Loading() => 'Loading…',
        Failure(:final reason) => 'Failed: $reason',
        Ready(:final items) => '${items.length} items',
      };
      ```
      
      **Dot shorthands (Dart 3.10+).** When the context type is known, drop the
      redundant type name: `padding: .all(16)`, `case .dark`. Especially effective
      with well-known enum/const-heavy types like `EdgeInsets` and `Brightness`.
      
      ## Async
      
      **No redundant async/await on pass-through functions.** If a function only
      forwards a future, use an expression body:
      
      ```dart
      // Avoid — needless state machine
      Future<User> getUser() async => await repository.getUserDetails();
      
      // Prefer
      Future<User> getUser() => repository.getUserDetails();
      ```
      
      Use `async`/`await` only when you actually manipulate the awaited result (or
      need try/catch around it).
      
      ## Widgets
      
      **Replace `Container` with the dedicated widget for the job:**
      
      | Need | Use |
      |---|---|
      | spacing around a child | `Padding` |
      | a fixed size | `SizedBox` |
      | a background color | `ColoredBox` |
      | a decoration (border, gradient, radius) | `DecoratedBox` |
      | centering | `Center` |
      
      Why: dedicated widgets have `const` constructors (`Container` doesn't) and
      state intent instantly. `Container` is appropriate when genuinely combining
      several properties (e.g. padding + color together). Watch one subtlety:
      `Container` insets its child *inside* the border; a manual
      `DecoratedBox` + `Padding` composition does not, so verify visuals when
      converting.
      
      **Prefix sliver-returning widgets with `Sliver`** — e.g.
      `SliverDashboardAppBar`, not `DashboardAppBar`. Slivers used as box widgets
      (or vice versa) fail at runtime, not compile time; the name is the guard. The
      `leancode_lint` rule `prefix_widgets_returning_slivers` automates this.
      
      ## Testing
      
      **Tests should tell a story, not read like cryptic puzzles.** Use expressive
      matchers:
      
      ```dart
      expect(list, isEmpty);                       // not expect(list, [])
      expect(name, startsWith('Dr.'));
      expect(future, completion(equals(42)));
      expect(() => parse(bad), throwsA(isA<FormatException>()));
      ```
      
      **Minimize test dependencies.** Every dependency pulled into a test is
      another potential point of failure — a fragile test can pass while real
      functionality is broken, or fail for reasons unrelated to what it tests. In
      widget-test fixtures, prefer Flutter built-ins (`Text`, `SizedBox`) over
      custom design-system widgets unless the design system is what's under test.
      
      **Test business logic off the widget tree.** Cubits/view models have no
      `BuildContext`, so test them as plain Dart: construct, call, assert on
      emitted states. Reserve widget tests for actual widget behavior.
      
      ## Documentation & logging
      
      **Every `// ignore:` carries a justification:**
      
      ```dart
      // Solid black required by brand guidelines — intentionally not theme-dependent
      // ignore: avoid_hardcoded_colors
      const color = Color(0xFF000000);
      ```
      
      Without the reason, future maintainers can't tell an intentional exemption
      from a forgotten hack.
      
      **Log errors as structured parameters, never interpolation:**
      
      ```dart
      // Avoid — error tracking tools see one opaque string
      logger.warning('Failed to sync $error $stackTrace');
      
      // Prefer — formatters and error trackers get structured data
      logger.warning('Failed to sync', error, stackTrace);
      ```
      
      ## Linting
      
      - Adopt a strict shared lint config — `package:leancode_lint` bundles rules
        like `use_padding`, `use_colored_box`, and
        `prefix_widgets_returning_slivers` that automate the widget guidance above.
      - Custom analyzer plugin rules can encode project-specific patterns; prefer
        a lint over a code-review comment for anything that recurs.
      - Set lints up at project start — retrofitting a strict config onto a mature
        codebase is drastically more expensive.
      
    • enterprise-scale.md 6.2 KB
      # Enterprise Scale
      
      Based on LeanCode's account of building a banking app with 30+ Flutter
      developers across 15 teams over two years
      (https://leancode.co/blog/building-an-enterprise-application-in-flutter).
      Read this when a project involves multiple teams, multiple packages, or
      clearly separable business domains. For single-team apps, `feature-structure.md`
      is usually enough — but the navigation, localization, and testing sections
      here apply earlier than you'd think.
      
      ## Contents
      
      1. [Monorepo & package structure](#monorepo--package-structure)
      2. [Code ownership](#code-ownership)
      3. [Cross-domain communication](#cross-domain-communication)
      4. [Navigation](#navigation)
      5. [Localization](#localization)
      6. [API contracts](#api-contracts)
      7. [UI/E2E testing](#uie2e-testing)
      8. [Legacy code & deprecation](#legacy-code--deprecation)
      9. [Design system](#design-system)
      
      ## Monorepo & package structure
      
      Structure follows Conway's Law: *"Any organization that designs a system
      will produce a design whose structure is a copy of the organization's
      communication structure."* So make packages mirror business-domain squads,
      not technical layers — teams then work autonomously without constant
      cross-team coordination.
      
      - One monorepo; local packages referenced by **path**, not published.
      - Packages are organized **vertically by business domain** (loans, payments,
        onboarding), never horizontally (ui-package, logic-package, data-package).
        A user story cuts through all layers, so a vertical package lets one team
        ship it end to end.
      - Inside each package: feature directories containing everything the feature
        needs (cubits, widgets, pages, data classes) — the structure from
        `feature-structure.md`, one level up.
      - A main **app package** integrates everything; it belongs to the technical
        squad (below).
      
      **Manage the monorepo with Melos** (`package:melos`):
      
      - concurrent command execution across packages (bootstrap, test, format,
        lint) with package filtering
      - automatic versioning and changelog generation
      - custom shell scripts as a task runner — one canonical way to restore
        dependencies, run tests, lint; huge for onboarding and CI
      - caveat: some Flutter commands take file locks and fail when run in
        parallel — serialize those.
      
      ## Code ownership
      
      - Assign ownership at the **team** level, never to individuals — ownership
        must survive people changing roles.
      - Owning code means maintaining it and keeping it aligned with evolving
        standards, not just having written it.
      - Create an explicit **technical squad** ("artificial" team) that owns what
        no domain team naturally would: cross-cutting concerns, shared/common
        packages, architecture oversight, and the main app package.
      
      ## Cross-domain communication
      
      Domains (packages) must not reach into each other's internals. Two sanctioned
      channels:
      
      **Synchronous — facades exposing streams.** A domain exposes data as streams
      (backed by `BehaviorSubject`, so late subscribers get the current value).
      Build automatic retry into the facade; don't surface errors through it when
      consumers couldn't react sensibly anyway.
      
      **Asynchronous — events.** To invert a dependency, publish an event: a
      self-contained data package describing what occurred ("user completed
      onboarding"). Other domains react — typically refreshing data or updating
      UI. This is *not* event sourcing; events are notifications, not the source
      of truth.
      
      ## Navigation
      
      - Split each page into a globally visible **target** and a package-private
        **builder**. A target is like an Android intent: a small class naming a
        page plus the context data it needs.
      - All targets live in one central **navigation package** every domain can
        depend on — so any domain can navigate anywhere without depending on the
        destination's package.
      - Decouple the navigation API from Flutter so that **business logic
        (cubits/blocs) can navigate** without touching `BuildContext`.
      
      ## Localization
      
      Move translation out of the dev workflow into a product workflow with a
      Translation Management System (e.g. Phrase) that speaks `.arb`:
      
      - translation history, in-context comments, domain glossaries for
        translator consistency
      - multi-stage pipeline: initial terms → translation → native-speaker review
        → acceptance
      - export/import synced with version control, tagged per release
      - business teams own the content; developers wire it via
        `flutter_localizations` + `intl`.
      
      ## API contracts
      
      - Generate Dart clients from **strongly-typed contract definitions** owned by
        the backend team — a single source of truth. (LeanCode uses their own
        contract tooling rather than OpenAPI, judging OpenAPI generators to have
        impedance mismatches; the principle matters more than the tool.)
      - Type safety end to end: if the contract says `int`, you cannot send a
        `String` — mismatches die at compile time, not in QA.
      - Generated clients are readable and discoverable in normal IDE tooling;
        documentation lives in code instead of an external doc that rots.
      - Version the schema with git commits/tags.
      
      ## UI/E2E testing
      
      - Use **Patrol** for UI tests — built for Flutter, and it can handle native
        OS surfaces plain integration tests can't: permission dialogs, SMS-code
        retrieval, push notifications.
      - Make UI tests **acceptance criteria inside sprint user stories**, owned by
        the whole SCRUM team (devs + QA). Tests written alongside features evolve
        with them; tests written by a separate downstream team rot.
      - Run a minimal viable subset on every build; run the full suite
        periodically.
      
      ## Legacy code & deprecation
      
      - Mark patterns to abandon with Dart's `@Deprecated(...)` — communicating
        through the analyzer beats communicating through wiki pages.
      - Actually track the migration (e.g. in technical-squad syncs); a deprecation
        nobody acts on makes corruption compound.
      - Fix "broken windows" proactively: visible tolerated code smells teach
        everyone that smells are tolerated.
      
      ## Design system
      
      - A dedicated design squad of developers **and** designers owns the design
        system; developers implement components in continuous collaboration with
        UX/UI.
      - Feature teams use design-system components **exclusively** — that's what
        keeps look, behavior, and color usage consistent across 15 teams' output.
      
    • feature-structure.md 6.5 KB
      # Feature-Based Structure
      
      Based on LeanCode's feature-based Flutter architecture
      (https://leancode.co/blog/feature-based-flutter-architecture), proven across
      40+ projects from small apps to enterprise scale.
      
      ## The principle
      
      Group code by **what it does for the user**, not by what kind of class it is.
      A developer implements a user story ("let users see their loan documents"),
      which cuts through every technical layer — UI, state, data. So everything
      related to one feature lives under one directory:
      
      ```
      lib/
      └── features/
          └── comment_section/
              ├── bloc/
              │   └── comment_section_cubit.dart
              ├── comment_section.dart        # feature entrypoint widget
              └── widgets/
                  └── upvote_button.dart
      ```
      
      Not this (layer-first — avoid):
      
      ```
      lib/
      ├── blocs/          # every feature's cubits mixed together
      ├── widgets/        # every feature's widgets mixed together
      └── models/
      ```
      
      Why feature-first wins:
      
      - developers work in parallel on different features without collisions
      - all code for a feature is findable in one place, not scattered
      - **feature-level innovation**: one feature's internal architecture can be
        rewritten without touching any other feature
      - scales from small projects to enterprise (at large scale, features graduate
        into packages — see `enterprise-scale.md`)
      
      ## Anatomy of a feature
      
      ### 1. Entrypoint widget
      
      The feature's public face: a `StatelessWidget` that takes all required
      context via constructor and sets up the feature's dependencies (providers,
      cubit creation) for its subtree.
      
      ```dart
      class CommentSection extends StatelessWidget {
        const CommentSection({super.key, required this.postId});
      
        final Guid postId;
      
        @override
        Widget build(BuildContext context) {
          return BlocProvider(
            create: (context) => CommentSectionCubit(
              client: context.read<ApiClient>(),
              postId: postId,
            )..fetch(),
            child: const _CommentSectionView(),
          );
        }
      }
      ```
      
      Other features interact with this widget only — never with the feature's
      internals.
      
      ### 2. State as a union type
      
      Express the cubit's state as a sealed/union type so the UI must handle every
      case. With `freezed`:
      
      ```dart
      @freezed
      class CommentSectionState with _$CommentSectionState {
        const factory CommentSectionState.initial() = CommentSectionStateInitial;
        const factory CommentSectionState.inProgress() = CommentSectionStateInProgress;
        const factory CommentSectionState.failure({
          required CommentSectionFailureReason reason,
        }) = CommentSectionStateFailure;
        const factory CommentSectionState.ready({
          required List<Comment> comments,
        }) = CommentSectionStateReady;
      }
      ```
      
      (Plain Dart 3 `sealed class` hierarchies work too; freezed adds
      value-equality and copyWith for free.)
      
      ### 3. Cubit (state management)
      
      The Cubit owns the data and every function that alters it. It guides UI
      behavior purely through emitted states.
      
      - **No `BuildContext` in a Cubit.** Ever. This keeps it detached from the
        rendering pipeline and unit-testable in isolation.
      - Dependencies (API client, repositories) come in through the constructor.
      - Emit the union-type states; the widget maps state → UI.
      
      ```dart
      class CommentSectionCubit extends Cubit<CommentSectionState> {
        CommentSectionCubit({required this.client, required this.postId})
            : super(const CommentSectionState.initial());
      
        final ApiClient client;
        final Guid postId;
      
        Future<void> fetch() async {
          emit(const CommentSectionState.inProgress());
          try {
            final response = await client.get(GetCommentSection(postId: postId));
            emit(CommentSectionState.ready(comments: response.comments));
          } catch (e, st) {
            emit(const CommentSectionState.failure(
              reason: CommentSectionFailureReason.network,
            ));
          }
        }
      }
      ```
      
      ### 4. Presentation events (one-off effects)
      
      Snackbars, dialogs, and navigation triggers are **not state** — they happen
      once and shouldn't persist or replay on rebuild. Use
      `package:bloc_presentation` to emit them as a separate event stream, and
      listen in the widget (with `flutter_hooks` to avoid `StatefulWidget`
      boilerplate for subscriptions).
      
      Rule of thumb: if re-emitting the value on a rebuild would be wrong (showing
      the snackbar twice), it's a presentation event, not state.
      
      ### 5. UI rendering
      
      Map the state union exhaustively:
      
      ```dart
      switch (state) {
        CommentSectionStateInitial() ||
        CommentSectionStateInProgress() => const _Loading(),
        CommentSectionStateFailure(:final reason) => _Error(reason: reason),
        CommentSectionStateReady(:final comments) => _CommentList(comments: comments),
      }
      ```
      
      ## Dependency injection
      
      Use `package:provider`, scoped to widget subtrees:
      
      - a dependency's lifetime is tied to the widget tree — injected when the tree
        mounts, disposed when it unmounts
      - **global** dependencies (ApiClient, auth session) are provided at the app
        root
      - **feature-scoped** dependencies are provided in the feature entrypoint and
        constructor-injected into the Cubit
      
      Provider was chosen with eyes open: it lacks compile-time safety when
      consuming dependencies, but the alternatives (e.g. riverpod) were judged to
      introduce larger issues. Known limit: on very large apps, deeply nested trees
      of global Providers can hit StackOverflowError (Flutter issue #85026) —
      another reason to keep global providers few and push the rest into features.
      
      ## Data access
      
      - Prefer a **backend-for-frontend** style: a dedicated, typed endpoint per
        screen (`GetCommentSection(postId: ...)`) over generic REST endpoints that
        over-fetch.
      - It is fine for a Cubit to call the typed API client **directly**. Introduce
        a repository as a *feature-level decision* when the feature actually needs
        caching, offline mode, or merging sources — see `architecture-layers.md`
        for what a repository owes you once you add it.
      
      ## Shared code between features
      
      Shared components (design-system widgets, common utilities, shared domain
      models) live outside `features/` — in `lib/common/` or dedicated packages —
      and features depend on them, never on each other's internals. If feature A
      needs something from feature B, either promote it to shared code or
      communicate through events (see `enterprise-scale.md` for cross-domain
      communication patterns).
      
      ## What this file deliberately doesn't decide
      
      Feature structure is one slice of a real app. Navigation/deeplinks,
      flavoring, monitoring, CI/CD, localization, design systems, golden/E2E tests,
      and analytics all need their own decisions — several are covered in
      `enterprise-scale.md`; the rest follow project conventions.
      
    • localization-package-boundaries.md 3 KB
      # Localization and Package Boundaries
      
      Use this when a Flutter app has app-level localization and reusable feature or
      UI packages that render user-facing strings.
      
      ## Default Recommendation
      
      Keep localization owned by the host app:
      
      - Use Flutter gen-l10n with `flutter_localizations`, `intl`, `l10n.yaml`, and
        `.arb` files in the app package.
      - Expose generated app strings through an app helper such as `context.l10n`.
      - Pass localized copy into reusable packages through explicit constructor
        values, not by importing the app's generated localizations from the package.
      - Keep package APIs dumb and portable: state in, callbacks out, labels in.
      
      This keeps one translation surface for the product while allowing packages to
      remain reusable.
      
      ## Reusable Package Pattern
      
      For a reusable settings page, prefer a host-owned wrapper:
      
      ```dart
      pkg_settings.SettingsPage(
        strings: SettingsPageStrings.fromL10n(context.l10n),
        themeMode: settingsState.themeMode,
        locale: settingsState.locale,
        supportedLocales: AppLocalizations.supportedLocales,
        showLanguageSelector: AppLocalizations.supportedLocales.length > 1,
        localeLabels: {
          const Locale('en'): context.l10n.localeEnglish,
        },
        onThemeModeChanged: context.read<SettingsCubit>().setThemeMode,
        onLocaleChanged: context.read<SettingsCubit>().setLocale,
        onAboutTapped: () => context.push('/about'),
        onLegalTapped: () => context.push('/legal'),
        onLogoutTapped: context.read<AuthCubit>().logOut,
      )
      ```
      
      The package owns layout and interactions. The app owns localization, routing,
      state management, and business behavior.
      
      ## Language Controls
      
      Do not show a visible language picker when only one locale is supported. A
      visible selector that cannot change rendered text is a no-op preference.
      
      Recommended behavior:
      
      - Keep locale persistence in the settings cubit or view model.
      - Pass `showLanguageSelector: AppLocalizations.supportedLocales.length > 1`.
      - Keep `locale: settingsState.locale` wired into `MaterialApp`.
      - Add the second `.arb` file before exposing the picker.
      
      ## Very Good Open Source Pattern
      
      Very Good Core treats localization as an app-level concern:
      
      - The generated app structure includes `l10n.yaml`, `lib/l10n/arb`, and a
        `lib/l10n/l10n.dart` helper.
      - Their docs use `context.l10n` from app code after adding strings to
        `lib/l10n/arb/app_en.arb`.
      - Adding languages means adding new app ARB files and platform locale config.
      
      Very Good App UI Package is described as a design-system layer that separates
      UI components from business logic. Very Good Flame Game follows the same core
      architecture style. Together, these examples point to app-owned localization
      and package-owned reusable UI, rather than each UI package creating an
      independent translation universe.
      
      ## When Package-Owned l10n Is Appropriate
      
      Let a package ship its own ARB bundle only when it is a genuinely reusable,
      published package with stable copy that should be translated independently of
      the host app. For app-specific feature packages in a monorepo, host-passed
      strings are usually simpler and safer.
      
  • SKILL.md 6.5 KB
    ---
    name: flutter-best-practices
    description: "Use when writing, reviewing, refactoring, or planning Flutter/Dart code — screens, features, project structure, state management, folders, widgets, cubits/blocs, repositories, services, or tests."
    license: MIT
    ---
    
    # Flutter Best Practices
    
    Standards for building maintainable Flutter apps, distilled from the official
    Flutter architecture guide and LeanCode's experience shipping 40+ Flutter
    projects (including a 30-developer banking app). Apply these when writing new
    code; when touching existing code, prefer consistency with the surrounding
    codebase and raise conflicts with these standards rather than silently
    rewriting.
    
    ## How to use this skill
    
    Read the reference file that matches the task. Read more than one when tasks
    overlap (e.g. a new feature touches both structure and coding style).
    
    | Task | Read |
    |---|---|
    | Design layers, decide where logic lives, MVVM, repositories/services | [references/architecture-layers.md](references/architecture-layers.md) |
    | Create/organize a feature, folder structure, state management wiring | [references/feature-structure.md](references/feature-structure.md) |
    | Write or review Dart/Flutter code, widgets, tests, lints | [references/dart-coding-practices.md](references/dart-coding-practices.md) |
    | Multi-team/multi-package apps, monorepo, navigation, localization, API contracts, E2E tests | [references/enterprise-scale.md](references/enterprise-scale.md) |
    | App localization setup, reusable UI package string ownership, language picker visibility | [references/localization-package-boundaries.md](references/localization-package-boundaries.md) |
    
    For a quick task (small widget fix, one-line review comment), the core rules
    below may be enough on their own.
    
    ## Core rules (always apply)
    
    ### Architecture
    
    1. **Separate UI from data.** Two broad layers: UI (views + view models /
       cubits) and Data (repositories + services). Dependencies point one way:
       `View → ViewModel → Repository → Service`. Lower layers never import upper
       layers. Repositories never depend on each other.
    2. **Views hold no business logic.** Widgets may contain show/hide
       conditionals, animation, layout, and simple routing logic — nothing that
       transforms or decides about data. All data logic lives in the view model
       (or cubit/bloc), which has no access to `BuildContext`.
    3. **Organize by feature, not by layer.** Everything a feature needs — state
       management, widgets, models — lives under one feature directory. Don't
       create top-level `blocs/`, `widgets/`, `models/` buckets that scatter a
       feature across the tree.
    4. **State is immutable and explicit.** Model UI state as a sealed/union type
       (initial / inProgress / failure / ready) so every case is handled
       exhaustively. One-off effects (snackbars, navigation) are events, not
       state.
    5. **Add layers only when they pay for themselves.** Start with
       view-model → API client. Introduce a repository when you need caching,
       offline, or merging sources. Introduce a use case only when logic merges
       multiple repositories, is genuinely complex, or is reused by several view
       models.
    
    ### Coding
    
    6. **Prefer intent-revealing widgets over `Container`.** Use `Padding`,
       `SizedBox`, `ColoredBox`, `DecoratedBox`, `Center` — they are const-able
       and self-describing. `Container` is fine only when combining several
       properties at once.
    7. **Use modern Dart.** Pattern matching (`if (x case final v?)`), switch
       expressions with exhaustiveness, records and destructuring, collection
       `if`/`for`/spreads instead of `.add()` loops, expression bodies for
       pass-through async functions (no redundant `async`/`await`).
    8. **Prefix sliver-returning widgets with `Sliver`** so misuse in the wrong
       scroll context is caught at a glance.
    9. **Tests tell a story.** Use expressive matchers (`isEmpty`, `throwsA`,
       `isA`, `completion`) and minimize dependencies — plain `Text`/`SizedBox`
       over design-system widgets in test fixtures. Test cubits/view models in
       isolation from the widget tree.
    10. **Every `// ignore:` gets a reason** on the same or preceding line. Log
        errors with dedicated `error`/`stackTrace` parameters, never string
        interpolation.
    
    ## Workflow checklists
    
    ### Adding a new feature/screen
    
    1. Read [references/feature-structure.md](references/feature-structure.md)
       and mirror the existing project's conventions for the feature directory.
    2. Define the state as a union type first; then the cubit/view model; then
       the widgets. Constructor-inject dependencies; scope them to the feature's
       widget subtree.
    3. Data comes in through a repository or typed API client — never fetched
       inside a widget.
    4. Add unit tests for the cubit/view model logic before wiring UI details.
    
    ### Reviewing Flutter code
    
    Check, in order of importance:
    
    1. Logic in the right layer (rule 1–2)? Any `BuildContext` in business logic?
    2. State modeled as immutable union types, all cases handled?
    3. Feature self-contained, or does it reach into another feature's internals?
    4. Widget choices (rule 6), modern Dart (rule 7), sliver naming (rule 8)?
    5. Tests present for logic, readable, minimal dependencies?
    6. Unexplained `// ignore:`, string-interpolated error logs, deprecated
       patterns still spreading?
    
    ### Starting a new project
    
    1. Read [references/architecture-layers.md](references/architecture-layers.md)
       for the layer blueprint and
       [references/feature-structure.md](references/feature-structure.md) for the
       folder skeleton.
    2. If more than ~2 teams or clearly separable domains are involved, read
       [references/enterprise-scale.md](references/enterprise-scale.md) and
       consider a Melos monorepo with one package per domain from day one.
    3. Set up strict lints early (`leancode_lint` or equivalent + custom rules) —
       retrofitting is far more expensive.
    
    ## Package palette
    
    Defaults that these standards assume (swap for project-local equivalents when
    the codebase already uses something else):
    
    - **State:** `bloc` (Cubit) + `freezed` for union-type states;
      `bloc_presentation` for one-off UI events
    - **DI:** `provider` scoped to widget subtrees (accepting its lack of
      compile-time safety as the lesser evil)
    - **Boilerplate reduction:** `flutter_hooks`
    - **Monorepo:** `melos`
    - **Localization:** `flutter_localizations` + `intl` with `.arb` files
    - **Lints:** `leancode_lint`
    - **E2E/UI tests:** `patrol`
    
    The official Flutter guide is state-management-agnostic (MVVM with
    ChangeNotifier works too); what matters is the layer separation, not the
    package. See the reference files for rationale and trade-offs.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related