'How to apply Scroll controller for multiple SilverList or ListView items?

I have found the internet a way to hide arrow when scrolling horizontally through FlutterLogo with CustomSrollView. The functionality works and i want to place it inside a ListView.builder or SilverList so i have multilple widgets with the scrollfunctionality but once i put CustomScrollView inside a list weither its ListView.builder or SilverList i get error:

ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 108 pos 12: '_positions.length == 1'

This is the full code:

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

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

class _AppViewState extends State<AppView2> {
  late ScrollController _hideButtonController;
  var _isVisible;
 
  @override
  void initState() {
    _isVisible = true;
    _hideButtonController = ScrollController();
    _hideButtonController.addListener(() {
      if (_hideButtonController.position.userScrollDirection ==
          ScrollDirection.reverse) {
        if (_isVisible == true) {
          /* only set when the previous state is false
             * Less widget rebuilds
             */
          print("**** $_isVisible up"); //Move IO away from setState
          setState(() {
            _isVisible = false;
          });
        }
      } else {
        if (_hideButtonController.position.userScrollDirection ==
            ScrollDirection.forward) {
          if (_isVisible == false) {
            setState(() {
              _isVisible = true;
            });
          }
        }
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
        child: Scaffold(
            appBar: AppBar(
              title: Text("Example app bar"),
            ),
            body: CustomScrollView(slivers: <Widget>[
              SliverList(
                delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                  return SizedBox(
                    height: 500,
                    child: Stack(
                      children: [
                        CustomScrollView(
                          controller: _hideButtonController,
                          scrollDirection: Axis.horizontal,
                          shrinkWrap: true,
                          slivers: <Widget>[
                            SliverPadding(
                              padding: const EdgeInsets.all(20.0),
                              sliver: SliverList(
                                delegate: SliverChildListDelegate(
                                  <Widget>[
                                    FlutterLogo(
                                      size: 600,
                                    )
                                  ],
                                ),
                              ),
                            ),
                          ],
                        ),
                        Visibility(
                          visible: _isVisible,
                          child: Align(
                            alignment: Alignment.centerRight,
                            child: FloatingActionButton(
                                child: const Icon(Icons.arrow_forward_ios),
                                onPressed: () {}),
                          ),
                        )
                      ],
                    ),
                  );
                }, childCount: 10),
              )
            ])));
  }
}

How can i make sure that _hideButtonController works with multiple widget inside SilverList or ListView?



Solution 1:[1]

It is not very clear what you are trying to accomplish, but here is the reason why the error is thrown, and maybe some suggestions to help.

The error is thrown because you use the position getter of the ScrollController inside your listener code, while having multiple positions attached. Here is a quote from the documentation:

Calling this is only valid when only a single position is attached.

https://api.flutter.dev/flutter/widgets/ScrollController/position.html

You could use positions in your listener and check your conditions for any of the attached positions, although that is probably not what you want.

_hideButtonController.addListener(() {
      if (_hideButtonController.positions.any((pos) => pos.userScrollDirection ==
          ScrollDirection.reverse)) {
        if (_isVisible == true) {
          /* only set when the previous state is false
             * Less widget rebuilds
             */
          print("**** $_isVisible up"); //Move IO away from setState
          setState(() {
            _isVisible = false;
          });
        }
      } else {
        if (_hideButtonController.positions.any((pos) => pos.userScrollDirection ==
            ScrollDirection.forward)) {
          if (_isVisible == false) {
            setState(() {
              _isVisible = true;
            });
          }
        }
      }
    });

If you only want to share the code for hiding the button when a condition is met for a singular horizontal scroller, your best bet is probably to write your own widget which holds its own ScrollController with the same listener code you already have. This allows every child of the vertical list to have its own ScrollController for the horizontal list and thus allows you to only hide the button for the affected controller:

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

  _LogoHidingScrollViewState createState() => _LogoHidingScrollViewState();
}

class _LogoHidingScrollViewState extends State<LogoHidingScrollView> {
  final ScrollController _scrollController = ScrollController();
  bool _isVisible = true;

  void initState() {
    _isVisible = true;
    _scrollController.addListener(() {
      if (_scrollController.position.userScrollDirection ==
          ScrollDirection.reverse) {
        if (_isVisible == true) {
          /* only set when the previous state is false
             * Less widget rebuilds
             */
          print("**** $_isVisible up"); //Move IO away from setState
          setState(() {
            _isVisible = false;
          });
        }
      } else {
        if (_scrollController.position.userScrollDirection ==
            ScrollDirection.forward) {
          if (_isVisible == false) {
            setState(() {
              _isVisible = true;
            });
          }
        }
      }
    });
    super.initState();
  }

  Widget build(BuildContext context) {
    return SizedBox(
      height: 500,
      child: Stack(
        children: [
          CustomScrollView(
            controller: _scrollController,
            scrollDirection: Axis.horizontal,
            shrinkWrap: true,
            slivers: <Widget>[
              SliverPadding(
                padding: const EdgeInsets.all(20.0),
                sliver: SliverList(
                  delegate: SliverChildListDelegate(
                    <Widget>[
                      FlutterLogo(
                        size: 600,
                      )
                    ],
                  ),
                ),
              ),
            ],
          ),
          Visibility(
            visible: _isVisible,
            child: Align(
              alignment: Alignment.centerRight,
              child: FloatingActionButton(
                  child: const Icon(Icons.arrow_forward_ios), onPressed: () {}),
            ),
          )
        ],
      ),
    );
  }
}

If you truly want to synchronize the scrolling behaviour between all those horizontal scroll views you could have a look at: https://pub.dev/packages/linked_scroll_controller

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 puelo