Flutter Widget Skill
A skill for producing Flutter widgets that are simple, testable, and high-performance by default — and for refusing to produce widgets that are not.
This skill assumes plain Flutter state management (setState + ValueNotifier / ValueListenableBuilder). It does not use Riverpod, BLoC, or Provider.
How to use this skill
Before writing any widget code, follow this loop:
- Read
reference/anti-patterns.md— the hard-blocked patterns. Most quality issues come from these. - Read
reference/extraction-rules.md— when to split a widget into another widget. - If the user asked about performance specifically, read
reference/performance-checklist.md. - If the user asked for tests or said "testable matters here", read
reference/testability-patterns.md. - Draft the code using the templates in
templates/as starting points. - Run the self-check at the bottom of this file before returning code. This is non-negotiable.
You do not need to read every reference file every time. But you must read anti-patterns.md every time — it is short and it is what prevents the most common failure modes.
Core principles (in priority order)
- A widget is a class, not a method. If you find yourself writing
Widget _buildSomething(), stop and extract aStatelessWidget. There is no exception to this rule in this skill. consteverywhere it compiles. Every constructor that can beconstmust beconst. Every widget instance that can beconstmust beconst.build()is for composition, not logic. Put computation, side effects, and decisions outsidebuild().build()reads state and returns widgets.- Smallest possible rebuild scope. Push
ValueListenableBuilder/AnimatedBuilder/StreamBuilderas deep as possible. Never rebuild a screen for a checkbox. - Stateless until proven otherwise. Default to
StatelessWidget. Promote toStatefulWidgetonly when local mutable state is needed. - Widgets receive data; they don't fetch it. Constructors take what the widget needs. Pages or controllers handle loading. This is what makes widgets testable.
When to use StatelessWidget vs StatefulWidget vs ValueNotifier
| Situation | Use |
|---|---|
| Pure presentation from constructor args | StatelessWidget |
| One-off local state (a toggle, a controller lifecycle) | StatefulWidget |
| State that is read in many places or needs scoped rebuilds | ValueNotifier<T> + ValueListenableBuilder<T> |
| Multiple related fields that change together | Custom ChangeNotifier + AnimatedBuilder or a dedicated Listenable |
| Async data | FutureBuilder / StreamBuilder at the smallest scope possible |
Avoid setState at the top of a large widget. Either split the widget, or lift the changing data into a ValueNotifier and rebuild only the leaves that depend on it.
Default file shape
A widget file should look like this, in this order:
// 1. Imports (dart:, package:flutter, then package: third-party, then relative)
// 2. Public widget(s) — the API the file exposes
class MyThing extends StatelessWidget { ... }
// 3. Private widgets used only inside this file
class _Header extends StatelessWidget { ... }
class _Body extends StatelessWidget { ... }
// 4. Private helpers (pure functions, no widget returns)
String _formatLabel(int n) => ...;
Never put private widgets at the bottom as _buildX() methods on the main class. Make them real classes — prefixed with _ to keep them library-private.
Mandatory self-check before returning code
Before you return Flutter code to the user, walk this list. If any item fails, fix it before responding.
- No
Widget _build*()methods. Search the code. Zero matches. Anything that returned a widget from a method is now aStatelessWidget(orStatefulWidget) class. - Every possible constructor is
const. A constructor can beconstif all its fields arefinaland all its field initializers are constant expressions. If you can addconstand the analyzer would accept it, it must be there. - Every possible widget instance is
const.const SizedBox(height: 8), notSizedBox(height: 8). Same forText,Icon,Paddingwith literal values, etc. build()bodies are short. Aim for under ~30 lines of widget tree. Ifbuild()is longer, you almost certainly need to extract a child widget. LongColumn/Rowchildren lists are a strong extraction signal.- No business logic in widget classes. No HTTP calls, no database access, no
Timers started frombuild(), no parsing, no validation logic. Those belong in plain Dart classes the widget receives via its constructor. - No
MediaQuery.of(context)when a narrower accessor works. PreferMediaQuery.sizeOf(context),MediaQuery.viewInsetsOf(context),MediaQuery.paddingOf(context), etc. The narrow versions only rebuild on the relevant slice. Same idea: preferTheme.of(context)only when you actually need the full theme; for colors preferTheme.of(context).colorSchemeaccessed once and stored locally. - Keys where they matter. Lists of widgets that can reorder/insert/remove need keys. Conditional widgets that swap between similar types may need keys. Don't sprinkle
Keys where they don't help, but don't omit them where state would be lost. StatefulWidgetlifecycle is correct. Controllers (TextEditingController,AnimationController,ScrollController,FocusNode) created ininitStatemust be disposed indispose. Subscriptions opened must be cancelled.- No anonymous closures rebuilt every frame for hot paths. For callbacks passed to children that are themselves rebuilt often (lists, animations), prefer named methods on the State class so identity is stable. For low-frequency callbacks (a button onPressed on a static screen), inline closures are fine.
- The widget would be testable as-is. Could you instantiate this widget in a
testWidgetstest by passing constructor arguments only, without mocking anything global? If not, push the dependency up into the constructor.
If the user asked specifically for tests, also generate a test file using templates/widget-test.dart as a starting point.
What to tell the user
Be brief in your prose. The user wants the widget, not a lecture. After returning the code, optionally call out one or two things if they're notable — e.g., "Extracted _Header and _StatsRow so the counter rebuild stays scoped" or "Used ValueListenableBuilder instead of setState so the parent doesn't rebuild." Don't list every rule you followed.
If the user's request would require breaking one of the core principles (for example, "put everything in one method"), explain the problem briefly and offer the right structure instead. Do not produce code you would have to flag in the self-check.
Reference files
reference/anti-patterns.md— Read every time. The blocked patterns and why.reference/extraction-rules.md— When to split a widget into a child widget.reference/performance-checklist.md— Rebuild scope,const,RepaintBoundary, etc.reference/testability-patterns.md— How to structure widgets so atestWidgetstest is trivial.
Templates
templates/stateless-widget.dart— Starting point for a presentational widget.templates/stateful-widget.dart— Starting point with proper controller lifecycle.templates/widget-test.dart— Starting point for a widget test (use only when asked).
Source: artebiakin/flutter_claude_config — distributed by TomeVault.