Provider does not refresh the list of a DropDownMenu - Flutter - flutter

I have a personal API from where I retrieve all the information I need, the connection works fine, but the problem is when I want to show this values, I have a provider where I put all the data I need from the API, and I call it to put those items in the DropDownMenu, but the problem is that the values does not appear, even after the data have arrived, I know this thanks to debugging, here is some code:
this is the code of the widget where I'm calling the provider:
Column(
children: [
Container(
color: Colors.green,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsets.only(top: 20, bottom: 20),
height: 10,
width: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.grey),
)
]),
),
IconList(
labelText: 'Landmarks',
icon: Icons.panorama_horizontal_outlined,
items: context.watch<Landmarks>().getLandmarksNameList(), //Here is my provider
),
const IconInput(
labelText: '1', icon: Icons.addchart_outlined),
const IconInput(
labelText: '1', icon: Icons.addchart_outlined),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: 2,
itemBuilder: (BuildContext context, int index) {
return UserCard(user: usersList[index]);
},
),
],
),
this is the widget iconList, where I have the dropDownMenu:
import 'package:flutter/material.dart';
class IconList extends StatefulWidget {
final String labelText;
final IconData icon;
final List<String> items = [];
IconList({super.key, required this.labelText, required this.icon, items});
#override
State<IconList> createState() => _IconListState();
}
class _IconListState extends State<IconList> {
#override
Widget build(BuildContext context) {
String selectedItem = '';
if (widget.items.isNotEmpty) {
selectedItem = widget.items.first;
}
return Container(
margin: const EdgeInsets.all(10.0),
child: Row(
children: [
Icon(widget.icon),
DropdownButton<String>(
value: selectedItem,
items: widget.items.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? value) {
setState(() {
selectedItem = value!;
});
},
),
],
),
);
}
}
And this is my provider:
class Landmarks with ChangeNotifier {
final List<Landmark> _items = [];
List<Landmark> get landmarks => _items;
void addLandmarks(List<Landmark> value) {
_items.addAll(value);
notifyListeners();
}
List<String> getLandmarksNameList() {
List<String> list = [];
print(_items.length);
for (int i = 0; i < _items.length; i++) {
list.add(_items[i].name);
}
return list;
}
}

You have two problems with you code:
Problem 1
In your IconList constructor
IconList({super.key, required this.labelText, required this.icon, items});
you are accessing items, but really you should access this.items.
To do so, you also shouldn't initialize items with:
final List<String> items = [];
Your code should look like this:
class IconList extends StatefulWidget {
final String labelText;
final IconData icon;
final List<String> items;
IconList(
{super.key,
required this.labelText,
required this.icon,
required this.items});
Problem 2
Within your build method:
#override
Widget build(BuildContext context) {
String selectedItem = '';
if (widget.items.isNotEmpty) {
selectedItem = widget.items.first;
}
you are initializing selectedItem, which means that every setState, selectedItem will get reset to its original value since it's within the build method. Instead, move selectedItem to outside the build, to your _state class:
class _IconListState extends State<IconList> {
String selectedItem = ''; //<-- Initialize it here
#override
Widget build(BuildContext context) {
if (widget.items.isNotEmpty) {
selectedItem = widget.items.first;
}
return Container(
Your IconList should look like this:
class IconList extends StatefulWidget {
final String labelText;
final IconData icon;
final List<String> items;
IconList(
{super.key,
required this.labelText,
required this.icon,
required this.items});
#override
State<IconList> createState() => _IconListState();
}
class _IconListState extends State<IconList> {
String selectedItem = ''; //<-- Initialize it here
#override
Widget build(BuildContext context) {
if (widget.items.isNotEmpty) {
selectedItem = widget.items.first;
}
return Container(
margin: const EdgeInsets.all(10.0),
child: Row(
children: [
Icon(widget.icon),
DropdownButton<String>(
value: selectedItem,
items: widget.items.map<DropdownMenuItem<String>>((String value) {
print("item is $value");
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
onChanged: (String? value) {
setState(() {
selectedItem = value!;
});
},
),
],
),
);
}
}

Related

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 dynamic Checkboxes don't check

Hi im new to Flutter and coding and tried do build my first to do app. I've created a textformfield to add new todos with a button in a container above. By pressing a button, a new todo will appear on the todo Container. I managed to dynamically give a column new todos with a CheckboxListtTitle. However, when adding a new todo, the todo with a checkbox appears but can't be checked. What did I do wrong here?
to_do_section.dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/key.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_application_1/presentation/widgets/landing_page.dart';
import 'package:flutter_application_1/responsive_layout.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
var toDoList = <String>[userInput];
class ToDos extends StatefulWidget {
final List<String> list;
const ToDos({
Key? key,
required this.list,
}) : super(key: key);
#override
State<ToDos> createState() => _ToDosState();
}
class _ToDosState extends State<ToDos> {
#override
Widget build(BuildContext context) {
SizeConfig().init(context);
bool checked= false;
bool? value_1;
var isSelected = [false, false];
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Padding(
padding: EdgeInsets.only(
top: SizeConfig.blockSizeHorizontal * 10,
left: SizeConfig.blockSizeVertical * 2.5,
right: SizeConfig.blockSizeVertical * 2.5,
bottom: SizeConfig.screenHeight / 8),
child: SizedBox(
width: SizeConfig.blockSizeHorizontal * 100,
height: SizeConfig.blockSizeVertical * 40,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.black45, style: BorderStyle.solid, width: 4)),
child: Padding(
padding: EdgeInsets.all(8.0),
child: ToDoColumn(setState, checked),
),
),
),
);
});
}
Column ToDoColumn(
StateSetter setState, bool checked) {
return Column(children: [
for (final value in widget.list)
CheckboxListTile(
value: checked,
onChanged: (_value_1) {
setState(() {
checked = _value_1!;
});
},
title: Text('${value}'),
activeColor: Colors.green,
checkColor: Colors.black,
)
]);
}
}
landing_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_application_1/presentation/widgets/to_do_section.dart';
final _textController = TextEditingController();
String userInput = "";
class LandingPage extends StatefulWidget {
const LandingPage({Key? key}) : super(key: key);
#override
State<LandingPage> createState() => _LandingPageState();
}
class _LandingPageState extends State<LandingPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Center(child: Text("To-Do-App")),
backgroundColor: Colors.redAccent,
),
body: SingleChildScrollView(
child: Column(
children: [ToDos(list: toDoList), ToDoAdd()],
),
),
);
}
Column ToDoAdd() {
return Column(
children: [
Padding(
padding: EdgeInsets.only(top: 8.0, left: 20, right: 20, bottom: 20),
child: TextField(
onChanged: (value) => setState(() => userInput = value) ,
textAlign: TextAlign.center,
decoration: InputDecoration(
border: OutlineInputBorder(),
hintText: "Add a new ToDo",
) ,
),
),
MaterialButton(
color: Colors.redAccent,
onPressed: () {
setState(() {
toDoList.add(userInput);
});
},
child: Text("Admit", style: TextStyle(color: Colors.white),),
),
Text(userInput)
],
);
}
}
Every variable declared inside the build method will be redeclared on each build. You could move its declaration outside and assign a value in the initState method.
Also, the use of the StatefulBuilder is not justified in your code. You could simply use the StatefulWidget as it is.
I will take the model class from #yeasin-sheikh
class Task {
final String text;
final bool isChecked;
Task({
required this.text,
this.isChecked = false,
});
Task copyWith({
String? text,
bool? isChecked,
}) {
return Task(
text: text ?? this.text,
isChecked: isChecked ?? this.isChecked,
);
}
}
This example is a reduced one that accomplish what you are trying to do. Please notice that I'm not reproducing your design. The function addRandomToDo() could be replaced with your code for adding Tasks to the app.
class ToDoPage extends StatefulWidget {
const ToDoPage({Key? key}) : super(key: key);
#override
State<ToDoPage> createState() => _ToDoPageState();
}
class _ToDoPageState extends State<ToDoPage> {
final tasks = <Task>[];
void addRandomToDo() {
setState(() {
tasks.add(Task(text: 'Random ToDo ${tasks.length + 1}'));
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ToDo App'),
),
body: ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return CheckboxListTile(
key: ObjectKey(tasks[index]),
title: Text(tasks[index].text),
value: tasks[index].isChecked,
onChanged: (isChecked) {
setState(() {
tasks[index] = tasks[index].copyWith(isChecked: isChecked);
});
},
);
}),
floatingActionButton: FloatingActionButton(
onPressed: addRandomToDo,
tooltip: 'Add ToDo',
child: const Icon(Icons.add),
),
);
}
}
Tips:
The use of Column for this example is not recommended. The best is to use a ListView.builder() and the reason is that when you use a Column and you have more element than the screen can render the app will also render the ones that are offscreen and will result in a lack of performance but the use of ListView will be best because it only renders a few elements offscreen (maybe two or three) to avoid a memory overload and it will load and render the elements as you scroll.
Is a best practice to not use functions to return a piece of the screen but to split it into widgets. (Instead of having a ToDoColumn function just change it for a ToDoColumnWidget). The explanation for this is that will incurre in a lack of performance because all functions will be re-rendered on every widget build.
This piece of code :
EdgeInsets.only(top: 8.0, left: 20, right: 20, bottom: 20)
Could be changed for this one:
EdgeInsets.fromLTRB(20, 8, 20, 20)
You have defined isSelected inside the build method. The build method is invoked each time you set the state. Please move it to initState.
bool checked= false;
Define bool checked = false outside the build method or in initstate
Putting variables inside build method will reset the bool and always be false. Also on your snippet you are just holding text without check state. On evey-build it will also reset on this point.
You can remove StatefulBuilder while this is already a StatefulWidget. Try using state-management like riverpod or bloc instead of using global variables in project cases.
As for the solution, I am creating a model class.
class Task {
final String text;
final bool isChecked;
Task({
required this.text,
this.isChecked = false,
});
Task copyWith({
String? text,
bool? isChecked,
}) {
return Task(
text: text ?? this.text,
isChecked: isChecked ?? this.isChecked,
);
}
}
Now the list will be var toDoList = [Task(text: "")];
And your ToDos
class ToDos extends StatefulWidget {
final List<Task> list;
const ToDos({
Key? key,
required this.list,
}) : super(key: key);
#override
State<ToDos> createState() => _ToDosState();
}
class _ToDosState extends State<ToDos> {
#override
Widget build(BuildContext context) {
//todo: remove [StatefulBuilder]
return StatefulBuilder(builder: (BuildContext context, setStateSB) {
return Container(
child: Padding(
padding: EdgeInsets.all(8.0),
child: ToDoColumn(setStateSB),
),
);
});
}
//current
Column ToDoColumn(
StateSetter setStateSB,
) {
return Column(children: [
for (int i = 0; i < widget.list.length; i++)
CheckboxListTile(
value: widget.list[i].isChecked,
onChanged: (_value_1) {
widget.list[i] = widget.list[i].copyWith(isChecked: _value_1);
setState(() {});
setStateSB(() {});
},
title: Text('${widget.list[i].isChecked}'),
activeColor: Colors.green,
checkColor: Colors.black,
)
]);
}
}
More about StatefulWidget and cookbook on flutter

How to refresh a widget based on another widget's on tap method

In my Flutter UI, I want to refresh the item list whenever a user chooses a particular category. I also want to make the chosen category become the active category. Here's how my UI looks like:
Here's my code which displays the categories:
#override
Widget build(BuildContext context) {
var categoriesData = Provider.of<Categories>(context);
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: categoriesData.items
.map(
(catData) => CategoryItemNew(
id:catData.id,
title:catData.title,
isActive:categoriesData.items.indexOf(catData)==0?true:false
),
)
.toList(),
),
);
}
Here's the code for each category item:
class CategoryItemNew extends StatefulWidget {
final String title;
final String id;
bool isActive;
final Function press;
CategoryItemNew({
#required this.id,
#required this.title,
this.isActive
});
#override
_CategoryItemNewState createState() => _CategoryItemNewState();
}
class _CategoryItemNewState extends State<CategoryItemNew> {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => selectCategory(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
children: <Widget>[
Text(
widget.title,
style: widget.isActive
? TextStyle(
color: kTextColor,
fontWeight: FontWeight.bold,
)
: TextStyle(fontSize: 12),
),
if (widget.isActive)
Container(
margin: EdgeInsets.symmetric(vertical: 5),
height: 3,
width: 22,
decoration: BoxDecoration(
color: kPrimaryColor,
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
);
}
void selectCategory(BuildContext ctx) {
// What should I do here?
}
}
And Here's the code to display the products:
class ItemList extends StatefulWidget {
static const routeName = '/item-list';
String title = '';
ItemList(this.title);
#override
_ItemListState createState() => _ItemListState();
}
class _ItemListState extends State<ItemList> {
var _isInit = true;
var _isLoading = false;
void didChangeDependencies() {
if (_isInit) {
setState(() {
_isLoading = true;
});
Provider.of<Products>(context).fetchAndSetProducts(widget.title, true).then((_) {
setState(() {
_isLoading = false;
});
});
}
_isInit = false;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final productsData = Provider.of<Products>(context);
return _isLoading? CircularProgressIndicator():SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:productsData.items
.map(
(productData) => ItemCard(
svgSrc: "assets/icons/burger_beer.svg",
//id:catData.id,
title: productData.title
//title:productData.title,
),
)
.toList(),
),
);
}
}
When the app starts, I am able to fetch the categories and am also able to show the items for the "BURGERS & WRAPS" category. However, I don't know how to refresh the item list when user chooses another category and also how to make that category be the active category.
Updated CategoryItem class after Mickael's reponse
String previousId ='';
// ignore: must_be_immutable
class CategoryItemNew extends StatefulWidget {
final String title;
final String id;
bool isActive;
final Function press;
CategoryItemNew({
#required this.id,
#required this.title,
this.isActive,
});
#override
_CategoryItemNewState createState() => _CategoryItemNewState();
}
class _CategoryItemNewState extends State<CategoryItemNew> {
String id;
ValueNotifier valueNotifier;
void initState() {
id = widget.id;
valueNotifier = ValueNotifier(previousId);
print('ID in initstate is ' + id);
super.initState();
}
isIdUpdated(String id) {
print('previous in updateid is ' + previousId);
if(previousId != id)
{
previousId = id;
valueNotifier.value = id;
print('ID in updateid is ' + id);
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => selectCategory(context),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
child: Column(
children: <Widget>[
Text(
widget.title,
style: widget.isActive
? TextStyle(
color: kTextColor,
fontWeight: FontWeight.bold,
)
: TextStyle(fontSize: 12),
),
if (widget.isActive)
Container(
margin: EdgeInsets.symmetric(vertical: 5),
height: 3,
width: 22,
decoration: BoxDecoration(
color: kPrimaryColor,
borderRadius: BorderRadius.circular(10),
),
),
],
),
),
);
}
ValueListenableBuilder selectCategory(BuildContext ctx) {
isIdUpdated(id);
return ValueListenableBuilder(
valueListenable: valueNotifier,
builder: (context, value, child) {
print('inside listenabele builder');
return ItemList(widget.title);
},
);
}
}
Notice that I have a print statement inside the builder of ValueListenableBuilder. This print statement never gets executed. What am I doing wrong?
You have to add a ValueListener that will listen to the value of the categories you want to show. When you click on a category, you have to add the new value to your ValueListener.
Once it's done, create a ValueListenableBuilder which will build every time its value change. In the builder, you can show the widget you want according to the value you're listening.
EDIT : Same things for the "active" category, you can use the same value.

How do I extract this switch widget

I have a StatefulWidget with a ListView, the ListView has the bunch of switches with text next to them.
Now i want to extract this into a custom switch widget because i have this more than once.
I don't know how to do this, also I need to know inside my parent widget what state each switch has.
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Use custom dhcp server"),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Switch(
value: _dhcp,
activeColor: Colors.blue,
onChanged: (bool value) {
setState(() {
_dhcp = value;
});
},
),
),
],
),
),
You can create your own stateless widget like this:
class CustomSwitch extends StatelessWidget {
const CustomSwitch({
Key key,
#required this.value,
#required this.onChanged,
}) : super(key: key);
final bool value;
final void Function(bool) onChanged;
#override
Widget build(BuildContext context) {
return Switch(
value: value,
activeColor: Colors.blue,
onChanged: onChanged,
);
}
}
Where you can use it anywhere like this:
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool switchValue = false;
#override
Widget build(BuildContext context) {
return ListView(
children: [
CustomSwitch(
value: switchValue,
onChanged: (newValue) {
setState(() {
switchValue = newValue;
});
},
),
],
);
}
}

Flutter: Is there a widget to flex toggle buttons

So I'm the ToggleButtons introduced in 1.9.1 like this:
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(padding: EdgeInsets.only(top: 8),),
Text('Sort By: '),
Align(
alignment: Alignment.topLeft,
child: ToggleButtons(
borderColor: Color(0xffED7D31),
selectedBorderColor: Color(0xffED7D31),
selectedColor: Color(0xffAD7651),
fillColor: Color(0xffFBE5D6),
color: Color(0xffBF987E),
children: <Widget>[
...state.sortBy.keys.map((name) => Text(name)).toList()
],
onPressed: (index) => state.toggleSortBy(index),
isSelected: state.sortBy.values.toList(),
),
),
],
),
This obviously creates a list of button widgets from a map, the problem I'm facing is that I'm seeing the following warning in my screen
Right overflowed by X pixels
So my question is simply whether there is a way to "flex" the buttons like in CSS, meaning when the number of buttons exceeds the screen size, the buttons would automatically start from the next line.
EDIT:
The state.sortBy is just a Map where I store the texts for my widgets alongside their selected values:
LinkedHashMap<String,bool> sortBy = LinkedHashMap.from({
'Ascending' : true,
'Descending' : false,
})
So this was a bit of work, but I've made a Widget that I've called WrapIconToggleButtons and that should fit what you are looking for. It's rudimentary but you can customize it as you see fit. Please take a look:
How to use (similar to ToggleButtons)
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
List<bool> isSelected = [
false,
false,
false,
false,
false,
false,
false,
false,
];
#override
Widget build(BuildContext context) {
return Center(
child: Container(
child: WrapToggleIconButtons(
iconList: [
Icons.ac_unit,
Icons.shopping_cart,
Icons.shopping_cart,
Icons.done,
Icons.fiber_pin,
Icons.sentiment_satisfied,
Icons.looks_6,
Icons.apps,
],
isSelected: isSelected,
onPressed: (int index) {
setState(() {
for (int buttonIndex = 0; buttonIndex < isSelected.length; buttonIndex++) {
if (buttonIndex == index) {
isSelected[buttonIndex] = !isSelected[buttonIndex];
} else {
isSelected[buttonIndex] = false;
}
}
});
},
),
),
);
}
}
WrapToggleIconButtons Widget
class WrapToggleIconButtons extends StatefulWidget {
final List<IconData> iconList;
final List<bool> isSelected;
final Function onPressed;
WrapToggleIconButtons({
#required this.iconList,
#required this.isSelected,
#required this.onPressed,
});
#override
_WrapToggleIconButtonsState createState() => _WrapToggleIconButtonsState();
}
class _WrapToggleIconButtonsState extends State<WrapToggleIconButtons> {
int index;
#override
Widget build(BuildContext context) {
assert(widget.iconList.length == widget.isSelected.length);
index = -1;
return Wrap(
children: widget.iconList.map((IconData icon){
index++;
return IconToggleButton(
active: widget.isSelected[index],
icon: icon,
onTap: widget.onPressed,
index: index,
);
}).toList(),
);
}
}
class IconToggleButton extends StatelessWidget {
final bool active;
final IconData icon;
final Function onTap;
final int width;
final int height;
final int index;
IconToggleButton({
#required this.active,
#required this.icon,
#required this.onTap,
#required this.index,
this.width,
this.height,
});
#override
Widget build(BuildContext context) {
return Container(
width: width ?? 60,
height: height ?? 60,
child: InkWell(
child: Icon(icon,
color: active ? Theme.of(context).accentColor : Theme.of(context).disabledColor,
),
onTap: () => onTap(index),
),
);
}
}