How to make a ListView widget be permanently adjacent to the bottom of the AppBar?

Viewed 443

I'm building my app now and have run into the issue of trying to make my ListView widget of Chips act similarly to the way the tags act on the YouTube app home-page. For those not familiar, the YouTube mobile app has a sort of ListView widget that contains some tags to filter videos on your home page which I've added at the bottom of the post.

It looks like the AppBar is separate from the actual horizontal ListView that is beneath it, and yet appears whenever it appears (which is some kind of SliverAppBar behavior, it would seem) as if it's somehow stuck to it. Is there a good way of implementing this without using the 'bottom' property of the AppBar, or alternatively with the bottom trait but while still separating the two widgets with a border/shadow?

Specifically, I'm looking for the best practice in this kind of scenario, since I'm new to Flutter and wouldn't want to make a sub-par solution as a result.

Thanks!

YouTube labels ListView.

1 Answers

I've arrived at this solution using a CustomScrollView with two SliverAppBar appearing one after another (first one containing YouTube logo and second one with the list of ChoiceChip widgets).

SliverAppBar with a ListView consisting of ChoiceChips

This is what you need. The only problem is I couldn't solve setting the height of the app bar. It's too tall compared to the one in YouTube. The ability to set minimumHeight seems like something that's being worked on looking at this PR:

int _selectedIndex = 0;
List<String> chipTitles = [
  'Broski',
  'Atom',
  'Elon Musk',
  'Lemonade',
  'Ginger',
  'Tiger',
  'Drama',
  'Comedy',
  'COVID-19',
  'US'
];

SliverAppBar(
    pinned: true,
    floating: false,
    expandedHeight: 50.0,
    flexibleSpace: ListView.separated(
        scrollDirection: Axis.horizontal,
        itemCount: chipTitles.length,
        separatorBuilder: (context, index) => SizedBox(
          width: 10.0,
        ),
        itemBuilder: (context, index) {
          return ChoiceChip(
            selected: _selectedIndex == index,
            label: Text('${chipTitles[index]}'),
            onSelected: (selected) {
              if (selected) {
                setState(() {
                  _selectedIndex = index;
                });
              }
            },
          );
        },
      ),
    ),

Also including the full StatefulWidget class for a complete example:

class ScrollerDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _ScrollerDemoState();
}

class _ScrollerDemoState extends State<ScrollerDemo> {
  int _selectedIndex = 0;
  List<String> chipTitles = [
    'Broski',
    'Atom',
    'Elon Musk',
    'Lemonade',
    'Ginger',
    'Tiger',
    'Drama',
    'Comedy',
    'COVID-19',
    'US'
  ];

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: <Widget>[
        const SliverAppBar(
          // Results in the logo app bar mimicking the one used in the YouTube app.
          pinned: false,
          floating: true,
          expandedHeight: 80.0,
          flexibleSpace: FlexibleSpaceBar(
            title: Text('CustomScrollView Demo'),
          ),
        ),
        SliverAppBar(
          pinned: true,
          floating: false,
          expandedHeight: 50.0,
          flexibleSpace: ListView.separated(
            scrollDirection: Axis.horizontal,
            itemCount: chipTitles.length,
            separatorBuilder: (context, index) => SizedBox(
              width: 10.0,
            ),
            itemBuilder: (context, index) {
              return ChoiceChip(
                selected: _selectedIndex == index,
                label: Text('${chipTitles[index]}'),
                onSelected: (selected) {
                  if (selected) {
                    setState(() {
                      _selectedIndex = index;
                    });
                  }
                },
              );
            },
          ),
        ),
        SliverGrid(
          gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
            maxCrossAxisExtent: 200.0,
            mainAxisSpacing: 10.0,
            crossAxisSpacing: 10.0,
            childAspectRatio: 4.0,
          ),
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                alignment: Alignment.center,
                color: Colors.teal[100 * (index % 9)],
                child: Text('Grid item $index'),
              );
            },
            childCount: 20,
          ),
        ),
        SliverFixedExtentList(
          itemExtent: 50.0,
          delegate: SliverChildBuilderDelegate(
            (context, index) {
              return Container(
                alignment: Alignment.center,
                color: Colors.lightBlue[100 * (index % 9)],
                child: Text('List item $index'),
              );
            },
            childCount: 36,
          ),
        ),
      ],
    );
  }
}
Related