Flutter setState not updating variable inside ExpansionPanelList - flutter

I'm trying to create a filter that update the selected option like the image using a ExpansionPanelList, something like this...
Goal
In my code I'm trying to update a subtitle Text from a property returned from the body of the same ListTile Widget which contain the RadioListTile Widget inside of ExpansionPanel Widget inside of ExpansionPanelList Widget.
The value I want is from another StatefulWidget class where the RadioListTile works perfectly, and the value is returned by a Callback to the class I need to use this variable named _orderByOptionSelected, but the variable I'm using is not updated even inside of the setState method.
Here is the class that contains the RadioListTile selection:
class ElementFilterOrderBy extends StatefulWidget {
const ElementFilterOrderBy({Key? key, required this.onChanged})
: super(key: key);
static const String best = 'best';
static const String reviews = 'reviews';
static const String price = 'price';
static const String location = 'location';
final Function(String) onChanged;
#override
State<ElementFilterOrderBy> createState() => _ElementFilterOrderByState();
}
class _ElementFilterOrderByState extends State<ElementFilterOrderBy> {
String _orderBySelection = ElementFilterOrderBy.best;
#override
Widget build(BuildContext context) {
return Column(
children: [
RadioListTile<String>(
title: const Text(ElementFilterOrderBy.best),
value: ElementFilterOrderBy.best,
groupValue: _orderBySelection,
onChanged: (value) {
setState(() {
_orderBySelection = value!;
widget.onChanged(_orderBySelection);
});
},
activeColor: kPrimaryColor,
),
RadioListTile<String>(
title: const Text(ElementFilterOrderBy.reviews),
value: ElementFilterOrderBy.reviews,
groupValue: _orderBySelection,
onChanged: (value) {
setState(() {
_orderBySelection = value!;
widget.onChanged(_orderBySelection);
});
},
activeColor: kPrimaryColor,
),
RadioListTile<String>(
title: const Text(ElementFilterOrderBy.price),
value: ElementFilterOrderBy.price,
groupValue: _orderBySelection,
onChanged: (value) {
setState(() {
_orderBySelection = value!;
widget.onChanged(_orderBySelection);
});
},
activeColor: kPrimaryColor,
),
RadioListTile<String>(
title: const Text(ElementFilterOrderBy.location),
value: ElementFilterOrderBy.location,
groupValue: _orderBySelection,
onChanged: (value) {
setState(() {
_orderBySelection = value!;
widget.onChanged(_orderBySelection);
});
},
activeColor: kPrimaryColor,
),
],
);
}
}
And this is my class where I'm trying to update the value returned:
class CustomBottomSheet extends StatefulWidget {
const CustomBottomSheet({Key? key}) : super(key: key);
#override
State<CustomBottomSheet> createState() => _CustomBottomSheetState();
}
class _CustomBottomSheetState extends State<CustomBottomSheet> {
late String _orderByOptionSelected;
late String _searchLocation;
late List<ItemExpansionPanel> _optionsFilter;
#override
void initState() {
super.initState();
_orderByOptionSelected = 'best';
_searchLocation = 'Actual Location';
_optionsFilter = [
ItemExpansionPanel(
headerValue: kFilterOptionOrderBy,
widgetBody: ElementFilterOrderBy(
onChanged: (selectedOption) {
setState(() {
_orderByOptionSelected = selectedOption;
});
},
),
optionSelected: _orderByOptionSelected,
),
ItemExpansionPanel(
headerValue: kFilterOptionLocation,
widgetBody: Container(),
optionSelected: _searchLocation,
),
];
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(kPaddingApp),
child: Column(
children: [
const Text(
kFilterTitle,
style: kTextStyleBoldBlackBig,
),
const SizedBox(
height: kMarginApp,
),
Expanded(
child: SingleChildScrollView(
child: _buildPanel(),
),
),
],
),
);
}
Widget _buildPanel() {
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_optionsFilter[index].isExpanded = !isExpanded;
});
},
children: _optionsFilter.map<ExpansionPanel>((ItemExpansionPanel item) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
subtitle: Text(
item.optionSelected,
style: const TextStyle(
color: kAccentColor,
),
),
);
},
body: item.widgetBody,
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}
// stores ExpansionPanel state information
class ItemExpansionPanel {
ItemExpansionPanel({
required this.headerValue,
required this.widgetBody,
required this.optionSelected,
this.isExpanded = false,
});
final Widget widgetBody;
final String headerValue;
bool isExpanded;
String optionSelected;
}
Edit 1: Added more elements on the list to only change the ItemExpansionPanel selected

You should use _orderByOptionSelected as text value not item.optionSelected.
go to CustomBottomSheet then _buildPanel() widget then change it to this.
Widget _buildPanel() {
return ExpansionPanelList(
expansionCallback: (int index, bool isExpanded) {
setState(() {
_optionsFilter[index].isExpanded = !isExpanded;
});
},
children: _optionsFilter.map<ExpansionPanel>((ItemExpansionPanel item) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(item.headerValue),
subtitle: Text(
_orderByOptionSelected,
// item.optionSelected, <== DELETE This
style: const TextStyle(
color: Colors.purple,
),
),
);
},
body: item.widgetBody,
isExpanded: item.isExpanded,
);
}).toList(),
);
}
}

Related

controlling a dropdown value in flutter

I'm creating a dynamic form in which the user adds more group fields. so initially there is no form but an add button. with this button the user can add as many form fields as they need. this from is a group form consisting two TextFormField and one DropdownButton.
so lets say the user added 4 group forms and filled each form. but then they changed their minds and wanted to remove the second form. when they do that it removes the last index of the listview, but the value is removed correctly at the selected index. for the textfields i can create a list of controllers and dispose them. but how can i do it for the dropdown?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Purchased(),
);
}
}
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
List<TextEditingController> textControllerList = [];
List<TextEditingController> textControllerList1 = [];
#override
void dispose() {
textControllerList.forEach((element) {
element.dispose();
});
textControllerList1.forEach((element) {
element.dispose();
});
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
const Text('phone'),
Text(list[index].phone),
const Text('email'),
Text(list[index].email),
Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
TextEditingController controller = TextEditingController();
TextEditingController controller1 = TextEditingController();
textControllerList.add(controller);
textControllerList1.add(controller1);
return MyForm(
// dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
textEditingController: textControllerList[index],
textEditingController1: textControllerList1[index],
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
setState(() {
list[index].setPhone(phoneVal);
});
}
},
onChangeEmail: (emailVal) {
if (emailVal != null) {
list[index].setEmail(emailVal);
setState(() {});
}
},
onChangeCategory: (categoryVal) {
if (categoryVal != null) {
list[index].setCategory(categoryVal);
setState(() {});
}
},
// every changes here will update your current list value
onremove: () {
list.removeAt(index);
textControllerList.removeAt(index);
textControllerList1.removeAt(index);
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final Function(String?) onChangeEmail;
final Function(String?) onChangeCategory;
final TextEditingController textEditingController;
final TextEditingController textEditingController1;
final VoidCallback? onremove;
const MyForm({
super.key,
required this.initInfo,
required this.onChangePhone,
required this.onChangeEmail,
required this.onChangeCategory,
required this.onremove,
required this.textEditingController,
required this.textEditingController1,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
List<UserInfo> list = <UserInfo>;
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(
onPressed: widget.onremove,
icon: const Icon(
Icons.remove,
)),
TextFormField(
controller: widget.textEditingController,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: widget.textEditingController1,
onChanged: widget.onChangeEmail,
),
DropdownButton(
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
onChanged: widget.onChangeCategory)
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _email = '';
String _category = '';
/// getter
String get phone => _phone;
String get email => _email;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setEmail(String email) {
_email = email;
}
void setCategory(String category) {
_category = category;
}
}
PLEASE any help is appreciated.
In order this code to work you are going to need to install the Provider Package.
With this solution using provider, you dont need to worry about the controllers.
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => FormsProvider(),
),
],
child: const Purchased(),
),
);
}
}
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
Widget build(BuildContext context) {
return Consumer<FormsProvider>(
builder: (context, formsProvider, child) {
List<Form> formsList = formsProvider.listOfForms;
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
formsProvider
.addFormToList(DateTime.now().millisecondsSinceEpoch);
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: formsList.length,
itemBuilder: ((context, index) {
UserInfo formItemInfo = formsList[index].userInfo;
return Column(
children: [
const Text('phone'),
Text(formItemInfo.phone),
const Text('email'),
Text(formItemInfo.email),
const Text('category'),
Text(formItemInfo.category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: formsList.length,
itemBuilder: ((context, index) {
Form form = formsList[index];
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(
onPressed: () {
formsProvider.removeFormFromList(form);
},
icon: const Icon(
Icons.remove,
),
),
TextFormField(
onChanged: (phoneVal) {
formsProvider.setPhone(form.id, phoneVal);
},
),
TextFormField(
onChanged: (emailVal) {
formsProvider.setEmail(form.id, emailVal);
},
),
DropdownButton(
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
onChanged: (categoryVal) {
if (categoryVal != null) {
formsProvider.setCategory(
form.id, categoryVal);
}
},
)
],
),
);
})),
)
],
),
);
},
);
}
}
class FormsProvider extends ChangeNotifier {
List<Form> _listOfForms = [];
List<Form> get listOfForms => _listOfForms;
void addFormToList(int id) {
_listOfForms.add(
Form(id: id, userInfo: UserInfo(category: '', email: '', phone: '')));
notifyListeners();
}
void removeFormFromList(Form form) {
_listOfForms.remove(form);
notifyListeners();
}
void setEmail(int idForm, String newEmail) {
_listOfForms.firstWhere((element) => element.id == idForm).userInfo.email =
newEmail;
notifyListeners();
}
void setPhone(int idForm, String newPhone) {
_listOfForms.firstWhere((element) => element.id == idForm).userInfo.phone =
newPhone;
notifyListeners();
}
void setCategory(int idForm, String newCategory) {
_listOfForms
.firstWhere((element) => element.id == idForm)
.userInfo
.category = newCategory;
notifyListeners();
}
}
class Form {
int id;
UserInfo userInfo;
Form({
required this.id,
required this.userInfo,
});
}
class UserInfo {
String phone;
String email;
String category;
UserInfo({
this.email = '',
this.phone = '',
this.category = '',
});
}
you could create a class
class GroupForm extends StatefulWidget{
TextEditingController controller = TextEditingController();
TextEditingController controller1 = TextEditingController();
List category = [];
GroupForm(this.controller ,this.controller1,this.category)
Widget build(){
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(
onPressed: widget.onremove,
icon: const Icon(
Icons.remove,
)),
TextFormField(
controller: widget.textEditingController,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: widget.textEditingController1,
onChanged: widget.onChangeEmail,
),
DropdownButton(
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
onChanged: widget.onChangeCategory)
],
),
);
}
}
and then you could create a List<GroupForm> to add and remove any object.
List<GroupForm> items = []
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return items[index]
},
)

in flutter dynamic listview removing an item is removing the last instead of removing selected index

I've recently asked a question on how to create a group of form dynamically. and i've got an answer. but the problem was when removed an index of the group it removes the last added form. but the value is correct. the group form consists of two text form fields and one dropdown. (code is below)
for example if i add 3 dynamic group formfields and removed the second index index[1] the ui update will remove the last index but the removed value is only the selected index. why is the ui not working as expected?
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
Text('phone'),
Text(list[index].phone),
Text('email'),
Text(list[index].email),
Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return MyForm(
// dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
list[index].setPhone(phoneVal);
setState(() {});
}
},
// every changes here will update your current list value
onchangeEmail: (emailVal) {
if (emailVal != null) {
list[index].setEmail(emailVal);
setState(() {});
}
},
onchangeCategory: (categoryVal) {
if (categoryVal != null) {
list[index].setCategory(categoryVal);
setState(() {});
}
},
onremove: () {
list.removeAt(index);
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final Function(String?) onchangeEmail;
final Function(String?) onchangeCategory;
final VoidCallback? onremove;
const MyForm({
key,
required this.initInfo,
required this.onChangePhone,
required this.onchangeEmail,
required this.onchangeCategory,
required this.onremove,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
TextEditingController _phoneCtrl = TextEditingController();
TextEditingController _emailCtrl = TextEditingController();
String? selected;
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
void initState() {
super.initState();
// set init value
_phoneCtrl = TextEditingController(text: widget.initInfo.phone);
_emailCtrl = TextEditingController(text: widget.initInfo.email);
selected = widget.initInfo.category;
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
TextFormField(
controller: _phoneCtrl,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: _emailCtrl,
onChanged: widget.onchangeEmail,
),
DropdownButtonFormField2(
//key: _key,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
buttonHeight: 60,
buttonPadding: const EdgeInsets.only(left: 20, right: 10),
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
validator: (value) {
if (value == null) {
return 'Please select Catagory.';
}
},
onChanged: widget.onchangeCategory,
onSaved: widget.onchangeCategory)
/// same like TextFormField, you can create new widget below
/// for dropdown, you have to 2 required value
/// the initValue and the onchage function
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _email = '';
String _category = '';
/// getter
String get phone => _phone;
String get email => _email;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setEmail(String email) {
_email = email;
}
void setCategory(String category) {
_category = category;
}
}
any help is appreciated.
new approach. worked for text field but not dropdown
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
List<TextEditingController> textControllerList = [];
List<TextEditingController> textControllerList1 = [];
Map<String, String> listCtrl = {};
#override
void dispose() {
textControllerList.forEach((element) {
element.dispose();
});
textControllerList1.forEach((element) {
element.dispose();
});
listCtrl;
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
Text('phone'),
Text(list[index].phone),
Text('email'),
Text(list[index].email),
Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
TextEditingController controller = TextEditingController();
TextEditingController controller1 = TextEditingController();
textControllerList.add(controller);
textControllerList1.add(controller1);
return MyForm(
// dont forget use the key, to make sure every MyForm is has identity. to avoid missed build
textEditingController: textControllerList[index],
textEditingController1: textControllerList1[index],
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
dataCtrl: listCtrl,
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
list[index].setPhone(phoneVal);
setState(() {});
}
},
// every changes here will update your current list value
onchangeEmail: (emailVal) {
if (emailVal != null) {
list[index].setEmail(emailVal);
setState(() {});
}
},
onchangeCategory: (categoryVal) {
if (categoryVal != null) {
list[index].setCategory(categoryVal);
setState(() {});
}
},
onremove: () {
list.removeAt(index);
textControllerList.removeAt(index);
textControllerList1.removeAt(index);
if (listCtrl.containsKey(index)) {
listCtrl.remove(index);
}
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final Function(String?) onchangeEmail;
final Function(String?) onchangeCategory;
final TextEditingController textEditingController;
final TextEditingController textEditingController1;
Map<String, String> dataCtrl = {};
final VoidCallback? onremove;
MyForm({
key,
required this.initInfo,
required this.onChangePhone,
required this.onchangeEmail,
required this.onchangeCategory,
required dataCtrl,
required this.onremove,
required this.textEditingController,
required this.textEditingController1,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
TextEditingController _phoneCtrl = TextEditingController();
TextEditingController _emailCtrl = TextEditingController();
String? selected;
final List<String> category = [
'Manager',
'Reception',
'Sales',
'Service',
];
#override
void initState() {
super.initState();
// set init value
_phoneCtrl = TextEditingController(text: widget.initInfo.phone);
_emailCtrl = TextEditingController(text: widget.initInfo.email);
selected = widget.initInfo.category;
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(onPressed: widget.onremove, icon: Icon(Icons.remove)),
TextFormField(
controller: widget.textEditingController,
onChanged: widget.onChangePhone,
),
TextFormField(
controller: widget.textEditingController1,
onChanged: widget.onchangeEmail,
),
DropdownButtonFormField2(
//key: _key,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
buttonHeight: 60,
//value: category[1],
value: selected!.isEmpty ? null : selected,
buttonPadding: const EdgeInsets.only(left: 20, right: 10),
items: category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
validator: (value) {
if (value == null) {
return 'Please select Catagory.';
}
},
onChanged: widget.onchangeCategory,
onSaved: widget.onchangeCategory,
)
/// same like TextFormField, you can create new widget below
/// for dropdown, you have to 2 required value
/// the initValue and the onchage function
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _email = '';
String _category = '';
/// getter
String get phone => _phone;
String get email => _email;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setEmail(String email) {
_email = email;
}
void setCategory(String category) {
_category = category;
}
}
You are dynamically creating TextEditingControllers but have no way of keeping track of them. You need a way to keep track of all the controllers by creating a List<TextEditingController>
The reason your code is not working, other than the above, is because you are setting the text for each textEditingController in the initState() method. This only gets called once, so when the tree rebuilds it is using the 'old' value stored in the controller.
I propose the following:
MyForm() should take a textEditingController as a parameter
On the Purchase() class create a List<TextEditingControllers>
Using the index on ListView.builder dynamically add a textController to the list each time you add a new widget.
Remove the textController when the removeAt() method is called.
Don't forget to dispose your textEditingControllers
Please refer to the code below.
EDIT *** As requested I have added the implementation of the dropdownmenu. Enjoy
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Purchased(),
);
}
}
class Purchased extends StatefulWidget {
const Purchased({Key? key}) : super(key: key);
#override
State<Purchased> createState() => _PurchasedState();
}
class _PurchasedState extends State<Purchased> {
List<UserInfo> list = [];
List<TextEditingController> textControllerList = [];
List<String> catergories = [
'Manager',
'Reception',
'Sales',
'Service',
];
final List<String?> selectedValueList = [];
#override
void dispose() {
for (var element in textControllerList) {
element.dispose();
}
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
/// every time you add new Userinfo, it will generate new FORM in the UI
list.add(UserInfo());
setState(() {}); // dont forget to call setState to update UI
},
child: const Icon(Icons.add),
),
body: Column(
children: [
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
return Column(
children: [
const Text('phone'),
Text(list[index].phone),
const Text('category'),
Text(list[index].category)
],
);
})),
),
Expanded(
child: ListView.builder(
shrinkWrap: true,
itemCount: list.length,
itemBuilder: ((context, index) {
TextEditingController controller = TextEditingController();
textControllerList.add(controller);
String? selectedValue;
selectedValueList.add(selectedValue);
return MyForm(
category: catergories,
selectedValue: selectedValueList[index],
textEditingController: textControllerList[index],
key: ValueKey(index),
//pass init value so the widget always update with current value
initInfo: list[index],
// every changes here will update your current list value
onChangePhone: (phoneVal) {
if (phoneVal != null) {
setState(() {
list[index].setPhone(phoneVal);
});
}
},
// every changes here will update your current list value
onchangeCategory: (categoryVal) {
if (categoryVal != null) {
selectedValueList[index] = categoryVal;
list[index].setCategory(categoryVal);
setState(() {});
}
},
onremove: () {
list.removeAt(index);
textControllerList.removeAt(index);
selectedValueList.removeAt(index);
setState(() {});
});
})),
)
],
),
);
}
}
class MyForm extends StatefulWidget {
final UserInfo initInfo;
final Function(String?) onChangePhone;
final TextEditingController textEditingController;
final Function(String?) onchangeCategory;
final VoidCallback? onremove;
final String? selectedValue;
final List category;
const MyForm({
super.key,
required this.initInfo,
required this.onChangePhone,
required this.onremove,
required this.textEditingController,
required this.onchangeCategory,
required this.selectedValue,
required this.category,
});
#override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(12),
child: Column(
children: [
IconButton(
onPressed: widget.onremove,
icon: const Icon(
Icons.remove,
)),
TextFormField(
controller: widget.textEditingController,
onChanged: widget.onChangePhone,
),
DropdownButtonFormField2(
value: widget.selectedValue,
//key: _key,
decoration: InputDecoration(
isDense: true,
contentPadding: EdgeInsets.zero,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
),
),
isExpanded: true,
hint: const Text(
'Select Category',
style: TextStyle(fontSize: 14),
),
icon: const Icon(
Icons.arrow_drop_down,
color: Colors.black45,
),
iconSize: 30,
buttonHeight: 60,
buttonPadding: const EdgeInsets.only(left: 20, right: 10),
items: widget.category
.map((item) => DropdownMenuItem<String>(
value: item,
child: Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
))
.toList(),
validator: (value) {
if (value == null) {
return 'Please select Catagory.';
}
return null;
},
onChanged: widget.onchangeCategory,
)
],
),
);
}
}
class UserInfo {
///define
String _phone = '';
String _category = '';
/// getter
String get phone => _phone;
String get category => _category;
///setter
void setPhone(String phone) {
_phone = phone;
}
void setCategory(String category) {
_category = category;
}
}

Flutter - add together the number of SwitchListTile = true

I have an app I'm working on. It sounds simple in theory of what I want to do, but just cannot make it work.
I want to output Text of how many of the SwitchLiStTile's are true. There are 8 SwitchListTiles, if someone clicks the 3rd and 5th ones, I want the output to be 2. I cannot grasp how I would accomplish this. Everything I have tried has failed. If I could just make the value of the Switch an integer, this would be simple.
removed 1st example code
Granted, if there was truly on 2 switches, this would be way easier. There are 8 (4 in this example) and will be more. This is just shorthand code because I felt I needed to put something. How would I go about getting this solved? I have tried converting the Bools to integers and that just adds more problems. I Can't just use a Dart Operator to add them together when they are not integers anyways. Nothing seems to work without writing line after line, within a HUGE if statement. I'm working with 8 switches which give a huge number of possibilities.
Any help would be awesome.
*** OK so I am going to add some simple code and try and explain what I am doing and what I want.
I'll go ahead and add the 3 files I'm using.
main.dart
import 'package:flutter/material.dart';
import 'result.dart';
import 'data.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TestApp',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int cluecounter = 0;
final wat = [
false,
false,
false,
false,
];
final _formKey = GlobalKey<FormState>();
final _user = User();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TestApp'),
),
body: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Builder(
builder: (context) => Form(
key: _formKey,
child: Container(
child: Column(
children: [
Text('Just A Test'),
SwitchListTile(
title: const Text('SwitchListTile 1'),
value: _user.wat1,
onChanged: (bool val) {
return setState(() {
if (_user.wat1 == true) {
cluecounter--;
} else {
cluecounter++;
}
_user.wat1 = val;
});
}),
SwitchListTile(
title: const Text('SwitchListTile 2'),
value: _user.wat2,
onChanged: (bool val) {
return setState(() {
if (_user.wat2 == true) {
cluecounter--;
} else {
cluecounter++;
}
_user.wat2 = val;
});
}),
SwitchListTile(
title: const Text('SwitchListTile 3'),
value: _user.wat3,
onChanged: (bool val) {
return setState(() {
if (_user.wat3 == true) {
cluecounter--;
} else {
cluecounter++;
}
_user.wat3 = val;
});
}),
SwitchListTile(
title: const Text('SwitchListTile 4'),
value: _user.wat4,
onChanged: (bool val) {
return setState(() {
if (_user.wat4 == true) {
cluecounter--;
} else {
cluecounter++;
}
_user.wat4 = val;
});
}),
Text(
'counter value: $cluecounter\n',
textAlign: TextAlign.center,
),
FloatingActionButton.extended(
backgroundColor: const Color(0xff364976),
foregroundColor: const Color(0xffffffff),
onPressed: () {
final form = _formKey.currentState;
form?.save();
_user.save();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
Result(user:this._user),
),
);
},
icon: Icon(Icons.arrow_forward),
label: Text(' Save'),
),
],
),
),
),
),
),
);
}
}
result.dart
import 'package:flutter/material.dart';
import 'data.dart';
class Result extends StatelessWidget {
User user;
Result({Key? key, required this.user}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Results'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('THE RESULTS'),
if (user.wat1 == true)
const Text(
'Switch 1 is True',
style: TextStyle(fontSize: 16),
),
if (user.wat2 == true)
const Text(
'Switch 2 is True',
style: TextStyle(fontSize: 16),
),
if (user.wat3 == true)
const Text(
'Switch 3 is True',
style: TextStyle(fontSize: 16),
),
if (user.wat4 == true)
const Text(
'Switch 4 is True',
style: TextStyle(fontSize: 16),
),
Text('\n\nSwitch Count >> I WANT THE COUNT HERE <<'
),
],
),
),
);
}
}
and the models
data.dart
class User {
bool wat1 = false;
bool wat2 = false;
bool wat3 = false;
bool wat4 = false;
save() {}
}
On the results, I want to see a count of how many switches are true.
use a List:
final listCng = [true/false] // 8 value default;
Code:
final listCng = [false, false, false, false, false, false, false, false];
return ListView.builder(
itemBuilder: (context, index) => SwitchListTile(
title: const Text('Switch One'),
value: _user.wat1,
onChanged: (bool val) => setState(() {
// option value 3 || value 5
if (index == 2 || index == 4) {
listCng[index] = val;
return;
}
// option other
listCng[index] = val;
})));
You can wrap the data in GestureDetector and have a count variable which is updated onTap.
bool cng1 = false;
bool cng2 = false;
int count = 0;
GestureDetector(
onTap: () => count++,
SwitchListTile(
title: const Text('Switch One'),
value: _user.wat1,
onChanged: (bool val) =>
setState(() => cng1 = val)),
)
If the number is supposed to be displayed in the UI in reactive manner, you can wrap the count++ in a setState.

Cannot change Dropdown button value Flutter

I want to get the initial value of the Dropdown button from firebase;
but when I try to set the governorateDDValue = selectedUser.governorate; inside the build method
the value of Dropdown get the value from firebase but I cannot change it
DropdownButton.gif
this my code
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
var governorateDDValue;
#override
Widget build(BuildContext context) {
final userList= Provider.of<List<User>>(context);
final userID = ModalRoute.of(context)!.settings.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
governorateDDValue = selectedUser.governorate;
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
}
}
thanks in advance
Because you use governorateDDValue = selectedUser.governorate; inside build widget so the Dropdown menu will reset its value every time you change it
the build widget will rebuild and the value of the dropdown will stay equal to the value from firebase
you should use governorateDDValue = selectedUser.governorate; outside the build widget
this code should work will
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
var loading = false;
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
var governorateDDValue;
#override
void initState() {
super.initState();
loading = true;
print('Future.delayed outside');
print(loading);
Future.delayed(Duration.zero, () {
governorateDDValue = selectedUser.governorate;
setState(() {
loading = false;
});
});
}
#override
Widget build(BuildContext context) {
final userList = Provider.of<List<User>>(context);
final userID = ModalRoute
.of(context)!
.settings
.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
governorateDDValue = selectedUser.governorate;
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
The reason why you cannot change it is because every time setState is called your build method is called. Therefore your value will always be set to governorateDDValue = selectedUser.governorate; So to allow changes you should place this governorateDDValue = selectedUser.governorate; in iniState
Or what you can do is like this so that it will only set it once
class UserInfo extends StatefulWidget {
static const routeName = '/UserInfoScreen';
const UserInfo({Key? key}) : super(key: key);
#override
_UserInfoState createState() => _UserInfoState();
}
class _UserInfoState extends State<UserInfo> {
late User selectedUser;
final date = DateFormat('yyyy-MM-dd').format(DateTime.now()).toString();
bool initState = true; // ADD HERE
var governorateDDValue;
#override
Widget build(BuildContext context) {
final userList= Provider.of<List<User>>(context);
final userID = ModalRoute.of(context)!.settings.arguments as String;
selectedUser =
userList.firstWhere((user) => user.id == userID);
// this line makes dropdown value always equal to value from firestore
if(initState){ // ADD HERE
governorateDDValue = selectedUser.governorate;
initState = false; // ADD HERE
}
return Scaffold(
appBar: AppBar(
title: Text('report'),
),
body: SingleChildScrollView(
child: Container(
child: Row(
children: <Widget>[
Text('Governorate',),
Container(height: 5),
DropdownButton<String>(
value: governorateDDValue,
icon: const Icon(Icons.arrow_downward),
iconSize: 24,
elevation: 16,
style: const TextStyle(color: Colors.deepPurple),
onChanged: (String? newValue) {
setState(() {
governorateDDValue = newValue!;
});
},
items: Constants.governoratesOfEgypt
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
],
),
),
),
);
}

Set value of Dropdown Button manually

I have two widgets which are siblings in a container. One widget is a custom DropdownButton, the other one is a custom IconButton:
Parent widget:
static int _currentValue = 0;
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: GCWDropDownButton(
onChanged: (value) {
setState(() {
_currentValue = value;
});
}
),
),
GCWIconButton(
iconData: Icons.add,
onPressed: () {
print(_currentValue);
setState(() {
_currentValue++;
// <------------- how to set value to Dropdown Button
});
},
),
],
);
}
Dropdown widget:
class GCWDropDownButton extends StatefulWidget {
final Function onChanged;
const GCWDropDownButton({Key key, this.onChanged}) : super(key: key);
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
int _dropdownValue = 1;
#override
Widget build(BuildContext context) {
return Container(
child: DropdownButton(
value:_dropdownValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
_dropdownValue = newValue;
widget.onChanged(newValue);
});
},
items: ...
),
);
}
}
I want to change the DropdownButton's value to be increased after pressing the IconButton. If it were a TextField I'd use a Controller.
But how can I achieve this with the Dropdown?
You're trying to store the same value in 2 different states: in a parent and in a child one. In your case, it's better to do that in parent's state and to pass current value to the child.
int _currentIndex;
#override
Widget build(BuildContext context) {
...
child: Row(
children: <Widget>[
Expanded(
child: GCWDropDownButton(
currentIndex: _currentIndex,
onChanged: (index) {
setState(() {
_currentIndex = index;
});
},
),
),
GCWIconButton(
iconData: Icons.add,
onPressed: () {
setState(() {
if (_currentIndex == null) {
_currentIndex = 0;
} else {
_currentIndex++;
}
});
},
),
],
)
...
class GCWDropDownButton extends StatefulWidget {
final Function onChanged;
final int currentIndex;
const GCWDropDownButton({Key key, this.onChanged, this.currentIndex}) : super(key: key);
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
#override
Widget build(BuildContext context) {
final values = ['one', 'two', 'three'];
final currentValue = widget.currentIndex == null
? null
: values[min(values.length - 1, widget.currentIndex)]; // Not going out of range
return Container(
child: DropdownButton(
value: currentValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
widget.onChanged(values.indexOf(newValue));
});
},
items: values.map((v) =>
DropdownMenuItem(
child: Text(v.toString()),
value: v,
key: Key(v.toString())
)
).toList()
),
);
}
}
Or it would be even better to place DropdownButton and GCWIconButton in one stateful widget, so both widgets share the same state:
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: GCWDropDownButton()
),
);
}
}
class GCWDropDownButton extends StatefulWidget {
#override
_GCWDropDownButtonState createState() => _GCWDropDownButtonState();
}
class _GCWDropDownButtonState extends State<GCWDropDownButton> {
int _currentIndex;
final values = ['one', 'two', 'three'];
#override
Widget build(BuildContext context) {
final currentValue = _currentIndex == null ? null : values[_currentIndex];
return Row(
children: <Widget>[
Expanded(
child:Container(
child: DropdownButton(
value: currentValue,
icon: Icon(Icons.arrow_downward),
onChanged: (newValue) {
setState(() {
_currentIndex = values.indexOf(newValue);
});
},
items: values.map((v) =>
DropdownMenuItem(
child: Text(v.toString()),
value: v,
key: Key(v.toString())
)
).toList()
),
),
),
IconButton(
icon: Icon(Icons.add),
onPressed: () {
setState(() {
if (_currentIndex == null) {
_currentIndex = 0;
} else
// Not going out of range
if (_currentIndex != values.length - 1) {
_currentIndex++;
}
});
},
),
],
);
}
}