Reputation: 7974
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:
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
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
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
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
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
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
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
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.
scrollable: true
in AlertDialog
:AlertDialog(
scrollable: true, // <-- Set it to true
content: Column(
children: [...],
),
)
Column
in SingleChildScrollView
:AlertDialog(
content: SingleChildScrollView(
child: Column(
children: [...],
),
),
)
shrinkWrap: true
in ListView
:AlertDialog(
content: SizedBox(
width: double.maxFinite,
child: ListView(
shrinkWrap: true, // <-- Set this to true
children: [...],
),
),
)
Upvotes: 56
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
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
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
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
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
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