From ScrollSpy Assistant
Migrates Flutter widgets from visibility_detector to scroll_spy for scrollable feeds, lists, grids, and pagers. Adds focus region, stable primary item, per-item metrics, and rebuild isolation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/scroll-spy:convert-visibility-detector-to-scroll-spyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
visibility_detector reports per-widget paint-based visibility anywhere in the
visibility_detector reports per-widget paint-based visibility anywhere in the
tree. scroll_spy is scoped to one scrollable and adds what VD lacks: a focus
region, a single stable primary with anti-flicker rules, and rich per-item
metrics with rebuild isolation. Convert when the detectors live inside a
scrollable (feeds, lists, grids, pagers). Do NOT convert detectors that are
not inside a scrollable (dialogs, overlays, static layouts) or that rely on
paint-level occlusion (Opacity(opacity: 0), Offstage) - scroll_spy
models scroll geometry, not painting; leave those on visibility_detector and
say so.
| visibility_detector | scroll_spy replacement |
|---|---|
VisibilityDetector(key:, onVisibilityChanged:, child:) per item | Wrap the scrollable once (ScrollSpyListView.builder, ScrollSpyGridView.builder, ScrollSpyCustomScrollView, ScrollSpyPageView.builder, or raw ScrollSpyScope<T>), then per item ScrollSpyItemLite<T>(id:, ...) or listener widgets |
Key('card-$id') identity | the typed id: on the item (stable, unique per scope); keep any widget key separately |
VisibilityInfo.visibleFraction | ScrollSpyItemFocus.visibleFraction (0..1, same meaning) via snapshot.items, itemFocusOf(id), or tryGetItemFocus(id) |
onVisibilityChanged enter/exit (fraction > 0 / == 0) | per item: ScrollSpyItemVisibleListener(onChanged: (was, is) ...); or one screen-level controller.snapshot listener diffing visibleIds (better when you also need fraction thresholds; absence from snapshot.items while unmounted = fraction 0) |
threshold logic (fraction >= 0.6) | scan snapshot.items.values in one snapshot listener with your own seen-set (dedup) |
VisibilityDetectorController.instance.updateInterval (global throttle) | updatePolicy: on the scope/wrapper: perFrame (default; per-scroll precision), ScrollSpyUpdatePolicy.hybrid(ballisticInterval: ...) (throttled flings, closest analog), or onScrollEnd(debounce: ...) (events only at rest). Non-const; store in a field |
VisibilityDetectorController.instance.notifyNow() / forget() | no equivalents needed: computes are scroll-driven; notifiers auto-evict |
final 0.0 callback on unmount | items leaving tracking flip their boolean listenables to false; a snapshot diff also observes disposal. Flush any pending exits in your State.dispose |
What has no VD equivalent (the usual reason to convert): isPrimary +
ScrollSpyStability (stable winner for autoplay/featuring via
ScrollSpyPrimaryListener), focusProgress/distanceToAnchorPx (effects),
regions/policies, and boolean-only rebuild isolation.
VD (paint-based) reports visibleFraction: 0 when an opaque route covers
the screen, so VD code often gets exit events "for free" on navigation.
scroll_spy freezes instead: no scrolling means no computes, so
primaryId, focusedIds, and per-item state keep their last values while
covered (verified behavior). If the analytics contract requires exits on
navigation (or must suppress events while covered):
TickerMode.getNotifier(context) from inside the
route; it flips to disabled exactly when an opaque push transition
completes and back on pop. Flush exits and set a _covered guard there.RouteObserver + RouteAware
(didPushNext = flush exits and pause; didPopNext = resync).On pop, re-entry events for still-visible items only fire if you emit them yourself during resync; scroll_spy will not re-notify unchanged state.
scroll_spy to pubspec (keep visibility_detector for now).ScrollSpyScope<T>), with a ScrollSpyController<T> as a
State field, disposed in dispose. Pick T = your id type.VisibilityDetector wrapper as its logic
moves; keep the child subtree in the item's child: slot.ScrollSpyPrimaryListener.flutter analyze; grep for visibility_detector returns nothing.Report the mapping applied per removed API, the covered-route strategy chosen, and any detectors you deliberately left on visibility_detector.
npx claudepluginhub omar-hanafy/scroll_spy --plugin scroll-spyGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.
Dispatches multiple subagents concurrently for independent tasks without shared state. Use when facing 2+ unrelated failures or subsystems that can be investigated in parallel.