Skip to content

Instantly share code, notes, and snippets.

@gasaichandesu
Created November 21, 2025 12:10
Show Gist options
  • Select an option

  • Save gasaichandesu/83dd7c3d046471725dc0531bc2252cab to your computer and use it in GitHub Desktop.

Select an option

Save gasaichandesu/83dd7c3d046471725dc0531bc2252cab to your computer and use it in GitHub Desktop.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
typedef VisibilityReportingSliverItemBuilder<T> =
Widget Function(BuildContext context, int index, T item);
/// A sliver-compatible list that reports which items are currently visible
/// in the viewport.
///
/// `VisibilityReportingSliverList<T>` behaves like a normal [SliverList], but
/// additionally tracks which list items intersect with the scroll viewport
/// and notifies the parent whenever the visible subset changes.
///
/// This is useful for:
/// • marking items as "seen" when they enter the viewport
/// • implementing infinite scroll prefetching
/// • analytics / viewability tracking
///
/// The list does **not** create its own scrollable. It must be placed inside
/// a [CustomScrollView] (or another sliver-based scrollable) and relies on
/// the surrounding sliver viewport.
///
/// ### How visibility is computed
/// Each built item is wrapped in an internal [GlobalKey]. On a scroll,
/// the widget:
/// 1. Gets the sliver viewport (using [Scrollable.of]).
/// 2. Computes the vertical bounds of the viewport in global coordinates.
/// 3. For each item, compares its global bounds with the viewport’s bounds.
/// Any item whose vertical range overlaps the viewport is considered visible.
///
/// When the set of visible items changes, [onVisibleItemsChanged] is called
/// with the list of currently visible items.
///
/// ### Usage
///
/// Wrap your `CustomScrollView` with a [NotificationListener] and call
/// `handleScroll()` on this widget’s state when a scroll notification
/// arrives:
///
/// ```dart
/// final sliverKey = GlobalKey<VisibilityReportingSliverListState<NotificationEntity>>();
///
/// NotificationListener<ScrollNotification>(
/// onNotification: (_) {
/// WidgetsBinding.instance.addPostFrameCallback((_) {
/// sliverKey.currentState?.handleScroll();
/// });
/// return false;
/// },
/// child: CustomScrollView(
/// slivers: [
/// VisibilityReportingSliverList<NotificationEntity>(
/// key: sliverKey,
/// items: notifications,
/// itemBuilder: (context, index, item) {
/// return NotificationTile(notification: item);
/// },
/// onVisibleItemsChanged: (visible) {
/// context
/// .read<NotificationsBloc>()
/// .add(VisibleNotificationsChanged(visible));
/// },
/// ),
/// ],
/// ),
/// );
/// ```
///
/// Note: [onVisibleItemsChanged] may be called frequently during scrolling.
/// If you do heavy work there (e.g. network calls), consider debouncing or
/// batching it at a higher level.
class VisibilityReportingSliverList<T> extends StatefulWidget {
const VisibilityReportingSliverList({
super.key,
required this.items,
required this.itemBuilder,
required this.onVisibleItemsChanged,
this.keyBy,
});
/// Items to be rendered in the sliver list.
final List<T> items;
/// Builder for each item in the list.
///
/// The returned widget will be wrapped in an internal [GlobalKey] used for
/// visibility calculations, so you don't need to worry about keys here
/// unless you have additional requirements.
final VisibilityReportingSliverItemBuilder<T> itemBuilder;
/// Called whenever the set of visible items in the viewport changes.
final void Function(List<T> visibleItems) onVisibleItemsChanged;
/// Unique identifier factory. It is used to assign [_VisibilityReportingSliverListItemGlobalKey] for an item.
/// Otherwise [GlobalKey] is used which may affect performance on larger lists
final String Function(T item)? keyBy;
@override
VisibilityReportingSliverListState<T> createState() =>
VisibilityReportingSliverListState<T>();
}
class VisibilityReportingSliverListState<T>
extends State<VisibilityReportingSliverList<T>> {
late List<GlobalKey> _itemKeys;
Set<T> _visibleItems = {};
@override
void initState() {
super.initState();
_itemKeys = _regenerateKeys();
// Initial measurement after first layout
WidgetsBinding.instance.addPostFrameCallback((_) => _onScroll());
}
@override
void didUpdateWidget(covariant VisibilityReportingSliverList<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (!listEquals(oldWidget.items, widget.items)) {
_itemKeys = _regenerateKeys();
WidgetsBinding.instance.addPostFrameCallback((_) => _onScroll());
}
}
List<GlobalKey> _regenerateKeys() {
return widget.items.map((i) {
final identifier = widget.keyBy?.call(i);
return identifier == null
? GlobalKey()
: _VisibilityReportingSliverListItemGlobalKey(identifier);
}).toList();
}
@override
Widget build(BuildContext context) {
return SliverList.builder(
itemCount: widget.items.length,
itemBuilder: (context, index) {
final item = widget.items[index];
final child = widget.itemBuilder(context, index, item);
return KeyedSubtree(key: _itemKeys[index], child: child);
},
);
}
/// Triggers a recalculation of which items are visible.
///
/// Call this from a [ScrollNotification] listener wrapped around the
/// [CustomScrollView] that contains this sliver.
void handleScroll() {
_onScroll();
}
void _onScroll() {
final scrollable = Scrollable.of(context);
final viewportBox = scrollable.context.findRenderObject() as RenderBox?;
if (viewportBox == null) return;
final viewportTop = viewportBox.localToGlobal(Offset.zero).dy;
final viewportBottom = viewportTop + viewportBox.size.height;
final visibleItems = <T>[];
for (var i = 0; i < _itemKeys.length; i++) {
final ctx = _itemKeys[i].currentContext;
if (ctx == null) continue;
final box = ctx.findRenderObject() as RenderBox?;
if (box == null || !box.attached) continue;
final top = box.localToGlobal(Offset.zero).dy;
final bottom = top + box.size.height;
final overlap = bottom > viewportTop && top < viewportBottom;
if (overlap) {
visibleItems.add(widget.items[i]);
}
}
final visibleItemsSet = visibleItems.toSet();
if (!setEquals(_visibleItems, visibleItemsSet)) {
_visibleItems = visibleItemsSet;
widget.onVisibleItemsChanged(visibleItems);
}
}
}
final class _VisibilityReportingSliverListItemGlobalKey
extends GlobalObjectKey {
const _VisibilityReportingSliverListItemGlobalKey(super.value);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment