Flutter Widget Patterns
•11 min read
This guide covers common widget patterns and best practices for building reusable UI components in Flutter.
1. Composition Patterns
Custom Container
class CustomContainer extends StatelessWidget { final Widget child; final EdgeInsets padding; final Color backgroundColor; final double borderRadius; final BoxShadow? shadow; const CustomContainer({ Key? key, required this.child, this.padding = const EdgeInsets.all(16), this.backgroundColor = Colors.white, this.borderRadius = 8, this.shadow, }) : super(key: key); @override Widget build(BuildContext context) { return Container( padding: padding, decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular(borderRadius), boxShadow: shadow != null ? [shadow!] : null, ), child: child, ); } } // Usage CustomContainer( child: Text('Hello World'), backgroundColor: Colors.blue, borderRadius: 12, shadow: BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 2), ), )
Custom Card
class CustomCard extends StatelessWidget { final Widget child; final EdgeInsets padding; final Color backgroundColor; final double borderRadius; final VoidCallback? onTap; const CustomCard({ Key? key, required this.child, this.padding = const EdgeInsets.all(16), this.backgroundColor = Colors.white, this.borderRadius = 8, this.onTap, }) : super(key: key); @override Widget build(BuildContext context) { return Material( color: backgroundColor, borderRadius: BorderRadius.circular(borderRadius), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(borderRadius), child: Padding( padding: padding, child: child, ), ), ); } } // Usage CustomCard( child: Column( children: [ Text('Title'), Text('Description'), ], ), onTap: () { // Handle tap }, )
2. Layout Patterns
Responsive Layout
class ResponsiveLayout extends StatelessWidget { final Widget mobile; final Widget? tablet; final Widget? desktop; const ResponsiveLayout({ Key? key, required this.mobile, this.tablet, this.desktop, }) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth >= 1200) { return desktop ?? tablet ?? mobile; } else if (constraints.maxWidth >= 600) { return tablet ?? mobile; } else { return mobile; } }, ); } } // Usage ResponsiveLayout( mobile: MobileLayout(), tablet: TabletLayout(), desktop: DesktopLayout(), )
Custom Grid
class CustomGrid extends StatelessWidget { final List<Widget> children; final int crossAxisCount; final double spacing; final double runSpacing; const CustomGrid({ Key? key, required this.children, this.crossAxisCount = 2, this.spacing = 8, this.runSpacing = 8, }) : super(key: key); @override Widget build(BuildContext context) { return Wrap( spacing: spacing, runSpacing: runSpacing, children: children, ); } } // Usage CustomGrid( children: [ CustomCard(child: Text('Item 1')), CustomCard(child: Text('Item 2')), CustomCard(child: Text('Item 3')), ], crossAxisCount: 3, )
3. State Management Patterns
InheritedWidget
class ThemeProvider extends InheritedWidget { final ThemeData theme; final VoidCallback toggleTheme; const ThemeProvider({ Key? key, required this.theme, required this.toggleTheme, required Widget child, }) : super(key: key, child: child); static ThemeProvider of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!; } @override bool updateShouldNotify(ThemeProvider oldWidget) { return theme != oldWidget.theme; } } // Usage ThemeProvider( theme: ThemeData.light(), toggleTheme: () { // Toggle theme }, child: MyApp(), )
ValueNotifier
class CounterProvider extends ValueNotifier<int> { CounterProvider() : super(0); void increment() { value++; } void decrement() { value--; } } // Usage final counterProvider = CounterProvider(); ValueListenableBuilder<int>( valueListenable: counterProvider, builder: (context, value, child) { return Text('Count: $value'); }, )
4. Animation Patterns
Animated Container
class AnimatedCustomContainer extends StatefulWidget { final Widget child; final Duration duration; final Curve curve; const AnimatedCustomContainer({ Key? key, required this.child, this.duration = const Duration(milliseconds: 300), this.curve = Curves.easeInOut, }) : super(key: key); @override _AnimatedCustomContainerState createState() => _AnimatedCustomContainerState(); } class _AnimatedCustomContainerState extends State<AnimatedCustomContainer> { bool _isExpanded = false; @override Widget build(BuildContext context) { return AnimatedContainer( duration: widget.duration, curve: widget.curve, height: _isExpanded ? 200 : 100, child: widget.child, ); } } // Usage AnimatedCustomContainer( child: Text('Animated Content'), duration: Duration(milliseconds: 500), curve: Curves.easeOut, )
Hero Animation
class HeroImage extends StatelessWidget { final String tag; final String imageUrl; final double width; final double height; const HeroImage({ Key? key, required this.tag, required this.imageUrl, this.width = 100, this.height = 100, }) : super(key: key); @override Widget build(BuildContext context) { return Hero( tag: tag, child: Image.network( imageUrl, width: width, height: height, fit: BoxFit.cover, ), ); } } // Usage HeroImage( tag: 'image_1', imageUrl: 'https://example.com/image.jpg', width: 200, height: 200, )
5. Form Patterns
Custom Form Field
class CustomFormField extends StatelessWidget { final String label; final String? Function(String?)? validator; final TextEditingController controller; final bool obscureText; final TextInputType? keyboardType; final Widget? prefixIcon; final Widget? suffixIcon; const CustomFormField({ Key? key, required this.label, required this.validator, required this.controller, this.obscureText = false, this.keyboardType, this.prefixIcon, this.suffixIcon, }) : super(key: key); @override Widget build(BuildContext context) { return TextFormField( controller: controller, obscureText: obscureText, keyboardType: keyboardType, validator: validator, decoration: InputDecoration( labelText: label, prefixIcon: prefixIcon, suffixIcon: suffixIcon, border: const OutlineInputBorder(), errorStyle: const TextStyle(color: Colors.red), ), ); } } // Usage CustomFormField( label: 'Email', controller: emailController, validator: (value) { if (value == null || value.isEmpty) { return 'Email is required'; } return null; }, keyboardType: TextInputType.emailAddress, prefixIcon: Icon(Icons.email), )
Form Validation
class LoginForm extends StatefulWidget { @override _LoginFormState createState() => _LoginFormState(); } class _LoginFormState extends State<LoginForm> { final _formKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Email is required'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Please enter a valid email'; } return null; } String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Password is required'; } if (value.length < 8) { return 'Password must be at least 8 characters'; } return null; } @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ CustomFormField( label: 'Email', controller: _emailController, validator: _validateEmail, keyboardType: TextInputType.emailAddress, prefixIcon: Icon(Icons.email), ), CustomFormField( label: 'Password', controller: _passwordController, validator: _validatePassword, obscureText: true, prefixIcon: Icon(Icons.lock), ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // Process form } }, child: const Text('Login'), ), ], ), ); } }
Best Practices
-
Composition
- Use composition over inheritance
- Break down complex widgets
- Keep widgets focused
- Reuse common patterns
-
Layout
- Use responsive layouts
- Follow Material Design
- Consider different screen sizes
- Optimize for performance
-
State Management
- Choose appropriate patterns
- Keep state local when possible
- Use immutable state
- Handle state changes properly
-
Animation
- Use appropriate animations
- Keep animations smooth
- Consider performance
- Follow platform guidelines
-
Forms
- Validate inputs properly
- Provide clear feedback
- Handle errors gracefully
- Follow accessibility guidelines
Conclusion
Remember these key points:
- Use composition patterns
- Implement responsive layouts
- Manage state effectively
- Add meaningful animations
- Handle forms properly
By following these patterns, you can:
- Build reusable components
- Improve code maintainability
- Enhance user experience
- Reduce development time
Keep building beautiful Flutter applications!