Flutter: Minimum height on horizontal list view
Asked Answered
A

12

137

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(),
          // ...
        ],
      ),
    ),
  ],
);
Ado answered 3/5, 2018 at 13:0 Comment(15)
Same problem, you ever figure this out?Freedom
No solution to this problem yet, no.Ado
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!Acosta
@Ado 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.Summer
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.Guillemot
Still no solution for this? I need to use a ReorderableListView so the hack doesn't apply...Shawnna
I also need that. Is there already an issue for our problem so that we can push it a bit?Margemargeaux
Not that I know of.Ado
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.Appomattox
Try this: #62155967Putnem
any work arround with this yet @AdoComedo
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.Ado
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?Besotted
Any update with this?Psychologize
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.Ado
E
61

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(),
      ),
Erdman answered 1/9, 2018 at 23:40 Comment(4)
shrinkWrap will shrink the ListView along the main-axis. Not along the cross-axis as asked.Beldam
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.htmlSapele
This is so obnoxious because you and everyone answering ignored the actual question and somehow this has so many upvotes.Manouch
the shrinkWrap param is harmful to app performance. The best way is to use CustomScrollViewNonsectarian
T
40

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: [...],
  ),
);
Tiffany answered 21/11, 2020 at 23:50 Comment(4)
the best answerDeal
agree (8 more to go)Adeno
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..)Therese
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/048acfdaf956640ea6fa8b3991dbbd81Valval
H
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(),
    ],

  ),
)
Hasty answered 31/3, 2019 at 14:24 Comment(4)
How would I do this without knowing the min/max heights of the list items?Ado
@Ado 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 somethingPanteutonism
In my case ListView grows to fit the maxHeight, so not suitable.Tiffany
@Ado See my answer here https://mcmap.net/q/166596/-flutter-minimum-height-on-horizontal-list-viewDaytoday
D
18

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.
Daytoday answered 23/4, 2021 at 16:39 Comment(2)
It's also worthy to note the disadvantages. I am suggesting an edit with them.Giro
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.Giro
M
14

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.

Mellar answered 1/8, 2022 at 6:45 Comment(5)
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.Giro
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.Giro
Here it is, still in non-stable version: pub.dev/packages/prototype_constrained_boxGiro
This also has lazily build compatibility?Pernicious
@Pernicious Yes, the list can have infinite items because it's dynamically loaded.Mellar
D
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')),
                    ),
                  ],
                ),
              )),
        ),
      ),
    );
  }
}
Discount answered 2/2, 2021 at 19:15 Comment(2)
this was perfect for me, thanksBoudreaux
:D glad to helpDiscount
M
8

(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.'),
  ],
)
Mellar answered 10/2, 2022 at 18:21 Comment(1)
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.Rafa
M
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(),
        ));
Modica answered 28/5, 2020 at 14:49 Comment(1)
Exactly what I was looking for. Thank you!Bhili
A
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]));
          }
Amorino answered 5/6, 2020 at 22:33 Comment(1)
This is a very good solution, thank you so much I wonder why I didn't think about it before :DRosalindarosalinde
B
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..
      ]);
    }
Besotted answered 30/10, 2020 at 14:4 Comment(0)
A
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);

        },)
      )  ,
Alarm answered 1/5, 2020 at 20:14 Comment(0)
S
-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(),
       ], 
     )
    
  ],
);
Shoddy answered 5/8, 2021 at 20:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.