128

I'm trying to create a horizontal scrolling list of items in Flutter, and I want that list to only take up the necessary height based on its children. By design “ListView tries to expand to fit the space available in its cross-direction” (from the Flutter docs), which I also notice in that it takes up the whole height of the viewport, but is there a way to make it not do this? Ideally something similar to this (which obviously doesn't work):

new ListView(
  scrollDirection: Axis.horizontal,
  crossAxisSize: CrossAxisSize.min,
  children: <Widget>[
    new ListItem(),
    new ListItem(),
    // ...
  ],
);

I realize that one way to do this is by wrapping the ListView in a Container with a fixed height. However, I don't necessarily know the height of the items:

new Container(
  height: 97.0,
  child: new ListView(
    scrollDirection: Axis.horizontal,
    children: <Widget>[
      new ListItem(),
      new ListItem(),
      // ...
    ],
  ),
);

I was able to hack together a “solution” by nesting a Row in a SingleChildScrollView in a Column with a mainAxisSize: MainAxisSize.min. However, this doesn't feel like a solution, to me:

new Column(
  mainAxisSize: MainAxisSize.min,
  children: <Widget>[
    new SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: new Row(
        children: <Widget>[
          new ListItem(),
          new ListItem(),
          // ...
        ],
      ),
    ),
  ],
);
sindrenm
  • 3,254
  • 3
  • 24
  • 30
  • 19
    Same problem, you ever figure this out? – AntonB Jan 07 '19 at 05:36
  • 2
    No solution to this problem yet, no. – sindrenm Apr 01 '19 at 16:09
  • 1
    Thanks, shrinkWrap didn't worked for me. But your code does : SingleChildScrollView(scrollDirection: Axis.horizontal, child: new Row( /* put your widgets here*/),) If you ever found a "better" solution, let us know! – Nk54 Apr 13 '19 at 12:19
  • 1
    @sindrenm I like the Column/SingleChildScrollView/Row combination, and I decided to simply build a StatelessWidget (that I called ScrollableRow) from it. I think it gets the job done and I don't really see an issue with it. I just see it as a way of explicitly stating exactly what you want and by building a widget from it, I can use it in a clean and concise manner. – NBTX Aug 01 '19 at 11:34
  • So, in the end, we cannot make the horizontal `ListView` have the height based on the height of the children? Is the `SingleChildScrollView` based `ListView` the only option? I would really like to use a `ListView` as it's more semantically correct. – Ionel Lupu Aug 26 '19 at 09:05
  • Still no solution for this? I need to use a ReorderableListView so the hack doesn't apply... – SakoDaemon Mar 05 '20 at 20:59
  • I also need that. Is there already an issue for our problem so that we can push it a bit? – Dennis Zoma Mar 08 '20 at 01:17
  • Not that I know of. – sindrenm Mar 09 '20 at 08:46
  • I'm a big fan of Flutter but at some points I think it lacks important fetures. This is one of them, I can't believe there is no good solution to this. Your Hack with SingleChildScrollView is quite good though. – Jonas May 14 '20 at 19:33
  • Try this: https://stackoverflow.com/questions/62155467/flutter-nested-vertical-and-horizontal-scrolling-with-auto-height – shranet Jun 02 '20 at 15:50
  • any work arround with this yet @sindrenm – shadow Aug 05 '20 at 03:03
  • Not as far as I am aware. I've not worked in Flutter for a very long time now, so I haven't had the need to keep looking for an answer. – sindrenm Oct 28 '20 at 12:25
  • No solution to this height issue in horizontal listview yet. none of any below answered suitably to work. what if we don't know any height? – Dhiren Basra Oct 30 '20 at 13:50
  • Any update with this? – s_o_m_m_y_e_e Jan 21 '22 at 06:48
  • 1
    Not from my side. I haven't used Flutter in a very long time, and probably won't – at least not professionally – in the foreseeable future. Considering closing this question, but that doesn't necessarily feel right, either, since it will prevent further (possibly correct) answers. – sindrenm Jan 24 '22 at 09:16

12 Answers12

62

Just set shrink property of ListView to true and it will fit the space rather than expanding.

Example:

ListView(
        shrinkWrap: true, //just set this property
        padding: const EdgeInsets.all(8.0),
        children: listItems.toList(),
      ),
Stephen Rauch
  • 47,830
  • 31
  • 106
  • 135
Taha Ali
  • 1,150
  • 9
  • 14
34

As far as I understand, you can't have a horizontal ListView inside a vertical ListView and have its height dynamically set. If your requirements allow (no infinite scrolling, small amount of elements, etc), you could use a SingleChildScrollView instead.

SingleChildScrollView(
  scrollDirection: Axis.horizontal,
  child: Row(
    children: [...],
  ),
);
lbrandao
  • 4,144
  • 4
  • 35
  • 43
27

Use ConstrainedBox to set minHeight and maxHeight

ConstrainedBox(
  constraints: new BoxConstraints(
    minHeight: 35.0,
    maxHeight: 160.0,
  ),

  child: new ListView(
    shrinkWrap: true,
    children: <Widget>[
      new ListItem(),
      new ListItem(),
    ],

  ),
)
  • 45
    How would I do this without knowing the min/max heights of the list items? – sindrenm Apr 01 '19 at 16:08
  • 1
    @sindrenm Do you really have to know the min/max heights? I mean you can set the height to be between 1 to 1000 as an example, and all that matters is that it will adjust the size of ListView to the desired height. let me know if i'm missing something – Texv May 05 '20 at 14:16
  • 7
    In my case ListView grows to fit the maxHeight, so not suitable. – lbrandao Nov 21 '20 at 22:43
  • @sindrenm See my answer here https://stackoverflow.com/a/67233853/106941 – MSpeed Jul 19 '21 at 08:41
15

Use a SingleChildScrollView with scrollDirection: Axis.horizontal and a Row inside.

Big advantages:

  1. It doesn't matter how many of widgets you need.

  2. You don't need to know the heights of the widgets.

    Widget _horizontalWrappedRow(List data) {
    
     var list = <Widget>[SizedBox(width: 16)]; // 16 is start padding
    
     //create a new row widget for each data element
     data.forEach((element) {
        list.add(MyRowItemWidget(element));
     });
    
     // add the list of widgets to the Row as children
     return SingleChildScrollView(
       scrollDirection: Axis.horizontal,
       child: Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: list,
       ),
     );
    

    }

Disadvantage:

  • Items not built lazily, so maybe avoid it for long lists.
MSpeed
  • 8,153
  • 7
  • 49
  • 61
  • It's also worthy to note the disadvantages. I am suggesting an edit with them. – Mateus Felipe Oct 13 '22 at 23:46
  • 1
    Okay, I couldn't suggest an edit because the queue is full, so I am going to consider it here. The disadvantage of this approach is that you can't build the elements lazily. This means that, although this approach should work well for small lists, for bigger lists it may cause an overhead, as all items are going to be processed, independently of being visible on the screen or not. – Mateus Felipe Oct 13 '22 at 23:49
9

This is very similar to a question asked here: Flutter ListView.builder() widget's cross Axis is taking up the entire Screen height

I believe ListViews require every item to have the same Cross Axis size. That means in this case, we are unable to set a unique height for every object, the cross axis size is fixed.

If you want to have the height of a scrollable uniquely controlled by the child itself, then you can use a SingleChildScrollView paired with a Row (note: make sure to set the scrollDirection: Axis.horizontal) output

import 'package:flutter/material.dart';

final Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: SafeArea(
          child: Container(
              // height: 100, // no need to specify here
              color: Colors.white,
              child: SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Row(
                  children: <Widget>[
                    Container(
                      height: 300,
                      width: 300,
                      color: Colors.amber[600],
                      child: const Center(child: Text('Entry A')),
                    ),
                    Container(
                      height: 100,
                      color: Colors.amber[500],
                      child: const Center(child: Text('Entry B')),
                    ),
                    Container(
                      height: 50,
                      width: 100,
                      color: Colors.amber[100],
                      child: const Center(child: Text('Entry C')),
                    ),
                    Container(
                      height: 300,
                      width: 300,
                      color: Colors.amber[600],
                      child: const Center(child: Text('Entry A')),
                    ),
                    Container(
                      height: 100,
                      color: Colors.amber[500],
                      child: const Center(child: Text('Entry B')),
                    ),
                    Container(
                      height: 50,
                      width: 100,
                      color: Colors.amber[100],
                      child: const Center(child: Text('Entry C')),
                    ),
                    Container(
                      height: 300,
                      width: 300,
                      color: Colors.amber[600],
                      child: const Center(child: Text('Entry A')),
                    ),
                    Container(
                      height: 100,
                      color: Colors.amber[500],
                      child: const Center(child: Text('Entry B')),
                    ),
                    Container(
                      height: 50,
                      width: 100,
                      color: Colors.amber[100],
                      child: const Center(child: Text('Entry C')),
                    ),
                    Container(
                      height: 300,
                      width: 300,
                      color: Colors.amber[600],
                      child: const Center(child: Text('Entry A')),
                    ),
                    Container(
                      height: 100,
                      color: Colors.amber[500],
                      child: const Center(child: Text('Entry B')),
                    ),
                    Container(
                      height: 50,
                      width: 100,
                      color: Colors.amber[100],
                      child: const Center(child: Text('Entry C')),
                    ),
                    Container(
                      height: 300,
                      width: 300,
                      color: Colors.amber[600],
                      child: const Center(child: Text('Entry A')),
                    ),
                    Container(
                      height: 100,
                      color: Colors.amber[500],
                      child: const Center(child: Text('Entry B')),
                    ),
                    Container(
                      height: 50,
                      width: 100,
                      color: Colors.amber[100],
                      child: const Center(child: Text('Entry C')),
                    ),
                  ],
                ),
              )),
        ),
      ),
    );
  }
}
Gene
  • 374
  • 3
  • 8
9

I figured out a way to do dynamic sizing for a horizontally scrolling ListView. The list can have infinite items, but each item should have the same height.

(If you only have a few items, or if your items must have different heights, see this answer instead.)

Key point:

The idea is to measure one item, let's call it a "prototype". And based on the assumption that all items are the same height, we then set the height of the ListView to match the prototype item.

This way, we can avoid having to hardcode any values, and the list can automatically resize when, for example, the user sets a larger font in the system and caused the cards to expand.

Demo:

a demo gif of infinite items scrolling

Code:

I made a custom widget, called PrototypeHeight (name inspired from IntrinsicHeight). To use it, simply pass in a Widget as a "prototype", then pass in a horizontally scrolling ListView, for example:

PrototypeHeight(
  prototype: MyItem(),
  listView: ListView.builder(
    scrollDirection: Axis.horizontal,
    itemBuilder: (_, index) => MyItem(),
  ),
)

And the implementation of PrototypeHeight is as follows (you can just paste it somewhere and start using it, or modify it as you need):

class PrototypeHeight extends StatelessWidget {
  final Widget prototype;
  final ListView listView;

  const PrototypeHeight({
    Key? key,
    required this.prototype,
    required this.listView,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        IgnorePointer(
          child: Opacity(
            opacity: 0.0,
            child: prototype,
          ),
        ),
        SizedBox(width: double.infinity),
        Positioned.fill(child: listView),
      ],
    );
  }
}

Explaination:

Let me briefly explain how this works.

Simply put, the size of a Stack widget, depends on all of its "non-positioned" children. Basically, if a Stack has 3 children, but 2 of which are Positioned, then the Stack will simply match the remaining "non-positioned" child.

In this case, I'm using the "prototype item" as one of the non-positioned children, to help the Stack decide its height (which will be the same height as the prototype). Then I'm using a very wide SizedBox to help the Stack decide its width (which will be the same width as the parent's width, typically the device's screen width, because remember: constraints go down, size go up.). I'm also adding an Opacity widget to hide the prototype (this also increases performance by skipping parts of the rendering pipeline), and an IgnorePointer widget to prevent user interactions, in case there are buttons on the prototype item.

Next, I'm using a Positioned.fill to make its child (the ListView) match the size of the Stack, which in turn, would match the height of the prototype item, and match the width of the parent. This will be the constraint passed to the ListView, so the ListView will create a "viewport" of that size, which is exactly what we wanted to achieve.

This actually covers quite a few concepts in Flutter. I plan to make a video tutorial in a bit.

WSBT
  • 33,033
  • 18
  • 128
  • 133
  • I am using this workaround. I don't know why we don't have a way to provide a `prototypeItem` that applies to the `crossAxis`. The one we have only applies to the main axis. This would make everything much easier. – Mateus Felipe Oct 05 '22 at 21:02
  • Or, at least, have a native `PrototypeConstraints` widget. I think I am going to make a package that implements something like this so we don't have to rely on `Stack`. – Mateus Felipe Oct 05 '22 at 21:03
  • 1
    Here it is, still in non-stable version: https://pub.dev/packages/prototype_constrained_box – Mateus Felipe Oct 13 '22 at 23:45
7

(This answer covers a workaround when items have different sizes. If your items have the same size/height, see this answer for a ListView approach.)

If the items have different sizes, it should not be done with a ListView, and for a good reason:

A ListView could potentially have a lot of items (or even unlimited), but figuring out "the biggest one" requires going through every item, which is against the whole point of using a ListView (dynamically load each item using the builder method).

On the other hand, a Row widget always lay out all children at once, so it's easy to find the biggest one. So if you don't have too many items (perhaps less than 100 items or so), you can use Row instead. If you need scrolling, wrap Row with a SingleChildScrollView.

demo gif

Code used in the example:

Column(
  children: [
    Container(
      color: Colors.green.shade100,
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
          children: [
            FlutterLogo(),
            FlutterLogo(size: 100),
            for (int i = 0; i < 50; i++) FlutterLogo(),
          ],
        ),
      ),
    ),
    const Text('The row has ended.'),
  ],
)
WSBT
  • 33,033
  • 18
  • 128
  • 133
  • 2
    This is the best answer. I can even shorten it further: the `ListView` was designed in a such way that prohibits the way OP wants it to use. So `ListView` should always go with defined height. And `shrinkwrap` won't help because the Flutter team itself advocates against it, as it causes performance issues. So the easiest and best solution is to set the height. – Konstantin Kozirev Jul 17 '22 at 17:59
4

Wrapping the widget by Column with mainAxisSize: MainAxisSize.min worked for me. You can try to change the height here.

var data = [
      {'name': 'Shopping', 'icon': Icons.local_shipping},
      {'name': 'Service', 'icon': Icons.room_service},
      {'name': 'Hotel', 'icon': Icons.hotel},
      {'name': 'More', 'icon': Icons.more}
];

new Container(
        constraints: new BoxConstraints(
          minHeight: 40.0,
          maxHeight: 60.0,
        ),
        color: Colors.transparent,
        child: new ListView(
          scrollDirection: Axis.horizontal,
          children: data
              .map<Widget>((e) => Column(
                    mainAxisSize: MainAxisSize.min,
                    children: <Widget>[
                      new Container(
                        width: 40,
                        height: 50, // try to change this.
                        color: Colors.transparent,
                        margin: EdgeInsets.only(right: 20, left: 4),
                        child: ClipOval(
                          child: Container(
                            padding: EdgeInsets.all(4),
                            color: Colors.white,
                            child: Icon(
                              e["icon"],
                              color: Colors.grey,
                              size: 30,
                            ),
                          ),
                        ),
                      ),
                    ],
                  ))
              .toList(),
        ));
Sanjay Sharma
  • 3,687
  • 2
  • 22
  • 38
4

I'm using a horizontal listview builder of container so to stop the Container for taking the full height i just wrapped it with Center and it worked perfectly.

itemBuilder: (context, i){
              return Center(child: word_on_line(titles[i]));
          }
Fethi
  • 766
  • 7
  • 18
  • 1
    This is a very good solution, thank you so much I wonder why I didn't think about it before :D – Alessio Nov 16 '20 at 13:03
4

By applying all the above solution there is no suitable answer found yet, which help us to set horizontal Listview If we don't know the height. we must have to set height in all the cases. so I applied the below solution which works for me without specifying any height for the list items. which @sindrenm has already mentioned in his question. so I would like to go with it till a feasible solution will found. I applied shrinkWrap: true but it will shrink the ListView along the main-axis. (only for vertical scrollView) and Not along the cross-axis as asked in the question. so in the nearest future, anyone can go with this solution in the production as well, it works great for me for my all horizontal lists.

//below declared somewhere in a class above any method and assume list has filled from some API.
var mylist = List<myModel>();


SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Row(
          children: [
                for (int index = 0; index < mylist.length; index++) 
                      listItem(mylist[index]);
         ],
        ),
      ),
    ),

    listItem(myModelObj){
      return Column(
         children[
            widget1(),
            widget2().... etc..
      ]);
    }
Dhiren Basra
  • 820
  • 9
  • 24
3

Use Expanded

 Expanded(
        child:ListView.separated(
          shrinkWrap: true,
        padding: EdgeInsets.all(10),
        separatorBuilder: (BuildContext context, int index) {
          return Align(
            alignment: Alignment.centerRight,
            child: Container(
              height: 0.5,
              width: MediaQuery.of(context).size.width / 1.3,
              child: Divider(),
            ),
          );
        },
        itemCount: dataMasterClass.length,
        itemBuilder: (BuildContext context, int index) {

          Datum datum = dataMasterClass[index];
         return sendItem(datum);

        },)
      )  ,
Rajesh Narwal
  • 898
  • 8
  • 8
0

If you do not want the listview to force your widget to fit in the cross axis make suure that they are placed in a Column() or Row() widget instead of placing it directly in a listview()

new ListView(
  scrollDirection: Axis.horizontal,
  crossAxisSize: CrossAxisSize.min,
  children: <Widget>[
    Column(
      children:[
    new ListItem(),
    new ListItem(),
       ], 
     )
    
  ],
);
Norbert
  • 6,874
  • 14
  • 40
  • 65