indexed_scroll_controller 0.3.2
indexed_scroll_controller: ^0.3.2 copied to clipboard
Scroll a Flutter ListView.builder to any item by index, including items not yet built or measured. Vertical and horizontal.
indexed_scroll_controller #
IndexedScrollController scrolls a single Flutter ListView.builder to a logical item index, including items that have not yet been laid out.
Installation #
flutter pub add indexed_scroll_controller
import 'package:indexed_scroll_controller/indexed_scroll_controller.dart';
Usage #
Create one controller and wrap every list row with watch. The index must be the itemBuilder index: consecutive, zero-based, and in physical list order.
final controller = IndexedScrollController(
scrollDuration: const Duration(milliseconds: 250),
);
ListView.builder(
controller: controller,
itemCount: items.length,
itemBuilder: (context, index) => controller.watch(
index: index,
child: ListTile(title: Text(items[index].title)),
),
);
await controller.scrollTo(42, alignment: 0);
The controller supports vertical and horizontal ListView.builders, fractional indices, reversed lists, leading padding, and indexed lists in a CustomScrollView with finite preceding slivers. It supports one attached ScrollPosition.
Data changes #
Call invalidateMeasurements() after inserting, deleting, reordering, or resizing rows, and after changing scroll axis. An in-flight operation is cancelled with ScrollCancelReason.dataInvalidated.
For automatic invalidation, provide both callbacks. A fingerprint must identify the row and every data value that can affect its size on the scroll axis. Layout changes outside row data, such as available width, text scale, or theme, still require manual invalidation.
final controller = IndexedScrollController(
scrollDuration: const Duration(milliseconds: 250),
itemCount: () => items.length,
contentFingerprint: (index) => (items[index].id, items[index].expanded),
);
When data changes, the controller compares the fingerprint of each row to detect which measurements are invalid. The fingerprint does not estimate geometry; it is only a tag for invalidation. After a detected change, the controller re-measures the affected row and any preceding rows whose layout may have shifted.
Separators #
ListView.builder rows that include their own separator (for example a Column with the item and a trailing divider) need nothing extra: just watch the whole row as usual.
For ListView.separated, wrap every item with watch and every separator with separator(index: index, ...), keyed by the same logical item index the separator follows:
ListView.separated(
controller: controller,
itemCount: items.length,
itemBuilder: (context, index) => controller.watch(
index: index,
child: ListTile(title: Text(items[index].title)),
),
separatorBuilder: (context, index) => controller.separator(
index: index,
child: const Divider(),
),
);
Every separator must be wrapped this way, even a zero-size one (separator(index: index, child: const SizedBox())) — an unwrapped separator throws StateError rather than silently landing at the wrong offset, because watch's logical indices would otherwise skip every separator's physical slot.
By default, scrollTo's alignment measures against the whole row (item plus its trailing separator) — ScrollAlignmentTarget.row. Pass alignmentTarget: ScrollAlignmentTarget.item to align the item alone, ignoring its separator:
await controller.scrollTo(10, alignment: 1, alignmentTarget: ScrollAlignmentTarget.item);
ScrollAlignmentTarget.item requires that item's separator to have been registered via separator() — it is meaningless without one.
Gestures #
Wrap the scrollable with IndexedScrollGestureDetector when a user drag must cancel an active scrollTo operation:
IndexedScrollGestureDetector(
controller: controller,
child: list,
)
scrollTo throws RangeError for an invalid or unreachable index, ArgumentError for invalid arguments, StateError for an unsupported list state, and ScrollCancelledException when cancelled.
Example #
See the example application for vertical, horizontal, padding, reverse, ListView.separated, and fingerprint-invalidation scenarios.