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.
Install
npx skills add https://github.com/evanca/flutter-ai-rules/tree/main/skills/flutter-best-practices
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install evanca-flutter-ai-rules@llmmart
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
- 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. - 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. - 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. - 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.
- 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
- Prefer intent-revealing widgets over
Container. UsePadding,SizedBox,ColoredBox,DecoratedBox,Center— they are const-able and self-describing.Containeris fine only when combining several properties at once. - Use modern Dart. Pattern matching (
if (x case final v?)), switch expressions with exhaustiveness, records and destructuring, collectionif/for/spreads instead of.add()loops, expression bodies for pass-through async functions (no redundantasync/await). - Prefix sliver-returning widgets with
Sliverso misuse in the wrong scroll context is caught at a glance. - Tests tell a story. Use expressive matchers (
isEmpty,throwsA,isA,completion) and minimize dependencies — plainText/SizedBoxover design-system widgets in test fixtures. Test cubits/view models in isolation from the widget tree. - Every
// ignore:gets a reason on the same or preceding line. Log errors with dedicatederror/stackTraceparameters, never string interpolation.
Workflow checklists
Adding a new feature/screen
- Read references/feature-structure.md and mirror the existing project's conventions for the feature directory.
- 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.
- Data comes in through a repository or typed API client — never fetched inside a widget.
- Add unit tests for the cubit/view model logic before wiring UI details.
Reviewing Flutter code
Check, in order of importance:
- Logic in the right layer (rule 1–2)? Any
BuildContextin business logic? - State modeled as immutable union types, all cases handled?
- Feature self-contained, or does it reach into another feature's internals?
- Widget choices (rule 6), modern Dart (rule 7), sliver naming (rule 8)?
- Tests present for logic, readable, minimal dependencies?
- Unexplained
// ignore:, string-interpolated error logs, deprecated patterns still spreading?
Starting a new project
- Read references/architecture-layers.md for the layer blueprint and references/feature-structure.md for the folder skeleton.
- 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.
- Set up strict lints early (
leancode_lintor 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) +freezedfor union-type states;bloc_presentationfor one-off UI events - DI:
providerscoped to widget subtrees (accepting its lack of compile-time safety as the lesser evil) - Boilerplate reduction:
flutter_hooks - Monorepo:
melos - Localization:
flutter_localizations+intlwith.arbfiles - 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.
Reviews (0)
No reviews yet.
No comments yet.