Custom Widgets
Building reusable, composable widgets in Flutter — the foundation of every great Flutter app.
Custom widgets
are the building blocks of Flutter applications. While Flutter
provides a rich set of built-in widgets (like
Container
,
Row
,
Column
,
Text
, etc.), you'll quickly find that you need to create
your own widgets to encapsulate reusable UI components and business logic.
💡 Key Concept
In Flutter, everything is a widget . Custom widgets allow you to compose existing widgets into new, reusable components. This is the core philosophy of Flutter — building UIs by composing widgets.
Understanding when to use StatelessWidget vs StatefulWidget is crucial for building efficient Flutter apps.
StatelessWidget
- Immutable — properties can't change
- Build method runs once unless parent rebuilds
- Perfect for presentation-only widgets
- More performant
-
Example:
Text,Icon,Container
StatefulWidget
- Mutable — can change over time
- Has a separate State object
-
Can call
setState()to rebuild - More complex lifecycle
-
Example:
Checkbox,TextField, animations
class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
final Color color;
CustomButton({
required this.text,
required this.onPressed,
this.color = Colors.blue,
});
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: color),
onPressed: onPressed,
child: Text(text),
);
}
}
class CounterWidget extends StatefulWidget {
final String label;
CounterWidget({required this.label});
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void _increment() {
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('${widget.label}: $_count'),
ElevatedButton(onPressed: _increment, child: Text('Increment')),
],
);
}
}
✅ Best Practice: Prefer Stateless
Always start with a StatelessWidget . Only switch to StatefulWidget when you need to manage local state that changes over time. This keeps your code simpler and more performant.
In Flutter, composition is preferred over inheritance. Instead of extending a widget to customize it, you compose it with other widgets.
// DON'T extend a widget to customize it
class MyCustomButton extends ElevatedButton {
MyCustomButton({required VoidCallback onPressed, required Widget child})
: super(onPressed: onPressed, child: child);
}
// Compose widgets to build new ones
class MyCustomButton extends StatelessWidget {
final VoidCallback onPressed;
final String text;
MyCustomButton({required this.onPressed, required this.text});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.purple,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text(
text,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
);
}
}
💡 Why Composition?
- Flexibility – You can combine widgets in any way
- Reusability – Composed widgets can be used in many places
- Testability – Smaller, focused widgets are easier to test
- Maintainability – Changes are isolated to specific widgets
- Performance – Composition is more efficient than deep inheritance
Custom widgets shine when building complex, reusable UI components. Here's how to build a User Profile Card that can be reused across your app:
class ProfileCard extends StatelessWidget {
final String name;
final String email;
final String avatarUrl;
final VoidCallback onTap;
final bool isOnline;
ProfileCard({
required this.name,
required this.email,
required this.avatarUrl,
required this.onTap,
this.isOnline = false,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
// Avatar with online indicator
Stack(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(avatarUrl),
),
if (isOnline)
Positioned(
bottom: 0,
right: 0,
child: Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
SizedBox(width: 16),
// User info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
email,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
),
Icon(Icons.chevron_right, color: Colors.grey[400]),
],
),
),
),
);
}
}
// Usage in any screen
ProfileCard(
name:
'John Doe'
,
email:
'john@example.com'
,
avatarUrl:
'https://i.pravatar.cc/150?img=1'
,
isOnline:
true
,
onTap: () {
// Navigate to user profile
Navigator.
push
(context, MaterialPageRoute(
builder: (context) => UserProfileScreen(userId:
'123'
),
));
},
)
1. The "Container" Pattern
Create widgets that accept a
child
and wrap it with styling, layout, or behavior.
class CardContainer extends StatelessWidget {
final Widget child;
final EdgeInsets padding;
final double elevation;
CardContainer({
required this.child,
this.padding = const EdgeInsets.all(16),
this.elevation = 2,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: elevation,
child: Padding(padding: padding, child: child),
);
}
}
2. The "List View" Pattern
Create reusable list builders for consistent data display.
class UserList extends StatelessWidget {
final List<User> users;
final void Function(User) onUserTap;
UserList({required this.users, required this.onUserTap});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ProfileCard(
name: user.name,
email: user.email,
avatarUrl: user.avatarUrl,
isOnline: user.isOnline,
onTap: () => onUserTap(user),
);
},
);
}
}
3. The "Loading State" Pattern
Build widgets that handle loading, error, and success states.
class AsyncContent<T> extends StatelessWidget {
final AsyncSnapshot<T> snapshot;
final Widget Function(T data) onSuccess;
final Widget Function(String error)? onError;
final Widget? onLoading;
AsyncContent({
required this.snapshot,
required this.onSuccess,
this.onError,
this.onLoading,
});
@override
Widget build(BuildContext context) {
if (snapshot.connectionState == ConnectionState.waiting) {
return onLoading ?? Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return onError?.call(snapshot.error.toString()) ??
Center(child: Text('Error: ${snapshot.error}'));
}
if (snapshot.hasData) {
return onSuccess.call(snapshot.data!);
}
return SizedBox.shrink();
}
}
The Flutter team produces Widget of the Week videos — short, 1-minute explainer videos covering individual widgets. These are excellent resources for learning about specific widgets and how to use them.
🎥 Popular Widget Videos
- Container – The most common layout widget
- Row & Column – Flexible layout in one dimension
- Stack – Overlapping widgets
- ListView – Scrollable list of widgets
- GridView – Scrollable grid of widgets
- FutureBuilder – Async data handling
- StreamBuilder – Real-time data handling
- Hero – Shared element transitions
Follow these steps to create your own custom widgets:
const
when possible for performance
Not every widget that accepts data needs to be Stateful. If the widget doesn't change its own state, use StatelessWidget.
Always start with StatelessWidget. Only switch to StatefulWidget when you need to manage local state that changes over time.
Extending widgets to customize them leads to brittle code and makes it harder to change the implementation.
Build custom widgets by combining existing widgets. This is more flexible and maintainable.
Non-const widgets are rebuilt every frame, even when nothing changes, hurting performance.
Make your widget's constructor
const
when possible. This allows Flutter to
optimize rebuilds.
Putting business logic inside widgets makes them harder to test and reuse.
Use ViewModels, BLoCs, or providers for business logic. Keep widgets focused on presentation.
🎯 Key Takeaway
Custom widgets
are the foundation of Flutter development. By
composing
existing widgets into new ones, you can build reusable,
maintainable, and performant UIs. Always
prefer StatelessWidget
over
StatefulWidget and
composition over inheritance
. Use
const
constructors for performance and keep your widgets focused on presentation.