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:
and make it look like this:
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),
);