This article shows you how to get the current scroll offset (that indicates the distance to the top, in pixels) in a ListView
or other scrollable widgets in Flutter like GridView
, SingleSchildScrollView
, CustomScrollView
, etc. We’ll discover the mechanism and then examine a complete example of applying that mechanism in practice.
Table of Contents
What Is The Point?
To calculate the scroll offset in a scrollable widget in Flutter, the first thing to do is to initialize a ScrollController
instance:
final ScrollController _controller = ScrollController();
Then connect this controller to the scrollable widget:
ListView.builder(
// attact the controller
controller: _controller,
/* ... */
),
Now you can add a listener and get the result like so:
@override
void initState() {
_controller.addListener(() {
// print out the scroll offset
print(_controller.offset);
});
super.initState();
}
If the list view is scrolled to its end, the scroll offset will be equal to or greater than the following:
controller.position.maxScrollExtent
To attain a better understanding, see the complete example below.
Example
App Preview
The demo app we’re going to make contains an app bar, a list view (which renders a lot of items), and a bottom bar. The scroll offset will be shown in the bottom bar. The background color of the bottom bar depends on the scroll offset:
- If the scroll offset is equal to or less than zero, the background is green (it’s possible that a scroll offset is a negative number)
- If you scroll to the end of the list view, the background color will be red. In addition, a snackbar with the message “You have reached the end of the list view” will appear
- In the remaining cases, the bottom bar’s background is blue
Words might be confusing and boring. Here’s how it works:
This example aims to demonstrate how you can retrieve the scroll position in a scrollable widget and detect whether it is scrolled to the bottom/top. Updating the UI every time the user scrolls might be heavy and expensive.
The Code
The full source code in main.dart
(with explanations in the comments):
// main.dart
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
// Remove the debug banner
debugShowCheckedModeBanner: false,
title: 'KindaCode.com',
theme: ThemeData(
useMaterial3: true,
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// Generate dummy data to fill the list view
final List _items = List.generate(50, (index) => 'Item $index');
// The controller that is assigned to the list view
final ScrollController _controller = ScrollController();
// The scroll offset
double _scrollOffset = 0;
// The maximum scroll offset
// In other words, this means the user has reached the bottom of the list view
double? _maxOffset;
@override
void initState() {
_controller.addListener(() {
_maxOffset = _controller.position.maxScrollExtent;
setState(() {
_scrollOffset = _controller.offset;
if (_maxOffset != null && _scrollOffset >= _maxOffset!) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text('You have reached the end of the list view')));
} else {
ScaffoldMessenger.of(context).removeCurrentSnackBar();
}
});
});
super.initState();
}
// Discards any resources used by the scroll controller object
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('KindaCode.com')),
// implement the list view
body: ListView.builder(
// attact the controller
controller: _controller,
itemBuilder: ((context, index) => Card(
key: ValueKey(_items[index]),
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
elevation: 6,
color: Colors.amber.shade100,
child: ListTile(
title: Text(_items[index]),
),
)),
itemCount: _items.length,
),
// display the scroll offset
bottomNavigationBar: BottomAppBar(
elevation: 6,
// set the background color of the bottom bar based on the the current offset position
// if at the top: green
// if at the bottom: red
// otherwise: blue
color: _scrollOffset <= 0
? Colors.green
: _maxOffset != null && _scrollOffset >= _maxOffset!
? Colors.red
: Colors.blue,
child: Padding(
padding: const EdgeInsets.only(top: 20, left: 20),
child: Text(
"Offset: ${_scrollOffset.toString()}",
style: const TextStyle(
fontSize: 21, color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
);
}
}
Conclusion
You’ve learned how to find the scroll offset position in a scrollable widget in Flutter. If you’d like to explore more new and interesting stuff about ListView, GridView, and things like that, take a look at the following articles:
- Flutter: ListView Pagination (Load More) example
- Flutter SliverList – Tutorial and Example
- Flutter AnimatedList – Tutorial and Examples
- Flutter: Safely nesting ListView, GridView inside a Column
- Flutter: SliverGrid example
- Creating Masonry Layout in Flutter with Staggered Grid View
You can also tour around our Flutter topic page or Dart topic page for the most recent tutorials and examples.