Search code examples
flutterscrollcontroller

How to implement automatic collapsing/expanding of a section while scrolling in Flutter?


I'm working on a mobile application using Flutter and encountered a certain issue: I have a section on the screen that can be long and scrollable. I want this section to automatically collapse when it goes out of view while scrolling down and expand back when the user returns to that section.

Are there any built-in features in Flutter to achieve this behavior? Or perhaps, does anyone have suggestions on how to best implement this?

Thank you for your help!

I was thinking of implementing this using ExpansionPanelList and ScrollController.


Solution

  • A performative way, which does not depend on packages, and will deliver a very similar result, is to use a custom scrollview with slivers. Here you can use a SliverAppBar with pinned true instead of the headerBuilder of the ExpansionPanel, and any other Sliver widget to display the content.

    As you said that the panel's content can be long, you can use a SliverList, so Flutter will only load what is being displayed on the screen, and will not need to compute the entire list of widgets from that screen before building the screen, as happens in the case of the ExpansionPanel where all widgets are loaded before being displayed. This way it will be much more performative, because besides this optimized rendering, you don't need to keep listening and checking the scroll position all the time.

    class MyHomePage extends StatelessWidget {
      const MyHomePage({
      super.key,
    });
    
    @override
    Widget build(BuildContext context) {
       return Scaffold(
            body: CustomScrollView(
              slivers: [
                const SliverAppBar(
                  title: Text("AppBar SliverList"),
                  pinned: true,
                ),
                SliverList(
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Container(
                        color: Colors.blue,
                        child: Text('list item $index'),
                      );
                    },
                    childCount: 20,
                  ),
                ),
                const SliverAppBar(
                  title: Text("AppBar SliverToBoxAdapter"),
                  pinned: true,
                ),
                SliverToBoxAdapter(
                  child: Container(
                    height: 100,
                    color: Colors.red,
                    child: const Center(
                      child: Text("Item"),
                    ),
                  ),
                ),
                const SliverAppBar(
                  title: Text("AppBar SliverFixedExtentList"),
                  pinned: true,
                ),
                SliverFixedExtentList(
                  itemExtent: 50.0,
                  delegate: SliverChildBuilderDelegate(
                    (BuildContext context, int index) {
                      return Container(
                        alignment: Alignment.center,
                        color: Colors.green[100 * (index % 9)],
                        child: Text('list item $index'),
                      );
                    },
                    childCount: 100,
                  ),
                ),
              ],
            ),
          );
        }
      }
    

    The result:The result

    If you want, you can also control which section is displayed with a scroll controller, just using animateTo. If you use SliverFixedExtentList() it becomes extremely simple to navigate between sections, as everything will have a fixed height defined.

    However, there is a disadvantage. As the AppBars line up one under the other as you scroll, if there are many AppBars, they will cover the entire screen.

    In this case, a simple and efficient solution is to use the sliver_tools package. It has exactly the functionalities you are looking for already built, like the MultiSliver: MultiSliver