Home > OS >  Detect scrolling to end of a list with NotificationListener
Detect scrolling to end of a list with NotificationListener

Time:01-28

I have a list view that show limited number of items. When the user scroll to the end I wanted to load next batch of items to the list.

I decided to use "NotificationListener" for this.

With the following code I was able to detect user reaching the end.

  @  @override
  Widget build(BuildContext context) {
    return Container(
      height: 430,
      child: NotificationListener<ScrollNotification>(
        child: ListView.builder(
          controller: controller,
          physics: const AlwaysScrollableScrollPhysics(),
          scrollDirection: Axis.horizontal,
          itemCount: widget.resList.length,
          itemBuilder: (BuildContext ctx, int index) {
            return GestureDetector(
              onTap: null,
              child: ReservationListTile(),
            );
          },
        ),
        onNotification: (ScrollNotification notification) {
          print(notification.metrics.pixels);
          if (notification.metrics.atEdge) {
            
            if (notification.metrics.pixels == 0) {
              print('At left');
            } else {
              print('At right');
            }
          }
          return true;
        },
      ),
    );
  }

What I hoped was, when the user reach the end of the list she will swipe the list again and there is a trigger to detect that and I would be able to load the next set of items.

The problem I have is when the user reached the end, the edge event get triggered multiple times.

Is there a way to detect the user pulling the list back?

CodePudding user response:

Yes, there is a way to detect when the user is pulling the list back. One way to achieve this is by using the NotificationListener and checking the metrics.extentAfter and metrics.extentBefore properties of the ScrollNotification.

extentAfter represents the amount of pixels that are not visible after the last item of the list, and extentBefore represents the amount of pixels that are not visible before the first item of the list.

You can use these properties to check if the user is pulling back the list and take action accordingly. For example:

NotificationListener<ScrollNotification>(
  child: ListView.builder(
    // ...
  ),
  onNotification: (ScrollNotification notification) {
    if (notification.metrics.extentAfter == 0 && notification.metrics.extentBefore > 0) {
      // The user is pulling the list back
      // You can load the next set of items here
    }
    return true;
  },
),

Also, you can use the package like infinite_scroll to handle the loading of next batch of items when user reached the end of the list.

CodePudding user response:

I had a very similar requirement, and I used a listener to detect when the user reached the end of the list. I actually decided not to wait until the user reaches the very end to provide a smoother user experience, so for example at 80% I already add the new items to the list.

See the following code. If you change _scrollThreshold to 0.75 for example, you will see the print statement executing. This is the place where you can add the new items.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) => const MaterialApp(
        home: Scaffold(
          body: Center(
            child: MyWidget(),
          ),
        ),
      );
}

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  MyWidgetState createState() => MyWidgetState();
}

class MyWidgetState extends State<MyWidget> {
  static const _itemCount = 32;
  static const _scrollThreshold = 1.00;
  late final ScrollController _scrollController;

  @override
  void initState() {
    super.initState();
    _scrollController = ScrollController();
    _scrollController.addListener(_scrollListener);
  }

  @override
  void dispose() {
    _scrollController.removeListener(_scrollListener);
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => SizedBox(
      height: 400,
      child: Center(
          child: ListView.builder(
              controller: _scrollController,
              physics: const AlwaysScrollableScrollPhysics(),
              scrollDirection: Axis.horizontal,
              itemCount: _itemCount,
              itemBuilder: (BuildContext ctx, int index) => Card(
                    child: Padding(
                        padding: const EdgeInsets.all(32.0),
                        child: Center(child: Text('Item $index'))),
                  ))));

  void _scrollListener() {
    if (_scrollController.offset >=
            _scrollController.position.maxScrollExtent * _scrollThreshold &&
        !_scrollController.position.outOfRange) {
      print('Scroll position is at ${_scrollThreshold * 100}%.');
    }
  }
}
  • Related