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]
},
)
Related
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;
}
}
I have a code that is responsible for building a menu filter. It allows you to filter data by category and then by subcategory.
Initially, subcategories are in a closed state, but when you click on the arrow, they can be opened. Take a look
But my problem is that if I click on the arrow for any category (Country in my case), then all subcategories open at once. Take a look
It's my code
class _FilterDialogUserState extends State<FilterDialogUser> {
Map<String, List<String>?> filters = {};
bool needRefresh = false;
bool isClickedCountry = false;
#override
void initState() {
super.initState();
filters = widget.initialState;
}
List<FilterItem> children = [
FilterItem('Georgia', subitems: [
FilterItem('Tbilisi'),
FilterItem('Batumi'),
]),
FilterItem('Poland', subitems: [
FilterItem('Warsaw'),
FilterItem('Krakow'),
FilterItem('Wroclaw'),
]),
FilterItem('Armenia', subitems: [
FilterItem('Erevan'),
FilterItem('Gyumri'),
]),
];
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontFamily: 'SuisseIntl',
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
children: children.map(
(e) {
return Column(
children: [
InkWell(
onTap: () async {
setState(() {
isClickedCountry = !isClickedCountry;
});
},
child: Row(
children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.subitems.forEach((element) =>
element.selected = value as bool);
e.selected = value as bool;
}),
),
Text(e.text),
const Spacer(),
isClickedCountry
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
],
),
),
if (e.subitems.isNotEmpty)
!isClickedCountry
? Container()
: Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: Column(
children: e.subitems.map((e) {
return Row(children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.selected = value as bool;
}),
),
Text(e.text),
]);
}).toList(),
),
)
],
);
},
).toList(),
),
]);
}
}
class FilterItem {
final String text;
bool selected;
List<FilterItem> subitems;
FilterItem(
this.text, {
this.selected = false,
this.subitems = const [],
});
}
Tell me, is it possible to change my code so that not all subcategories are opened, but only the one that the user clicks on?
The each main filter item must be controlled one by one.
Define List isClickedCountry variable
Save and load state from List isClickedCountry variable
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
Widget _buildBody() {
return FilterDialogUser();
}
}
class FilterDialogUser extends StatefulWidget {
FilterDialogUser({Key key}) : super(key: key);
#override
State<FilterDialogUser> createState() => _FilterDialogUserState();
}
class _FilterDialogUserState extends State<FilterDialogUser> {
Map<String, List<String>> filters = {};
bool needRefresh = false;
List<bool> isClickedCountry = List.filled(3, false);
#override
void initState() {
super.initState();
// filters = widget.initialState;
}
List<FilterItem> children = [
FilterItem('Georgia', subitems: [
FilterItem('Tbilisi'),
FilterItem('Batumi'),
]),
FilterItem('Poland', subitems: [
FilterItem('Warsaw'),
FilterItem('Krakow'),
FilterItem('Wroclaw'),
]),
FilterItem('Armenia', subitems: [
FilterItem('Erevan'),
FilterItem('Gyumri'),
]),
];
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontFamily: 'SuisseIntl',
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
children: children.map(
(e) {
final int index = children.indexOf(e);
return Column(
children: [
InkWell(
onTap: () async {
setState(() {
isClickedCountry[index] = !isClickedCountry[index];
});
},
child: Row(
children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.subitems.forEach((element) =>
element.selected = value as bool);
e.selected = value as bool;
}),
),
Text(e.text),
const Spacer(),
isClickedCountry[index]
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
],
),
),
if (e.subitems.isNotEmpty)
!isClickedCountry[index]
? Container()
: Padding(
padding: const EdgeInsets.fromLTRB(30, 0, 0, 0),
child: Column(
children: e.subitems.map((e) {
return Row(children: [
Checkbox(
value: e.selected,
onChanged: (value) => setState(() {
e.selected = value as bool;
}),
),
Text(e.text),
]);
}).toList(),
),
)
],
);
},
).toList(),
),
]);
}
}
class FilterItem {
final String text;
bool selected;
List<FilterItem> subitems;
FilterItem(
this.text, {
this.selected = false,
this.subitems = const [],
});
}
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(),
);
}
}
This question already has answers here:
Could not find the correct provider above this widget
(6 answers)
Closed 1 year ago.
ok hi guys i need your help. i am creating a todolist app right now and then the todolist widget which is the main page of the todo list (basically showing the todo) is not working when I run the code it give me this error
The following ProviderNotFoundException was thrown building TodoListWdiget(dirty):
Error: Could not find the correct Provider above this TodoListWdiget Widget
This happens because you used a BuildContext that does not include the provider
of your choice. There are a few common scenarios:
You added a new provider in your main.dart and performed a hot-reload.
To fix, perform a hot-restart.
The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
You used a BuildContext that is an ancestor of the provider you are trying to read.
Make sure that TodoListWdiget is under your MultiProvider/Provider.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
consider using builder like so:
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
i try all the solution given but cannot help. so guys please help me.
this my code for the TodoListWdiget
class TodoListWdiget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final provider = Provider.of<TodosProvider>(context);
///get things in todos provider from all the context
final todos = provider.todos;
///this means we will get provider and provider is
///things in the todosprovider and there is a variable call todos and store inside final todos here
return todos.isEmpty
? Center(
child: Text(
'no todos',
style: TextStyle(fontSize: 20),
))
:
ListView.separated(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(16),
itemBuilder: (context, index) {
final todo = todos[index];
return TodoWidget(
todo:
todo); //it means all the things in the todo that we have written
},
separatorBuilder: (context, index) => SizedBox(height: 8),
itemCount: todos.length);
}
}
whole code of this todo list app
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'utils.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'todolist',
theme: ThemeData(
primarySwatch: Colors.pink,
scaffoldBackgroundColor: Color(0xFFf6f5ee),
),
home: hompage(),
);
}
}
class hompage extends StatefulWidget {
#override
_hompageState createState() => _hompageState();
}
class _hompageState extends State<hompage> {
int selectedindex = 0;
final tabs = [
TodoListWdiget(),
Completedtodolist(),
];
final _formKey = GlobalKey<FormState>();
String title = '';
String description = '';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('todolist'),
),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.white.withOpacity(1.0),
selectedItemColor: Colors.white,
currentIndex: selectedindex,
onTap: (index) => setState(() {
selectedindex = index;
}),
items: [
BottomNavigationBarItem(
icon: Icon(Icons.fact_check_outlined),
label: 'todo',
),
BottomNavigationBarItem(
icon: Icon(
Icons.done,
size: 28,
),
label: 'done',
),
],
),
body: tabs[selectedindex],
floatingActionButton: FloatingActionButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
backgroundColor: Colors.black,
onPressed: () {
showtextfield();
},
child: Icon(Icons.add),
),
);
}
///after press show the dialog
showtextfield() {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
content: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'add todo',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22),
),
const SizedBox(height: 8),
todoformwidget(
onChangedTitle: (title) =>
setState(() => this.title = title),
onChangeDescription: (description) =>
setState(() => this.description = description),
onSavedTodo: addTodo,
),
],
),
),
);
});
}
void addTodo() {
final isValid = _formKey.currentState!.validate();
if (isValid) {
final todo = Todo(
id: DateTime.now().toString(),
title: title,
description: description,
createdTime: DateTime.now(),
);
final provider = Provider.of<TodosProvider>(context, listen: false);
provider.addTodo(todo);
Navigator.of(context).pop();
} else {
return;
}
}
}
class todoformwidget extends StatelessWidget {
final String title;
final String description;
final ValueChanged<String> onChangedTitle;
final ValueChanged<String> onChangeDescription;
final VoidCallback onSavedTodo;
const todoformwidget({
this.title = '',
this.description = '',
required this.onChangeDescription,
required this.onChangedTitle,
required this.onSavedTodo,
});
#override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
buildTitle(),
SizedBox(height: 8),
buildDescription(),
SizedBox(height: 8),
buildButton(),
],
),
);
}
Widget buildTitle() => TextFormField(
autofocus: true,
initialValue: title,
validator: (title) {
if (title != null) {
title.isEmpty ? 'Enter an email' : null;
}
},
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelText: 'title',
),
onChanged: onChangedTitle,
);
Widget buildDescription() => TextFormField(
autofocus: true,
initialValue: description,
decoration: InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Description',
),
onChanged: onChangeDescription,
);
Widget buildButton() => SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.black),
),
onPressed: onSavedTodo,
child: Text('Save'),
),
);
}
class TodosProvider extends ChangeNotifier {
// all the task is going to be here
List<Todo> _todos = [
Todo(
createdTime: DateTime.now(),
title: 'Buy Food 😋',
description: '''- Eggs
- Milk
- Bread
- Water''',
),
Todo(
createdTime: DateTime.now(),
title: 'Plan family trip to Norway',
description: '''- Rent some hotels
- Rent a car
- Pack suitcase''',
),
Todo(
createdTime: DateTime.now(),
title: 'Walk the Dog 🐕',
),
Todo(
createdTime: DateTime.now(),
title: 'Plan Jacobs birthday party 🎉🥳',
),
];
List<Todo> get todos => _todos.where((todo) => todo.isDone == false).toList();
///add todo function is the todo thing that requried name todo here and then
void addTodo(Todo todo) {
_todos.add(todo);
notifyListeners();
}
void updateTodo(Todo todo, String title, String description) {
todo.title = title;
todo.description = description;
notifyListeners();
}
void removeTodo(Todo todo) {
_todos.remove(todo);
notifyListeners();
}
bool toggleTodoStatus(Todo todo) {
todo.isDone = !todo.isDone;
notifyListeners();
return todo.isDone;
}
List<Todo> get todosCompleted =>
_todos.where((todo) => todo.isDone == true).toList();
}
/// this list of todo and we want to get it public where the todo here that is only
/// false will be in this public todos list
///things required for the todo widget means the todo box
class TodoField {
static const createdTime = 'createdTime';
}
class Todo {
DateTime createdTime;
String title;
String id;
String description;
bool isDone;
Todo({
required this.createdTime,
required this.title,
this.description = '',
this.id = '',
this.isDone = false,
});
}
///this is the whole box thing
class TodoListWdiget extends StatelessWidget {
#override
Widget build(BuildContext context) {
final provider = Provider.of<TodosProvider>(context);
///get things in todos provider from all the context
final todos = provider.todos;
///this means we will get provider and provider is
///things in the todosprovider and there is a variable call todos and store inside final todos here
return todos.isEmpty
? Center(
child: Text(
'no todos',
style: TextStyle(fontSize: 20),
))
:
ListView.separated(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(16),
itemBuilder: (context, index) {
final todo = todos[index];
return TodoWidget(
todo:
todo); //it means all the things in the todo that we have written
},
separatorBuilder: (context, index) => SizedBox(height: 8),
itemCount: todos.length);
}
}
///create the box thing of a todolist
class TodoWidget extends StatelessWidget {
const TodoWidget({required this.todo, Key? key}) : super(key: key);
final Todo todo;
#override
Widget build(BuildContext context) => ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Slidable(
child: buildTodo(context),
actionPane: SlidableDrawerActionPane(),
key: Key(todo.id),
// show text
actionExtentRatio: 0.30,
actions: [
IconSlideAction(
color: Colors.green,
onTap: editTodo(context, todo),
caption: 'edit',
icon: Icons.edit),
],
secondaryActions: [
IconSlideAction(
color: Colors.red,
onTap: deletetodo(context, todo),
caption: 'delete',
icon: Icons.delete,
),
],
));
Widget buildTodo(BuildContext context) => Container(
padding: EdgeInsets.all(20),
color: Colors.white,
child: Row(
children: [
Checkbox(
activeColor: Theme.of(context).primaryColor,
value: todo.isDone,
onChanged: (_) {
final provider =
Provider.of<TodosProvider>(context, listen: false);
final isDone = provider.toggleTodoStatus(todo);
Utils.showSnackBar(context,
isDone ? 'task completed' : 'task marked incomplete');
},
checkColor: Colors.white,
),
SizedBox(
width: 20,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
todo.title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
fontSize: 22,
),
),
if (todo.description.isNotEmpty)
Container(
margin: EdgeInsets.only(top: 4),
child: Text(
todo.description,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
fontSize: 22,
),
),
)
],
))
],
),
);
deletetodo(BuildContext context, Todo todo) {
final provider = Provider.of<TodosProvider>(context, listen: false);
provider.removeTodo(todo);
///showsnackbar(context);
Utils.showSnackBar(context, 'delete the task');
}
///void showsnackbar(BuildContext context,Todo todo){
/// final snackbar =SnackBar(
///content:Text(context,'has been delete',style:TextStyle(fontSize:16)
/// );
/// Scaffold.of(context)..showSnackBar(snackbar);
/// }
editTodo(BuildContext context, Todo todo) => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EditTodoPage(todo: todo),
),
);
}
class EditTodoPage extends StatefulWidget {
final Todo todo;
const EditTodoPage({
Key? key,
required this.todo,
}) : super(key: key);
#override
_EditTodoPageState createState() => _EditTodoPageState();
}
class _EditTodoPageState extends State<EditTodoPage> {
final _formKey = GlobalKey<FormState>();
String title = '';
String description = '';
#override
void initState() {
// TODO: implement initState
super.initState();
title = widget.todo.title;
description = widget.todo.description;
}
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('Edit Todo'),
actions: [
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
final provider =
Provider.of<TodosProvider>(context, listen: false);
provider.removeTodo(widget.todo);
Navigator.of(context).pop();
},
)
],
),
body: Padding(
padding: EdgeInsets.all(16),
child: Form(
key: _formKey,
child: todoformwidget(
title: title,
description: description,
onChangedTitle: (title) => setState(() => this.title = title),
onChangeDescription: (description) =>
setState(() => this.description = description),
onSavedTodo: saveTodo,
),
),
),
);
void saveTodo() {
final isValid = _formKey.currentState!.validate();
if (!isValid) {
return;
} else {
final provider = Provider.of<TodosProvider>(context, listen: false);
provider.updateTodo(widget.todo, title, description);
Navigator.of(context).pop();
}
}
}
class Completedtodolist extends StatelessWidget {
const Completedtodolist({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
final provider = Provider.of<TodosProvider>(context);
final todos = provider.todosCompleted;
return todos.isEmpty ?Center(
child: Text(
'No completed tasks.',
style: TextStyle(fontSize: 20),
),
):ListView.separated(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(40),
itemBuilder:(context, index) {
final todo = todos[index];
return TodoWidget(todo: todo);
},
separatorBuilder: (context, index) => SizedBox(height: 8),
itemCount: todos.length,);
}
}
class Utils{
static late BuildContext context;
static void showSnackBar(BuildContext,String text)=>
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content:Text(text)));
}
Change this part of your code to look like this:
return MultiProvider(
providers: [
ChangeNotifierProvider<TodosProvider >(
create: (_) => TodosProvider(),
),
],
builder: (context, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'todolist',
theme: ThemeData(
primarySwatch: Colors.pink,
scaffoldBackgroundColor: Color(0xFFf6f5ee),
),
home: hompage(),
);
});
}
}
Add all other providers you intend to use inside the multiprovider also. Read up on providers, and how they are scoped, and that providers need to be inserted above children widgets who are going to use. We put it now above material because it's the very top of your widget tree.
I have a form widget, a list widget, and a "wrapper" widget or in other words, a parent/container widget. So to give an idea of the widget tree, it is as such.
Parent/Container Widget
Form Widget
Button Widget
List Widget
Notice that the form, buttons and list widget are all siblings, inside of the parent/container widget. What I want to happen, is tap on a list item in the list widget, and populate the form widget with the data that gets passed from the list widget.
Here is my parent widget.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:andplus_flutter_7_gui/services/user_service.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'crud_form.dart';
import 'crud_list.dart';
class Crud extends StatefulWidget {
Crud({Key key, this.title}) : super(key: key);
final String title;
_CrudContainerState createState() => _CrudContainerState();
}
class _CrudContainerState extends State<Crud> {
List<User> users;
User user = User();
UserService userService;
#override
void initState() {
super.initState();
if (userService == null) {
userService = UserService(user);
}
}
#override
void dispose() {
// TODO: implement dispose
super.dispose();
userService.dispose();
}
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(widget.title),
),
body: Builder(
builder: (context) => Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 2,
child: StreamBuilder(
builder: (context, AsyncSnapshot<User> snapshot) {
return CrudForm(
user: snapshot.data,
onUserAdded: (user) {
userService.addUser(user);
},
);
},
stream: userService.userObservable,
),
),
Expanded(
child: Text("Future button widget"),
),
Expanded(
flex: 3,
child: StreamBuilder(
builder: (ctx, AsyncSnapshot<List<User>> snap) {
return CrudList(
onUserSelected: userService.userSelected,
users: snap.data,
);
},
stream: userService.usersObservable,
),
),
],
),
),
),
);
}
void onEditUser(User user) {
setState(() {
user = user;
});
}
}
The above widget wraps the three widgets I mentioned.
Here are the children widget:
Form:
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:flutter/material.dart';
class CrudForm extends StatefulWidget {
CrudForm({Key key, this.onUserAdded, this.user}) : super(key: key);
final User user;
final void Function(User user) onUserAdded;
_CrudFormState createState() => _CrudFormState(user: user);
}
class _CrudFormState extends State<CrudForm> {
_CrudFormState({this.user});
User user = User();
var _key = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Container(
child: Builder(
builder: (context) => Container(
color: Colors.blueAccent[100],
child: Form(
key: _key,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Text(
"First Name",
style: TextStyle(fontSize: 20),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
initialValue: widget.user?.firstName == null ||
widget.user.firstName.isEmpty
? user.firstName
: widget.user.firstName,
validator: (value) {
if (value.isEmpty) {
return "First name is required";
}
return null;
},
onSaved: (value) {
setState(() {
user.firstName = value;
});
},
),
),
)
],
),
Row(
children: <Widget>[
Text(
"Last Name",
style: TextStyle(fontSize: 20),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
validator: (value) {
if (value.isEmpty) {
return "Last name is required";
}
return null;
},
onSaved: (value) {
setState(() {
user.lastName = value;
});
},
),
),
),
],
),
RaisedButton(
child: Text(
"Save",
),
splashColor: Colors.blueGrey,
onPressed: () {
if (!_key.currentState.validate()) {
return;
}
_key.currentState.save();
widget.onUserAdded(
new User(
firstName: user.firstName,
lastName: user.lastName,
),
);
},
)
],
),
),
),
),
),
);
}
}
Here is my list widget.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:flutter/material.dart';
class CrudList extends StatefulWidget {
CrudList({Key key, this.users, this.onUserSelected}) : super(key: key);
final List<User> users;
final SelectUser onUserSelected;
_CrudListState createState() => _CrudListState();
}
class _CrudListState extends State<CrudList> {
#override
Widget build(BuildContext context) {
return Container(
color: Colors.green,
child: ListView.builder(
itemCount: widget.users?.length ?? 0,
itemBuilder: (BuildContext context, int index) {
var user = widget.users[index];
return ListTile(
key: Key(index.toString()),
title: Center(
child: Text(
"${user.firstName} ${user.lastName}",
style: TextStyle(color: Colors.white),
),
),
onTap: () {
print("${widget.users[index]} $index");
widget.onUserSelected(widget.users[index]);
},
);
},
),
);
}
}
typedef void SelectUser(User user);
And just for further context, here is my user service, responsible for adding the objects to the stream, and using the stream builder within rxdart to notify of state changes.
import 'package:andplus_flutter_7_gui/model/user.dart';
import 'package:rxdart/rxdart.dart';
class UserService {
User _editedUser = User();
List<User> _users = <User>[];
BehaviorSubject<User> _userSubject;
BehaviorSubject<List<User>> _usersSubject;
UserService(this._editedUser) {
_userSubject = BehaviorSubject<User>.seeded(_editedUser);
_usersSubject = BehaviorSubject<List<User>>.seeded(_users);
}
Observable<List<User>> get usersObservable => _usersSubject.stream;
Observable<User> get userObservable => _userSubject.stream;
addUser(User user) {
_users.add(user);
_usersSubject.add(_users);
}
dispose() {
_userSubject.close();
_usersSubject.close();
}
void userSelected(User user) {
_editedUser = user;
_userSubject.add(_editedUser);
}
}
What am I missing? It looks like my widget rebuilds, and tries to set the initial value in the form when I tap the user in the list widget. But the actual field doesn't get updated and I'm not sure why.
I'd appreciate any documentation or articles on how to better approach data and state management between sibling widgets within the flutter framework.
Here's a similar use case that I tried to implement locally. What I'm doing here is I generate TextFormFields dynamically and assign TextEditingController.
Column textField(int n) {
List<Widget> listForm = [];
while (n > 0) {
var textEditingController = TextEditingController();
listForm.add(
TextFormField(
controller: textEditingController,
onTap: () {
_selectedField = textEditingController;
},
),
);
n--;
}
return Column(children: listForm);
}
Clicking a ListView item updates the text of the currently selected TextFormField.
InkWell(
onTap: () {
debugPrint('Selected $index!');
if (_selectedField != null) {
_selectedField!.value = TextEditingValue(text: 'Item $index');
}
},
child: ListTile(
title: Text('Item $index'),
),
);
Complete sample
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
TextEditingController? _selectedField;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(flex: 1, child: textField(3)),
Expanded(flex: 1, child: listItems()),
],
),
),
),
);
}
Column textField(int n) {
List<Widget> listForm = [];
while (n > 0) {
var textEditingController = TextEditingController();
listForm.add(
TextFormField(
controller: textEditingController,
onTap: () {
debugPrint('Current Controller: $textEditingController');
_selectedField = textEditingController;
},
),
);
n--;
}
return Column(children: listForm);
}
ListView listItems() {
return ListView.builder(
itemCount: 5,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
if (_selectedField != null) {
_selectedField!.value = TextEditingValue(text: 'Item $index');
}
},
child: ListTile(
title: Text('Item $index'),
),
);
},
);
}
}
Demo