Docs
Tutorials
Supporting new Widgets

Supporting new Widgets

Mix is a powerful tool, and to make it even more powerful, we can add support for new widgets and increase its functionality for more and more use cases. Therefore, this guide will help you add support for new widgets.

To make this tutorial more practical, we will add support for a widget from Material, the TextField. Specifically, we will add support for the decoration attribute, which defines most of the visual aspects of the TextField. As a result, we will be able to go from the default TextField, which looks like this:

alt text

and make it look like this:

alt text

1. Define the Spec

As we are adding support for a new widget and its attributes that you want to customize, we need to define the spec for it. The spec is an object that defines the attributes the widget has and how to handle them. The spec for the InputDecoration is:

class InputDecorationSpec extends Spec<InputDecorationSpec> {
  final InputBorder? border;
  final bool? filled;
  final Color? fillColor;
  final EdgeInsetsGeometry? contentPadding;
 
  const InputDecorationSpec({
    this.border,
    this.filled,
    this.fillColor,
    this.contentPadding,
  });
 
  const InputDecorationSpec.empty()
      : border = null,
        filled = null,
        fillColor = null,
        contentPadding = null;
 
  static InputDecorationSpec of(BuildContext context) {
    final mix = Mix.of(context);
    return mix.attributeOf<InputDecorationSpecAttribute>()?.resolve(mix) ??
        const InputDecorationSpec.empty();
  }
 
  @override
  InputDecorationSpec lerp(InputDecorationSpec? other, double t) {
    return InputDecorationSpec(
      border: other?.border,
      filled: t < 0.5 ? filled : other?.filled,
      fillColor: Color.lerp(fillColor, other?.fillColor, t),
      contentPadding: EdgeInsetsGeometry.lerp(
        contentPadding,
        other?.contentPadding,
        t,
      ),
    );
  }
 
  InputDecorationSpec copyWith({
    InputBorder? border,
    bool? filled,
    Color? fillColor,
    EdgeInsetsGeometry? contentPadding,
  }) {
    return InputDecorationSpec(
      border: border ?? this.border,
      filled: filled ?? this.filled,
      fillColor: fillColor ?? this.fillColor,
      contentPadding: contentPadding ?? this.contentPadding,
    );
  }
 
  @override
  get props => [
        border,
        filled,
        fillColor,
        contentPadding,
      ];
}

At first glance, it may seem a bit complex, but it's not. The InputDecorationSpec is a class that extends Spec and has a constructor that receives the attributes of the InputDecoration. For this example, I chose just four attributes: border, filled, fillColor, and contentPadding. However, for a real implementation, you should add all the attributes that the InputDecoration has.

Looking at the rest of the class, you can see the named constructor empty, which returns a default implementation of the InputDecorationSpec, the of method that receives a MixData and returns an InputDecorationSpec with the values of the MixData. The lerp method is used to interpolate between two InputDecorationSpec instances.

The Spec is the base for supporting a new widget and its attributes.

2. Create the Attribute

The attribute is the class that will be used to handle the InputDecorationSpec. The InputDecorationSpecAttribute is:

class InputDecorationSpecAttribute extends SpecAttribute<InputDecorationSpec> {
  final InputBorder? border;
  final bool? filled;
  final ColorDto? fillColor;
  final SpacingDto? contentPadding;
 
  const InputDecorationSpecAttribute({
    this.border,
    this.filled,
    this.fillColor,
    this.contentPadding,
  });
 
  @override
  InputDecorationSpec resolve(MixData mix) {
    return InputDecorationSpec(
      border: border,
      filled: filled,
      fillColor: fillColor?.resolve(mix),
      contentPadding: contentPadding?.resolve(mix),
    );
  }
 
  @override
  InputDecorationSpecAttribute merge(
      covariant InputDecorationSpecAttribute? other) {
    if (other == null) return this;
 
    return InputDecorationSpecAttribute(
      border: other.border ?? border,
      filled: other.filled ?? filled,
      fillColor: other.fillColor ?? fillColor,
      contentPadding: other.contentPadding ?? contentPadding,
    );
  }
 
  @override
  get props => [
        border,
        filled,
        fillColor,
        contentPadding,
      ];
}
 

Looking at the properties' definition, you will see some changes in the property types. For example, now fillColor is a ColorDto, and contentPadding is a SpacingDto. This is because the Mix uses Dto classes to handle more complex types and provide facilities to use them.

The InputDecorationSpecAttribute is a class that extends SpecAttribute and has a resolve method, which is a useful method to transform the InputDecorationSpec from the MixData. The merge method is used to merge two different declarations of the InputDecorationSpecAttribute. A good example of this is:

final style = Style(
    InputDecorationSpecAttribute(
      border: OutlineInputBorder(),
      fillColor: ColorDto(Colors.blue),
      filled: true,
    ),
    InputDecorationSpecAttribute(
      fillColor: ColorDto(Colors.red),
      contentPadding: SpacingDto.only(top: 10),
    ),
)

In this case, the style will have the border and filled from the first declaration, and the fillColor and contentPadding from the second declaration. Be aware that the second declaration of fillColor will override the first one, so the final InputDecorationSpec will have the fillColor as Colors.red.

3. Build the Custom Widget

The custom widget is the widget that will use the InputDecorationSpec to build itself. It's a class that has a SpecBuilder in its composition. For this example, the CustomTextField is:

class StyledTextField extends StyledWidget {
  const StyledTextField({
    super.key,
    super.style,
    super.orderOfModifiers = const [],
  });
 
  @override
  Widget build(BuildContext context) {
    return SpecBuilder(
      style: style,
      orderOfModifiers: orderOfModifiers,
      builder: (context) {
        final inputDecoration = InputDecorationSpec.of(context);
 
        return TextField(
          decoration: InputDecoration(
            border: inputDecoration.border,
            filled: inputDecoration.filled,
            fillColor: inputDecoration.fillColor,
            contentPadding: inputDecoration.contentPadding,
          ),
        );
      },
    );
  }
}

The SpecBuilder is a class that extends StyledWidget. The build method uses the withMix method to transform the style received from StyledTextField to a MixData, it also verifies if there are some inheritable attributes from the parent widgets, but this only happens if the inherit property is true. With the MixData instance in hands, you can use the InputDecorationSpec.of method to get the InputDecorationSpec from the MixData. Now you are able to apply the InputDecorationSpec to the TextField and return it.

Now you are already ready to use the StyledTextField in your app and customize the InputDecoration of the TextField with Mix.

final style = Style(
  InputDecorationSpecAttribute(
    border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(20),
                borderSide: BorderSide.none,
            ),
    fillColor: ColorDto(Colors.blueAccent.shade200),
    filled: true,
    contentPadding: SpacingDto.all(24),
  ),
);

4. Create the Utility

In the last step, we mentioned that you're ready to use the StyledTextField in your app, which is true. However, there's an opportunity to enhance its usability further. By creating a utility, you can simplify the process of applying attributes to the InputDecoration, making it even more convenient to customize your text fields.

class InputDecorationUtility<T extends Attribute>
    extends SpecUtility<T, InputDecorationSpecAttribute> {
  InputDecorationUtility(super.builder);
 
  @override
  T only({
    InputBorder? border,
    bool? filled,
    ColorDto? fillColor,
    SpacingDto? contentPadding,
  }) {
    return builder(
      InputDecorationSpecAttribute(
        border: border,
        filled: filled,
        fillColor: fillColor,
        contentPadding: contentPadding,
      ),
    );
  }
 
  late final fillColor = ColorUtility((color) => only(fillColor: color));
 
  late final filled = BoolUtility((boolean) => only(filled: boolean));
 
  late final contentPadding =
      SpacingUtility((spacing) => only(contentPadding: spacing));
 
  T border(InputBorder inputBorder) => only(border: inputBorder);
}
 

The InputDecorationUtility is a class that extends SpecUtility and has methods to create the InputDecorationSpecAttribute with the attributes of the InputDecoration. The awesome thing about it is that each pre-defined Utility has some facilities to use the attributes. For example, the filled returns a BoolUtility that comes with on and off methods to make the code more readable, or the fillColor that returns a ColorUtility that comes with a bunch of methods to make it easier to use the Color attributes and perform operations with them.

To finish, you can create a variable to use the InputDecorationUtility, like this:

final $inputDecoration = InputDecorationUtility(MixUtility.selfBuilder);

So you can write the Style like this:

final style = Style(
  $inputDecoration.border(
      OutlineInputBorder(
      borderRadius: BorderRadius.circular(20),
      borderSide: BorderSide.none,
      ),
  ),
  $inputDecoration.filled.on(),
  $inputDecoration.fillColor.,
  $inputDecoration.contentPadding.all(24),
  $with.scale(2),
);