Remove item from PopupMenuButton while it is open - flutter

I have a PopupMenuButton that displays some PopupMenuItem<String>'s generated from a List<String>. Each item has a delete button, which removes the String from the list. The problem is that the popup menu doesn't get rebuilt after deleting an item, until it's closed and opened again.
It seems that no matter what I do, even using a GlobalKey and calling key.currentState.setState(), it doesn't cause the popup menu to be rebuilt until it's closed and opened again.
GlobalKey _favoritesKey = new GlobalKey();
PopupMenuButton<String>(
key: _favoritesKey,
icon: Icon(Icons.bookmark_border),
itemBuilder: (context){
List<PopupMenuItem<String>> result = [];
model.favorites.forEach((x){
result.add(PopupMenuItem<String>(value: x, child: Row(
children: [
IconButton(icon: Icon(Icons.delete_outline), onPressed: (){
model.removeFavorite(x);
_favoritesKey.currentState?.setState((){});
setState(() {});
}),
Text(x)
]
)));
});
return result;
},
onSelected: (x){
// Do something with the selected value
},
)
How can I make the popup menu rebuild itself while it is opened?

Related

Return dropdown other value click button (this dropdown on another page)

I have one filtering dropdown. I have one button on another page. When I click this button on another page, how can I return to other options of the dropdown on the other page?
DropdownPage
CustomDropdownButton<bool>(
hintText: "Select Filter",
value: controller.selectedFilter.value,
items: [
DropdownMenuItem<bool>(
child: Text("Sales"),
value: true,
),
DropdownMenuItem<bool>(
child: Text("Refunds"),
value: false,
)
],
onChanged: (bool selectedFilter) {
controller.selectedFilter.value = selectedFilter;
},
)
Otherpage (button is here, this dropdown on another page)
Expanded(
child: ElevatedButton(
onPressed: () {
// return dropdown other value (this dropdown on another page).
},
child: Text("Update")),
)
Thank you,
Assuming you are navigating to your other page in ElevatedButton.onPressed you can await for the navagitor
onPressed: () {
// return dropdown other value (this dropdown on another page).
final result = Navigator.of(context).push(...);
},
and from your CustomDropdownButton
onChanged: (bool selectedFilter) {
controller.selectedFilter.value = selectedFilter;
Navigator.of(context).pop(selectedFilter);
},
Read more about this from the official recipe https://docs.flutter.dev/cookbook/navigation/returning-data

Listview with radio buttons in flutter slows performance

I would like to update when the value of radio button changes in flutter But its slowing down the app.
I have created the following listview with the radio buttons yes and no
final List<ItemModel> _questions =
List.generate(100, (index) => ItemModel(name: 'nm_$index', id: index + 1, checked: ''));
ListTile(
title: Text((index + 1).toString() + ". " + question.name),
subtitle: Column(
children: [
ListTile(
onTap: () {
setState(() {
_questions[index].checked = 'yes';
});
},
title: const Text("Yes"),
leading: Radio(
value: 'yes',
groupValue: question.checked,
onChanged: (val) {
setState(() {
_questions[index].checked = 'yes';
});
}),
),
ListTile(
title: const Text("No"),
onTap: () {
setState(() {
_questions[index].checked = 'no';
});
},
leading: Radio(
value: 'no',
groupValue: question.checked,
onChanged: (val) {
setState(() {
_questions[index].checked = 'no';
});
}),
)
],
));
The above listview has two radio buttons where each row is either marked as yes or no. It works well for a small list but when the list gets larger like the above with 100 items the app starts to slow down. I believe its caused by the calling of setState on each radio button click which then causes the ui to be rebuilt.
Is there a way i can update the status of the selected radio without causing the ui to rebuild or is there a way i can improve the above to make the app stop lagging and slowing down when there is alot of items.

how to rebuild dialog Widget on changing bool variable state?

im trying to submit form on Dialog and i have a DateTimePicker button and need to make a validation on it also before submitting , what i want to do is showing a text error in case no date picked by changing my own variable "isValid" to false but the UI is not updating , i have to close the dialog and reopen it to see the error text even though i wrapped my column with a StatefulBuilder
my dialog photo here
here is my code
StatefulBuilder(builder: (context, StateSetter setState) {
return isValid == false
? Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
Text(
'error',
style: TextStyle(
color: Colors.red, fontSize: 10),
),
],
)
: Column(
children: [
ElevatedButton(
onPressed: () {
DateTimePicker(context)
.then((value) => setState(() {
_appointmentDateTime = value;
}));
},
child: Text(getTimeDate())),
],
);
})
Validating form + toggling the isValid Value is working fine
OutlinedButton(
onPressed: () async {
if (_formKey.currentState.validate() &&
_appointmentDateTime != null) {
String date = DateFormat('yyyy-MM-dd hh:mm')
.format(_appointmentDateTime);
var appointment = Appointment(
patientName: widget.patient.name,
date: date,
hospital: _hospitalController.text,
await FirebaseApi.addPatientAppointment(
widget.patient.id, appointment);
print('Appointment Created ');
_formKey.currentState.reset();
setState(() {
translator = null;
_appointmentDateTime = null;
});
Navigator.pop(context);
}
else {
setState(() {
isValid = !isValid;
});
}
},
child: Text('Add Appointment')),
It can get confusing when writing the code like this when dealing with Dialogs. The setState you are using in the OutlinedButton is not the same as the setState used in the StatefulBuilder. You need to enclose your OutlinedButton inside the StatefulBuilder too. If you ever use a StatefulBuilder inside a stateful widget, it is better to use a different name like e.g setDialogState.
It is even better to create a separate stateful widget class just for your Dialog contents and pass the formKey and anything else than using a StatefulBuilder in this case to avoid confusion.

Dynamic adding and removing elements from Widget list

I want to create a list of widgets(TextFormField) in which I can add a new element with button Add, and remove any element with the button next to that element. So I would have unknown number of TextFormFields in array and would be able to add a new one, and destroy any one TextFormField.
I was able to make adding of new TextFormFields but removing only works if I want to remove last one.
Is there any way to determine the index of removeButton that was clicked?
List<Widget> proba = new List<Widget>();
List<TextEditingController> _controllers = new List<TextEditingController>();
...
IconButton(
icon: Icon(Icons.add_circle_outline),
onPressed: () {
setState(() {
_controllers.add(new TextEditingController());
});
setState(() {
proba.add(Row(
children: [
Icon(Icons.radio_button_unchecked),
Expanded(
child: TextFormField(
controller: _controllers[_controllers.length - 1],
decoration:
InputDecoration(hintText: "Add text..."),
),
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
setState(() {
_controllers.removeAt(_controllers.length - 1);
proba.removeAt(proba.length - 1);
});
},
)
],
));
});
},
),
Adding works fine. The code removes last element but I would like to remove the element whose button was clicked.
I think you could use a ListView (for example with the builder constructor), so that each Row is a ListTile. The itemBuilder builds the item and you have access to the index. It would look something like this:
int itemCount = 3;
ListView.builder(
itemCount: _counter,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.radio_button_unchecked),
title: TextFormField(),
trailing: IconButton(
onPressed: () {
setState(() {
_counter--;
});
},
icon: Icon(Icons.delete),
),
);
},
),
In the setState Method in the onPressed property you have access to the index. In the example the ListView takes care to create the ListTiles based on the itemCount. You might want to create a list of objects instead of just the int itemCount to store data (maybe the text in the TextFormField). But you can still delete the item based on the index from the itemBuilder: values.deleteAt(index).
Have a look at the docs for the ListView and the ListTile classes:
https://api.flutter.dev/flutter/widgets/ListView-class.html
https://api.flutter.dev/flutter/material/ListTile-class.html

Flutter widget test does not trigger DropdownButton.onChanged when selecting another item

I am writing a Flutter web app, and adding some widget tests to my codebase. I am having difficulty making flutter_test work as intended. The current problem I face is trying to select a value in a DropdownButton.
Below is the complete widget test code that reproduces the problem:
void main() {
group('description', () {
testWidgets('description', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp(
home: Card(
child: Column(
children: [
Expanded(
child: DropdownButton(
key: Key('LEVEL'),
items: [
DropdownMenuItem<String>(
key: Key('Greater'),
value: 'Greater',
child: Text('Greater'),
),
DropdownMenuItem<String>(
key: Key('Lesser'),
value: 'Lesser',
child: Text('Lesser'),
),
],
onChanged: (value) {
print('$value');
},
value: 'Lesser',
),
)
],
),
),
));
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Lesser'));
await tester.tap(find.byKey(Key('LEVEL')));
await tester.tap(find.byKey(Key('Greater')));
await tester.pumpAndSettle();
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Greater'));
});
});
}
This test fails on the final expectation -- expect(widget.value, equals('Greater'));
The onChanged callback is never invoked, as I can see in the debugger, or looking for my print statement in the output.
What is the magic to test the behavior of a DropdownButton?
While testing it is important to add a tester.pump() call after any user interaction related code.
The testing of dropdown button is slightly different from usual widgets. For all the cases it is better to refer here which is the actual test for dropdownbutton.
Some hints while testing.
DropdownButton is composed of an 'IndexedStack' for the button and a normal stack for the list of menu items.
Somehow the key and the text you assign for DropDownMenuItem is given to both the widgets in the above mentioned stacks.
While tapping choose the last element in the returned widgets list.
Also the dropdown button takes some time to animate so we call tester.pump twice as suggested in the referred tests from flutter.
The value property of the DropdownButton is not changed automatically. It has to set using setState. So your last line of assertion is wrong and will not work unless you wrap your test in a StatefulBuilder like here.
For more details on how to use DropDownButton check this post
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('description', () {
testWidgets('description', (WidgetTester tester) async {
String changedValue = 'Lesser';
await tester.pumpWidget(MaterialApp(
home: Scaffold(
body: Center(
child: RepaintBoundary(
child: Card(
child: Column(
children: [
Expanded(
child: DropdownButton(
key: Key('LEVEL'),
items: [
DropdownMenuItem<String>(
key: ValueKey<String>('Greater'),
value: 'Greater',
child: Text('Greater'),
),
DropdownMenuItem<String>(
key: Key('Lesser'),
value: 'Lesser',
child: Text('Lesser'),
),
],
onChanged: (value) {
print('$value');
changedValue = value;
},
value: 'Lesser',
),
)
],
),
),
),
),
),
));
expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
equals('Lesser'));
// Here before the menu is open we have one widget with text 'Lesser'
await tester.tap(find.text('Lesser'));
// Calling pump twice once comple the the action and
// again to finish the animation of closing the menu.
await tester.pump();
await tester.pump(Duration(seconds: 1));
// after opening the menu we have two widgets with text 'Greater'
// one in index stack of the dropdown button and one in the menu .
// apparently the last one is from the menu.
await tester.tap(find.text('Greater').last);
await tester.pump();
await tester.pump(Duration(seconds: 1));
/// We directly verify the value updated in the onchaged function.
expect(changedValue, 'Greater');
/// The follwing expectation is wrong because you haven't updated the value
/// of dropdown button.
// expect((tester.widget(find.byKey(Key('LEVEL'))) as DropdownButton).value,
// equals('Greater'));
});
});
}