Dynamic list of check box tile in alert dialog not working - flutter

There is no clear answer on how to implement a checkbox tile in a dialog and set the state to work.
A print statement is working in setting the state of the checkbox is not changing, but other statements are working. Where can I find the answer?
I am using a dialog with multiple check boxes for multi select. Is there another of implementing multiselect in Flutter?
child: TextFormField(
decoration: InputDecoration(
labelText: 'Team Leader',
labelStyle: TextStyle(color: Colors.black)),
controller: teamLeaderController,
enabled: false,
style: TextStyle(color: Colors.black),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return CheckBoxDialog(context, teamLeader,
"Choose Team Leader", teamLeaderController, onSubmit);
});
}),
class CheckBoxState extends State<CheckBoxDialog> {
BuildContext context;
List<String> places;
String title;
TextEditingController con;
bool state;
CheckBoxState(this.context, this.places, this.title, this.con);
#override
void initState() {
super.initState();
state = false;
}
#override
Widget build(BuildContext context) {
return new AlertDialog(
title: new Text(title),
content:
Column(children: getMultiSelectOption(context, places, con, state)),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
}),
FlatButton(
child: Text('Ok'),
onPressed: () {
widget.onSubmit("");
Navigator.of(context).pop();
})
],
);
}
List<Widget> getMultiSelectOption(BuildContext context, List<String> places,
TextEditingController con, bool state) {
List<Widget> options = [];
List<String> selectedList = [];
for (int i = 0; i < places.length; i++) {
options.add(CheckboxListTile(
title: Text(places[i]),
value: selectedList.contains(places[i]),
onChanged: (bool value) {
print("on change: $value title: ${places[i]}");
setState(() {
if (value) {
selectedList.add(places[i]);
} else {
selectedList.remove(places[i]);
}
print("contains: ${selectedList.contains(places[i])}");
print("status: $value");
});
}));
}
return options;
}
}

Suppose you have a Dialog with some Widgets such as RadioListTile, DropdowButton… or anything that might need to be updated WHILE the dialog remains visible, how to do it?
Look at this example here.
https://www.didierboelens.com/2018/05/hint-5-how-to-refresh-the-content-of-a-dialog-via-setstate/
Suppose you have a Dialog with some Widgets such as RadioListTile, DropdowButton… or anything that might need to be updated WHILE the dialog remains visible, how to do it?
Difficulty: Beginner
Background
Lately I had to display a Dialog to let the user select an item from a list and I wanted to display a list of RadioListTile.
I had no problem to show the Dialog and display the list, via the following source code:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class Sample extends StatefulWidget {
#override
_SampleState createState() => new _SampleState();
}
class _SampleState extends State<Sample> {
List<String> countries = <String>['Belgium','France','Italy','Germany','Spain','Portugal'];
int _selectedCountryIndex = 0;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){_showDialog();});
}
_buildList(){
if (countries.length == 0){
return new Container();
}
return new Column(
children: new List<RadioListTile<int>>.generate(
countries.length,
(int index){
return new RadioListTile<int>(
value: index,
groupValue: _selectedCountryIndex,
title: new Text(countries[index]),
onChanged: (int value) {
setState((){
_selectedCountryIndex = value;
});
},
);
}
)
);
}
_showDialog() async{
await showDialog<String>(
context: context,
builder: (BuildContext context){
return new CupertinoAlertDialog(
title: new Text('Please select'),
actions: <Widget>[
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Cancel');},
child: new Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Accept');},
child: new Text('Accept'),
),
],
content: new SingleChildScrollView(
child: new Material(
child: _buildList(),
),
),
);
},
barrierDismissible: false,
);
}
#override
Widget build(BuildContext context) {
return new Container();
}
}
I was surprised to see that despite the setState in lines #34-36, the selected RadioListTile was not refreshed when the user tapped one of the items.
Explanation
After some investigation, I realized that the setState() refers to the stateful widget in which the setState is invoked. In this example, any call to the setState() rebuilds the view of the Sample Widget, and not the one of the content of the dialog. Therefore, how to do?
Solution
A very simple solution is to create another stateful widget that renders the content of the dialog. Then, any invocation of the setState will rebuild the content of the dialog.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class Sample extends StatefulWidget {
#override
_SampleState createState() => new _SampleState();
}
class _SampleState extends State<Sample> {
List<String> countries = <String>['Belgium','France','Italy','Germany','Spain','Portugal'];
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_){_showDialog();});
}
_showDialog() async{
await showDialog<String>(
context: context,
builder: (BuildContext context){
return new CupertinoAlertDialog(
title: new Text('Please select'),
actions: <Widget>[
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Cancel');},
child: new Text('Cancel'),
),
new CupertinoDialogAction(
isDestructiveAction: true,
onPressed: (){Navigator.of(context).pop('Accept');},
child: new Text('Accept'),
),
],
content: new SingleChildScrollView(
child: new Material(
child: new MyDialogContent(countries: countries),
),
),
);
},
barrierDismissible: false,
);
}
#override
Widget build(BuildContext context) {
return new Container();
}
}
class MyDialogContent extends StatefulWidget {
MyDialogContent({
Key key,
this.countries,
}): super(key: key);
final List<String> countries;
#override
_MyDialogContentState createState() => new _MyDialogContentState();
}
class _MyDialogContentState extends State<MyDialogContent> {
int _selectedIndex = 0;
#override
void initState(){
super.initState();
}
_getContent(){
if (widget.countries.length == 0){
return new Container();
}
return new Column(
children: new List<RadioListTile<int>>.generate(
widget.countries.length,
(int index){
return new RadioListTile<int>(
value: index,
groupValue: _selectedIndex,
title: new Text(widget.countries[index]),
onChanged: (int value) {
setState((){
_selectedIndex = value;
});
},
);
}
)
);
}
#override
Widget build(BuildContext context) {
return _getContent();
}
}

Related

Flutter use_build_context_synchronously and SearchDelegate.close after await

My use case: I show a search UI the user can pick an item with - when the user taps an item, something async happens and depending on the result, close (https://api.flutter.dev/flutter/material/SearchDelegate/close.html) is called with either the picked item or a null. In my snippet below the async something is simply asking the user for confirmation with an AlertDialog.
This works without an issue but I updated the lint rules recently, and turned this one on: https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html. Now the linter complains about the BuildContext being used after an await in the call to close. Here is the snippet (full reproducible sample below, written with Flutter 2.10.4):
onTap: () async {
final confirmed = await _confirm(context, item) ?? false;
// Triggers 'Do not use BuildContexts across async gaps.'
// https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
close(context, confirmed ? item : null);
},
I guess this makes sense and could be dangerous in some scenarios, so I'd better fix it. My question is: how do I implement my use case the 'right' way? One solution that works is to extract suggestions to a separate StatefulWidget and guard the call with a isMounted check, but this has the following drawbacks:
It requires a separate StatefulWidget that I would otherwise not need (boilerplate I don't want).
I need to pass a callback to the widget to call close (it belongs to SearchDelegate but now it will be called by code in the widget).
As close requires a BuildContext, I either have to pass the one that the SearchDelegate has to the widget to use (yuck) or just use the one from my widget - in this case it works (as close just uses Navigator.of(context)), but what if it were necessary to pass exactly the one from SearchDelegate?
Full code:
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: _Home(),
);
}
}
class _Home extends StatefulWidget {
#override
State<_Home> createState() => _HomeState();
}
class _HomeState extends State<_Home> {
String? _picked;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton(
onPressed: () => _maybePick(context),
child: const Text('Maybe pick'),
),
Center(
child: Text(_picked != null ? 'Last pick: $_picked' : 'No pick'),
),
],
),
);
}
Future<void> _maybePick(BuildContext context) async {
final result = await showSearch<String?>(
context: context,
delegate: _PickerDelegate(),
);
if (result != null) {
setState(() {
_picked = result;
});
}
}
}
class _PickerDelegate extends SearchDelegate<String?> {
final _allItems = List.generate(200, (index) => 'Item $index');
#override
Widget buildLeading(BuildContext context) {
return IconButton(
onPressed: () => close(context, null),
icon: const BackButtonIcon(),
);
}
#override
List<Widget> buildActions(BuildContext context) {
return [
IconButton(
onPressed: () => query = '',
icon: const Icon(Icons.clear),
),
];
}
#override
Widget buildSuggestions(BuildContext context) {
final items = _allItems.where((element) => element.contains(query));
return ListView(
children: items.map((item) {
return ListTile(
title: Text(item),
onTap: () async {
final confirmed = await _confirm(context, item) ?? false;
// Triggers 'Do not use BuildContexts across async gaps.'
// https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
close(context, confirmed ? item : null);
},
);
}).toList(growable: false),
);
}
#override
Widget buildResults(BuildContext context) {
// Keep it simple for the snippet.
throw UnimplementedError('results are not supported');
}
Future<bool?> _confirm(BuildContext context, String item) async {
return showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
content: Text("Pick '$item'?"),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Yes'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('No'),
),
],
);
},
);
}
}

Setstate cannot update the value

I am a new starter for flutter, Currently I have a problem that Setstate cannot update the value.
If the user after select the date using date picker, it should be update the "_value" and display under "new Text('Current Date')".
It is successful to Setstate if I separate in Widget build, but it cannot update in List.
Where I got wrong?
import 'dart:convert';
import 'package:flutter/material.dart';
class AddFieldDynamicTest extends StatefulWidget {
#override
_AddFieldDynamicTest createState() => _AddFieldDynamicTest();
}
class _AddFieldDynamicTest extends State<AddFieldDynamicTest> {
Map<String, String> _formdata = {};
var _myPets = List<Widget>();
int _index = 1;
String _value = '';
Future _selectDate() async {
DateTime picked = await showDatePicker(
context: context,
initialDate: new DateTime.now(),
firstDate: new DateTime(2000),
lastDate: new DateTime(2100)
);
if(picked != null) setState(() => _value = picked.toString());
}
void _add() {
int keyValue = _index;
_myPets = List.from(_myPets)
..add(
Column(
key: Key("${keyValue}"),
children: <Widget>[
ListTile(
leading: Text('Pet $_index : '),
title: TextField(
onChanged: (val) => _formdata['pet${keyValue - 1}'] = val,
),
),
ListTile(
leading: Text('Name of Pet $_index : '),
title: TextField(
onChanged: (val) {
_formdata['name${keyValue - 1}'] = val;
},
),
),
new Column(
children: <Widget>[
new Text('Current Date'),
new Text(_value),
new RaisedButton(onPressed: _selectDate, child: new Text('Date picker'),)
],
)
],
));
setState(() => ++_index);
}
#override
void initState() {
// TODO: implement initState
super.initState();
_add();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => print(json.encode(_formdata)),
child: Text('Save'),
),
appBar: AppBar(
title: Text('Add Test 2'),
actions: <Widget>[
FlatButton(
child: Text('Add another'),
onPressed: _add,
),
],
),
body: ListView(
children: _myPets,
),
);
}
}
In your demo, you use a variable _myPets to store the ListView's children, and at initialization time you add a child into _myPets.
When the "Date Picker" of the child is clicked, the _value is updated and setState is performed, but the data in _myPets remains unchanged, so even if the build is redone, the UI will still remain unchanged.
At this point, if you click "Add Another", a new child will be inserted into _myPets, and since _value has a value, when the build is finished, you will see that the date selected by the last child will be displayed on the new child.
I think you should know more about how setState is used.
The best way to implement this scenario is to have your own setState for each child, as shown below (simplifying your code) :
class AddFieldDynamicTest extends StatefulWidget {
#override
_AddFieldDynamicTest createState() => _AddFieldDynamicTest();
}
class _AddFieldDynamicTest extends State<AddFieldDynamicTest> {
Map<String, String> _formdata = {};
var _myPets = List<Widget>();
int _index = 1;
void _add() {
_myPets = List.from(_myPets)
..add(AddFieldDynamicItem(_index));
setState(() {
_index += 1;
});
}
#override
void initState() {
// TODO: implement initState
super.initState();
_add();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () => print(json.encode(_formdata)),
child: Text('Save'),
),
appBar: AppBar(
title: Text('Add Test 2'),
actions: <Widget>[
FlatButton(
child: Text('Add another'),
onPressed: _add,
),
],
),
body: ListView(
children: _myPets,
),
);
}
}
class AddFieldDynamicItem extends StatefulWidget {
AddFieldDynamicItem(this._index);
final int _index;
#override
_AddFieldDynamicItem createState() => _AddFieldDynamicItem(_index);
}
class _AddFieldDynamicItem extends State<AddFieldDynamicItem> {
_AddFieldDynamicItem(this._index);
String _value = '';
final int _index;
Future _selectDate() async {
DateTime picked = await showDatePicker(
context: context,
initialDate: new DateTime.now(),
firstDate: new DateTime(2000),
lastDate: new DateTime(2100)
);
if(picked != null)
setState(() {
_value = picked.toString();
});
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
new Column(
children: <Widget>[
new Text('$_index . Current Date'),
new Text(_value),
new RaisedButton(onPressed: _selectDate, child: new Text('Date picker'),)
],
)
],
);
}
}
When setState is called the framework is notified that the internal state of the object has changed and therefore it might be necessary to rebuild the widget tree. Hence the framework schedules a build for the state.
The build function is called to rebuild the widget tree. However your Text widget [Text(_value)] that you wish to redraw is within a function private function _add(). This function is not called during the build.
However as you have correctly found out that when you place the Text widget Text(_value) within the return of build function it is redrawn everytime setState is called.
I find a hint from this post:
Flutter - Dynamically adding widget that needs to change state as well
They should have their own state
import 'dart:convert';
import 'package:flutter/material.dart';
class AddFieldDynamicTest extends StatefulWidget {
#override
_AddFieldDynamicTest createState() => _AddFieldDynamicTest();
}
class _AddFieldDynamicTest extends State<AddFieldDynamicTest> {
List<Widget> playButtonList;
void initState() {
playButtonList = <Widget>[];
super.initState();
}
var eue='';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Speech Aid'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
FlatButton(
child: Text('Add Play Button'),
color: Colors.black26,
onPressed: () {
setState(() {
playButtonList.add(PlayButton());
});
},
),
Expanded(
child: ListView.builder(
itemCount: playButtonList.length,
itemBuilder: (context, index) => playButtonList[index],
),
)
],
),
);
}
}
class PlayButton extends StatefulWidget {
#override
_PlayButtonState createState() => _PlayButtonState();
}
class _PlayButtonState extends State<PlayButton> {
int _index = 0;
#override
void initState() {
super.initState();
}
Map<String, dynamic> _formdata={};
var _value='';
#override
Widget build(BuildContext context) {
int keyValue = _index;
return Column(
key: Key("${keyValue}"),
children: <Widget>[
ListTile(
leading: Text('Pet $_index : '),
title: TextField(
onChanged: (val) => _formdata['pet${keyValue - 1}'] = val,
),
),
ListTile(
leading: Text('Name of Pet $_index : '),
title: TextField(
onChanged: (val) {
_formdata['name${keyValue - 1}'] = val;
},
),
),
new Column(
children: <Widget>[
new Text('Current Date'),
new Text(_value),
new RaisedButton(onPressed: _selectDate, child: new Text('Date picker'),)
],
)
],
);
}
Future _selectDate() async {
DateTime picked = await showDatePicker(
context: context,
initialDate: new DateTime.now(),
firstDate: new DateTime(2000),
lastDate: new DateTime(2100)
);
if(picked != null) setState(() => _value = picked.toString());
}
}

Flutter Checkbox not changing/updating/working

I am trying to learn checkboxes in Flutter.
The problem is, when I want to use checkboxes in Scaffold(body:) it is working. But I want to use it in different places like an item in ListView.
return Center(
child: Checkbox(
value: testValue,
onChanged: (bool value) {
setState() {
testValue = value;
}
},
));
But it is not working, updating and changing anything.
Edit: I solved my problem with putting checkbox in a StatefulBuilder. Thanks to #cristianbregant
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
});
Try these maybe:
return Center(
child: CheckboxListTile(
title: const Text('Animate Slowly'),
value: _valueCheck,
onChanged: (bool value) {
setState(() {
_valueCheck = value;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
);
and remember that if you are using it in a dialog or bottomsheet you need to wrap the Checkbox Widget in a Stateful builder because the state does not update.
Checkboxes require you have a Scaffold or Material as their parent. Without either of these, you get this helpful error message:
The following assertion was thrown building Checkbox(dirty, state: _CheckboxState#1163b):
No Material widget found.
Checkbox widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material.
In Flutter's material library, that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. Because of this, many material library widgets require that there be a Material widget in the tree above them.
Once you have a material ancestor, you can place the ListView as it's child and it should show fine:
class SettingsPage extends StatefulWidget {
#override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
var _foo = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Toggle Foo'),
Checkbox(
value: _foo,
onChanged: (bool value) {
setState(() => _foo = value);
},
),
],
),
],
),
);
}
}
Seems like you will have to use both initState and dispose.
See my code example below:
class SettingsOrder extends StatefulWidget {
#override
_SettingsOrderState createState() => _SettingsOrderState();
}
class _SettingsOrderState extends State<SettingsOrder> {
List options = [];
List<bool> newoptions = [];
int selectedoption;
bool checkedstatus;
bool initialcall;
Future getproductlist(selectedoption, checkedstatus, initialcall) async{
List updatedlist = [];
final arguments = ModalRoute.of(context).settings.arguments as Map;
int itempos = 0;
options.clear();
if(initialcall == false){
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : newoptions[itempos]
});
itempos++;
}
} else {
for(var item in arguments['options']){
updatedlist.add({
'checkbox' : checkedstatus
});
newoptions.add(false);
itempos++;
}
}
setState(() {
options = updatedlist;
});
}
#override
void initState(){
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
getproductlist(0, false, true);
return Scaffold(
body: SingleChildScrollView(
child: Container(
width: double.infinity,
child: ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index){
return Container(
child: Theme(
data: ThemeData(
unselectedWidgetColor: Colors.grey
),
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.trailing,
title: Text(options[index]['name']),
value: options[index]['checkbox'],
onChanged: (newvalue){
int indexposition = index;
newoptions.removeAt(indexposition);
newoptions.insert(indexposition, newvalue);
getproductlist(indexposition, newvalue, false);
},
activeColor: Color.fromRGBO(0, 130, 214, 1),
checkColor: Colors.white,
),
),
);
}
),
),
),
);
}

How to get shared preferences data before widget build?

I have a few textfields with hint text and bottom navigation. Text entered into the textfield (in Page 1) will be saved to shared preferences "on changed".
When I click on the bottom navigation next page (Page 2) and back to Page 1 again, it seems like the widget rebuild and will show the hintText before shared preference stored data display on the widget.
I have tried to get the sharedpreference data during initState but it does not work. I have also tried to use future builder however when I typed the value in the TextField is not that smooth, sometimes the text would flicker between the characters before and after. I am not sure which method should I use or whether is my my coding wrong.
Could someone advise which method should I use?
Thanks in advance!
login.dart
#override
Widget build(BuildContext context){
return new Scaffold(
appBar: new AppBar(
title: Text('Login Page'),
),
body: bottomNav[currentBottomNavIndex],
bottomNavigationBar: BottomNavigationBar(
onTap: onTapped,
currentIndex: currentBottomNavIndex,
type: BottomNavigationBarType.fixed,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text("Page1"),
),
BottomNavigationBarItem(
icon: Icon(Icons.mail),
title: Text('Page2'),
),
],
),
);
}
page1.dart
class Page1 extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _Page1State();
}
}
class _Page1State extends State<Page1> {
TextEditingController name = TextEditingController();
String name_str;
Future<String> getName(String key) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
name_str = prefs.getString(key);
setState(() {
name = new TextEditingController(text: name_str);
});
return name_str;
}
Future<bool> setName(String key, String value) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.setString(key, value);
}
#override
void initState() {
super.initState();
getName('name');
}
#override
Widget build(BuildContext context) {
//return FutureBuilder(
//future: getName('name'),
//builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
// if (snapshot.hasData) {
return Container(
margin: EdgeInsets.all(100.0),
width: 185,
child: Center(
child: TextField(
controller: name,
textAlign: TextAlign.center,
decoration: new InputDecoration(
hintText: "Name (Original)",
),
onTap:() {
name.clear();
setName('name', '');
},
onChanged: (String str) {
setState(() {
name_str = str;
setName('name', str);
});
},
)
)
);
// }else{
// return Container();
// }
// }
//);
}
}
Page2.dart
class Page2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text('Page2'),
),
);
}
}
I think the problem resides here
setState(() {
name = new TextEditingController(text: name_str);
});
doing this
new TextEditingController(text: name_str);
will create a new instance instead just update the value of name controller using shared preferences.
setState(() {
name.text=name_str;
});
This should work for you

Is there an equivalent widget in flutter to the "select multiple" element in HTML

I am searching for a widget in flutter that is equal to
<select multiple=""></select>
in flutter.
An example implementation (for the web) is MaterializeCSS Select Multiple
As seen above I should be able to provide a list of items (with some of them preselected) and at the end retrieve a list of selected items or a map or something else.
An example implementation or a link to a documentation is very appreciated.
I don't think that a widget like that currently exists in Flutter, but you can build one yourself.
On mobile phones with limited screen space it would probably make sense to display a dialog with a submit button, like this native Android dialog.
Here is a rough sketch how to implement such a dialog in less than 100 lines of code:
class MultiSelectDialogItem<V> {
const MultiSelectDialogItem(this.value, this.label);
final V value;
final String label;
}
class MultiSelectDialog<V> extends StatefulWidget {
MultiSelectDialog({Key key, this.items, this.initialSelectedValues}) : super(key: key);
final List<MultiSelectDialogItem<V>> items;
final Set<V> initialSelectedValues;
#override
State<StatefulWidget> createState() => _MultiSelectDialogState<V>();
}
class _MultiSelectDialogState<V> extends State<MultiSelectDialog<V>> {
final _selectedValues = Set<V>();
void initState() {
super.initState();
if (widget.initialSelectedValues != null) {
_selectedValues.addAll(widget.initialSelectedValues);
}
}
void _onItemCheckedChange(V itemValue, bool checked) {
setState(() {
if (checked) {
_selectedValues.add(itemValue);
} else {
_selectedValues.remove(itemValue);
}
});
}
void _onCancelTap() {
Navigator.pop(context);
}
void _onSubmitTap() {
Navigator.pop(context, _selectedValues);
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Select animals'),
contentPadding: EdgeInsets.only(top: 12.0),
content: SingleChildScrollView(
child: ListTileTheme(
contentPadding: EdgeInsets.fromLTRB(14.0, 0.0, 24.0, 0.0),
child: ListBody(
children: widget.items.map(_buildItem).toList(),
),
),
),
actions: <Widget>[
FlatButton(
child: Text('CANCEL'),
onPressed: _onCancelTap,
),
FlatButton(
child: Text('OK'),
onPressed: _onSubmitTap,
)
],
);
}
Widget _buildItem(MultiSelectDialogItem<V> item) {
final checked = _selectedValues.contains(item.value);
return CheckboxListTile(
value: checked,
title: Text(item.label),
controlAffinity: ListTileControlAffinity.leading,
onChanged: (checked) => _onItemCheckedChange(item.value, checked),
);
}
}
You can use it like this:
void _showMultiSelect(BuildContext context) async {
final items = <MultiSelectDialogItem<int>>[
MultiSelectDialogItem(1, 'Dog'),
MultiSelectDialogItem(2, 'Cat'),
MultiSelectDialogItem(3, 'Mouse'),
];
final selectedValues = await showDialog<Set<int>>(
context: context,
builder: (BuildContext context) {
return MultiSelectDialog(
items: items,
initialSelectedValues: [1, 3].toSet(),
);
},
);
print(selectedValues);
}
Is this what you want?
In case you need a short and ready to use code, follow this article
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/widgets/multi_select_dialog.dart';
/// A demo page that displays an [ElevatedButton]
class DemoPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
/// Stores the selected flavours
List<String> flavours = [];
return ElevatedButton(
child: Text('Flavours'),
onPressed: () async {
flavours = await showDialog<List<String>>(
context: context,
builder: (_) => MultiSelectDialog(
question: Text('Select Your Flavours'),
answers: [
'Chocolate',
'Caramel',
'Vanilla',
'Peanut Butter'
])) ??
[];
print(flavours);
// Logic to save selected flavours in the database
});
}
}
import 'package:flutter/material.dart';
/// A Custom Dialog that displays a single question & list of answers.
class MultiSelectDialog extends StatelessWidget {
/// List to display the answer.
final List<String> answers;
/// Widget to display the question.
final Widget question;
/// List to hold the selected answer
/// i.e. ['a'] or ['a','b'] or ['a','b','c'] etc.
final List<String> selectedItems = [];
/// Map that holds selected option with a boolean value
/// i.e. { 'a' : false}.
static Map<String, bool> mappedItem;
MultiSelectDialog({this.answers, this.question});
/// Function that converts the list answer to a map.
Map<String, bool> initMap() {
return mappedItem = Map.fromIterable(answers,
key: (k) => k.toString(),
value: (v) {
if (v != true && v != false)
return false;
else
return v as bool;
});
}
#override
Widget build(BuildContext context) {
if (mappedItem == null) {
initMap();
}
return SimpleDialog(
title: question,
children: [
...mappedItem.keys.map((String key) {
return StatefulBuilder(
builder: (_, StateSetter setState) => CheckboxListTile(
title: Text(key), // Displays the option
value: mappedItem[key], // Displays checked or unchecked value
controlAffinity: ListTileControlAffinity.platform,
onChanged: (value) => setState(() => mappedItem[key] = value)),
);
}).toList(),
Align(
alignment: Alignment.center,
child: ElevatedButton(
style: ButtonStyle(visualDensity: VisualDensity.comfortable),
child: Text('Submit'),
onPressed: () {
// Clear the list
selectedItems.clear();
// Traverse each map entry
mappedItem.forEach((key, value) {
if (value == true) {
selectedItems.add(key);
}
});
// Close the Dialog & return selectedItems
Navigator.pop(context, selectedItems);
}))
],
);
}
}
import 'package:flutter/material.dart';
import 'package:multiple_selection_dialogue_app/pages/demo_page.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: DemoPage(),
),
),
);
}
}