| name | flutter-pageview-url-routing-reorder-loop |
| description | Fix infinite rebuild loop in Flutter when using PageView with URL-based routing (GoRouter)
and reactive state management (Riverpod/BLoC). Use when: (1) RAPID REBUILD warnings appear
in console (#20+), (2) "setState() or markNeedsBuild() called during build" errors from
ValueListenableBuilder, (3) Video feed or list-based PageView is frozen and won't swipe,
(4) Overlay or UI elements flicker between visible/invisible rapidly, (5) Logs show item
"moved from index X to Y" repeatedly. Caused by tracking items by ID and updating URL
when reactive list reorders.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-06T00:00:00.000Z" |
Flutter PageView + URL Routing Reorder Detection Loop
Problem
When a PageView is synced bidirectionally with URL-based routing (e.g., GoRouter /home/:index)
and the data source is a reactive provider (Riverpod, BLoC stream), attempting to track the
currently-viewed item by its ID and update the URL when the item moves to a different index
in the list creates an infinite feedback loop.
Context / Trigger Conditions
Symptoms:
- Console shows
RAPID REBUILD #42! warnings (build count growing rapidly)
setState() or markNeedsBuild() called during build errors from ValueListenableBuilder
- PageView is frozen - user swipes but page bounces back to same position
- UI overlay (social buttons, controls) flickers between visible and invisible
- Logs show repeated pattern:
Video X moved from index 4 -> 3, updating URL
Architecture that triggers this:
PageView.builder with onPageChanged updating URL via context.go('/feed/$index')
- URL-derived index synced back to PageController via
jumpToPage() during build
- Reactive data source (Riverpod provider, stream) that can re-emit with reordered items
- Item tracking:
_currentItemId compared against list to detect "moves"
Example of the anti-pattern:
// IN BUILD METHOD - creates feedback loop!
if (_currentVideoStableId != null && videos.isNotEmpty) {
final currentVideoIndex = videos.indexWhere(
(v) => v.stableId == _currentVideoStableId,
);
if (currentVideoIndex != -1 && currentVideoIndex != urlIndex) {
// This triggers URL update -> rebuild -> PageController sync ->
// onPageChanged -> URL update -> INFINITE LOOP
context.go('/home/$currentVideoIndex');
}
}
Solution
Remove item-tracking reorder detection entirely. The PageController should be the sole
source of truth for which page the user is viewing.
Step 1: Remove tracking state
// REMOVE these fields:
// String? _currentVideoStableId;
// bool _urlUpdateScheduled = false;
Step 2: Remove reorder detection block
Remove any code in build() that:
- Searches for a tracked item ID in the current list
- Compares found index against URL index
- Schedules URL updates when indices differ
Step 3: Keep only one-way sync patterns
User swipe -> URL (one-way):
onPageChanged: (newIndex) {
if (newIndex != urlIndex) {
context.go('/home/$newIndex');
}
}
External navigation -> PageController (one-way):
if (urlIndex != _lastUrlIndex) {
_lastUrlIndex = urlIndex;
controller.jumpToPage(urlIndex);
}
These two paths don't create loops because:
- User swipe: URL updates, next build sees matching urlIndex -> no sync needed
- External nav: URL changes, sync fires, onPageChanged fires but
newIndex == urlIndex -> no URL update
The Feedback Loop Explained
┌─ Reactive provider re-emits (new data from server) ─┐
│ │
▼ │
Build runs with new video list │
│ │
▼ │
Reorder detection: "Video X moved from idx 4 to 3" │
│ │
▼ │
Schedule URL update: context.go('/home/3') │
│ │
▼ │
Build runs: urlIndex=3, PageController at page 4 │
│ │
▼ │
Sync: jumpToPage(3) │
│ │
▼ │
onPageChanged(3) fires → context.go('/home/3') │
│ │
▼ │
But PageController was at 4, which showed different │
video → _currentVideoStableId mismatches → detects │
"move" again → URL update to /home/4 │
│ │
└───────── INFINITE LOOP ──────────────────────────────┘
Verification
After removing reorder detection:
- No more
RAPID REBUILD warnings in console
- No
setState() called during build errors
- Swiping between pages works smoothly
- UI overlays stay visible on the active page
- No "moved from index X to Y" log spam
Notes
-
Why reorder detection seems needed: When a reactive list reorders (e.g., new items
prepended), the user's current page index points to a different item. However, trying to
"follow" the item creates worse UX (infinite loop) than staying on the same page index.
-
Alternative if position preservation is critical: Instead of URL-based reorder detection,
stabilize the list order. Either:
- Don't reorder while the user is actively viewing (batch updates)
- Use a stable sort that preserves relative positions of existing items
- Only prepend/append new items, never reorder existing ones
-
Related pattern: flutter-pageview-implicit-scrolling-snapback addresses a different
PageView issue (snap-back on state rebuild) but can co-occur with this loop.
-
Framework-agnostic: While this was discovered with GoRouter + Riverpod, the same
pattern applies to any PageView + URL routing + reactive state combination (Navigator 2.0,
auto_route, BLoC streams, etc.)