Flutter - Auto size AlertDialog to fit list content
Asked Answered
C

12

96

I need to load list cities dynamically from rest webservice and let user choose a city from alert dialog. My code:

createDialog() {

    fetchCities().then((response) {

      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Wybierz miasto'),
              content: Container(
                height: 200.0,
                width: 400.0,
                child: ListView.builder(
                  shrinkWrap: true,
                  itemCount: response.length,
                  itemBuilder: (BuildContext context, int index) {
                    return ListTile(
                      title: Text(response[index].name),
                      onTap: () => citySelected(response[index].id),
                    );
                  },
                ),
              ),
            );
          }
      );
    });
  }

Result - dialog is always 200x400, even if only 2 cities are available, there is an unnecessary room left at the bottom:

enter image description here

How to make dialog width/height to fit actual items size? If I ommit height and width parameters, I'm getting exception and no dialog shown. In native Android Java I never need to specify any dimensions, because dialog sizes itself automatically to fit.

How to fix my code to get dialog sized correctly? Note: that I don't know item count, it's dynamic.

[edit]

As suggested, I wrapped content with column:

createDialog() {
    fetchCities().then((response) {
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: Text('Wybierz miasto'),
              content: Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Container(
                      child: ListView.builder(
                        shrinkWrap: true,
                        itemCount: response.length,
                        itemBuilder: (BuildContext context, int index) {
                          return ListTile(
                            title: Text(response[index].name),
                            onTap: () => citySelected(response[index].id),
                          );
                        },
                      ),
                    )
                  ]
              ),
            );
          }
      );
    });
  }

Result - exception:

I/flutter ( 5917): ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════ I/flutter ( 5917): The following assertion was thrown during performLayout(): I/flutter ( 5917): RenderViewport does not support returning intrinsic dimensions. I/flutter ( 5917): Calculating the intrinsic dimensions would require instantiating every child of the viewport, which I/flutter ( 5917): defeats the point of viewports being lazy.

More generic code to test:

showDialog(
       context: context,
       builder: (BuildContext context) {
         return AlertDialog(
           title: Text('Select city'),
           content: Column(
               mainAxisSize: MainAxisSize.min,
               children: <Widget>[
                 Container(
                   child: ListView.builder(
                     shrinkWrap: true,
                     itemCount: 2,
                     itemBuilder: (BuildContext context, int index) {
                       return ListTile(
                         title: Text("City"),
                         onTap: () => {},
                       );
                     },
                   ),
                 )
               ]
           ),
         );
       }
   );
Courland answered 13/2, 2019 at 11:55 Comment(2)
Try taking the width and height values off of your container?Disarming
What do you mean? If width, height are missing I am getting exception 'RenderViewport does not support returning intrinsic dimensions.'Courland
G
253

Wrap your Container inside a Column, in the content parameter, inside of it, set the mainAxisSize.min, in Column property

Container(
  child: Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      ...
    ],
  )
)
Gwendolyngweneth answered 13/2, 2019 at 12:28 Comment(4)
Worked! Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ //list ], );Creamcups
Thank you! worked like a charm. I had no clue that it defaultes to mainAxisSize: MainAxisSize.max.. Makes sense.Intervenient
I want to create a showDialog of 150 x 150 pixels fixed size. I have updated container width and height as return Container( height: 150, width: 150, ); but still not working. I am getting 3:2 ratio rectangle box instead of square share. any suggestion.Kandis
Container is not required. Just mainAxisSize is enough.Lifetime
V
70

I know it's quite late, but have you tried this?

Column(
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
       Container(
         child: ListView.builder(
           shrinkWrap: true,
           ...
         ),
       )
    ],
);
Veradi answered 27/8, 2019 at 11:21 Comment(4)
This worked for me! It starts expanding unnecessarily everytime I use a Flexible widget. Widgets of fixed sizes seem to be using the space efficiently.Galenical
This for me is as good as calling wrap_content onto the widget :)Eichman
Wow! It was very simple solution, it was enough to add mainAxisSize: MainAxisSize.minSepalous
Thanks. >> mainAxisSize: MainAxisSize.minUrbina
P
54

Don't set mainAxisSize.min in your Column otherwise you might run into overflow error if the content is longer than the viewport. To solve this issue, use either of the approaches.

1. Set scrollable: true in AlertDialog:

AlertDialog(
  scrollable: true, // <-- Set it to true
  content: Column(
    children: [...],
  ),
)

2. Wrap Column in SingleChildScrollView:

AlertDialog(
  content: SingleChildScrollView( 
    child: Column(
      children: [...],
    ),
  ),
)

3. Set shrinkWrap: true in ListView:

AlertDialog(
  content: SizedBox(
    width: double.maxFinite,
    child: ListView(
      shrinkWrap: true, // <-- Set this to true
      children: [...],
    ),
  ),
)
Pinnatipartite answered 31/12, 2020 at 10:59 Comment(0)
C
17

I have a similar problem. I fixed it by adding: scrollable: true in AlertDialog

Updated Code will be :

 createDialog() {
    fetchCities().then((response) {
      showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              scrollable: true,
              title: Text('Wybierz miasto'),
              content: Container(
                height: 200.0,
                width: 400.0,
                child: ListView.builder(
                  shrinkWrap: true,
                  itemCount: response.length,
                  itemBuilder: (BuildContext context, int index) {
                    return ListTile(
                      title: Text(response[index].name),
                      onTap: () => citySelected(response[index].id),
                    );
                  },
                ),
              ),
            );
          }
      );
    });
  }
Chalone answered 25/6, 2020 at 1:57 Comment(3)
Added it, but now I am getting Unhandled Exception: Cannot hit test a render box that has never been laid out.Alyse
adding scrollable ass here solved the issue for me, thanksEscent
Adding only scrollable solved my issueFlattop
P
6

Don't use a lazy viewport like listView and wrap the column with a SingleChildScrollView

AlertDialog tries to size itself using the intrinsic dimensions of its children, widgets such as ListView, GridView, and CustomScrollView, which use lazy viewports, will not work. Consider using a scrolling widget for large content, such as SingleChildScrollView, to avoid overflow. Read more here!

So you have something like this

SingleChildScrollView(          
   child: Column(
      mainAxisSize: MainAxisSize.min,
        children: <Widget>[
           Container(
             child: ListView.builder(
               shrinkWrap: true,
           ...
         ),
       )
    ],
);
Pipsqueak answered 15/1, 2021 at 5:34 Comment(3)
But using a ListView works as expected.Self
Only listview? Can you share a code snippet.Pipsqueak
ListView, GridView, ... it works in all cases. Here's the sample codeSelf
S
5

You can take a look at how SimpleDialog does it.

Widget dialogChild = IntrinsicWidth(
  stepWidth: 56.0,
  child: ConstrainedBox(
    constraints: const BoxConstraints(minWidth: 280.0),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        if (title != null)
          Padding(
            padding: titlePadding,
            child: DefaultTextStyle(
              style: theme.textTheme.title,
              child: Semantics(namesRoute: true, child: title),
            ),
          ),
        if (children != null)
          Flexible(
            child: SingleChildScrollView(
              padding: contentPadding,
              child: ListBody(children: children),
            ),
          ),
      ],
    ),
  ),
);
Submaxillary answered 26/2, 2020 at 11:40 Comment(0)
B
1

I had a very similar problem and I came up with a solution that works for both Material and Cupertino.

The performance (especially if the list of elements gets long) as compared to what the alert dialogs with scrollable flag = true and a Column with mainAxisSize: MainAxisSize.min have to offer is way way better both loading and scrolling of the contents - just have a look at the video here: https://www.youtube.com/watch?v=2nKTGFZosr0

Also the title of the dialog does not get "scrolled up" with the rest of the elements (similar to your solution), so you can add say a filtering tool at the top and display only the elements that match a search phrase.

The source code is available here https://github.com/hicnar/fluttery_stuff Just checkout the whole thing and run the main() located in lib/dialogs/main.dart Obviously you can copy, paste, modify and use it in any way you like. No copyrights here.

Finally, in the example I have limited the height of the ListView based dialog content to max 45% height of the screen, you will find it with ease and if you change the factor to 1.0 you will get the same sizing behaviour as from the Column based approach (search for a field named screenHeightFactor)

Bargello answered 7/12, 2020 at 19:47 Comment(0)
F
1
return AlertDialog(
  shape: ShapeConstant.shapeBorder(radius: 18, borderSide: false),
  actionsPadding: PaddingConstant.defaultPadding16,
  insetPadding: const EdgeInsets.all(64.0),
  contentPadding: PaddingConstant.defaultPadding,
  title: BaseDialogTopBar.baseDialogTopBar(
      isVisible: false,
      text: StringConst.settingText9,
      onSubmit: () {
        Navigator.pop(context);
      }),
  actions: [
    Align(
      alignment: Alignment.center,
      child: Buttons.basePositiveBtn(
          width: 200,
          widgetKey: "widgetKey",
          text: "add",
          onSubmit: () {
            debugPrint("callClick");
          }),
    )
  ],
  content: SizedBox(
    width: 500,
    child: ListView.builder(
      itemCount: colorList.length,
      shrinkWrap: true,
      itemBuilder: (context, int index) {
        return ListTile(
          title: Text(colorList[index]),
          onTap: () {
            Navigator.pop(context, colorList[index]);
          },
        );
      },
    ),
  ),
);
Firecure answered 20/7, 2022 at 14:34 Comment(1)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Aland
R
0

Could you try this out?
It worked at least for me. If you need an example tell me.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class SmartDialog extends StatelessWidget {
  const SmartDialog({
    Key key,
    this.title,
    this.titlePadding,
    this.content,
    this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0),
    this.actions,
    this.semanticLabel,
  }) : assert(contentPadding != null),
       super(key: key);

  final Widget title;
  final EdgeInsetsGeometry titlePadding;
  final Widget content;
  final EdgeInsetsGeometry contentPadding;
  final List<Widget> actions;
  final String semanticLabel;

  @override
  Widget build(BuildContext context) {
    final List<Widget> children = <Widget>[];
    String label = semanticLabel;

    if (title != null) {
      children.add(new Padding(
        padding: titlePadding ?? new EdgeInsets.fromLTRB(24.0, 24.0, 24.0, content == null ? 20.0 : 0.0),
        child: new DefaultTextStyle(
          style: Theme.of(context).textTheme.title,
          child: new Semantics(child: title, namesRoute: true),
        ),
      ));
    } else {
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          label = semanticLabel;
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          label = semanticLabel ?? MaterialLocalizations.of(context)?.alertDialogLabel;
      }
    }

    if (content != null) {
      children.add(new Flexible(
        child: new Padding(
          padding: contentPadding,
          child: new DefaultTextStyle(
            style: Theme.of(context).textTheme.subhead,
            child: content,
          ),
        ),
      ));
    }

    if (actions != null) {
      children.add(new ButtonTheme.bar(
        child: new ButtonBar(
          children: actions,
        ),
      ));
    }

    Widget dialogChild = new Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: children,
    );

    if (label != null)
      dialogChild = new Semantics(
        namesRoute: true,
        label: label,
        child: dialogChild
      );

    return new Dialog(child: dialogChild);
  }
}

UPDATE

You just need to show this AreaPicker after button or something pressed.

class AreaPicker extends StatelessWidget {
  final List<Area> items;
  AreaPicker(this.items);
  @override
  Widget build(BuildContext context) {
    return SmartDialog(
      title: Text('Select Area'),
      actions: <Widget>[
        FlatButton(
          textColor: Colors.black,
          child: Text('Rather not say'),
          onPressed: () {
            Navigator.of(context, rootNavigator: true).pop();
          },
        )
      ],
      content: Container(
        height: MediaQuery.of(context).size.height / 4,
        child: ListView.builder(
          shrinkWrap: true,
          itemExtent: 70.0,
          itemCount: areas.length,
          itemBuilder: (BuildContext context, int index) {
            final Area area = areas[index];
            return GestureDetector(
              child: Center(
                child: Text(area.name),
              ),
              onTap: () { 
                Navigator.of(context, rootNavigator: true).pop();
                // some callback here.
              }
            );
          },
        ),
      )
    );
  }
}
Radiochemistry answered 13/2, 2019 at 13:7 Comment(7)
How to use it in my example?Courland
I added generic code not bound to my data source, for quick testing. It throws exception.Courland
I want to show alertDialog, with list view inside. Look at picture I included in my question.Courland
As I can see, you wrote custom dialog, but I'm not sure how to show itCourland
My result: i.imgur.com/fCAJCJA.png text is centered and extra room is also thereCourland
I ended with this (poor solution): assume some height for single list item and calculate final dialog height as multiplication of items countCourland
I want to create a showDialog of 150 x 150 pixels fixed size. I have updated container width and height as return Container( height: 150, width: 150, ); but still not working. I am getting 3:2 ratio rectangle box instead of square share. any suggestion.Kandis
C
0

So that's my final solution:

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';

typedef Widget ItemBuilder<T>(T item);

class CityChoiceDialog<T> extends StatefulWidget {
  final T initialValue;
  final List<T> items;
  final ValueChanged<T> onSelected;
  final ValueChanged<T> onSubmitted;
  final ValueChanged<T> onCancelled;
  final Widget title;
  final EdgeInsetsGeometry titlePadding;
  final EdgeInsetsGeometry contentPadding;
  final String semanticLabel;
  final ItemBuilder<T> itemBuilder;
  final List<Widget> actions;
  final Color activeColor;
  final String cancelActionButtonLabel;
  final String submitActionButtonLabel;
  final Color actionButtonLabelColor;

  final Widget divider;

  CityChoiceDialog({
    Key key,
    this.initialValue,
    @required this.items,
    this.onSelected,
    this.onSubmitted,
    this.onCancelled,
    this.title,
    this.titlePadding,
    this.contentPadding = const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0),
    this.semanticLabel,
    this.actions,
    this.itemBuilder,
    this.activeColor,
    this.cancelActionButtonLabel,
    this.submitActionButtonLabel,
    this.actionButtonLabelColor,
    this.divider = const Divider(height: 0.0),
  })  : assert(items != null),
        super(key: key);

  @override
  _CityChoiceDialogState<T> createState() =>
      _CityChoiceDialogState<T>();
}

class _CityChoiceDialogState<T>
    extends State<CityChoiceDialog<T>> {
  T _chosenItem;

  @override
  void initState() {
    _chosenItem = widget.initialValue;
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MyAlertDialog(
      title: widget.title,
      titlePadding: widget.titlePadding,
      contentPadding: widget.contentPadding,
      semanticLabel: widget.semanticLabel,
      content: _buildContent(),
      actions: _buildActions(),
      divider: widget.divider,
    );
  }

  _buildContent() {
    return ListView(
      shrinkWrap: true,
      children: widget.items
          .map(
            (item) => RadioListTile(
          title: widget.itemBuilder != null
              ? widget.itemBuilder(item)
              : Text(item.toString()),
          activeColor:
          widget.activeColor ?? Theme.of(context).accentColor,
          value: item,
          groupValue: _chosenItem,
          onChanged: (value) {
            if (widget.onSelected != null) widget.onSelected(value);
            setState(() {
              _chosenItem = value;
            });
          },
        ),
      )
          .toList(),
    );
  }

  _buildActions() {
    return widget.actions ??
        <Widget>[
          FlatButton(
            textColor:
            widget.actionButtonLabelColor ?? Theme.of(context).accentColor,
            child: Text(widget.cancelActionButtonLabel ?? 'ANULUJ'),
            onPressed: () {
              Navigator.pop(context);
              if (widget.onCancelled!= null) widget.onCancelled(_chosenItem);
            },
          ),
          FlatButton(
            textColor:
            widget.actionButtonLabelColor ?? Theme.of(context).accentColor,
            child: Text(widget.submitActionButtonLabel ?? 'WYBIERZ'),
            onPressed: () {
              Navigator.pop(context);
              if (widget.onSubmitted != null) widget.onSubmitted(_chosenItem);
            },
          )
        ];
  }
}

class MyAlertDialog<T> extends StatelessWidget {
  const MyAlertDialog({
    Key key,
    this.title,
    this.titlePadding,
    this.content,
    this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0),
    this.actions,
    this.semanticLabel,
    this.divider = const Divider(
      height: 0.0,
    ),
    this.isDividerEnabled = true,
  })  : assert(contentPadding != null),
        super(key: key);

  final Widget title;
  final EdgeInsetsGeometry titlePadding;
  final Widget content;
  final EdgeInsetsGeometry contentPadding;
  final List<Widget> actions;
  final String semanticLabel;
  final Widget divider;

  final bool isDividerEnabled;

  @override
  Widget build(BuildContext context) {
    final List<Widget> children = <Widget>[];
    String label = semanticLabel;

    if (title != null) {
      children.add(new Padding(
        padding: titlePadding ??
            new EdgeInsets.fromLTRB(
                24.0, 24.0, 24.0, isDividerEnabled ? 20.0 : 0.0),
        child: new DefaultTextStyle(
          style: Theme.of(context).textTheme.title,
          child: new Semantics(child: title, namesRoute: true),
        ),
      ));
      if (isDividerEnabled) children.add(divider);
    } else {
      switch (defaultTargetPlatform) {
        case TargetPlatform.iOS:
          label = semanticLabel;
          break;
        case TargetPlatform.android:
        case TargetPlatform.fuchsia:
          label = semanticLabel ??
              MaterialLocalizations.of(context)?.alertDialogLabel;
      }
    }

    if (content != null) {
      children.add(new Flexible(
        child: new Padding(
          padding: contentPadding,
          child: new DefaultTextStyle(
            style: Theme.of(context).textTheme.subhead,
            child: content,
          ),
        ),
      ));
    }

    if (actions != null) {
      if (isDividerEnabled) children.add(divider);
      children.add(new ButtonTheme.bar(
        child: new ButtonBar(
          children: actions,
        ),
      ));
    }

    Widget dialogChild = new Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: children,
    );

    if (label != null)
      dialogChild =
      new Semantics(namesRoute: true, label: label, child: dialogChild);

    return new Dialog(child: dialogChild);
  }
}

It's based on https://pub.dev/packages/easy_dialogs and so far it works fine. I'm sharing it, as it may be useful, problem is not trivial.

Courland answered 9/9, 2019 at 6:48 Comment(0)
F
0

In addition to what has already been said about content part, wrap AlertDialog with FractionallySizedBox to give a dynamic height and width to the art box.

Faller answered 25/10, 2022 at 14:18 Comment(0)
A
0

I try not to use shrinkWrap with long lists. I measure how many items are expected to fill the available space and add a few to make sure that there is no strange behavior.

// e.g. dialog should be no bigger than 70% of the screen
final maximumHeightOfDialog = MediaQuery.of(context).size.height * 0.7;
Dialog(
      child: ConstrainedBox(
        constraints: BoxConstraints(
          maxHeight: maximumHeightOfDialog,
        ),
        child: Padding(
          padding: EdgeInsets.all(18),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // probably title or sth
              SizedBox(
                height: 16,
              ),
              // better than Expanded as it does not force to fill the available space, 
              //but only takes as much as necessary
              Flexible(
                child: AnimatedSize( // animate changes in size, when the list 
                  //size is changing, e.g. search functionality
                  duration: Duration(milliseconds: 200),
                  child: ListView.builder(
                    itemCount: options.length,
                    // e.g. I expect only 10 items to fill my screen, 
                    //so I added 2 additional items. I get better 
                    //performance with shrinkWrap = false when there are lots of items
                    shrinkWrap: options.length < 12,
                    itemBuilder: (context, index) {
                      return SomeItem()
                    },
                  ),
                ),
              ),
              SizedBox(
                height: 28,
              ),
              // action buttons
            ],
          ),
        ),
      ),
    );
Arelus answered 27/3 at 8:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.