Flutter
Purpose
Build Flutter applications whose widget tree rebuilds only where it must. Flutter's performance model is simple and unforgiving: a setState at the top of the tree rebuilds everything below it.
When to Use
- Building or reviewing a Flutter application.
- Choosing and applying a state-management approach.
- Diagnosing jank or excessive rebuilds.
- Integrating with platform-native code.
Capabilities
- Widget composition and the
StatelessWidget/StatefulWidget boundary.
- State management with Riverpod, Bloc, or Provider — and when plain state suffices.
- Build-method optimization:
const constructors, selective rebuilds, keys.
- Async:
Future, Stream, FutureBuilder, and their failure states.
- Platform channels for native functionality.
Inputs
- The feature set and target platforms.
- The current state-management approach, if any.
- The jank or rebuild symptom, if debugging.
Outputs
- A widget tree where rebuilds are scoped to what changed.
- State that is testable without a widget tree.
- Async UI that handles loading, error, and empty explicitly.
Workflow
- Compose small widgets — A 300-line
build method rebuilds as one unit. Extracting subtrees into widgets is the primary performance tool in Flutter, not a style preference.
- Mark everything possible
const — A const widget is never rebuilt. This is the cheapest optimization available and most codebases leave it on the table.
- Scope the rebuild —
Consumer, Selector, or a Riverpod provider that watches one field. setState in a parent rebuilds every child that is not const.
- Handle all three async states — Loading, error, and data.
FutureBuilder without an error branch shows a spinner forever when the request fails.
- Profile in profile mode — Debug mode is meaningfully slower and will mislead you in both directions. Use the DevTools timeline on a real device.
Best Practices
const constructors are the highest-leverage change in most Flutter codebases. Enable prefer_const_constructors in the linter and fix every warning.
- Never build a widget inside a
build method as a function call (Widget _buildHeader()). It defeats the element-tree diffing entirely. Extract a real widget class.
- Keys matter when reordering or removing items from a list of stateful widgets. Without them, state attaches to the wrong item.
- Business logic does not belong in a widget. If it cannot be tested without pumping a widget tree, it is in the wrong place.
ListView.builder, not ListView(children: [...]), for anything that could be long. The latter builds every child immediately.
- An
AnimationController without a dispose is a leak that ticks forever.
Examples
Scoped rebuild versus a rebuild of everything:
// Costly: setState here rebuilds the whole screen, including the static header
// and the entire list, on every counter tick.
class _DashboardState extends State<Dashboard> {
int _count = 0;
@override
Widget build(BuildContext context) => Column(
children: [
const DashboardHeader(), // const: spared, correctly
OrderList(orders: widget.orders), // not const: rebuilt every tick
Text('$_count'),
ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),
],
);
}
// Better: only the Text listening to the counter rebuilds.
class Dashboard extends ConsumerWidget {
const Dashboard({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) => Column(
children: [
const DashboardHeader(),
const OrderList(),
Consumer(builder: (_, ref, __) => Text('${ref.watch(counterProvider)}')),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Text('+'),
),
],
);
}
Async with every state handled:
switch (ref.watch(ordersProvider)) {
AsyncData(:final value) when value.isEmpty => const EmptyState(),
AsyncData(:final value) => OrderList(orders: value),
AsyncError(:final error) => ErrorState(error: error, onRetry: _retry),
_ => const LoadingSkeleton(),
}
Notes
- Extracting a subtree into a
const widget removes it from the rebuild path entirely — the framework short-circuits on identity. This is why "just extract widgets" is genuine performance advice in Flutter and not merely tidiness.
RepaintBoundary isolates a subtree's painting. It helps when a small animated element sits inside an expensive static one, and hurts if applied indiscriminately.
- Impeller replaced Skia as the default renderer on iOS (and now Android), which eliminates the shader-compilation jank that used to affect first-run animations.
1---2name: flutter3description: Use when building Flutter applications. Covers widget composition, state management, build-method performance, platform channels, and the rendering behavior behind most Flutter jank.4---56# Flutter78## Purpose910Build Flutter applications whose widget tree rebuilds only where it must. Flutter's performance model is simple and unforgiving: a `setState` at the top of the tree rebuilds everything below it.1112## When to Use1314- Building or reviewing a Flutter application.15- Choosing and applying a state-management approach.16- Diagnosing jank or excessive rebuilds.17- Integrating with platform-native code.1819## Capabilities2021- Widget composition and the `StatelessWidget`/`StatefulWidget` boundary.22- State management with Riverpod, Bloc, or Provider — and when plain state suffices.23- Build-method optimization: `const` constructors, selective rebuilds, keys.24- Async: `Future`, `Stream`, `FutureBuilder`, and their failure states.25- Platform channels for native functionality.2627## Inputs2829- The feature set and target platforms.30- The current state-management approach, if any.31- The jank or rebuild symptom, if debugging.3233## Outputs3435- A widget tree where rebuilds are scoped to what changed.36- State that is testable without a widget tree.37- Async UI that handles loading, error, and empty explicitly.3839## Workflow40411. **Compose small widgets** — A 300-line `build` method rebuilds as one unit. Extracting subtrees into widgets is the primary performance tool in Flutter, not a style preference.422. **Mark everything possible `const`** — A `const` widget is never rebuilt. This is the cheapest optimization available and most codebases leave it on the table.433. **Scope the rebuild** — `Consumer`, `Selector`, or a Riverpod provider that watches one field. `setState` in a parent rebuilds every child that is not `const`.444. **Handle all three async states** — Loading, error, and data. `FutureBuilder` without an error branch shows a spinner forever when the request fails.455. **Profile in profile mode** — Debug mode is meaningfully slower and will mislead you in both directions. Use the DevTools timeline on a real device.4647## Best Practices4849- `const` constructors are the highest-leverage change in most Flutter codebases. Enable `prefer_const_constructors` in the linter and fix every warning.50- Never build a widget inside a `build` method as a function call (`Widget _buildHeader()`). It defeats the element-tree diffing entirely. Extract a real widget class.51- Keys matter when reordering or removing items from a list of stateful widgets. Without them, state attaches to the wrong item.52- Business logic does not belong in a widget. If it cannot be tested without pumping a widget tree, it is in the wrong place.53- `ListView.builder`, not `ListView(children: [...])`, for anything that could be long. The latter builds every child immediately.54- An `AnimationController` without a `dispose` is a leak that ticks forever.5556## Examples5758**Scoped rebuild versus a rebuild of everything:**5960```dart61// Costly: setState here rebuilds the whole screen, including the static header62// and the entire list, on every counter tick.63class _DashboardState extends State<Dashboard> {64 int _count = 0;6566 @override67 Widget build(BuildContext context) => Column(68 children: [69 const DashboardHeader(), // const: spared, correctly70 OrderList(orders: widget.orders), // not const: rebuilt every tick71 Text('$_count'),72 ElevatedButton(onPressed: () => setState(() => _count++), child: const Text('+')),73 ],74 );75}7677// Better: only the Text listening to the counter rebuilds.78class Dashboard extends ConsumerWidget {79 const Dashboard({super.key});8081 @override82 Widget build(BuildContext context, WidgetRef ref) => Column(83 children: [84 const DashboardHeader(),85 const OrderList(),86 Consumer(builder: (_, ref, __) => Text('${ref.watch(counterProvider)}')),87 ElevatedButton(88 onPressed: () => ref.read(counterProvider.notifier).increment(),89 child: const Text('+'),90 ),91 ],92 );93}94```9596**Async with every state handled:**9798```dart99switch (ref.watch(ordersProvider)) {100 AsyncData(:final value) when value.isEmpty => const EmptyState(),101 AsyncData(:final value) => OrderList(orders: value),102 AsyncError(:final error) => ErrorState(error: error, onRetry: _retry),103 _ => const LoadingSkeleton(),104}105```106107## Notes108109- Extracting a subtree into a `const` widget removes it from the rebuild path entirely — the framework short-circuits on identity. This is why "just extract widgets" is genuine performance advice in Flutter and not merely tidiness.110- `RepaintBoundary` isolates a subtree's painting. It helps when a small animated element sits inside an expensive static one, and hurts if applied indiscriminately.111- Impeller replaced Skia as the default renderer on iOS (and now Android), which eliminates the shader-compilation jank that used to affect first-run animations.