diff --git a/website/src/content/documentation/tutorials/creating-a-widget.mdx b/website/src/content/documentation/tutorials/creating-a-widget.mdx index 823ab913a6..157758137c 100644 --- a/website/src/content/documentation/tutorials/creating-a-widget.mdx +++ b/website/src/content/documentation/tutorials/creating-a-widget.mdx @@ -1,7 +1,7 @@ # Building a Design System Widget -This guide walks through creating a design system button with Mix, demonstrating Specs, Stylers, variants, and state handling. +This guide walks through creating a design system button with Mix, demonstrating Specs, Stylers, variants, and state handling — using **annotations and code generation** to eliminate boilerplate. ![Button Example](./images/button-example.png) @@ -33,56 +33,82 @@ This guide walks through creating a design system button with Mix, demonstrating ### Create a Button Spec -A `Spec` defines resolved visual properties. `ButtonSpec` contains specs for container, icon, and label: +A `Spec` defines resolved visual properties. With `@MixableSpec()`, the generator creates `copyWith()`, `lerp()`, `debugFillProperties()`, and `props` for you: ```dart +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:mix/mix.dart'; +import 'package:mix_annotations/mix_annotations.dart'; -class ButtonSpec extends Spec { +part 'button_spec.g.dart'; + +@MixableSpec() +@immutable +final class ButtonSpec extends Spec + with Diagnosticable, _$ButtonSpecMethods { + @override final StyleSpec? container; + @override final StyleSpec? icon; + @override final StyleSpec? label; const ButtonSpec({this.container, this.icon, this.label}); +} +``` + +That's it — no manual `copyWith`, `lerp`, or `props`. The generated mixin `_$ButtonSpecMethods` provides all of those. Run code generation with: + +```bash +dart run build_runner build +``` +
+Generated code (`button_spec.g.dart`) — key signatures + +```dart +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'button_spec.dart'; + +mixin _$ButtonSpecMethods on Spec, Diagnosticable { @override - ButtonSpec copyWith({ - StyleSpec? container, - StyleSpec? icon, - StyleSpec? label, - }) { - return ButtonSpec( - container: container ?? this.container, - icon: icon ?? this.icon, - label: label ?? this.label, - ); - } + ButtonSpec copyWith({StyleSpec? container, ...}) { ... } @override - ButtonSpec lerp(covariant ButtonSpec? other, double t) { - return ButtonSpec( - container: container?.lerp(other?.container, t), - icon: icon?.lerp(other?.icon, t), - label: label?.lerp(other?.label, t), - ); - } + ButtonSpec lerp(ButtonSpec? other, double t) { ... } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { ... } @override List get props => [container, icon, label]; } ``` +
+ ### Create a Button Styler -`ButtonStyler` provides a fluent interface for styling. It extends `Style` and uses `WidgetStateVariantMixin` for state support: +`ButtonStyler` provides a fluent interface for styling. With `@MixableStyler()`, the generator creates setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props`. + +Use `@MixableField(setterType: ...)` to tell the generator the public type for each setter. Since the fields are `Prop>` internally but users pass Styler types, `setterType` bridges that gap: ```dart -class ButtonStyler extends Style - with VariantStyleMixin, - WidgetStateVariantMixin { +part 'button_style.g.dart'; + +@MixableStyler() +class ButtonStyler extends MixStyler + with _$ButtonStylerMixin { + @override + @MixableField(setterType: FlexBoxStyler) final Prop>? $container; + @override + @MixableField(setterType: IconStyler) final Prop>? $icon; + @override + @MixableField(setterType: TextStyler) final Prop>? $label; ButtonStyler({ @@ -96,20 +122,18 @@ class ButtonStyler extends Style $icon = Prop.maybeMix(icon), $label = Prop.maybeMix(label); - // Component methods - ButtonStyler container(FlexBoxStyler value) { - return merge(ButtonStyler(container: value)); - } - - ButtonStyler icon(IconStyler value) { - return merge(ButtonStyler(icon: value)); - } - - ButtonStyler label(TextStyler value) { - return merge(ButtonStyler(label: value)); - } + const ButtonStyler.create({ + Prop>? container, + Prop>? icon, + Prop>? label, + super.animation, + super.modifier, + super.variants, + }) : $container = container, + $icon = icon, + $label = label; - // Convenience methods + // Convenience methods (beyond the generated container/icon/label setters) ButtonStyler backgroundColor(Color value) { return merge(ButtonStyler(container: FlexBoxStyler().color(value))); } @@ -132,50 +156,54 @@ class ButtonStyler extends Style ); } - ButtonStyler.create({ - Prop>? container, - Prop>? icon, - Prop>? label, - super.animation, - super.modifier, - super.variants, - }) : $container = container, - $icon = icon, - $label = label; - - @override - ButtonStyler merge(covariant ButtonStyler? other) { - return ButtonStyler.create( - container: MixOps.merge($container, other?.$container), - icon: MixOps.merge($icon, other?.$icon), - label: MixOps.merge($label, other?.$label), - animation: MixOps.mergeAnimation($animation, other?.$animation), - modifier: MixOps.mergeModifier($modifier, other?.$modifier), - variants: MixOps.mergeVariants($variants, other?.$variants), - ); + ButtonStyler scale(double value) { + return merge(ButtonStyler(container: FlexBoxStyler().scale(value))); } +} +``` + +The `@MixableField(setterType: FlexBoxStyler)` annotation tells the generator to produce `ButtonStyler container(FlexBoxStyler value)` instead of `ButtonStyler container(StyleSpec value)`. This gives users the fluent Styler API they expect. + +`MixStyler` already provides `WidgetStateVariantMixin` (for `onPressed`, `onHovered`, `onDisabled`, etc.), `VariantStyleMixin`, and `AnimationStyleMixin` — so you get state handling for free. + +The generated `_$ButtonStylerMixin` handles setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props`: + +
+Generated code (`button_style.g.dart`) — key signatures + +```dart +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'button_style.dart'; + +mixin _$ButtonStylerMixin on Style, Diagnosticable { + // Setters generated from @MixableField(setterType:) + ButtonStyler container(FlexBoxStyler value) { ... } + ButtonStyler icon(IconStyler value) { ... } + ButtonStyler label(TextStyler value) { ... } + + // Base methods from MixStyler + ButtonStyler animate(AnimationConfig value) { ... } + ButtonStyler variants(List> value) { ... } + ButtonStyler wrap(WidgetModifierConfig value) { ... } @override - List get props => [$container, $icon, $label]; + ButtonStyler merge(ButtonStyler? other) { ... } @override - StyleSpec resolve(BuildContext context) { - final container = MixOps.resolve(context, $container); - final icon = MixOps.resolve(context, $icon); - final label = MixOps.resolve(context, $label); + StyleSpec resolve(BuildContext context) { ... } - return StyleSpec( - spec: ButtonSpec(container: container, icon: icon, label: label), - ); - } + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { ... } @override - ButtonStyler variant(Variant variant, ButtonStyler style) { - return merge(ButtonStyler(variants: [VariantStyle(variant, style)])); - } + List get props => + [$container, $icon, $label, $animation, $modifier, $variants]; } ``` +
+ ### Define Variants Use an enum to define button variants with their styles: @@ -457,9 +485,18 @@ class ButtonExampleScreen extends StatelessWidget { This tutorial covered: -- **ButtonSpec**: Resolved visual properties with animation support -- **ButtonStyler**: Fluent API with state handling via `WidgetStateVariantMixin` +- **`@MixableSpec`**: Generates `copyWith()`, `lerp()`, `debugFillProperties()`, and `props` — no manual boilerplate +- **`@MixableStyler`**: Generates setters, `merge()`, `resolve()`, `debugFillProperties()`, and `props` — you only write convenience methods +- **`@MixableField(setterType:)`**: Controls the public type of generated setters (e.g., accept `FlexBoxStyler` instead of `StyleSpec`) +- **`MixStyler`**: Base class that provides `WidgetStateVariantMixin`, `VariantStyleMixin`, and `AnimationStyleMixin` for free - **ButtonVariant**: Enum associating variants with styles - **CustomButton**: Widget combining `Pressable` and `StyleBuilder` -This pattern extends to other components: cards, inputs, dialogs, etc. +### What you write vs. what's generated + +| You write | Generator provides | +|---|---| +| Spec fields and constructor | `copyWith()`, `lerp()`, `debugFillProperties()`, `props` | +| Styler fields, constructors, convenience methods | Setters (via `@MixableField`), `merge()`, `resolve()`, `debugFillProperties()`, `props` | + +This pattern extends to other components: cards, inputs, dialogs, etc. Add `mix_annotations` to your `dependencies` and `mix_generator` + `build_runner` to `dev_dependencies`, then run `dart run build_runner build` to generate code.