Viewport focus for Flutter
One visible item owns playback.
Detect every visible item, choose one stable primary winner, and keep Flutter video feeds from fighting over the player.
01 / THE JOB
Visibility is a measurement. Playback needs a decision.
Two cards can both be 60% visible. A visibility callback tells you that fact, but it cannot tell your feed which controller should play. That missing decision is where flicker, double audio, and controller churn begin.
02 / ONE SIGNAL
A stable primary ID can run the whole feed.
Play the winner. Pause the previous one. Warm the neighbors. Record impressions only after meaningful attention.
- 01ScrollListView, GridView, PageView, or your own scrollable
- 02ComputeVisible fraction, region overlap, anchor distance
- 03SelectExactly one stable primary item
- 04ActAutoplay, analytics, prefetch, reading position
03 / CHOOSE BY JOB
Use the primitive that answers your actual question.
These packages are not interchangeable. A visibility percentage is the right primitive for many screens. scroll_spy adds the selection and stability layer needed when one item must own attention.
| Capability | scroll_spy | visibility_detector | inview_notifier_list |
|---|---|---|---|
| Core question | Which item should own attention? | How much of this widget is visible? | Is this list child in the configured range? |
| One winner | Built in | Application code | Application code |
| Anti-flicker rules | Hysteresis + minimum hold | Application code | Application code |
| Per-item geometry | Visibility + focus metrics | Detailed visibility info | Threshold-oriented notification |
| Best fit | Autoplay, attention, reading position | General widget visibility | Simple indexed-list in-view UI |
Comparison describes package responsibilities, not a universal speed ranking. Read the full comparison or inspect the reproducible benchmark.
04 / IMPLEMENT
Small surface, explicit policy
Your UI stays yours.
Wrap the scrollable, register stable item IDs, then react to primary changes. The video engine remains outside the package and under your control.
Read the API documentationBuild a one-player video feed
final spy = ScrollSpyController<int>();
ScrollSpyListView<int>.builder(
controller: spy,
region: ScrollSpyRegion.zone(
anchor: const ScrollSpyAnchor.fraction(0.5),
extentPx: 200,
),
policy: const ScrollSpyPolicy.closestToAnchor(),
stability: const ScrollSpyStability(
hysteresisPx: 24,
minPrimaryDuration: Duration(milliseconds: 150),
),
itemBuilder: (context, index) => ScrollSpyItem<int>(
id: index,
builder: (context, focus, child) => VideoCard(
isPlaying: focus.isPrimary,
),
),
);
05 / HOT PATH
Built for the part of the frame budget you cannot get back.
During steady linear scrolling, item positions come from a cached scroll model with O(1) arithmetic per item. The hot path avoids render-tree walks, matrix math, and allocations. Exotic transforms fall back to a general path.
These are implementation invariants covered by the package test suite. They are not claims about every app, device, or competing package.
Stop coordinating players by accident
Give attention one owner.
Start with the live feed, inspect the overlay, then copy the pattern into your app.