user1209216
user1209216

Reputation: 7974

Flutter - Auto size AlertDialog to fit list content

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: () => {},
                       );
                     },
                   ),
                 )
               ]
           ),
         );
       }
   );

Upvotes: 102

Views: 124730

Answers (13)

Seishin
Seishin

Reputation: 389

I faced the problem and my solution is:

AlertDialog(
 title: const Text('Title'),
 content: SingleChildScrollView( // <--- HERE
 child: SizedBox(
  width: 200, // <--- HERE, you can use MediaQuery
  child: Column(
   mainAxisSize: MainAxisSize.min, // <--- HERE
   children: [
    ListView.builder(
     shrinkWrap: true, // <--- HERE
     itemCount: 11,
      ...

It dynamic either few items and many items

Upvotes: 1

pawelsa
pawelsa

Reputation: 48

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

Upvotes: 0

Drogbut
Drogbut

Reputation: 37

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.

Upvotes: 0

Asquare17
Asquare17

Reputation: 426

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

Upvotes: 6

Fellipe Malta
Fellipe Malta

Reputation: 3530

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

Upvotes: 262

Ved Prakash
Ved Prakash

Reputation: 11

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

Upvotes: 1

CopsOnRoad
CopsOnRoad

Reputation: 268424

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

Upvotes: 56

hicnar
hicnar

Reputation: 193

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)

Upvotes: 1

bshears
bshears

Reputation: 1139

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

Column(
    mainAxisSize: MainAxisSize.min,
    children: <Widget>[
       Container(
         child: ListView.builder(
           shrinkWrap: true,
           ...
         ),
       )
    ],
);

Upvotes: 72

Prashant Dwivedi
Prashant Dwivedi

Reputation: 179

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

Upvotes: 17

Panda World
Panda World

Reputation: 1996

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

Upvotes: 5

user1209216
user1209216

Reputation: 7974

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.

Upvotes: 0

Daibaku
Daibaku

Reputation: 12626

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.
              }
            );
          },
        ),
      )
    );
  }
}

Upvotes: 0

Related Questions