How to check if scroll position is at top or bottom in ListView?

FlutterDartFlutter Layout

Flutter 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(),
    ],
  ),
),

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionSaloGalaView Question on Stackoverflow
Solution 1 - FlutterCopsOnRoadView Answer on Stackoverflow
Solution 2 - FlutterCollin JacksonView Answer on Stackoverflow
Solution 3 - FlutterEsteban DíazView Answer on Stackoverflow
Solution 4 - FlutterAkash MehtaView Answer on Stackoverflow
Solution 5 - FlutterMihaiView Answer on Stackoverflow
Solution 6 - FlutterGabriel RohdenView Answer on Stackoverflow
Solution 7 - FlutterOmar AlshyokhView Answer on Stackoverflow
Solution 8 - FlutterAnuj SharmaView Answer on Stackoverflow
Solution 9 - FluttergoodonionView Answer on Stackoverflow