flutterlistviewscrollflutter-pageview

How to Put ListView Inside PageView and Scroll Both of Them Vertically?


As the title says, we wanna put a vertical ListView inside a vertical PageView and make them scroll smoothly,

We will achieve something like that:

enter image description here


Solution

  • The Concept:

    When the user scrolls the list, if they reach its bottom and scroll in the same direction again, we want the page to scroll to the next one not the list. And vice versa.

    To achieve that we are gonna handle the scrolling of both widgets manually, depending on the touch gestures of the user.

    The Code:

    Firstly, in the state of the parent widget, declare these fields.

    PageController pageController;
    ScrollController activeScrollController;
    Drag drag;
    
    //These variables To detect if we are at the
    //top or bottom of the list.
    bool atTheTop;
    bool atTheBottom;
    

    Then initialize and dispose them:

    @override
    void initState() {
      super.initState();
    
      pageController = PageController();
    
      atTheTop = true;
      atTheBottom = false;
    }
    
    @override
    void dispose() {
      pageController.dispose();
    
      super.dispose();
    }
    

    now let's create five methods for handling the vertical dragging of the user.

    void handleDragStart(DragStartDetails details, ScrollController 
    scrollController) {
      if (scrollController.hasClients) {
        if (scrollController.position.context.storageContext != null) {
          if (scrollController.position.pixels == scrollController.position.minScrollExtent) {
            atTheTop = true;
          } else if (scrollController.position.pixels == scrollController.position.maxScrollExtent) {
            atTheBottom = true;
          } else {
            atTheTop = false;
            atTheBottom = false;
    
            activeScrollController = scrollController;
            drag = activeScrollController.position.drag(details, disposeDrag);
            return;
          }
        }
      }
    
      activeScrollController = pageController;
      drag = pageController.position.drag(details, disposeDrag);
    }
    
    void handleDragUpdate(DragUpdateDetails details, ScrollController 
    scrollController) {
      if (details.delta.dy > 0 && atTheTop) {
        //Arrow direction is to the bottom.
        //Swiping up.
    
        activeScrollController = pageController;
        drag?.cancel();
        drag = pageController.position.drag(
            DragStartDetails(globalPosition: details.globalPosition, localPosition: details.localPosition),
            disposeDrag);
      } else if (details.delta.dy < 0 && atTheBottom) {
        //Arrow direction is to the top.
        //Swiping down.
    
        activeScrollController = pageController;
        drag?.cancel();
        drag = pageController.position.drag(
            DragStartDetails(
              globalPosition: details.globalPosition,
              localPosition: details.localPosition,
            ),
            disposeDrag);
      } else {
        if (atTheTop || atTheBottom) {
          activeScrollController = scrollController;
          drag?.cancel();
          drag = scrollController.position.drag(
              DragStartDetails(
                globalPosition: details.globalPosition,
                localPosition: details.localPosition,
              ),
              disposeDrag);
        }
      }
      drag?.update(details);
    }
    
    void handleDragEnd(DragEndDetails details) {
      drag?.end(details);
    
      if (atTheTop) {
        atTheTop = false;
      } else if (atTheBottom) {
        atTheBottom = false;
      }
    }
    
    void handleDragCancel() {
      drag?.cancel();
    }
    
    void disposeDrag() {
      drag = null;
    }
    

    And Finally, let's build the widgets:

    PageView:

    @override
    Widget build(BuildContext context) {
      return PageView(
        controller: pageController,
        scrollDirection: Axis.vertical,
        physics: const NeverScrollableScrollPhysics(),
        children: [
          MyListView(
            handleDragStart: handleDragStart,
            handleDragUpdate: handleDragUpdate,
            handleDragEnd: handleDragEnd,
            pageStorageKeyValue: '1', //Should be unique for each widget.
          ),
          ...
        ],
      );
    }
    

    ListView:

    class MyListView extends StatefulWidget {
      const MyListView({
        Key key,
        @required this.handleDragStart,
        @required this.handleDragUpdate,
        @required this.handleDragEnd,
        @required this.pageStorageKeyValue,
      })  : assert(handleDragStart != null),
            assert(handleDragUpdate != null),
            assert(handleDragEnd != null),
            assert(pageStorageKeyValue != null),
            super(key: key);
    
      final ValuesChanged<DragStartDetails, ScrollController> handleDragStart;
      final ValuesChanged<DragUpdateDetails, ScrollController> handleDragUpdate;
      final ValueChanged<DragEndDetails> handleDragEnd;
      
      //Notice here, the key to save the position scroll of the list.
      final String pageStorageKeyValue;
    
      @override
      _MyListViewState createState() => _MyListViewState();
    }
    
    class _MyListViewState extends State<MyListView> {
      ScrollController scrollController;
    
      @override
      void initState() {
        super.initState();
    
        scrollController = ScrollController();
      }
    
      @override
      void dispose() {
        scrollController.dispose();
    
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onVerticalDragStart: (details) {
            widget.handleDragStart(details, scrollController);
          },
          onVerticalDragUpdate: (details) {
            widget.handleDragUpdate(details, scrollController);
          },
          onVerticalDragEnd: widget.handleDragEnd,
          child: ListView.separated(
            key: PageStorageKey<String>(widget.pageStorageKeyValue),
            physics: const NeverScrollableScrollPhysics(),
            controller: scrollController,
            itemCount: 15,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text('Item $index'),
              );
            },
            separatorBuilder: (context, index) {
              return const Divider(
                thickness: 3,
              );
            },
          ),
        );
      }
    }
    

    typedef for injecting the methods:

    typedef ValuesChanged<T, E> = void Function(T value, E valueTwo);
    

    Notes:

    References:

    If you have anything to say, I'm here to reply. Thanks.