fix(mix): order variant merges by declared state dependencies - #1016
Merged
Conversation
mergeActiveVariants grouped active variants by class (is WidgetStateVariant)
instead of by the declared semantics (widgetStateDependencies). FocusVisibleVariant
declares {focused} but is not a WidgetStateVariant, so it always merged in the
low-priority group and any widget-state variant silently overrode it on shared
properties regardless of declaration order.
Group by the declaration instead, so priority and state-tracking discovery read
the same source of truth. FocusVisibleVariant, and NotVariant forwarding a
state-driven inner variant, move to the high-priority group and compete by
declaration order.
Also replace the sort with a linear partition. List.sort falls back to an
unstable quicksort at 32 elements, and with 40 equal-priority variants declared
in order it resolved variant 26 as the winner rather than variant 40.
Refs #967
leoafarias
force-pushed
the
fix/variant-priority
branch
from
August 10, 2026 17:23
d7b0eac to
82530d4
Compare
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Related to #967 — this covers that issue's Variant priority section, which prescribes exactly this shape:
The rest of #967 (inherited-style reactivity, controller allocation,
scrolledUnder) is untouched, so the issue stays open.No issue covers the focus-visible half —
FocusVisibleVariantshipped in2.2.0-beta.3, after #967 was filed.Description
Style.mergeActiveVariantsdecided variant priority by class (variant is WidgetStateVariant) instead of by the declared semantics (ContextVariant.widgetStateDependencies).FocusVisibleVariantdeclares{WidgetState.focused}but is not aWidgetStateVariant, so it always landed in the low-priority group. Any widget-state variant —.onHovered(...), aselectedvariant, and so on — silently overrode.onFocusVisible(...)on every property the two shared, regardless of declaration order. Downstream this shows up as a focus ring being replaced by a selected border. The only workaround was to duplicate the.onFocusVisiblebranch inside each competing variant, which is easy to forget and re-breaks silently.Priority now keys off the same declaration that already drives state-tracking discovery, so the two can no longer drift apart. That drift is what produced the bug: one concept was being answered by two independent classifications.
Separately, the two-value
List.sortwas not stable. Dart falls back to an unstable quicksort at 32 elements, so declaration order — which the priority contract depends on — was not preserved in large styles.Changes
mergeActiveVariantsgroups active variants bywidgetStateDependencies.isNotEmptyrather thanis WidgetStateVariant.List.sortwith a linear partition into two groups, preserving declaration order within each by construction.StyleVariationpriority tier that never existed in the code.main.## Unreleased.The
widgetStateDependenciesgetter deliberately stays onContextVariantrather than moving up toVariant.ContextVariantis the only kindStyle.widgetStateswalks, so a dependency declared on aNamedVariantorContextVariantBuilderwould raise its priority but never get tracking installed — an incoherent state that hoisting would make expressible. The reasoning is recorded in a comment at the branch.What the fix changes in practice
Each row below is a case where the old ordering was the defect, not a contract being broken. No API changes; nothing needs migrating.
.variant(selected, red)then.onFocusVisible(blue), both active.onEnabled(blue).onDark(black), enabled + darkThe middle row deserves a look before merging.
onEnabled(...)isnot(disabled), so it forwards a widget-state dependency and moves into the high-priority group along with everything else that reads state. It reads like base styling but no longer behaves like it. Swapping the declaration order, or nesting the ambient branch insideonEnabled(...), restores the old result. It is pinned by a test and called out in its own changelog bullet.The alternative — excluding
NotVariantfrom the promotion — was considered and rejected: it needs avariant is! NotVariantcheck, reintroducing the priority-by-class coupling this change removes, and it contradicts the downstream fingerprint run that validated the current behavior across 603 recipes.Custom
ContextVariantsubclasses that read state inside their closure without overriding the getter keep their existing low-priority placement, which the getter's documentation already calls for.Review Checklist
style.dartand pass with it. The fifth pins declaration order within a group and passes either way by design.### Fixesin the CHANGELOG. TheonEnabled(...)row in the table is the one most likely to surprise, and is called out there in its own bullet.mergeActiveVariantsdartdoc.Additional Information
Validation
cd packages/mix && flutter test— 2891 passeddart analyze packages/mix— no issuescd packages/mix && dcm analyze . --fatal-style --fatal-warnings— no issuesdart format— cleanmelos run test:dart— passed (mix_lint, mix_generator)melos run analyzeandmelos run test:flutterdo not pass in full, for reasons that predate this branch. Both were re-run against a stashed tree atmainand fail identically:schema:inventoryfails on type errors inmix_protocol/tool/inventory_check.dart, anddart analyzefails in 8 packages.mix_winds/mix_winds_exampleresolvemixfrom.pub-cache/hosted/pub.dev/mix-2.1.0instead of the workspace, andmix_windscannot resolve dependencies at all locally.Root cause, in one line:
widgetStateDependencieswas introduced in2.2.0-beta.3as the declaration of "this variant reads widget state," but the priority sort was never migrated onto it and kept its original class check.