Flutter Custom Widgets
Building reusable components
🎨 What are Custom Widgets?
Custom widgets are reusable components you create by combining existing widgets. They help organize code, promote reusability, and make your app easier to maintain and scale effectively.
// Simple custom widget
class MyButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {},
child: Text('Click Me'),
);
}
}
Widget Types
StatelessWidget
Immutable widgets
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container();
}
}
StatefulWidget
Widgets with state
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
return Container();
}
}
Parameters
Pass data to widgets
class MyWidget extends StatelessWidget {
final String title;
MyWidget({required this.title});
@override
Widget build(BuildContext context) {
return Text(title);
}
}
Callbacks
Handle user actions
class MyWidget extends StatelessWidget {
final VoidCallback onTap;
MyWidget({required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(),
);
}
}
🔹 Basic Custom Widget
Create a simple reusable button widget:
import 'package:flutter/material.dart';
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(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
text,
style: TextStyle(fontSize: 16, color: Colors.white),
),
);
}
}
// Usage
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomButton(
text: 'Click Me',
onPressed: () {
print('Button pressed!');
},
color: Colors.green,
),
),
);
}
}
Output:
🔹 Custom Card Widget
Create a reusable card component:
class ProfileCard extends StatelessWidget {
final String name;
final String role;
final String imageUrl;
ProfileCard({
required this.name,
required this.role,
required this.imageUrl,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(imageUrl),
),
SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
role,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
);
}
}
// Usage
ProfileCard(
name: 'John Doe',
role: 'Software Developer',
imageUrl: 'https://example.com/avatar.jpg',
)
Output:
👤
John Doe
Software Developer
🔹 Stateful Custom Widget
Create a widget with internal state:
class CounterWidget extends StatefulWidget {
final int initialValue;
CounterWidget({this.initialValue = 0});
@override
_CounterWidgetState createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
late int _counter;
@override
void initState() {
super.initState();
_counter = widget.initialValue;
}
void _increment() {
setState(() {
_counter++;
});
}
void _decrement() {
setState(() {
_counter--;
});
}
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blue[50],
borderRadius: BorderRadius.circular(15),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Counter',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'$_counter',
style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.blue),
),
SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(Icons.remove),
onPressed: _decrement,
color: Colors.red,
),
SizedBox(width: 20),
IconButton(
icon: Icon(Icons.add),
onPressed: _increment,
color: Colors.green,
),
],
),
],
),
);
}
}
Output:
Counter
5
🔹 Custom Input Widget
Create a styled text input field:
class CustomTextField extends StatelessWidget {
final String label;
final IconData icon;
final TextEditingController controller;
final bool obscureText;
CustomTextField({
required this.label,
required this.icon,
required this.controller,
this.obscureText = false,
});
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: TextField(
controller: controller,
obscureText: obscureText,
decoration: InputDecoration(
labelText: label,
prefixIcon: Icon(icon, color: Colors.blue),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blue, width: 2),
),
filled: true,
fillColor: Colors.grey[100],
),
),
);
}
}
// Usage
CustomTextField(
label: 'Email',
icon: Icons.email,
controller: emailController,
)
Output:
📧
🔹 Custom Widget Best Practices
Tips for creating custom widgets:
- Keep it focused - One widget, one purpose
- Use StatelessWidget when possible for better performance
- Make it configurable - Use parameters for flexibility
- Document your widget - Add comments explaining usage
- Follow naming conventions - Use descriptive names
- Separate concerns - Keep logic and UI separate