Reusable Composite Widgets
Reusable Composite Widgets
As Flutter applications grow, you will notice the same UI patterns repeated across multiple screens. A profile card here, a labeled text field there, a status badge in three different places. Copying and pasting widget trees leads to maintenance nightmares: a single design change requires updating every duplicate. The solution is to extract repeated UI trees into standalone, named widget classes with clearly typed constructor parameters.
This lesson teaches you how to identify extraction opportunities, decide between StatelessWidget and StatefulWidget, write clean constructor signatures, and understand when composition is preferable to inheritance.
Why Extract Into a Widget Class?
- Single source of truth — change the design once, every usage updates automatically.
- Readability —
ProfileCard(user: user)communicates intent far better than 20 lines of nestedCard/Row/Columninline. - Testability — small, focused widgets are trivial to unit- and widget-test in isolation.
- Rebuild efficiency — Flutter can short-circuit rebuilds on widgets whose
==comparison shows no change, which is only possible when you have named, const-constructable widgets.
Widget _buildHeader()) when a separate class would do. Methods always rebuild with their parent; a separate class gives Flutter the opportunity to skip the rebuild entirely.StatelessWidget: Pure Presentation
Use StatelessWidget when the widget's appearance is entirely determined by its constructor arguments and the build context. It has no internal mutable data. Every property you receive from the outside becomes a final field, and you expose them through a const-capable constructor.
Example — InfoBadge (StatelessWidget)
import 'package:flutter/material.dart';
/// A small coloured badge used to display a status label.
/// Fully determined by its constructor arguments — no internal state.
class InfoBadge extends StatelessWidget {
final String label;
final Color color;
final Color textColor;
const InfoBadge({
super.key,
required this.label,
this.color = Colors.blue,
this.textColor = Colors.white,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
// Usage — same widget, three different contexts:
InfoBadge(label: 'New', color: Colors.green)
InfoBadge(label: 'Sale', color: Colors.orange)
InfoBadge(label: 'Sold', color: Colors.grey)
Notice the const constructor. When Flutter sees const InfoBadge(label: 'New', color: Colors.green) it can reuse the same object across rebuilds, entirely skipping the build step for that subtree.
StatefulWidget: Self-Contained Interactivity
Use StatefulWidget when the widget must track its own mutable data — data that lives inside the widget and does not need to be shared with the outside world. Classic examples include an expandable panel, a favourite-toggle button, or an animated counter.
Example — ExpandableSection (StatefulWidget)
import 'package:flutter/material.dart';
/// A collapsible content section that manages its own open/closed state.
class ExpandableSection extends StatefulWidget {
final String title;
final Widget child;
const ExpandableSection({
super.key,
required this.title,
required this.child,
});
@override
State<ExpandableSection> createState() => _ExpandableSectionState();
}
class _ExpandableSectionState extends State<ExpandableSection> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: Row(
children: [
Expanded(
child: Text(
widget.title,
style: Theme.of(context).textTheme.titleMedium,
),
),
Icon(_isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
),
if (_isExpanded)
Padding(
padding: const EdgeInsets.only(top: 8),
child: widget.child,
),
],
);
}
}
// Usage:
ExpandableSection(
title: 'Shipping Details',
child: Text('Arrives in 3–5 business days.'),
)
State class through widget.propertyName — for example widget.title above. The widget getter is provided by the State<T> base class and always returns the current widget instance.Composing vs Inheriting
Flutter's widget model is built around composition — wrapping and combining existing widgets — rather than classical inheritance. You almost never subclass Container, Row, or any concrete widget. Instead, you build a new class that contains those widgets as part of its build method.
- Prefer composition when you want to assemble a new UI from existing primitives.
- Use inheritance only when you genuinely need to override framework behaviour, such as creating a custom
RenderObjector a customScrollActivity. - For sharing logic between widgets without inheritance, extract the logic into a plain Dart class or a mixin, then inject it via the constructor.
class MyCard extends Card looks tempting but breaks quickly — Flutter's concrete widgets are not designed as stable base classes and their internal APIs change across versions. Always compose; never extend concrete widgets.Typed Constructor Parameters — Best Practices
A composite widget is only as good as its interface. Follow these guidelines when designing constructor parameters:
- Mark every parameter that the widget cannot function without as
required. - Provide sensible defaults for optional styling parameters so callers don't need to specify them every time.
- Accept callbacks as typed
VoidCallbackorValueChanged<T>rather than genericFunction. - Accept child content as a
Widget(orList<Widget>) parameter rather than building it internally when flexibility is needed. - Add a
Key? keyparameter forwarded tosuper.keyso the widget participates correctly in the element reconciliation algorithm.
Summary
Extracting repeated UI trees into named StatelessWidget or StatefulWidget subclasses is one of the most impactful habits you can develop as a Flutter developer. It produces readable, maintainable, and efficiently-rendering UIs. Use StatelessWidget for pure display driven by constructor arguments, and StatefulWidget when the widget owns self-contained mutable state. Always compose widgets rather than extending them, and design constructor signatures that are explicit, typed, and provide sensible defaults.