Skip to content

Fix style resolution correctness and deterministic variant ordering #967

Description

@leoafarias
  • 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:

  1. 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.
  2. 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.
  3. Every StyleBuilder eagerly creates a WidgetStatesController, including
    styles that never install automatic interaction tracking.
  4. 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.
  5. 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:

  1. A changing StyleProvider with an identical child updates the resolved
    spec.
  2. A state-independent StyleBuilder does not rebuild when hovered, pressed,
    or focused state changes above it.
  3. A pressed-only style depends only on pressed state, not hover.
  4. An inherited hover variant still installs/reuses the correct state scope.
  5. No internal controller is created for a state-free style.
  6. Focus-only/controller-owned variants do not install a pointer detector that
    cannot activate them.
  7. scrolledUnder activates through an external WidgetStatesController and
    notifies only dependent consumers.
  8. Large and interleaved variant lists preserve relative order within both
    buckets.
  9. 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

  • Inherited style updates cannot remain stale solely because child identity is stable.
  • State-independent styles do not re-resolve for widget-state changes.
  • State-dependent styles retain aspect-specific rebuild behavior.
  • The default state-free path owns no unused controller or ticker provider.
  • Variant order is deterministic by construction, not by sort implementation.
  • scrolledUnder works through the public state variant/controller path.
  • Existing targeted tests pass and new regression tests fail on origin/main.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmix

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions