← Articles

Flutter custom widgets: composition over inheritance

By Ann Tech · 25 July 2025

Composition over inheritance means building complex widgets by combining simpler ones rather than subclassing. In Flutter, this is the standard approach — and the widget framework is designed for it.

Why composition over inheritance

Flutter's widget tree is a hierarchy of composed widgets, not a deep class hierarchy. Each widget does one thing:

  • Padding adds padding
  • Center centers its child
  • Container combines decoration, padding, size, and alignment

Building your own widgets follows the same principle.

Inheritance: when it's wrong

// BAD: inheriting from a concrete widget to add behaviour
class RoundedCard extends Card {
  const RoundedCard({super.key, required this.child});
  final Widget child;

  // Problem: you can't override Card's internal layout
  // and you're locked into Card's implementation details
}

Composition: the Flutter way

// GOOD: compose existing widgets
class RoundedCard extends StatelessWidget {
  const RoundedCard({super.key, required this.child, this.padding});
  final Widget child;
  final EdgeInsetsGeometry? padding;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Theme.of(context).cardColor,
        borderRadius: BorderRadius.circular(12),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      padding: padding ?? const EdgeInsets.all(16),
      child: child,
    );
  }
}

Component pattern: slots

Use named widget parameters to create flexible layouts with "slots":

class ProductCard extends StatelessWidget {
  const ProductCard({
    super.key,
    required this.image,
    required this.title,
    required this.price,
    this.badge,      // Optional slot
    this.actions,   // Optional slot
  });

  final Widget image;
  final Widget title;
  final Widget price;
  final Widget? badge;
  final Widget? actions;

  @override
  Widget build(BuildContext context) {
    return RoundedCard(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Stack(
            children: [
              image,
              if (badge != null)
                Positioned(top: 8, right: 8, child: badge!),
            ],
          ),
          const SizedBox(height: 12),
          title,
          const SizedBox(height: 4),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              price,
              if (actions != null) actions!,
            ],
          ),
        ],
      ),
    );
  }
}

// Usage is descriptive and flexible:
ProductCard(
  image: CachedNetworkImage(imageUrl: product.imageUrl),
  title: Text(product.name, style: Theme.of(context).textTheme.titleMedium),
  price: PriceLabel(price: product.price, currency: 'USD'),
  badge: product.isNew ? const NewBadge() : null,
  actions: Row(
    children: [
      WishlistButton(productId: product.id),
      const SizedBox(width: 8),
      AddToCartButton(product: product),
    ],
  ),
)

Builder pattern for stateful composition

When a child widget needs to react to parent state, use a builder callback:

class CollapsibleSection extends StatefulWidget {
  const CollapsibleSection({
    super.key,
    required this.title,
    required this.builder,
    this.initiallyExpanded = false,
  });

  final Widget title;
  final WidgetBuilder builder; // Called only when expanded
  final bool initiallyExpanded;

  @override
  State<CollapsibleSection> createState() => _CollapsibleSectionState();
}

class _CollapsibleSectionState extends State<CollapsibleSection> {
  late bool _expanded = widget.initiallyExpanded;

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        GestureDetector(
          onTap: () => setState(() => _expanded = !_expanded),
          child: Row(
            children: [
              Expanded(child: widget.title),
              Icon(_expanded ? Icons.expand_less : Icons.expand_more),
            ],
          ),
        ),
        if (_expanded) widget.builder(context),
      ],
    );
  }
}

// Usage:
CollapsibleSection(
  title: const Text('Order Items'),
  builder: (_) => OrderItemsList(items: order.items),
)

Mixin for shared behaviour

When multiple widgets need the same behaviour (not the same structure), use a mixin:

mixin LoadingStateMixin<T extends StatefulWidget> on State<T> {
  bool isLoading = false;

  Future<void> runWithLoading(Future<void> Function() operation) async {
    setState(() => isLoading = true);
    try {
      await operation();
    } finally {
      if (mounted) setState(() => isLoading = false);
    }
  }
}

class _CreateOrderScreenState extends State<CreateOrderScreen>
    with LoadingStateMixin {
  Future<void> _submit() async {
    await runWithLoading(() async {
      await orderRepo.createOrder(orderRequest);
      if (mounted) context.go('/orders');
    });
  }
}

When to use StatelessWidget vs StatefulWidget

StatelessWidget: no mutable state, receives data from parent
StatefulWidget: manages its own state (expanded, loading, cursor position)
HookWidget (flutter_hooks): functional stateful widgets, less boilerplate

Common pitfalls

Widget classes with too many parameters. A widget with 15 constructor parameters is trying to do too much. Split it into composed smaller widgets with fewer responsibilities.

Passing callbacks 3 levels deep. If a button at the bottom of a widget tree needs to call a function from a grandparent widget, use a state management solution (BLoC, Riverpod) instead of threading the callback through intermediate widgets.

Using InheritedWidget for non-theme data. InheritedWidget causes all descendants that read it to rebuild on every change. For app state, use Riverpod or BLoC instead.

Sign in to like, dislike, or report.

Flutter custom widgets: composition over inheritance — ANN Tech