78

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?

CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
SaloGala
  • 1,904
  • 3
  • 14
  • 19

11 Answers11

159

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,
  ),
)
CopsOnRoad
  • 237,138
  • 77
  • 654
  • 440
  • 1
    Thansk, you save my time – DolDurma Aug 16 '19 at 07:48
  • 3
    This answer should be higher. Tried the answers above but this answer is cleaner and easier to implement. – Maki Nov 21 '20 at 12:43
  • 3
    It detects the top of list but not the bottom. – Syed Ali Raza May 20 '21 at 11:01
  • NotificationListener works for me ;) thnx a lot – Golnar sheikh bahaie Jul 19 '21 at 11:48
  • @CopsOnRoad, How to resolve scroll-controller error when i want to use another listview.builder and track scroll inside first list view Item Builder ? i meant here : ListTile(title: Text('Item $i')) My target is to use nested /more inner list and track scroll top/bottom in inner to change physics and scroll immediate parent list view. – Jamshed Alam Nov 24 '21 at 03:29
  • Like this improvement : https://github.com/nirav4273/flutter_nested_listview/issues , https://stackoverflow.com/questions/69422101/flutter-nested-list-scroll-parent-child-scroll-control-is-not-working , https://stackoverflow.com/questions/69795503/flutter-nested-list-scroll-parent-when-reach-to-end-start-of-inner-list – Jamshed Alam Nov 24 '21 at 03:32
  • It does not detect an end-scroll on a small number of items (where the listview is pulled but is not actually scrolled). Is there any solution to detect end-scroll on small lists? – genericUser May 09 '22 at 14:21
42

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.

Collin Jackson
  • 110,240
  • 31
  • 221
  • 152
39
_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....

Esteban Díaz
  • 491
  • 4
  • 7
  • 1
    @Pierre.Vriens is the answer to noam-aghai question ... this code creates a scrollController used to control the list and sticks a listener to let you know when is coming to its end and proceed to load more content or do whatever – Esteban Díaz Aug 04 '18 at 22:05
  • 1
    on what basis we define value of "delta" ? – Anuj Sharma Aug 29 '18 at 13:46
  • @AnujSharma there is no criteria.. its a random number for the sake of the example ... you can ignore it altogether and implement something different (like showing a loader at the end of the list) .... – Esteban Díaz Sep 03 '18 at 22:34
  • 1
    make sure to limit your api call in this function because I will called as many time as possible if the you are at end of scroll and your first api call taking time to return data. – Natwar Singh Dec 27 '18 at 17:09
  • @NatwarSingh We can set a variable flag to know any api call is running now or not. If no api is running then can send api request on server or in vice-versa case system will not send any api request to server. – Kamlesh Aug 08 '20 at 18:06
19

I would like to add example for answer provided by collin jackson. 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.

Akash Mehta
  • 358
  • 1
  • 3
  • 10
6

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';

Mihai
  • 1,212
  • 16
  • 25
3

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"

Gabriel Rohden
  • 1,307
  • 9
  • 19
2
  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);
 }
Omar Alshyokh
  • 605
  • 5
  • 7
1

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();
        }
Anuj Sharma
  • 4,294
  • 2
  • 37
  • 53
1

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(),
    ],
  ),
),
goodonion
  • 1,401
  • 13
  • 25
1

You can use any one of below conditions :

 NotificationListener<ScrollNotification>(
    onNotification: (notification) {
      final metrices = notification.metrics;

      if (metrices.atEdge && metrices.pixels == 0) {
        //you are at top of  list

      }
      
      if (metrices.pixels == metrices.minScrollExtent) {
         //you are at top of list
      }

      if (metrices.atEdge && metrices.pixels > 0) {
        //you are at end of  list

      }

      if (metrices.pixels >= metrices.maxScrollExtent) {
        //you are at end of list
      }

      return false;
    },
     child: ListView.builder());
Abhishek Ghimire
  • 1,826
  • 2
  • 8
  • 11
0
  final _scrollController = ScrollController();
@override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      if (_scrollController.position.pixels >
          _scrollController.position.minScrollExtent + 5) {
        // do Something
      }
      if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent) {
        // do Something
      }
    });
  }

and add controller to your listview

ListView(controller: _scrollController, ...),
tansangle
  • 1
  • 1