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 cases. Therefore, This is a guide to help you to add support for new widgets.
To make this tutorial more real, we will add support for a widget from Material, the TextField. To be more specific, we will add support for the decoration
attribute, that defines the most of the visual aspects of the TextField, and as result, we will be able to go from the default TextField, that looks like this:
and make it look like this:
1. Define the Spec
As we are adding support for a new widget and its own attributes you want to customize, we need to define the spec for it. The spec is a object that defines the attributes that 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 look, it may look a little bit complex, but it's not. The InputDecorationSpec
is a class that extends Spec
and has a constructor that receives the attributes that the InputDecoration
has, for this example I chose just four, the 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
, that returns a default implementation of the InputDecorationSpec
, the of
method that receives a MixData
and returns a InputDecorationSpec
with the values of the MixData
. The lerp
method is used to interpolate between two InputDecorationSpec
.
The Spec is the base of the support for 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 its properties definition, you will see some changes in the property types, for example, now fillColor
is a ColorDto
and contentPadding
is a SpacingDto
, it's because the Mix uses a Dto
classes to handle more complex types and supply some facilities to use them.
The InputDecorationSpecAttribute
is a class that extends SpecAttribute
and has a method resolve
that 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 the widget. 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 verify if exist some inheritable attributes from the parent widgets, but it's just happen 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 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 more readable the code, or the fillColor
that returns a ColorUtility
that comes with a bunch of methods to make it easier to use the Color
attributes and do 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),
);