Skip to main content
Technology & EngineeringFlutter236 lines

Widgets

Widget composition patterns for building reusable, performant Flutter UIs

Quick Summary16 lines
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 lines
Paste into your CLAUDE.md or agent config

Widget 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 inside build() causes repeated work and visible jank. Side effects belong in initState, state management solutions, or FutureBuilder/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 own Element, 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 separate Element. When the parent rebuilds, the helper method runs again and produces a new widget tree, even if nothing in the header changed. A separate HeaderWidget class has its own Element and can be skipped during reconstruction if its parameters have not changed.

  • Forgetting keys in dynamic lists: Without a Key on 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. Use ValueKey(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, using MediaQuery.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 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.

Common Pitfalls

  • 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.

Install this skill directly: skilldb add flutter-skills

Get CLI access →