- Branch:
fix/style-resolution-correctness
- Base:
origin/main
- Package:
packages/mix
- Priority: P0
- Change type: Correctness and hot-path cleanup
Summary
Make inherited styles reactive, prevent unrelated widget-state changes from
re-resolving state-independent styles, remove dormant state-management work,
and make variant precedence deterministic without relying on an unstable sort.
This should preserve the current Style → StyleSpec → Widget architecture. It
is a focused correction to dependency ownership and ordering, not a pipeline
redesign.
Why this matters
The current implementation has inverse dependency problems:
Style.maybeOf uses getInheritedWidgetOfExactType, so a StyleBuilder
reads an inherited style without subscribing to StyleProvider changes. An
otherwise stable child can keep stale resolved styling after its provider
changes.
- Every
StyleBuilder calls WidgetStateProvider.of(context) with no aspect,
even when its style has no widget-state variants. Hover, press, or other
state changes can therefore re-resolve unrelated descendant styles.
- Every
StyleBuilder eagerly creates a WidgetStatesController, including
styles that never install automatic interaction tracking.
mergeActiveVariants calls List.sort with only two priority values. Dart
does not guarantee stable ordering for items that compare equal, even though
Mix contract tests expect stored order to decide the winner within each
priority bucket.
WidgetState.scrolledUnder is accepted by ContextVariant.widgetState and
the controller extension, but WidgetStateProvider.hasStateOf always returns
false for it.
Evidence and source anchors
packages/mix/lib/src/core/style.dart
Style.maybeOf
Style.widgetStates
Style.mergeActiveVariants
packages/mix/lib/src/core/style_builder.dart
_initController
_buildStyle
_StyleBuilderState.build
packages/mix/lib/src/core/providers/style_provider.dart
packages/mix/lib/src/core/providers/widget_state_provider.dart
packages/mix/test/src/core/style_builder_test.dart
packages/mix/test/src/core/style_get_all_variants_test.dart
packages/mix/test/src/specs/pressable/widget_state_controller_test.dart
A focused probe against the current checkout confirmed both runtime effects:
an identical child retained the old inherited color after StyleProvider
changed, while a style with no state variants re-resolved when an ancestor
hover state changed.
Required behavior
Inherited style lookup
Style.of and Style.maybeOf must register an inherited-widget dependency,
consistent with normal Flutter of APIs.
- Updating a same-spec
StyleProvider must re-resolve dependent
StyleBuilders even when the provider's child widget instance is identical.
- If a non-listening lookup is genuinely needed internally, expose it with an
explicit name such as Style.peek; do not make the normal of path
non-listening.
Widget-state dependencies and ownership
- A style with no widget-state variants must not depend on
WidgetStateProvider.
- Checking whether a state provider already exists must not create an
all-aspects dependency.
- Active
WidgetStateVariant predicates remain responsible for subscribing to
only the state aspects they read.
- Create an internal
WidgetStatesController only when automatic interaction
tracking is actually installed.
- Remove
TickerProviderStateMixin from StyleBuilder; it does not create a
ticker.
- Automatic pointer tracking should be installed for states it can produce
(hovered and pressed). Focus, disabled, selected, dragged, error, and
scrolled-under states require an external owner or an existing provider.
scrolledUnder must be stored, queried, and included in dependent-update
calculations like the other controller-owned states.
Variant priority
Replace sorting with a stable linear partition:
active non-widget-state variants, in stored order
then active widget-state variants, in stored order
Do not rely on List.sort to preserve equal-priority order. Preserve the
existing two-bucket priority contract and recursive nested-variant behavior.
Implementation guidance
- Prefer a boolean/capability query over allocating a
Set<WidgetState> merely
to decide whether automatic tracking is needed.
- Use an identity-cycle guard if direct static nested variant branches are
scanned recursively.
- Keep
ContextVariantBuilder output conservative: it cannot be inspected
before it executes. Callers needing controller-owned states from a dynamic
builder must provide a controller/provider.
- Avoid changing variant merge-key semantics here. Namespaced keys and
collision diagnostics should be a separate compatibility discussion.
Test plan
Add or update focused tests for:
- A changing
StyleProvider with an identical child updates the resolved
spec.
- A state-independent
StyleBuilder does not rebuild when hovered, pressed,
or focused state changes above it.
- A pressed-only style depends only on pressed state, not hover.
- An inherited hover variant still installs/reuses the correct state scope.
- No internal controller is created for a state-free style.
- Focus-only/controller-owned variants do not install a pointer detector that
cannot activate them.
scrolledUnder activates through an external WidgetStatesController and
notifies only dependent consumers.
- Large and interleaved variant lists preserve relative order within both
buckets.
- Nested variants and
StyleVariation behavior remain unchanged.
Run at minimum:
cd packages/mix
flutter test test/src/core/style_builder_test.dart
flutter test test/src/core/style_get_all_variants_test.dart
flutter test test/src/core/style_nested_variants_test.dart
flutter test test/src/specs/pressable/widget_state_controller_test.dart
Before handoff, run the repository-required generation, CI, and analysis checks
if generated or public API output changed.
Acceptance criteria
Compatibility and risks
- Correctly listening to
StyleProvider can increase rebuilds where callers
previously received stale data. That is intended, but profile the affected
tests and avoid broad dependencies.
- Removing accidental state dependencies should reduce work without changing
visible output.
- Stable partitioning formalizes the order current tests already claim; it
should not intentionally reorder existing variants.
- Do not combine public named-variant APIs, caching, tracing, or variant-key
redesign here.
fix/style-resolution-correctnessorigin/mainpackages/mixSummary
Make inherited styles reactive, prevent unrelated widget-state changes from
re-resolving state-independent styles, remove dormant state-management work,
and make variant precedence deterministic without relying on an unstable sort.
This should preserve the current Style → StyleSpec → Widget architecture. It
is a focused correction to dependency ownership and ordering, not a pipeline
redesign.
Why this matters
The current implementation has inverse dependency problems:
Style.maybeOfusesgetInheritedWidgetOfExactType, so aStyleBuilderreads an inherited style without subscribing to
StyleProviderchanges. Anotherwise stable child can keep stale resolved styling after its provider
changes.
StyleBuildercallsWidgetStateProvider.of(context)with no aspect,even when its style has no widget-state variants. Hover, press, or other
state changes can therefore re-resolve unrelated descendant styles.
StyleBuildereagerly creates aWidgetStatesController, includingstyles that never install automatic interaction tracking.
mergeActiveVariantscallsList.sortwith only two priority values. Dartdoes not guarantee stable ordering for items that compare equal, even though
Mix contract tests expect stored order to decide the winner within each
priority bucket.
WidgetState.scrolledUnderis accepted byContextVariant.widgetStateandthe controller extension, but
WidgetStateProvider.hasStateOfalways returnsfalse for it.
Evidence and source anchors
packages/mix/lib/src/core/style.dartStyle.maybeOfStyle.widgetStatesStyle.mergeActiveVariantspackages/mix/lib/src/core/style_builder.dart_initController_buildStyle_StyleBuilderState.buildpackages/mix/lib/src/core/providers/style_provider.dartpackages/mix/lib/src/core/providers/widget_state_provider.dartpackages/mix/test/src/core/style_builder_test.dartpackages/mix/test/src/core/style_get_all_variants_test.dartpackages/mix/test/src/specs/pressable/widget_state_controller_test.dartA focused probe against the current checkout confirmed both runtime effects:
an identical child retained the old inherited color after
StyleProviderchanged, while a style with no state variants re-resolved when an ancestor
hover state changed.
Required behavior
Inherited style lookup
Style.ofandStyle.maybeOfmust register an inherited-widget dependency,consistent with normal Flutter
ofAPIs.StyleProvidermust re-resolve dependentStyleBuilders even when the provider's child widget instance is identical.explicit name such as
Style.peek; do not make the normalofpathnon-listening.
Widget-state dependencies and ownership
WidgetStateProvider.all-aspects dependency.
WidgetStateVariantpredicates remain responsible for subscribing toonly the state aspects they read.
WidgetStatesControlleronly when automatic interactiontracking is actually installed.
TickerProviderStateMixinfromStyleBuilder; it does not create aticker.
(
hoveredandpressed). Focus, disabled, selected, dragged, error, andscrolled-under states require an external owner or an existing provider.
scrolledUndermust be stored, queried, and included in dependent-updatecalculations like the other controller-owned states.
Variant priority
Replace sorting with a stable linear partition:
Do not rely on
List.sortto preserve equal-priority order. Preserve theexisting two-bucket priority contract and recursive nested-variant behavior.
Implementation guidance
Set<WidgetState>merelyto decide whether automatic tracking is needed.
scanned recursively.
ContextVariantBuilderoutput conservative: it cannot be inspectedbefore it executes. Callers needing controller-owned states from a dynamic
builder must provide a controller/provider.
collision diagnostics should be a separate compatibility discussion.
Test plan
Add or update focused tests for:
StyleProviderwith an identical child updates the resolvedspec.
StyleBuilderdoes not rebuild when hovered, pressed,or focused state changes above it.
cannot activate them.
scrolledUnderactivates through an externalWidgetStatesControllerandnotifies only dependent consumers.
buckets.
StyleVariationbehavior remain unchanged.Run at minimum:
Before handoff, run the repository-required generation, CI, and analysis checks
if generated or public API output changed.
Acceptance criteria
scrolledUnderworks through the public state variant/controller path.origin/main.Compatibility and risks
StyleProvidercan increase rebuilds where callerspreviously received stale data. That is intended, but profile the affected
tests and avoid broad dependencies.
visible output.
should not intentionally reorder existing variants.
redesign here.