scrollable positioned list is archived and scroll to index stopped in 2022. Here is why jumping to index 842,013 is hard, and the viewport primitive that…
You have a list of a million rows and you want to jump to row 842,013.
ScrollController.jumpTo takes a pixel offset, not an index. To convert one
to the other you need the total height of the 842,012 rows above your target —
and a lazy list has never built them, so it does not know how tall they are.
That is not an oversight. It is the entire point of a lazy list.
Flutter ships no answer. The two packages that did are both dead:
scrollable_positioned_list — archived by Googlescroll_to_index — last published in 2022Between them they still serve over a million downloads a month, which tells you
how many apps are relying on an unmaintained solution to this.
scrollable_positioned_list built a second complete list, anchored at the
target index, and cross-faded from the old one to the new one.
It works. It is also why jumping felt the way it did: for the duration of the
transition there are two full sets of children alive in the tree, both being
laid out, both painting. On a heavy row widget that is visible — a flash, a
frame drop, sometimes a scrollbar that jumps twice.
And it is why the package was hard to maintain. Two lists that must agree about
scroll position, item extents and which one is currently authoritative is a lot
of state to keep correct across Flutter versions.
Before the real answer, it is worth walking the approaches people try first,
because each fails in a way that teaches something.
Fixed item extent. If every row is exactly 72 pixels, index 842,013 is at
offset 60,624,936 and you are done. SliverFixedExtentList exists precisely for
this and it is genuinely the fastest option. It also stops working the moment one
row wraps to two lines, and "all my rows are identical forever" is a promise most
products break within a month.
Estimate, jump, correct. Guess an average height, jump to the estimate, then
measure where you landed and adjust. This visibly hunts — the list arrives near
the target and then shuffles, sometimes several times. Worse, the correction
happens after paint, so the user sees the wrong content first.
ensureVisible on a GlobalKey. This works beautifully and only for items
that are already built. The element for index 842,013 does not exist, so there is
no context to scroll to. It is the right tool for "scroll to this form field",
and no tool at all for "jump into a list".
Build everything. A million ListTiles is a million elements, a million
render objects, and a frame budget measured in seconds. This is what
shrinkWrap: true quietly does to you in some nestings, which is why it has the
reputation it has.
Each of these is the sensible next idea after the previous one fails. The reason
none of them work is that they are all trying to compute an offset that the
list fundamentally does not know.
Viewport can nominate a centre sliver via its center property. It is
there for chat-style lists that grow upwards, and it does something unusual:
content before the centre sliver lays out at negative scroll offset.
That is the whole solution, once you see it.
Split the list in two at the anchor index:
zero into negative offsets
centre
Now scroll offset zero is the anchor. Not "approximately the anchor once we
have measured our way there" — it is the anchor, by definition, because that is
what nominating a centre sliver means.
Jumping to index 842,013 becomes: rebuild with the anchor set to 842,013, offset
zero. There is nothing to measure, nothing to estimate, and nothing above it to
build. It costs the same as jumping to index 3.
final controller = AnchoredListController();AnchoredList.builder( controller: controller, itemCount: 1000000, itemBuilder: (context, index) => ListTile(title: Text('Item $index')),);controller.jumpToIndex(842013); // same cost as jumping to item 3One viewport, one set of children, no cross-fade.
If nominating a centre sliver and laying content out at negative offsets sounds
like an exotic trick, it is worth knowing it is what Flutter uses for its own
reverse-scrolling lists. CustomScrollView exposes center as public API, and
the framework's chat-style examples rely on exactly this behaviour.
The insight this package contributes is not the primitive. It is noticing that
"the offset origin can be placed anywhere" is the same problem as "jump to an
arbitrary index" — and that once the origin is the target, there is no
arithmetic left to do.
That is why the jump is O(1) rather than merely fast. It is not that the search
got quicker; there is no search.
There is a related problem the same structure solves for free.
You are reading a chat, or a feed, or a log. New items arrive above where you
are looking. In an ordinary ListView every insertion above your position pushes
your content down, because your scroll offset is measured from the top of a list
that just got taller.
With a centre sliver, items above the anchor live at negative offsets. Inserting
one extends the list upwards, into more-negative territory. Your position is
measured from the anchor, and the anchor has not moved — so your content does
not move either.
That is why the package is called anchored_list rather than something about
jumping. Holding your place while things arrive above you is the same mechanism
as jumping instantly, seen from a different angle.
Two features become straightforward that are usually quietly dropped.
Deep linking into a list. A notification says "someone replied to your
comment", and the comment is 4,000 items down. Ordinarily you either load a
separate detail screen or you build a paginated approximation and hope. With an
anchored list you open the list at that comment, and scrolling up from it works
normally because the items above are real list items, lazily built as the user
reaches them.
Restoring scroll position properly. Saving a pixel offset and restoring it
is unreliable, because content above may have changed length between sessions.
Saving an index and restoring the anchor is exact — the user comes back to the
item they were reading, not to a coordinate that used to contain it.
Both of these usually get cut during estimation because "scroll to an arbitrary
position in a long list" sounds like a week of work. It is a controller call.
Honest limitations, because a list package that claims none is hiding some:
something exotic, this changes it.
own total height, so the thumb size is a best guess that improves as more is
measured. Every solution to this problem shares that limit, including the
archived ones.
jumpToIndex is instant bydesign. If you want a visible scroll across a million rows, that is a
different feature and a much slower one — and one nobody actually wants, since
a five-second animated scroll past 800,000 rows is not a better experience than
arriving.
as you scroll down. Jumping to index 842,013 and immediately flinging upward
will build items in that direction — which is correct, but means the work is
proportional to how far you scroll, not zero.
If you are migrating from scrollable_positioned_list, the mental model changes
in one way worth flagging. That package thinks in terms of "scroll such that item
N is at alignment 0.0–1.0 within the viewport". This one thinks in terms of "item
N is the origin". For the common case — put item N at the top — they express
the same thing. For "put item N one third down the viewport", you are adjusting
an offset from the anchor rather than passing an alignment.
anchored_list is on pub.dev — MIT, no
dependencies, all six platforms, 160/160 pub points.
The repository has a demo that jumps around a million-row list while showing the
live child count. That counter is the part worth watching: it stays flat as you
jump from index 3 to 842,013 and back. A flat child count during a jump is the
evidence that nothing above the anchor is being built — and it is the number that
distinguishes this approach from every workaround above, all of which make it
spike.