conceptadev / conceptadev/mix

Fix style resolution correctness and deterministic variant ordering

Open
#967 0 comments 0 reactions 0 assignees View on GitHub
bug mix
Dominant language
Dart
Stars
800
Forks
49
Avg merge
3h 39m
Merged PRs (30d)
22

Description

- 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
`StyleBuilder`s 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:

```text
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` 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:

```bash
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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/mix/lib/src/core/style.dart, style_builder.dart, and the two provider files, tracing inherited lookup, state dependencies, controller ownership, and variant merging. Run the focused tests in test/src/core/style_builder_test.dart, style_get_all_variants_test.dart, style_nested_variants_test.dart, and specs/pressable/widget_state_controller_test.dart. Done means the listed regression cases pass, including stable variant order, aspect-specific updates, and scrolledUnder behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart, flutter
Domain
design, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.