Custom Widget System
Creating a custom widget system in Flutter allows for better code organization, reusability, and maintainability. This tutorial will guide you through implementing a custom widget system with features like component composition, state management, and theming.
Features
- Custom components
- Component composition
- State management
- Theming support
- Animation system
- Gesture handling
- Accessibility
- Performance optimization
- Testing utilities
- Documentation
Implementation Steps
-
Setup Dependencies
# pubspec.yaml dependencies: flutter: sdk: flutter provider: ^6.1.1 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 flutter_test: sdk: flutter
-
Create Base Widget Models
abstract class BaseWidget extends StatelessWidget { final String? key; final Widget? child; final Map<String, dynamic>? properties; const BaseWidget({ this.key, this.child, this.properties, }); @override Widget build(BuildContext context) { return buildWidget(context); } Widget buildWidget(BuildContext context); } abstract class BaseStatefulWidget extends StatefulWidget { final String? key; final Widget? child; final Map<String, dynamic>? properties; const BaseStatefulWidget({ this.key, this.child, this.properties, }); @override State<BaseStatefulWidget> createState() => createState(); State<BaseStatefulWidget> createState(); }
-
Create Component Models
class ComponentConfig { final String name; final Map<String, dynamic> properties; final List<ComponentConfig> children; ComponentConfig({ required this.name, this.properties = const {}, this.children = const [], }); factory ComponentConfig.fromJson(Map<String, dynamic> json) { return ComponentConfig( name: json['name'] as String, properties: json['properties'] as Map<String, dynamic>? ?? {}, children: (json['children'] as List<dynamic>?) ?.map((e) => ComponentConfig.fromJson(e as Map<String, dynamic>)) .toList() ?? [], ); } Map<String, dynamic> toJson() { return { 'name': name, 'properties': properties, 'children': children.map((e) => e.toJson()).toList(), }; } } class ComponentState { final Map<String, dynamic> data; final Map<String, Function> actions; ComponentState({ this.data = const {}, this.actions = const {}, }); ComponentState copyWith({ Map<String, dynamic>? data, Map<String, Function>? actions, }) { return ComponentState( data: data ?? this.data, actions: actions ?? this.actions, ); } }
-
Create Component Registry
class ComponentRegistry { static final Map<String, WidgetBuilder> _builders = {}; static final Map<String, ComponentConfig> _configs = {}; static void registerComponent( String name, WidgetBuilder builder, { ComponentConfig? config, }) { _builders[name] = builder; if (config != null) { _configs[name] = config; } } static Widget? buildComponent( String name, BuildContext context, { Map<String, dynamic>? properties, Widget? child, }) { final builder = _builders[name]; if (builder == null) return null; return builder(context); } static ComponentConfig? getConfig(String name) { return _configs[name]; } static List<String> get registeredComponents => _builders.keys.toList(); }
-
Create Custom Components
class CustomButton extends BaseWidget { final String text; final VoidCallback? onPressed; final ButtonStyle? style; const CustomButton({ required this.text, this.onPressed, this.style, super.key, }); @override Widget buildWidget(BuildContext context) { return ElevatedButton( onPressed: onPressed, style: style, child: Text(text), ); } } class CustomCard extends BaseWidget { final Widget child; final EdgeInsetsGeometry? padding; final BoxDecoration? decoration; const CustomCard({ required this.child, this.padding, this.decoration, super.key, }); @override Widget buildWidget(BuildContext context) { return Card( child: Container( padding: padding, decoration: decoration, child: child, ), ); } } class CustomTextField extends BaseStatefulWidget { final String? label; final String? hint; final TextEditingController? controller; final ValueChanged<String>? onChanged; final FormFieldValidator<String>? validator; const CustomTextField({ this.label, this.hint, this.controller, this.onChanged, this.validator, super.key, }); @override State<CustomTextField> createState() => _CustomTextFieldState(); } class _CustomTextFieldState extends State<CustomTextField> { late TextEditingController _controller; String? _errorText; @override void initState() { super.initState(); _controller = widget.controller ?? TextEditingController(); } @override void dispose() { if (widget.controller == null) { _controller.dispose(); } super.dispose(); } @override Widget build(BuildContext context) { return TextFormField( controller: _controller, decoration: InputDecoration( labelText: widget.label, hintText: widget.hint, errorText: _errorText, ), onChanged: (value) { if (widget.validator != null) { setState(() { _errorText = widget.validator!(value); }); } widget.onChanged?.call(value); }, ); } }
-
Create Component Factory
class ComponentFactory { static Widget buildFromConfig( ComponentConfig config, BuildContext context, { Map<String, dynamic>? properties, }) { final widget = ComponentRegistry.buildComponent( config.name, context, properties: properties, ); if (widget == null) return SizedBox(); if (config.children.isEmpty) return widget; return Column( children: config.children .map((child) => buildFromConfig(child, context)) .toList(), ); } static Widget buildFromJson( Map<String, dynamic> json, BuildContext context, ) { final config = ComponentConfig.fromJson(json); return buildFromConfig(config, context); } }
-
Create Main Screen
class CustomWidgetsDemoScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Custom Widgets Demo'), ), body: SingleChildScrollView( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Custom Button', style: Theme.of(context).textTheme.headlineMedium, ), SizedBox(height: 16), CustomButton( text: 'Click Me', onPressed: () { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Button clicked!')), ); }, ), SizedBox(height: 32), Text( 'Custom Card', style: Theme.of(context).textTheme.headlineMedium, ), SizedBox(height: 16), CustomCard( padding: EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Card Title', style: Theme.of(context).textTheme.titleLarge, ), SizedBox(height: 8), Text( 'This is a custom card component with shadow and padding.', style: Theme.of(context).textTheme.bodyMedium, ), ], ), ), SizedBox(height: 32), Text( 'Custom TextField', style: Theme.of(context).textTheme.headlineMedium, ), SizedBox(height: 16), CustomTextField( label: 'Username', hint: 'Enter your username', validator: (value) { if (value == null || value.isEmpty) { return 'Please enter a username'; } return null; }, onChanged: (value) { print('Username changed: $value'); }, ), ], ), ), ); } }
Best Practices
-
Component Design
- Keep components focused
- Use composition
- Follow conventions
- Document usage
-
State Management
- Minimize state
- Use providers
- Handle updates
- Test state
-
Performance
- Optimize rebuilds
- Cache values
- Use const
- Profile widgets
-
Testing
- Write unit tests
- Test interactions
- Verify behavior
- Mock dependencies
Conclusion
This tutorial has shown you how to implement a custom widget system in Flutter with features like:
- Custom components
- Component composition
- State management
- Theming support
You can extend this implementation by adding:
- More components
- Advanced animations
- Complex layouts
- Component testing
Remember to:
- Keep components simple
- Test thoroughly
- Document usage
- Handle edge cases
- Follow guidelines
This implementation provides a solid foundation for creating a maintainable widget system in Flutter.