Flutter

Pathrule2 Rules • 3 Memories • 1 Skill

A baseline for building maintainable Flutter apps in Dart 3. It keeps the widget tree cheap to rebuild with const constructors and small widgets, keeps business logic out of build(), disposes every controller and subscription so screens don't leak, and organizes state and navigation around Riverpod and go_router. These are the conventions that keep a Flutter codebase smooth at 60/120fps and readable as features pile up.

Suggested path map

Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.

/ workspace root
flutter-screen-checklist
lib/
Dispose every controller, stream, and listener
Keep build() pure and widgets small; use const
State management with Riverpod
Feature-first project structure
Navigation with go_router

Rules

2
Dispose every controller, stream, and listener/libhighstrictAnything with a lifecycle - controllers, animation controllers, stream subscriptions, focus nodes, listeners - must be released in dispose(); leaks here are the most common Flutter bug.
1Flutter does not garbage-collect your subscriptions. A controller or stream you create and forget keeps firing after the widget is gone - a leak, a memory climb, and often a `setState() called after dispose` crash.
2 
3- For every `TextEditingController`, `AnimationController`, `ScrollController`, `FocusNode`, `StreamSubscription`, or `addListener` you create in a `StatefulWidget`, release it in `dispose()` (`controller.dispose()`, `subscription.cancel()`, `removeListener(...)`).
4- Create these in `initState` (or as late finals), not in `build()`; creating a controller in `build` makes a new one every frame and leaks all of them.
5- Guard async callbacks that touch state with a `mounted` check before `setState`, so a response that arrives after the widget is disposed does not crash.
6- Prefer Riverpod providers or `AutomaticKeepAlive`/hooks that manage disposal for you when the lifecycle is non-trivial; the rule is that nothing with a lifecycle is left un-released.
Keep build() pure and widgets small; use const/libmediumadvisorybuild() only describes UI from current state - no I/O, no business logic, no allocation of long-lived objects; split large widgets and mark const subtrees const.
1`build()` can run many times per second. Anything expensive or stateful inside it runs that often, which is where jank comes from.
2 
3- `build()` is a pure function of the widget's inputs and state: read data and return widgets. Do not perform network calls, heavy computation, or side effects in it; move those to `initState`, an event handler, or a provider.
4- Mark widgets and subtrees `const` wherever the inputs are constant. A `const` widget is built once and skipped on rebuild, which is the cheapest performance win in Flutter and the one most often missed.
5- Split large `build` methods into small, focused widget classes rather than private `_buildX()` helper methods. Real widgets get their own rebuild boundary and can be `const`; helper methods rebuild with the whole parent.
6- Use `Key`s when reordering or conditionally swapping widgets of the same type so Flutter preserves state correctly instead of mismatching elements.

Memories

3
State management with Riverpod/libWe manage app and screen state with Riverpod providers, keeping logic and async state out of widgets and making it testable.
1We use Riverpod as the state-management boundary so business logic lives outside the widget tree and can be tested without pumping widgets.
2 
3- Keep mutable and async state in providers, not in widget `setState`. `setState` is fine for purely local, ephemeral UI state (a toggled expansion, a hover); anything shared, fetched, or business-relevant belongs in a provider.
4- Use the provider type that fits: a plain provider for derived values, an async notifier for fetched state (it exposes loading/error/data so the UI renders all three), a notifier for mutable state with methods.
5- Read providers with `ref.watch` in `build` to rebuild on change, and `ref.read` in callbacks for one-off actions. Don't `watch` inside a callback or `read` something the UI must react to.
6- Keep providers small and composable; one provider per concern, composed via `ref.watch` of other providers, beats one giant app-state object.
7 
8See /lib for the feature-first structure and go_router navigation memories.
Feature-first project structure/libOrganize lib/ by feature, each owning its UI, state, and data, rather than by technical layer across the whole app.
1We organize `lib/` by feature, not by global technical layer, so a feature's code lives together and stays easy to find, change, and delete.
2 
3- Structure: `lib/features/<feature>/` each containing its own `presentation/` (widgets/screens), `application/` or `providers/` (state), and `data/` (repositories/models). Shared building blocks live in `lib/core/` or `lib/shared/`.
4- Avoid the layer-first anti-pattern (`lib/widgets/`, `lib/models/`, `lib/services/` spanning every feature); it scatters one feature across the tree and makes ownership unclear.
5- Keep cross-feature dependencies explicit and one-directional through shared/core; a feature should not reach into another feature's internals.
6- Co-locate tests with the feature (or mirror the structure under `test/`) so behavior and its tests evolve together.
7 
8See /lib for the Riverpod state and go_router navigation memories.

Skills

1
flutter-screen-checklist/rootChecklist for adding or changing a Flutter screen/feature: disposal, build purity, state, and navigation.
1---
2name: flutter-screen-checklist
3description: Checklist for adding or changing a Flutter screen or feature. Run before merging any widget, provider, or route change.
4---
5 
6# Flutter screen/feature checklist
7 
8- [ ] Every controller, animation controller, stream subscription, focus node, and listener created is released in `dispose()`.
9- [ ] Controllers are created in `initState`/as fields, never in `build()`; async callbacks check `mounted` before `setState`.
10- [ ] `build()` is pure - no network, no heavy compute, no side effects; that work is in `initState`, handlers, or providers.
11- [ ] Constant subtrees are `const`; large builds are split into real widget classes, not `_buildX()` helpers.
12- [ ] `Key`s are set where same-type widgets are reordered or swapped so state is preserved.
13- [ ] Shared/async/business state is in Riverpod providers (loading/error/data handled); `setState` is only for local ephemeral UI.
14- [ ] `ref.watch` drives rebuilds in `build`; `ref.read` is used in callbacks.
15- [ ] Code lives under `lib/features/<feature>/` with presentation/state/data separated; shared code in core/shared.
16- [ ] Navigation goes through go_router (`context.go/push`, typed/named routes); auth handled in the router `redirect`.
17- [ ] `flutter analyze` is clean; widget/unit tests cover the new behavior.

Why this pattern

AI agents writing Flutter pile logic into build(), skip const, never dispose controllers, and scatter setState everywhere, producing janky, leaky, hard-to-test screens.

Built for Mobile teams building cross-platform apps with Flutter and Dart.

Keeps your assistant from:

  • Running expensive work or business logic inside build()
  • Leaking controllers, streams, and listeners by skipping dispose()
  • Rebuilding whole subtrees because const constructors and keys are missing
  • Spreading mutable state through setState instead of a state-management boundary
License
Apache-2.0
Version
1.0.0
Updated
2026-06-09
View source