How to check if scroll position is at top or bottom in ListView?
FlutterDartFlutter LayoutFlutter Problem Overview
I'm trying to implement a infinite scroll functionality.
I tried using a ListView
inside on a NotificationListener
to detect scroll events, but I can't see an event that says if the scroll has reached the bottom of the view.
Which would be the best way to achieve this?
Flutter Solutions
Solution 1 - Flutter
There are generally two ways of doing it.
1. Using ScrollController
// Create a variable
final _controller = ScrollController();
@override
void initState() {
super.initState();
// Setup the listener.
_controller.addListener(() {
if (_controller.position.atEdge) {
bool isTop = _controller.position.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
});
}
Usage:
ListView(controller: _controller) // Assign the controller.
2. Using NotificationListener
NotificationListener<ScrollEndNotification>(
onNotification: (scrollEnd) {
final metrics = scrollEnd.metrics;
if (metrics.atEdge) {
bool isTop = metrics.pixels == 0;
if (isTop) {
print('At the top');
} else {
print('At the bottom');
}
}
return true;
},
child: ListView.builder(
physics: ClampingScrollPhysics(),
itemBuilder: (_, i) => ListTile(title: Text('Item $i')),
itemCount: 20,
),
)
Solution 2 - Flutter
You can use a ListView.builder
to create a scrolling list with unlimited items. Your itemBuilder
will be called as needed when new cells are revealed.
If you want to be notified about scroll events so you can load more data off the network, you can pass a controller
argument and use addListener
to attach a listener to the ScrollController
. The position
of the ScrollController
can be used to determine whether the scrolling is close to the bottom.
Solution 3 - Flutter
_scrollController = new ScrollController();
_scrollController.addListener(
() {
double maxScroll = _scrollController.position.maxScrollExtent;
double currentScroll = _scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) { // whatever you determine here
//.. load more
}
}
);
Collin's should be accepted answer....
Solution 4 - Flutter
I would like to add example for [answer provided by collin jackson][1]. Refer following snippet
var _scrollController = ScrollController();
_scrollController.addListener(() {
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
// Perform your task
}
});
This will be only triggered when last item is visible in the list. [1]: https://stackoverflow.com/a/46379818/4764668
Solution 5 - Flutter
A more simpler aproach is like this:
NotificationListener<ScrollEndNotification>(
onNotification: onNotification,
child: <a ListView or Wrap or whatever widget you need>
)
and create a method to detect the position:
bool onNotification(ScrollEndNotification t) {
if (t.metrics.pixels >0 && t.metrics.atEdge) {
log('I am at the end');
} else {
log('I am at the start')
}
return true;
}
t.metrics.pixel
is 0 when the user is with the scrol at the top, as is more then 0 when the sure scrools.
t.metrics.atEdge
is true
when the user is either at the top with the scrol or at the end with the scrol
the log
method is from package import 'dart:developer';
Solution 6 - Flutter
I feel like this answer is a complement to Esteban's one (with extension methods and a throttle), but it's a valid answer too, so here it is:
Dart recently (not sure) got a nice feature, method extensions, which allow us to write the onBottomReach
method like a part of the ScrollController
:
import 'dart:async';
import 'package:flutter/material.dart';
extension BottomReachExtension on ScrollController {
void onBottomReach(VoidCallback callback,
{double sensitivity = 200.0, Duration throttleDuration}) {
final duration = throttleDuration ?? Duration(milliseconds: 200);
Timer timer;
addListener(() {
if (timer != null) {
return;
}
// I used the timer to destroy the timer
timer = Timer(duration, () => timer = null);
// see Esteban Díaz answer
final maxScroll = position.maxScrollExtent;
final currentScroll = position.pixels;
if (maxScroll - currentScroll <= sensitivity) {
callback();
}
});
}
}
Here's a usage example:
// if you're declaring the extension in another file, don't forget to import it here.
class Screen extends StatefulWidget {
Screen({Key key}) : super(key: key);
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
ScrollController_scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController()
..onBottomReach(() {
// your code goes here
}, sensitivity: 200.0, throttleDuration: Duration(milliseconds: 500));
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Note: if you're using method extensions, you need to configure some things, see "How to enable Dart Extension Methods"
Solution 7 - Flutter
final ScrollController controller = ScrollController();
void _listener() {
double maxPosition = controller.position.maxScrollExtent;
double currentPosition = controller.position.pixels;
/// You can change this value . It's a default value for the
/// test if the difference between the great value and the current value is smaller
/// or equal
double difference = 10.0;
/// bottom position
if ( maxPosition - currentPosition <= difference )
/// top position
else
if(mounted)
setState(() {});
}
@override
void initState() {
super.initState();
controller.addListener(_listener);
}
Solution 8 - Flutter
I used different approach for infinite scrolling. I used ChangeNotifier class for variable change listener. If there is change in variable It triggers the event and eventually hit the API.
class DashboardAPINotifier extends ChangeNotifier {
bool _isLoading = false;
get getIsLoading => _isLoading;
set setLoading(bool isLoading) => _isLoading = isLoading;
}
Initialize DashboardAPINotifier class.
@override
void initState() {
super.initState();
_dashboardAPINotifier = DashboardAPINotifier();
_hitDashboardAPI(); // init state
_dashboardAPINotifier.addListener(() {
if (_dashboardAPINotifier.getIsLoading) {
print("loading is true");
widget._page++; // For API page
_hitDashboardAPI(); //Hit API
} else {
print("loading is false");
}
});
}
Now the best part is when you have to hit the API.
If you are using SliverList
, Then at what point you have to hit the API.
SliverList(delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
Widget listTile = Container();
if (index == widget._propertyList.length - 1 &&
widget._propertyList.length <widget._totalItemCount) {
listTile = _reachedEnd();
} else {
listTile = getItem(widget._propertyList[index]);
}
return listTile;
},
childCount: (widget._propertyList != null)? widget._propertyList.length: 0,
addRepaintBoundaries: true,
addAutomaticKeepAlives: true,
),
)
_reachEnd() method take care to hit the api. It trigger the `_dashboardAPINotifier._loading`
// Function that initiates a refresh and returns a CircularProgressIndicator - Call when list reaches its end
Widget _reachedEnd() {
if (widget._propertyList.length < widget._totalItemCount) {
_dashboardAPINotifier.setLoading = true;
_dashboardAPINotifier.notifyListeners();
return const Padding(
padding: const EdgeInsets.all(20.0),
child: const Center(
child: const CircularProgressIndicator(),
),
);
} else {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
print("No more data found");
Utils.getInstance().showSnackBar(_globalKey, "No more data found");
}
}
Note: After your API response you need to notify the listener,
setState(() {
_dashboardAPINotifier.setLoading = false;
_dashboardAPINotifier.notifyListeners();
}
Solution 9 - Flutter
You can use the package scroll_edge_listener.
It comes with an offset and debounce time configuration which is quite useful. Wrap your scroll view with a ScrollEdgeListener
and attach a listener. That's it.
ScrollEdgeListener(
edge: ScrollEdge.end,
edgeOffset: 400,
continuous: false,
debounce: const Duration(milliseconds: 500),
dispatch: true,
listener: () {
debugPrint('listener called');
},
child: ListView(
children: const [
Placeholder(),
Placeholder(),
Placeholder(),
Placeholder(),
],
),
),