Flutter Custom Widgets and Themes
Writing the same card layout or button style in five screens wastes time and creates inconsistency. Custom widgets let you build reusable UI components. Themes give your entire app a unified look from a single configuration.
Why Build Custom Widgets
Without custom widgets: ──────────────────────────────────────────────── ProductScreen: Card → Padding → Column → Image + Text + Button OrderScreen: Card → Padding → Column → Image + Text + Button WishlistScreen:Card → Padding → Column → Image + Text + Button (Repeated 3 times — change one, must change all three) With a custom widget: ──────────────────────────────────────────────── ProductScreen: ProductCard(product: p) OrderScreen: ProductCard(product: p) WishlistScreen: ProductCard(product: p) (Change once → all three update automatically)
Creating a Reusable Custom Widget
class ProductCard extends StatelessWidget {
final String name;
final String imageUrl;
final double price;
final VoidCallback onAddToCart;
const ProductCard({
required this.name,
required this.imageUrl,
required this.price,
required this.onAddToCart,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
child: Image.network(imageUrl, height: 140, width: double.infinity, fit: BoxFit.cover),
),
Padding(
padding: EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
SizedBox(height: 4),
Text('₹${price.toStringAsFixed(0)}', style: TextStyle(color: Colors.green, fontSize: 15)),
SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: ElevatedButton(onPressed: onAddToCart, child: Text('Add to Cart')),
),
],
),
),
],
),
);
}
}
Using the Custom Widget
ProductCard(
name: 'Running Shoes',
imageUrl: 'https://example.com/shoes.jpg',
price: 2499,
onAddToCart: () => print('Added!'),
)
Custom Widget with Optional Parameters
class InfoBadge extends StatelessWidget {
final String label;
final Color color;
final IconData? icon; // Optional icon
const InfoBadge({
required this.label,
this.color = Colors.blue, // Default value
this.icon,
});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
border: Border.all(color: color),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: 14, color: color),
SizedBox(width: 4),
],
Text(label, style: TextStyle(color: color, fontSize: 12)),
],
),
);
}
}
// Usage
InfoBadge(label: 'In Stock', color: Colors.green, icon: Icons.check)
InfoBadge(label: 'New', color: Colors.orange)
InfoBadge(label: 'Sale', color: Colors.red, icon: Icons.local_offer)
ThemeData — App-Wide Styling
Define your app's colors, fonts, and component styles in one ThemeData object. Every widget reads from it automatically.
MaterialApp(
title: 'My App',
theme: ThemeData(
// Color scheme
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
// Typography
textTheme: TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.indigo),
bodyLarge: TextStyle(fontSize: 16, color: Colors.black87),
labelSmall: TextStyle(fontSize: 11, color: Colors.grey),
),
// Button styling for all ElevatedButtons
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
),
// Input field styling for all TextFields
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
filled: true,
fillColor: Colors.grey.shade100,
),
// Card styling
cardTheme: CardTheme(
elevation: 3,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
),
home: HomeScreen(),
)
Accessing Theme in Widgets
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Text(
'Primary Color Text',
style: theme.textTheme.headlineLarge, // Uses theme font
);
}
Dark Theme Support
MaterialApp(
theme: ThemeData.light(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
darkTheme: ThemeData.dark(useMaterial3: true).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system, // Follows device setting
home: HomeScreen(),
)
ThemeMode options: ─────────────────────────────────── ThemeMode.system → follows device (auto) ThemeMode.light → always light ThemeMode.dark → always dark
Custom Fonts
1. Download font files and place in assets/fonts/
2. Register in pubspec.yaml:
flutter:
fonts:
- family: Poppins
fonts:
- asset: assets/fonts/Poppins-Regular.ttf
- asset: assets/fonts/Poppins-Bold.ttf
weight: 700
3. Use in theme:
textTheme: TextTheme(
bodyLarge: TextStyle(fontFamily: 'Poppins'),
)
Summary
- Extract repeated UI patterns into custom StatelessWidgets with required and optional parameters.
- Define
ThemeDataonce in MaterialApp — all widgets inherit the styles automatically. - Use
Theme.of(context)inside widgets to read current theme values. - Support dark mode with
darkThemeandThemeMode.system.
