Tracking Scroll Physics and Metrics with NotificationListener in Flutter
Learn how to use NotificationListener<ScrollNotification> in Flutter to track scroll depth, detect user drag events, and trigger infinite scrolling pagination.
Published on • November 9, 2026
AI Assistant

Monitoring scroll position in Flutter is commonly handled by attaching a listener to a ScrollController. However, ScrollController requires creating, holding, and disposing an imperative object reference.
When you want to inspect scroll metrics declaratively—or intercept scroll events across descendant widgets without holding controller references—NotificationListener<ScrollNotification> offers a decoupled, message-passing mechanism.
How NotificationListener Works
In Flutter’s framework, descendant widgets dispatch Notification bubbles upwards through the widget tree. NotificationListener intercepts these notifications as they travel toward the root.
For scrollable widgets (ListView, GridView, CustomScrollView), the framework dispatches subclasses of ScrollNotification:
ScrollStartNotification: Fired when the user initiates a drag gesture or a scroll animation starts.ScrollUpdateNotification: Fired repeatedly on every frame while the scroll position updates.UserScrollNotification: Indicates changes in scroll direction (e.g.ScrollDirection.forwardvsScrollDirection.reverse).ScrollEndNotification: Fired when scrolling comes to a complete halt.
Intercepting Scroll Events
import 'package:flutter/material.dart';
class InfiniteScrollList extends StatefulWidget {
const InfiniteScrollList({super.key});
@override
State<InfiniteScrollList> createState() => _InfiniteScrollListState();
}
class _InfiniteScrollListState extends State<InfiniteScrollList> {
final List<int> _items = List.generate(20, (i) => i);
bool _isLoadingMore = false;
void _loadMoreItems() async {
if (_isLoadingMore) return;
setState(() => _isLoadingMore = true);
await Future.delayed(const Duration(milliseconds: 1500));
setState(() {
_items.addAll(List.generate(10, (i) => _items.length + i));
_isLoadingMore = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('NotificationListener Scroll Demo')),
body: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
final metrics = notification.metrics;
// Detect when scroll approaches 85% of total scroll extent
if (metrics.pixels >= metrics.maxScrollExtent * 0.85) {
_loadMoreItems();
}
// Return false to allow notification to bubble up further
return false;
},
child: Column(
children: [
Expanded(
child: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${_items[index]}')),
title: Text('Data Item #${_items[index]}'),
);
},
),
),
if (_isLoadingMore)
const Padding(
padding: EdgeInsets.all(12.0),
child: CircularProgressIndicator(),
),
],
),
),
);
}
}
Reading ScrollMetrics
The ScrollMetrics object passed with notification.metrics provides vital layout and scroll information:
metrics.pixels: Current scroll position in logical pixels.metrics.maxScrollExtent: Total scrollable distance available.metrics.minScrollExtent: Minimum scroll bound (typically0.0).metrics.viewportDimension: Height (or width) of the visible scroll window.metrics.extentBefore: Distance scrolled past the start edge.metrics.extentAfter: Distance remaining before reaching the end edge.metrics.axisDirection: Axis direction (AxisDirection.down,right, etc.).
Hiding Floating Action Buttons on Scroll Down
By listening to UserScrollNotification, you can hide UI chrome (such as a FloatingActionButton or bottom navigation bar) when the user scrolls down, and reveal it when scrolling up:
NotificationListener<UserScrollNotification>(
onNotification: (notification) {
if (notification.direction == ScrollDirection.reverse) {
// User is scrolling DOWN - hide FAB
_setFabVisibility(false);
} else if (notification.direction == ScrollDirection.forward) {
// User is scrolling UP - show FAB
_setFabVisibility(true);
}
return false;
},
child: ListView(/* ... */),
)
Takeaway: NotificationListener<ScrollNotification> provides decoupled, declarative access to scroll metrics, making infinite pagination and scroll-direction UI triggers easy to implement.