Widgets
Widget composition patterns for building reusable, performant Flutter UIs
You are an expert in Flutter widget composition for building cross-platform apps with Flutter. ## Key Points - **Keep build methods lean.** Extract sub-trees into separate widget classes rather than helper methods — this gives each piece its own `Element` and avoids unnecessary rebuilds. - **Use `const` constructors** wherever possible to enable the framework to skip rebuilding unchanged subtrees. - **Prefer named parameters** with `required` for readability and safety. - **Use `Widget` as the parameter type** (not a concrete class) when accepting child widgets to keep components flexible. - **Leverage `RepaintBoundary`** around expensive subtrees (e.g., animated or frequently changing areas) to isolate repaints. - **Putting logic in `build`**: Never perform I/O, start timers, or do heavy computation inside `build()`. It can be called many times per second. - **Over-using StatefulWidget**: If the state can live in a state management solution, prefer that over local `setState`. - **Forgetting keys in dynamic lists**: Without keys, Flutter may reuse state from the wrong item when the list changes. - **Deep nesting without extraction**: Deeply nested widget trees become unreadable. Extract sub-trees into well-named widget classes. - **Using `MediaQuery.of(context)` too broadly**: This makes the widget rebuild on any media query change. Use `MediaQuery.sizeOf(context)` or `MediaQuery.paddingOf(context)` for finer granularity.
skilldb get flutter-skills/WidgetsFull skill: 236 linesWidget Composition — Flutter
You are an expert in Flutter widget composition for building cross-platform apps with Flutter.
Core Philosophy
Everything in Flutter is a widget, and the framework is designed to make widget composition the primary tool for building UIs. Unlike platforms where you extend a base view class and override methods, Flutter favors composing small, focused widgets together. A ProductCard is not a subclass of Card -- it is a widget that contains a Card, an Image, a Text, and a PriceLabel, each doing one thing well. This compositional approach means you build complex UIs by assembling simple parts, and each part can be tested, reused, and modified independently.
The distinction between StatelessWidget and StatefulWidget is about state ownership, not complexity. A StatelessWidget can be visually complex and contain deeply nested child widgets, as long as it does not own mutable state that changes over time. A StatefulWidget is needed only when the widget itself needs to hold and mutate state (a timer, a text editing controller, an animation controller). If the state can be managed by a state management solution (Riverpod, Bloc) and passed in as a parameter, the widget should be stateless. Stateless widgets are simpler, cheaper, and easier to test.
The const constructor is Flutter's most underused optimization. When a widget has a const constructor and is instantiated with const, the framework knows it can never change, and it skips rebuilding that widget entirely during recomposition. For leaf widgets that render fixed content (icons, labels, dividers, spacers), adding const to the constructor and all instantiation sites eliminates rebuilds for free. The cumulative effect across a large widget tree is measurable.
Anti-Patterns
-
Performing I/O or heavy computation inside build(): The
build()method can be called many times per second during animations, scrolling, and state changes. Starting network requests, reading files, or sorting large lists insidebuild()causes repeated work and visible jank. Side effects belong ininitState, state management solutions, orFutureBuilder/StreamBuilder. -
Creating deeply nested widget trees without extraction: A
build()method with 200 lines of nested widgets is unreadable, untestable, and unmaintainable. Extract subtrees into separate widget classes (not just helper methods). Separate widget classes get their ownElement, enabling the framework to skip rebuilding unchanged subtrees. -
Using helper methods instead of widget classes for extraction: Extracting a subtree into a
Widget _buildHeader()method does not give it a separateElement. When the parent rebuilds, the helper method runs again and produces a new widget tree, even if nothing in the header changed. A separateHeaderWidgetclass has its ownElementand can be skipped during reconstruction if its parameters have not changed. -
Forgetting keys in dynamic lists: Without a
Keyon list items, Flutter matches items by index. When items are reordered, inserted, or removed, Flutter reuses state from the wrong item -- a checkbox on item 1 moves to item 2 because item 2 now occupies index 1. UseValueKey(item.id)for any list with mutable ordering. -
Using MediaQuery.of(context) when a specific accessor exists:
MediaQuery.of(context)subscribes the widget to all media query changes (size, orientation, text scale, padding, insets). If the widget only needs the screen size, usingMediaQuery.sizeOf(context)subscribes it only to size changes, avoiding unnecessary rebuilds when other media query properties change.
Overview
Flutter's UI is built entirely from widgets — immutable descriptions of part of the user interface. Mastering widget composition means knowing when to use StatelessWidget vs StatefulWidget, how to extract reusable components, and how to leverage the widget tree for maximum performance.
Core Concepts
StatelessWidget vs StatefulWidget
Use StatelessWidget when the widget depends only on its constructor arguments and inherited widgets. Use StatefulWidget only when the widget owns mutable state that changes over its lifetime.
class UserAvatar extends StatelessWidget {
const UserAvatar({super.key, required this.imageUrl, this.radius = 24});
final String imageUrl;
final double radius;
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: radius,
backgroundImage: NetworkImage(imageUrl),
);
}
}
Composition Over Inheritance
Flutter favors composition. Build complex widgets by combining smaller ones rather than extending existing widgets.
class ProductCard extends StatelessWidget {
const ProductCard({super.key, required this.product});
final Product product;
@override
Widget build(BuildContext context) {
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProductImage(url: product.imageUrl),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ProductTitle(title: product.name),
const SizedBox(height: 4),
PriceLabel(price: product.price),
],
),
),
],
),
);
}
}
Keys
Use Key objects to preserve state across widget tree rebuilds, especially in lists.
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ItemTile(
key: ValueKey(item.id), // preserves state when list reorders
item: item,
);
},
)
InheritedWidget and BuildContext
Access shared data via BuildContext without passing it through every constructor.
class AppConfig extends InheritedWidget {
const AppConfig({
super.key,
required this.apiBaseUrl,
required super.child,
});
final String apiBaseUrl;
static AppConfig of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppConfig>()!;
}
@override
bool updateShouldNotify(AppConfig oldWidget) {
return apiBaseUrl != oldWidget.apiBaseUrl;
}
}
Implementation Patterns
Builder Pattern for Conditional Layouts
class ResponsiveLayout extends StatelessWidget {
const ResponsiveLayout({
super.key,
required this.mobile,
required this.tablet,
this.desktop,
});
final Widget mobile;
final Widget tablet;
final Widget? desktop;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
return desktop ?? tablet;
} else if (constraints.maxWidth >= 600) {
return tablet;
}
return mobile;
},
);
}
}
Sliver-Based Scrolling
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Profile'),
background: Image.network(coverUrl, fit: BoxFit.cover),
),
),
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList.builder(
itemCount: posts.length,
itemBuilder: (context, index) => PostCard(post: posts[index]),
),
),
],
);
}
}
Render-Callback (Builder) Widgets
class FetchBuilder<T> extends StatefulWidget {
const FetchBuilder({super.key, required this.future, required this.builder});
final Future<T> Function() future;
final Widget Function(BuildContext, AsyncSnapshot<T>) builder;
@override
State<FetchBuilder<T>> createState() => _FetchBuilderState<T>();
}
class _FetchBuilderState<T> extends State<FetchBuilder<T>> {
late final Future<T> _future = widget.future();
@override
Widget build(BuildContext context) {
return FutureBuilder<T>(future: _future, builder: widget.builder);
}
}
Best Practices
- Keep build methods lean. Extract sub-trees into separate widget classes rather than helper methods — this gives each piece its own
Elementand avoids unnecessary rebuilds. - Use
constconstructors wherever possible to enable the framework to skip rebuilding unchanged subtrees. - Prefer named parameters with
requiredfor readability and safety. - Use
Widgetas the parameter type (not a concrete class) when accepting child widgets to keep components flexible. - Leverage
RepaintBoundaryaround expensive subtrees (e.g., animated or frequently changing areas) to isolate repaints.
Common Pitfalls
- Putting logic in
build: Never perform I/O, start timers, or do heavy computation insidebuild(). It can be called many times per second. - Over-using StatefulWidget: If the state can live in a state management solution, prefer that over local
setState. - Forgetting keys in dynamic lists: Without keys, Flutter may reuse state from the wrong item when the list changes.
- Deep nesting without extraction: Deeply nested widget trees become unreadable. Extract sub-trees into well-named widget classes.
- Using
MediaQuery.of(context)too broadly: This makes the widget rebuild on any media query change. UseMediaQuery.sizeOf(context)orMediaQuery.paddingOf(context)for finer granularity.
Install this skill directly: skilldb add flutter-skills
Related Skills
Animations
Implicit, explicit, and hero animation patterns for polished Flutter UIs
Local Storage
Hive, SharedPreferences, and Drift patterns for local data persistence in Flutter
Navigation
GoRouter navigation patterns for declarative, deep-linkable Flutter routing
Networking
Dio HTTP client and API integration patterns for Flutter applications
Platform Channels
Platform channels for bridging Flutter with native Android (Kotlin) and iOS (Swift) code
State Management
Riverpod and Bloc state management patterns for scalable Flutter applications