Horizontal ListView inside a Vertical ScrollView in Flutter
Asked Answered
K

11

86

I am trying to achieve a very common behavior nowadays which is to have a horizontal List within another widget that is at the same time scrollable. Think something like the home screen of the IMDb app:

enter image description here

So I want to have a widget that scrolls vertically with few items on them. At the top of it, there should be a horizontal ListView, followed up with some items called motivationCard. There are some headers in between the list and the cards as well.

I got something like this on my Widget:

@override
  Widget build(BuildContext context) => BlocBuilder<HomeEvent, HomeState>(
        bloc: _homeBloc,
        builder: (BuildContext context, HomeState state) => Scaffold(
              appBar: AppBar(),
              body: Column(
                children: <Widget>[
                  Text(
                    Strings.dailyTasks,
                  ),
                  ListView.builder(
                    scrollDirection: Axis.horizontal,
                    itemCount: tasks.length,
                    itemBuilder: (BuildContext context, int index) =>
                        taskCard(
                          taskNumber: index + 1,
                          taskTotal: tasks.length,
                          task: tasks[index],
                        ),
                  ),
                  Text(
                    Strings.motivations,
                  ),
                  motivationCard(
                    motivation: Motivation(
                        title: 'Motivation 1',
                        description:
                        'this is a description of the motivation'),
                  ),
                  motivationCard(
                    motivation: Motivation(
                        title: 'Motivation 2',
                        description:
                        'this is a description of the motivation'),
                  ),
                  motivationCard(
                    motivation: Motivation(
                        title: 'Motivation 3',
                        description:
                        'this is a description of the motivation'),
                  ),
                ],
              ),
            ),
      );

this is the error I get:

I/flutter (23780): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
I/flutter (23780): The following assertion was thrown during performResize():
I/flutter (23780): Horizontal viewport was given unbounded height.
I/flutter (23780): Viewports expand in the cross axis to fill their container and constrain their children to match
I/flutter (23780): their extent in the cross axis. In this case, a horizontal viewport was given an unlimited amount of
I/flutter (23780): vertical space in which to expand.

I have tried:

  • Wrapping the ListView with an Expanded widget

  • Wrapping the Column with SingleChildScrollView > ConstrainedBox > IntrinsicHeight

  • Having CustomScrollView as a parent, with a SliverList and the List within a SliverChildListDelegate

None of these work and I continue getting the same kind of error. This is a very common thing and shouldn't be any hard, somehow I just cannot get it to work :(

Any help would be much appreciated, thanks!

Edit:

I thought this could help me but it didn't.

Korte answered 4/2, 2019 at 8:6 Comment(2)
Where is your vertical ListView?Willow
There is no vertical ListView. I want the whole screen to be scrollable. Think of a column but scrollable. Then within that Column I would like to have a ListView which scrolls horizontally. The rest of the children in the column will be different items, i.e. headers, cards and other.Apotheosis
E
137

Well, Your Code Work Fine with wrapping your- ListView.builder with Expanded Widget & setting mainAxisSize: MainAxisSize.min, of Column Widget.

E.x Code of what you Have.

 body: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text(
            'Headline',
            style: TextStyle(fontSize: 18),
          ),
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              scrollDirection: Axis.horizontal,
              itemCount: 15,
              itemBuilder: (BuildContext context, int index) => Card(
                    child: Center(child: Text('Dummy Card Text')),
                  ),
            ),
          ),
          Text(
            'Demo Headline 2',
            style: TextStyle(fontSize: 18),
          ),
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              itemBuilder: (ctx,int){
                return Card(
                  child: ListTile(
                      title: Text('Motivation $int'),
                      subtitle: Text('this is a description of the motivation')),
                );
              },
            ),
          ),
        ],
      ),

enter image description here

Update:

Whole page Is Scroll-able with - SingleChildScrollView.

body: SingleChildScrollView(
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
      Text(
        'Headline',
        style: TextStyle(fontSize: 18),
      ),
      SizedBox(
        height: 200.0,
        child: ListView.builder(
          physics: ClampingScrollPhysics(),
          shrinkWrap: true,
          scrollDirection: Axis.horizontal,
          itemCount: 15,
          itemBuilder: (BuildContext context, int index) => Card(
                child: Center(child: Text('Dummy Card Text')),
              ),
        ),
      ),
      Text(
        'Demo Headline 2',
        style: TextStyle(fontSize: 18),
      ),
      Card(
        child: ListTile(title: Text('Motivation $int'), subtitle: Text('this is a description of the motivation')),
      ),
      Card(
        child: ListTile(title: Text('Motivation $int'), subtitle: Text('this is a description of the motivation')),
      ),
      Card(
        child: ListTile(title: Text('Motivation $int'), subtitle: Text('this is a description of the motivation')),
      ),
      Card(
        child: ListTile(title: Text('Motivation $int'), subtitle: Text('this is a description of the motivation')),
      ),
      Card(
        child: ListTile(title: Text('Motivation $int'), subtitle: Text('this is a description of the motivation')),
      ),
    ],
  ),
),

enter image description here

Elson answered 4/2, 2019 at 8:27 Comment(6)
Thanks for your fast reply! I achieved that as well but it's not exactly what I need. I want the Motivation items not to be in a list so that the whole screen scrolls down so the horizontal list would go out of the screen if you scroll enough. Any ideas for on how to achieve that?Apotheosis
Awesome, that was exactly was I was looking for. It was indeed not complicated, thank you!!Apotheosis
This comment added on behalf of Roman Matroskin: What if the height of horizontal scrollable items can be different?Automatize
I am trying to not set the size of the list fixed with a SizedBox, why does the shrinkWrap: true does not work when you have a scroll as a parent?Anechoic
I wasted several hours trying to get this to work, until I found this complete sample here, It did not take long after finding this post. The real problem for me was that scrolling does not work on linux desktop which is my preferred testing platform for speed.Outrelief
Wonderful answer! Saved an entire day! :)Defaulter
B
55

Screenshot:

enter image description here


class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: 7,
        itemBuilder: (_, i) {
          if (i < 2)
            return _buildBox(color: Colors.blue);
          else if (i == 3)
            return _horizontalListView();
          else
            return _buildBox(color: Colors.blue);
        },
      ),
    );
  }

  Widget _horizontalListView() {
    return SizedBox(
      height: 120,
      child: ListView.builder(
        scrollDirection: Axis.horizontal,
        itemBuilder: (_, __) => _buildBox(color: Colors.orange),
      ),
    );
  }

  Widget _buildBox({Color color}) => Container(margin: EdgeInsets.all(12), height: 100, width: 200, color: color);
}
Brilliance answered 29/2, 2020 at 8:10 Comment(2)
Is it possible to use horizontal listview without static height?Honorarium
@BalajiRamadoss see my answer here https://mcmap.net/q/238369/-horizontal-listview-inside-a-vertical-scrollview-in-flutterHomochromous
H
12

We have to use SingleScrollView inside another SingleScrollView, using ListView will require fixed height

SingleChildScrollView(
   child: Column(
      children: <Widget>[
        SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
          children: [Text('H1'), Text('H2'), Text('H3')])),
        Text('V1'),
        Text('V2'),
        Text('V3')]))
Homochromous answered 16/5, 2021 at 11:27 Comment(7)
this is not the solution.Kilian
@RishabhDeepSingh reasons? I used it in my app, lower performance than ListView but more flexible with dynamic height.Homochromous
but items are not dynamicKilian
@RishabhDeepSingh you can create a list of text widget and pass it as the children of Row that is similar with ListView. The only thing is that the view is not recycle that's why ListView has better performance. I just provide another choice.Homochromous
If the list is long then it'll leg when you are using this solution may be.Arterialize
yes, it is trueHomochromous
This isn't a perfect answer for the above question, but it actually what worked for me. In my case I don't have a lot of elements so performance doesn't matter, and I don't have a fixed height for the horizontal scrollable so other solutions didn't work.Down
F
5

I tried in this code and I fixed my problem I hope solved your want it.

       SingleChildScrollView(
          scrollDirection: Axis.horizontal,
          child: Row(
            children: [
              item(),
              item(),
              item(),
              item(),
            ],
          ),
        ),
Fluidextract answered 22/4, 2022 at 10:29 Comment(3)
If we implement this solution, then what can you do if the list is long? It's going to lag, may be.Arterialize
we can use Listview shrink wrap with InstrictedHeight widget Axis.Horizontal.Fluidextract
Exactly so Listview is the right option.Arterialize
B
4

If someone gets the renderview port was exceeded error. warp your ListView in a Container widget and give it the height and width property to fix the issue

Column(
      children: <Widget>[
        Text(
          Strings.dailyTasks,
        ),
      Container(
       height: 60,
       width: double.infinity,
        child: ListView.builder(
          scrollDirection: Axis.horizontal,
          itemCount: tasks.length,
          itemBuilder: (BuildContext context, int index) =>
            taskCard(
              taskNumber: index + 1,
              taskTotal: tasks.length,
              task: tasks[index],
             ),
         ),
       )
      ]
)
Beseem answered 27/9, 2021 at 7:59 Comment(0)
S
2

hardcoding the height of horizontal list would be unpredictable for some cases.

below is horizontal list that resize itself based on first child

usage:

SelfSizedHorizontalList(
  childBuilder: (i) => itemWidget(list[i]),
  itemCount: list.length,
),

source code:

class SelfSizedHorizontalList extends StatefulWidget {
  final Widget Function(int) childBuilder;
  final int itemCount;
  final double gapSize;
  const SelfSizedHorizontalList({
    super.key,
    required this.childBuilder,
    required this.itemCount,
    this.gapSize = 8,
  });

  @override
  State<SelfSizedHorizontalList> createState() => _SelfSizedHorizontalListState();
}

class _SelfSizedHorizontalListState extends State<SelfSizedHorizontalList> {
  final infoKey = GlobalKey();

  double? prevHeight;
  double? get height {
    if (prevHeight != null) return prevHeight;
    prevHeight = infoKey.globalPaintBounds?.height;
    return prevHeight;
  }

  bool get isInit => height == null;

  @override
  Widget build(BuildContext context) {

    if (height == null) {
      WidgetsBinding.instance.addPostFrameCallback((v) => setState(() {}));
    }
    if (widget.itemCount == 0) return const SizedBox();
    if (isInit) return Container(key: infoKey, child: widget.childBuilder(0));

    return SizedBox(
      height: height,
      child: ListView.separated(
        scrollDirection: Axis.horizontal,
        itemCount: widget.itemCount,
        itemBuilder: (c, i) => widget.childBuilder.call(i),
        separatorBuilder: (c, i) => SizedBox(width: widget.gapSize),
      ),
    );
  }
}

extension GlobalKeyExtension on GlobalKey {
  Rect? get globalPaintBounds {
    final renderObject = currentContext?.findRenderObject();
    final translation = renderObject?.getTransformTo(null).getTranslation();
    if (translation != null && renderObject?.paintBounds != null) {
      final offset = Offset(translation.x, translation.y);
      return renderObject!.paintBounds.shift(offset);
    } else {
      return null;
    }
  }
}
Stephi answered 3/7, 2023 at 13:51 Comment(4)
Likely a bug, in if (isInit) ..., widget.childBuilder(1) is called, but we do not have any guarantee that there willl be such an index available in the item list. Should be 0, if we assume that there's always at least one item to display.Sappington
@MarcinWróblewski i think it's already being handled by the if (widget.itemCount == 0)?Stephi
I don't think so, if (widget.itemCount == 0) checks if the list is empty, while widget.childBuilder(1) require at least 2 elements in the list, hence the exception will be thrown if you'll try to render a single element.Sappington
@MarcinWróblewski you're right. it should be childBuilder(0) . thanks for correctionStephi
A
0

for Web Chome you have to add MaterialScrollBehavior for horizontal scrolling to work. see(Horizontal listview not scrolling on web but scrolling on mobile) I demonstrate how to use the scrollcontroller to animate the list both left and right.

import 'package:flutter/gestures.dart';
class MyCustomScrollBehavior extends MaterialScrollBehavior {
  // Override behavior methods and getters like dragDevices
  @override
  Set<PointerDeviceKind> get dragDevices => {
        PointerDeviceKind.touch,
        PointerDeviceKind.mouse,
      };
}

return MaterialApp(
      title: 'Flutter Demo',
      scrollBehavior: MyCustomScrollBehavior(),
)


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


  @override
  State<TestHorizontalListView> createState() => _TestHorizontalListViewState();
}

class _TestHorizontalListViewState extends State<TestHorizontalListView> {
  List<String> lstData=['A','B','C','D','E','F','G'];
  
  final ScrollController _scrollcontroller = ScrollController();

_buildCard(String value)
{
  return Expanded(child:Container(
    margin: const EdgeInsets.symmetric(vertical: 20.0),
    width:300,height:400,child:Card(child: Expanded(child:Text(value,textAlign: TextAlign.center, style:TextStyle(fontSize:30))),)));
}

void _scrollRight() {
    _scrollcontroller.animateTo(
      _scrollcontroller.position.maxScrollExtent,
      duration: Duration(seconds: 1),
      curve: Curves.fastOutSlowIn,
    );
  }

void _scrollLeft() {
    _scrollcontroller.animateTo(
      0,
      duration: Duration(seconds: 1),
      curve: Curves.fastOutSlowIn,
    );
  }
_segment1()
{
  return     SingleChildScrollView(child:
    Expanded(child:
        Container(height:300,
          width:MediaQuery.of(context).size.width,
          child:Row(children: [
          FloatingActionButton.small(onPressed: _scrollRight, child: const Icon(Icons.arrow_right),),
          Expanded(child:Scrollbar(child:ListView.builder(
            itemCount: lstData.length,
            controller: _scrollcontroller,
            scrollDirection: Axis.horizontal,
            itemBuilder:(context,index)
            {
              return _buildCard(lstData[index]);
            })
          ,),
        ),
        FloatingActionButton.small(onPressed: _scrollLeft, child: const Icon(Icons.arrow_left),),
    ]))
    ,
    )
    );

}
@override
  void initState() {
    //   TODO: implement initState
    super.initState();

  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: Text("horizontal listview",)),body: 
    segment1(),
);
  }
}
Awake answered 22/3, 2022 at 20:0 Comment(0)
M
0

You just have to fix your height of your Listview (by wrapping it in a SizedBox for example).

This is because the content of your listview can't be known before the frame is drawn. Just imagine a list of hundreds of items.. There is no way to directly know the maximum height among all of them.

Mangan answered 17/9, 2022 at 20:19 Comment(0)
T
0

Horizontal ListView inside Vertical ListView using Builder

None of the answers proved to solve my issue, which was to have a horizontal ListView inside a Vertical ListView while still using ListBuilder (which is more performant than simply rendering all child elements at once).

original question image

Turned out it was rather simple. Simply wrap your vertical list child inside a Column, and check if index is 0 (or index % 3 == 0) then render the horizontal list.

Seems to work fine:

final verticalListItems = [];
final horizontalListItems = [];

ListView.builder(
  shrinkWrap: true,
  itemCount: verticalListItems.length,
  itemBuilder: (context, vIndex) {
    final Chat chat = verticalListItems[vIndex];

    return Column( // Wrap your child inside this column
      children: [
        // And then conditionally render your Horizontal list
        if (vIndex == 0) ListView.builder(itemCount: horizontalListItems.length itemBuilder: (context, hIndex) => Text('Horizontal List $hIndex')),

        // Vertical list
        Text('Item No. $vIndex')
      ],
    );
  },
),
Tedford answered 19/10, 2022 at 6:30 Comment(0)
P
0

Try using physics: BouncingScrollPhysics()

If you are using a ListView - horizontal scroll inside a ListView - vertical scroll this might create a problem of overscrolling for the child Listview. In this case what worked for me was I used physics: BouncingScrollPhysics() for the child ListView, it gave a good bouncing scrolling effect and resolved my error

Perreira answered 12/5, 2023 at 12:40 Comment(0)
P
0

Put all list items inside Row widget then wrap the Row with SingleChildScrollView. Now we got a horizontal listview with dynamic list items height, differ in height from one another item.

But the drawback is that it is not like a Listview.builder or Listview.separated because it not loads items on demand.

On the other hand, Listview.builder or Listview.separated cannot be used to dynamically height items if you don't specify the height first.

Purloin answered 27/9, 2023 at 7:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.