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.

160/160 pub points 6 Flutter platforms 0 third-party runtime deps

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.

MEASUREWhat is on screen?
FOCUSWhat is inside the attention zone?
SELECTWhich single item wins?
STABILIZEShould it keep winning?

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.

  1. 01ScrollListView, GridView, PageView, or your own scrollable
  2. 02ComputeVisible fraction, region overlap, anchor distance
  3. 03SelectExactly one stable primary item
  4. 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 documentation
Build a one-player video feed
feed.dart
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.

O(1)position math per tracked item
0steady-scroll hot-path allocations
lazysnapshots only when observed

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.