Back to blog
April 9, 2025
5 min read

Custom Widget System

Build reusable and maintainable custom widgets

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

  1. 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
    
  2. 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();
    }
    
  3. 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,
        );
      }
    }
    
  4. 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();
    }
    
  5. 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);
          },
        );
      }
    }
    
  6. 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);
      }
    }
    
  7. 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

  1. Component Design

    • Keep components focused
    • Use composition
    • Follow conventions
    • Document usage
  2. State Management

    • Minimize state
    • Use providers
    • Handle updates
    • Test state
  3. Performance

    • Optimize rebuilds
    • Cache values
    • Use const
    • Profile widgets
  4. 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.