Technical comparison
Three visibility jobs. One feed decision.
scroll_spy, visibility_detector, and inview_notifier_list solve related problems, but they do not promise the same result. The useful comparison starts with the question your Flutter screen asks.
Short answer
How much of this widget is painted?
Start with visibility_detector. It is a general, paint-aware visibility primitive.
Is this indexed list child inside my range?
Consider inview_notifier_list. Its list wrapper and item builder target that job directly.
Which visible item should own attention?
Use scroll_spy when the screen needs one policy-selected, stable primary item for autoplay, navigation, analytics, or prefetching.
Responsibility matrix
| Responsibility | scroll_spy | visibility_detector | inview_notifier_list |
|---|---|---|---|
| Visible state | Yes | Yes, paint-aware detail | Yes, configured list range |
| Focused region | Line, zone, or custom | Application code | Range condition |
| Single winner | Five policies or custom | Application code | Application code |
| Winner stability | Hysteresis + hold time | Application code | Application code |
| Global result | Primary, focused, visible IDs | Per-widget callbacks | Item builder state |
| Debug UI | Viewport overlay | Application code | Application code |
A narrow reproducible benchmark
The repository includes a widget-test harness for the common denominator: whether fixed-extent ListView items intersect the viewport. It uses 3,000 items, a 400 × 600 viewport, forty 25-pixel jumps, aligned zero delivery intervals, and three actual mounted-item counts. scroll_spy's focus and primary selection are disabled in this scenario.
The Flutter 3.44.4 reference run produced the following deterministic API-delivery counts:
| Implementation | Mounted | Reactive builds | Callbacks | Repeated deliveries |
|---|---|---|---|---|
| scroll_spy | 11 / 51 / 201 | 40 / 40 / 40 | 20 | 10 |
| inview_notifier_list | 11 / 51 / 201 | 220 / 1,020 / 4,020 | not exposed | 190 / 990 / 3,990 |
| visibility_detector | 11 / 51 / 201 | 30 / 30 / 30 | 80 | 60 |
All three integrations reported the same 20 normalized visibility transitions and the same final visible sets. The counts show delivery behavior in this one harness. A callback and a builder invocation are not equivalent CPU units, so these numbers are not relative speedups.
Debug wall time around `jumpTo` and two test pumps is printed only to catch major harness regressions. It is not device frame time, raster time, GPU time, release throughput, or FPS.
What to benchmark in your app
- Use the same device, Flutter version, build mode, list, cache extent, and scroll gesture.
- Separate detection work from video decoding, network fetches, image decoding, and application rebuilds.
- Measure profile-mode frame timing with DevTools or a FrameTiming harness.
- Test the semantics you need. Do not disable primary selection if your product depends on it.
- Record callback, rebuild, and allocation behavior separately.
Need one calm playback owner?
Run the real-video example and inspect every transition before choosing.