| name | flutter-pageview-scroll-position-preservation |
| description | Fix Flutter PageView "snap back" or scroll position reset issues on provider/state rebuilds.
Use when: (1) PageView scrolls then snaps back to previous page after state update,
(2) Scroll position resets when Riverpod/Provider rebuilds, (3) Feed scrolls back to top
on data refresh, (4) PageView loses position after orientation change or widget rebuild.
|
| author | Claude Code |
| version | 1.1.0 |
| date | "2026-01-31T00:00:00.000Z" |
Flutter PageView Scroll Position Preservation
Problem
Users scrolling through a PageView-based feed report that the scroll position resets or
"snaps back" to a previous page, especially after state updates from providers (Riverpod,
Provider, BLoC) or widget rebuilds.
Context / Trigger Conditions
- PageView with dynamic content from state management
- Provider/Riverpod state updates causing widget rebuilds
- Pagination that changes itemCount
- Orientation changes
- Tab switching and returning to the PageView
Root Cause
When Flutter rebuilds the widget tree (due to state changes), the PageView may lose its
scroll position if:
- No
PageStorageKey is provided to persist state
- The PageController is recreated during rebuilds
PageController.keepPage is set to false
Note: allowImplicitScrolling is for accessibility focus navigation, NOT scroll
position preservation. A specific bug (#76569) involving TextField focus was fixed in Flutter.
Solution
1. Add PageStorageKey (PRIMARY FIX)
PageView.builder(
// This key tells Flutter to persist scroll state across rebuilds
key: const PageStorageKey<String>('my_page_view'),
itemCount: videos.length,
controller: _pageController,
...
)
2. Keep PageController in State (not build)
class _MyScreenState extends State<MyScreen> {
// Create in initState, NOT in build()
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController(); // Created once
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Use the same controller instance
return PageView.builder(
controller: _pageController,
...
);
}
}
3. Ensure keepPage is true (default)
_pageController = PageController(
keepPage: true, // This is the default, but be explicit if needed
initialPage: 0,
);
4. For complex state: Use PageStorage widget
PageStorage(
bucket: PageStorageBucket(),
child: PageView.builder(
key: const PageStorageKey<String>('feed'),
...
),
)
What allowImplicitScrolling Actually Does
This is for accessibility, not scroll position:
false (default): Accessibility focus exits PageView at page boundaries
true: Accessibility focus moves to next page instead of exiting widget
It does NOT cause scroll position issues. Keep it true for better accessibility.
Verification
- Scroll to page 10+ in the feed
- Trigger a state update (like new data arriving)
- Verify scroll position is preserved
- Rotate device - verify position preserved
- Switch tabs and return - verify position preserved
Example
Full pattern for a Riverpod-based feed:
class _VideoFeedScreenState extends ConsumerState<VideoFeedScreen> {
late PageController _pageController;
@override
void initState() {
super.initState();
_pageController = PageController();
}
@override
void dispose() {
_pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final videos = ref.watch(videoFeedProvider);
return PageView.builder(
key: const PageStorageKey<String>('video_feed'),
controller: _pageController,
itemCount: videos.length,
allowImplicitScrolling: true, // For accessibility
itemBuilder: (context, index) => VideoItem(videos[index]),
);
}
}
Notes
- The Riverpod 3.2.0 issue #4661 about ProviderScope rebuilds is separate
- If using tabs, wrap in
AutomaticKeepAliveClientMixin to preserve state
- For very long lists, consider using
restorationId for app restart persistence
References