140

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(),
          // ...
        ],
      ),
    ),
  ],
);
7
  • 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! Commented Apr 13, 2019 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. Commented Aug 1, 2019 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. Commented Aug 26, 2019 at 9:05
  • 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. Commented May 14, 2020 at 19:33
  • 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. Commented Jan 24, 2022 at 9:16

12 Answers 12

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(),
      ),
Sign up to request clarification or add additional context in comments.

5 Comments

shrinkWrap will shrink the ListView along the main-axis. Not along the cross-axis as asked.
from api.flutter.dev: Shrink wrapping the content of the scroll view is significantly more expensive than expanding to the maximum allowed size... see: api.flutter.dev/flutter/widgets/AnimatedList/shrinkWrap.html
This is so obnoxious because you and everyone answering ignored the actual question and somehow this has so many upvotes.
the shrinkWrap param is harmful to app performance. The best way is to use CustomScrollView
Well, shrinkWrap helped me.
50

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: [...],
  ),
);

4 Comments

the best answer
agree (8 more to go)
I had used Single Child Scroll View. But even in 10 items its causing Jank. (Single item taking lots of memory in RAM because its developed my a bad programmer aka me, multiplying single item by 10 causing jank..)
Worked for me, here's my snippet for making this work with a List.generate (making this almost like a Listview.Builder): gist.github.com/Abushawish/048acfdaf956640ea6fa8b3991dbbd81
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(),
    ],

  ),
)

4 Comments

How would I do this without knowing the min/max heights of the list items?
@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
In my case ListView grows to fit the maxHeight, so not suitable.
@sindrenm See my answer here stackoverflow.com/a/67233853/106941
21

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.

2 Comments

It's also worthy to note the disadvantages. I am suggesting an edit with them.
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.
19

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.

6 Comments

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.
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.
Here it is, still in non-stable version: pub.dev/packages/prototype_constrained_box
This also has lazily build compatibility?
@Hayyaun Yes, the list can have infinite items because it's dynamically loaded.
|
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')),
                    ),
                  ],
                ),
              )),
        ),
      ),
    );
  }
}

1 Comment

:D glad to help
9

(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.'),
  ],
)

2 Comments

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.
This is a great solution.
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(),
        ));

Comments

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]));
          }

1 Comment

This is a very good solution, thank you so much I wonder why I didn't think about it before :D
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..
      ]);
    }

Comments

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);

        },)
      )  ,

Comments

-1

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(),
       ], 
     )
    
  ],
);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.