Flutter - Auto size AlertDialog to fit list content
FlutterListviewFlutter LayoutSizeAndroid AlertdialogFlutter Problem Overview
I need to load list cities dynamically from rest webservice and let user choice one 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 unnecesary room left at the bottom:
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: () => {},
);
},
),
)
]
),
);
}
);
Flutter Solutions
Solution 1 - Flutter
Wrap your Container
inside a Column
, in the content paramenter, inside of it, set the mainAxisSize.min
, in Column property
Solution 2 - Flutter
I know it's quite late, but have you tried this?
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: ListView.builder(
shrinkWrap: true,
...
),
)
],
);
Solution 3 - Flutter
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.
scrollable: true
in AlertDialog
:
1. Set AlertDialog(
scrollable: true, // <-- Set it to true
content: Column(
children: [...],
),
)
Column
in SingleChildScrollView
:
2. Wrap AlertDialog(
content: SingleChildScrollView(
child: Column(
children: [...],
),
),
)
shrinkWrap: true
in ListView
:
3. Set AlertDialog(
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true, // <-- Set this to true
children: [...],
),
),
)
Solution 4 - Flutter
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),
);
},
),
),
);
}
);
});
}
Solution 5 - Flutter
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),
),
),
],
),
),
);
Solution 6 - Flutter
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](https://https://api.flutter.dev/flutter/material/AlertDialog-class.html "AlertDialog class")!
So you have something like this
SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
child: ListView.builder(
shrinkWrap: true,
...
),
)
],
);
Solution 7 - Flutter
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)
Solution 8 - Flutter
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.
}
);
},
),
)
);
}
}
Solution 9 - Flutter
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.