flutter
/

Flutter Custom Widgets – Build Reusable Components

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Custom Widgets – Build Reusable Components

Why Create Custom Widgets?

As your Flutter app grows, you'll find yourself repeating the same UI patterns. Custom widgets allow you to encapsulate these patterns into reusable components. This improves code maintainability, reduces duplication, and makes your code easier to test. Custom widgets can be as simple as a styled button or as complex as a whole form. Flutter encourages composition – you build new widgets by combining existing ones.

Types of Custom Widgets

    • Stateless custom widget: For widgets that don't need to change over time. They are built once and rely only on their constructor parameters.
    • Stateful custom widget: For widgets that have mutable state that changes over time (e.g., animations, user input).

Basic Stateless Custom Widget

A stateless widget is the simplest form. It extends StatelessWidget and overrides the build method. It receives parameters through its constructor and uses them to build its UI.

DARTRead-only
1
class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;

  const CustomButton({
    super.key,
    required this.label,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
      child: Text(
        label,
        style: const TextStyle(fontSize: 16),
      ),
    );
  }
}

// Usage
CustomButton(
  label: 'Click Me',
  onPressed: () => print('Clicked'),
)

Basic Stateful Custom Widget

When your widget needs to manage mutable state, use StatefulWidget. You create a class that extends StatefulWidget and a corresponding State class that holds the mutable state.

DARTRead-only
1
class CounterWidget extends StatefulWidget {
  final int initialCount;

  const CounterWidget({super.key, this.initialCount = 0});

  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  late int _count;

  @override
  void initState() {
    super.initState();
    _count = widget.initialCount;
  }

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count', style: Theme.of(context).textTheme.titleLarge),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

Passing Callbacks

To communicate from a custom widget back to its parent, pass a callback function. This keeps the widget reusable and decoupled.

DARTRead-only
1
class CustomSwitch extends StatelessWidget {
  final bool value;
  final ValueChanged<bool> onChanged;

  const CustomSwitch({
    super.key,
    required this.value,
    required this.onChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: value,
      onChanged: onChanged,
      activeColor: Colors.blue,
    );
  }
}

// Usage in parent
CustomSwitch(
  value: _isSwitched,
  onChanged: (newValue) {
    setState(() {
      _isSwitched = newValue;
    });
  },
)

Composition – Building from Smaller Widgets

The most common way to create custom widgets is by composing existing Flutter widgets. This is more flexible and maintainable than subclassing widgets. For example, a custom card widget might combine Container, Column, and Text.

DARTRead-only
1
class ProfileCard extends StatelessWidget {
  final String name;
  final String email;
  final VoidCallback? onTap;

  const ProfileCard({
    super.key,
    required this.name,
    required this.email,
    this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: InkWell(
        onTap: onTap,
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            children: [
              const CircleAvatar(
                child: Icon(Icons.person),
              ),
              const SizedBox(width: 16),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      name,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      email,
                      style: const TextStyle(color: Colors.grey),
                    ),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Using const Constructors

Mark your widget constructor as const whenever possible. This allows Flutter to cache the widget and avoid unnecessary rebuilds. It's especially important for widgets that are instantiated many times (e.g., in lists).

DARTRead-only
1
class MyIcon extends StatelessWidget {
  final IconData icon;

  const MyIcon({super.key, required this.icon});

  @override
  Widget build(BuildContext context) {
    return Icon(icon);
  }
}

Passing key Parameter

Always include super.key in your widget constructor and pass it to the super call. This allows the parent to assign a Key to your widget, which helps Flutter identify it during tree reconciliation.

DARTRead-only
1
class CustomWidget extends StatelessWidget {
  const CustomWidget({super.key}); // super.key automatically passed

  @override
  Widget build(BuildContext context) { ... }
}

Generic Custom Widgets

You can make custom widgets generic to work with different data types. For example, a widget that displays a list of items can accept a generic type for the data model.

DARTRead-only
1
class DataList<T> extends StatelessWidget {
  final List<T> items;
  final Widget Function(T) itemBuilder;

  const DataList({
    super.key,
    required this.items,
    required this.itemBuilder,
  });

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) => itemBuilder(items[index]),
    );
  }
}

// Usage
DataList<User>(
  items: users,
  itemBuilder: (user) => ListTile(title: Text(user.name)),
)

Best Practices

    • Keep widgets small and focused: Each widget should do one thing well.
    • Use const constructors: Reduces rebuilds and improves performance.
    • Always pass key: Make your widgets keyable to preserve state.
    • Prefer composition over inheritance: Combine existing widgets rather than extending them.
    • Use named parameters: Makes the widget API self‑documenting.
    • Add required parameters for essential data: Use required for mandatory fields.
    • Add documentation: Use /// comments to explain the widget's purpose and parameters.

Common Mistakes

    • Creating state inside a stateless widget: Stateless widgets cannot have mutable state; use StatefulWidget.
    • Not disposing controllers in stateful widgets: Always dispose of TextEditingController, AnimationController, etc.
    • Using setState unnecessarily: Only update when the state actually changes.
    • Hard‑coding styles: Make styles customizable through parameters or theme.
    • Not using const where possible: Missed optimization opportunities.

Key Takeaways

    • Custom widgets promote code reuse and maintainability.
    • Stateless widgets are for static UI; stateful widgets for dynamic UI.
    • Pass callbacks to communicate changes to the parent.
    • Compose widgets to build complex UIs.
    • Always include super.key and make constructors const when possible.
    • Follow the same naming conventions as Flutter's built‑in widgets (PascalCase).

Try it yourself

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Widget Demo',
      home: Scaffold(
        appBar: AppBar(title: const Text('Custom Widgets')),
        body: const Center(
          child: CustomButton(
            label: 'Press Me',
            onPressed: null, // You can add an onPressed in real app
          ),
        ),
      ),
    );
  }
}

class CustomButton extends StatelessWidget {
  final String label;
  final VoidCallback? onPressed;

  const CustomButton({
    super.key,
    required this.label,
    this.onPressed,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(30),
        ),
      ),
      child: Text(
        label,
        style: const TextStyle(fontSize: 18),
      ),
    );
  }
}

Test Your Knowledge

Q1
of 4

Which class should you extend to create a stateless custom widget?

A
StatefulWidget
B
StatelessWidget
C
Widget
D
CustomWidget
Q2
of 4

How do you pass a callback from a custom widget to its parent?

A
Using a `setState` call
B
Passing a function parameter
C
Using a global variable
D
Using `InheritedWidget`
Q3
of 4

What is the purpose of marking a custom widget constructor as `const`?

A
To allow the widget to be immutable and improve performance
B
To prevent the widget from being rebuilt
C
To make the widget private
D
To enable hot reload
Q4
of 4

Which parameter should you always include in a custom widget constructor?

A
child
B
key
C
color
D
onTap

Frequently Asked Questions

When should I create a custom widget instead of using a helper method?

Custom widgets are more efficient because they can be marked const and have their own BuildContext, which allows better performance and state isolation. Helper methods rebuild every time the parent rebuilds, whereas custom widgets can be reused and are cached. Use custom widgets for reusable, self‑contained UI pieces.

How do I choose between `StatelessWidget` and `StatefulWidget`?

If the widget's appearance only depends on its constructor parameters and does not change over time, use StatelessWidget. If the widget needs to manage internal state that changes (e.g., animations, user input), use StatefulWidget.

Can I pass data from a child widget to its parent without callbacks?

The recommended way is to pass a callback function. For more complex scenarios, you can use state management solutions like Provider, Bloc, or Riverpod to share data across the widget tree.

Should I use `const` for all widgets?

Use const whenever the widget is truly constant (all its parameters are also constant). This allows Flutter to reuse the instance, saving memory and rebuild time. It's especially beneficial for widgets that are created many times, like in a list.

How do I make my custom widget accept a child?

Add a Widget? child parameter to your constructor. Then include it in your build method. You can also use children for multiple widgets.

Previous

flutter hero animation

Next

flutter responsive design

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.