Skip to content

Generate named-only @MixWidget constructors from enum-backed NamedVariants #998

Description

@leoafarias

Use case

@MixWidget currently derives named constructors only when a selected factory parameter is a named, non-nullable enum parameter literally named variant. It also always preserves the unnamed constructor and exposes variant as a public generated field.

For example:

@MixWidget(
  name: 'FortalButton',
  target: RemixButton.new,
  factoryParameters: .only({'variant', 'size'}),
)
ButtonStyler fortalButtonStyler({
  FortalButtonVariant variant = .solid,
  FortalButtonSize size = .size2,
  bool highContrast = false,
});

This produces both FortalButton(variant: ...) and constructors such as FortalButton.solid(...).

That API does not fit design-system recipes where:

  • visual variants are Mix NamedVariants;
  • variants should be selected only through named widget constructors;
  • ordinary recipe controls such as size should remain generated parameters;
  • the low-level target widget should receive only its own parameters and the computed style.

The desired public API is:

FortalButton.solid(size: .size2, label: 'Save');
FortalButton.soft(size: .size3, label: 'Save');

There should be no unnamed FortalButton(...), public variant: argument, or public variant field.

This is needed by Remix PR #84, which uses the direct-target support introduced in Mix PR #997.

Proposal

Support an opt-in convention that discovers an enum-backed NamedVariant domain independently from factory parameters.

Proposed usage sketch:

enum FortalButtonVariant with EnumVariant {
  solid,
  soft,
  surface,
  outline,
  ghost,
}

@MixWidget(target: RemixButton.new)
ButtonStyler fortalButtonStyler({
  FortalButtonSize size = .size2,
}) {
  return _fortalButtonBaseStyle(size).variantsFromEnum(
    FortalButtonVariant.values,
    (variant) => switch (variant) {
      .solid => _fortalButtonSolidStyle(size),
      .soft => _fortalButtonSoftStyle(size),
      .surface => _fortalButtonSurfaceStyle(size),
      .outline => _fortalButtonOutlineStyle(size),
      .ghost => _fortalButtonGhostStyle(size),
    },
  );
}

variantsFromEnum is an API sketch for registering every enum value as a VariantStyle; an equivalent exhaustive API would also satisfy the use case.

Discovery would be deterministic:

  1. Infer FortalButton from the annotated fortalButtonStyler declaration by stripping the conventional Styler suffix and converting the remaining identifier to PascalCase. No name: override is required.
  2. Look in the same library for an enum named FortalButtonVariant.
  3. Require that enum to implement EnumVariant.
  4. Generate one named constructor per accessible enum value.
  5. Generate no unnamed constructor for that widget.
  6. Store the selected enum value privately and apply it to the returned styler before passing the style to target.

Conceptual generated output:

class FortalButton extends StatelessWidget {
  const FortalButton.solid({
    super.key,
    this.size = .size2,
    required this.label,
  }) : _variant = FortalButtonVariant.solid;

  const FortalButton.soft({
    super.key,
    this.size = .size2,
    required this.label,
  }) : _variant = FortalButtonVariant.soft;

  final FortalButtonVariant _variant;
  final FortalButtonSize size;
  final String label;

  @override
  Widget build(BuildContext context) {
    return RemixButton(
      style: fortalButtonStyler(size: size).applyVariant(_variant),
      label: label,
    );
  }
}

The constructor-discovery scope should be deliberately narrow:

  • enum values implementing EnumVariant: generate constructors;
  • arbitrary NamedVariant('name') instances: remain manually applicable and do not generate constructors;
  • ContextVariant, widget-state variants, and builder variants: never generate constructors.

The existing factory-parameter behavior should remain unchanged when no matching EnumVariant enum is present.

Missing or invalid mappings should fail clearly rather than silently applying the base style. An exhaustive enum registration helper plus a strict singular applyVariant operation would provide compile-time switch coverage and runtime validation.

Acceptance criteria

  • @MixWidget(target: RemixButton.new) on fortalButtonStyler deterministically generates FortalButton; callers do not have to provide name:.
  • A target-backed function with only a size factory parameter can generate named-only constructors from a same-library <WidgetName>Variant with EnumVariant enum.
  • The generated wrapper has no unnamed constructor, public variant: parameter, or public variant field.
  • Every named constructor forwards the other factory and target parameters, including defaults and generic types.
  • The selected enum value is applied as a Mix NamedVariant before the style reaches the target widget.
  • Arbitrary named variants and all context/state variants do not generate constructors.
  • Missing variant-style mappings produce a clear failure.
  • Existing @MixWidget generation remains unchanged when the convention does not apply.
  • Generator unit/integration tests and annotation/generator documentation cover the new behavior.

Current implementation references:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions