devShakib

Scrolling to an Index in a Flutter Lazy List, Without Building Everything Above It

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:

Between them they still serve over a million downloads a month, which tells you

how many apps are relying on an unmaintained solution to this.

How the archived approach worked

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.

Why the obvious workarounds do not hold

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.

The primitive Flutter already has

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 3

One viewport, one set of children, no cross-fade.

Why negative offsets are not a hack

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.

The second thing this fixes

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.

Where this changes what you can build

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.

What it costs

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.

design. 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.

On replacing an archived package

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.

Try it

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.