Flutter TextFormField not updating value on rebuild - flutter

I'm trying to make it possible to edit a list of strings. The problem is, when I delete a list item via the IconButton, the TextField rebuilds but somehow still has the old value. With debugging I see, that my List of Strings are however updated correctly, meaning the deleted String is actually being deleted in my list.
This is my code:
class EditInfoItemDialog extends StatefulWidget {
#override
State<StatefulWidget> createState() => _EditInfoitemDialogState();
}
class _EditInfoitemDialogState extends State<EditInfoItemDialog> {
final _formKey = GlobalKey<FormState>();
List<String> values = ['A', 'B', 'C'];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
child: Column(
children: <Widget>[
...values.asMap().map((int index, String point) {
return MapEntry(index, Row(
children: [
Text(index.toString()),
Expanded(
child: TextFormField(
decoration: InputDecoration(hintText: 'Info'),
initialValue: values[index],
onChanged: (value) => setState(() {values[index] = value; }),
),
),
IconButton(
icon: Icon(Icons.delete),
color: Colors.red,
onPressed: () {
setState(() {
values.removeAt(index);
print(values);
});
},
)
]
));
}).values.toList(),
FlatButton(
child: Text('Add Bulletpoint'),
onPressed: () {
setState(() {
if (values == null) values = [];
values.add('');
});
},
)
],
),
),
);
}
Any Ideas?

You need to add a key to your TextFormField like this:
key: ObjectKey(values[index])
Here is an explanation and an example with the reason why you need to add a key in this case: What are Keys in the Stateless widgets class?
Key is something used by flutter engine at the step of recognizing
which widget in a list as changed
More info:
key property

Related

How can I make dropdownbutton using Getx in flutter?

I'm trying to make dropdownbutton using Getx in flutter
However, it doesn't work.
Even if I choose a value, the value does not been selected.
class BecomePlayerPage2 extends GetView<BecomePlayerController> {
const BecomePlayerPage2 ({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Obx(
() => Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child:
DropdownButton<RxString>(
onChanged: (newValue){
controller.selected=newValue!;
},
value: controller.selected,
items: [
for(var value in controller.tierList)
DropdownMenuItem(
child: new Text(
value,
),
value: value.obs,
),
]
),
),
),
}
class BecomePlayerController extends GetxController {
final tierList=['first', 'second', 'third'].obs;
var selected = "first".obs;
}
This is my first day of studying Getx so it is so hard to match with basic widget.
What should I do?
Try replacing the onChange statement with
onChanged: (newValue) {
controller.selected.value = newValue.toString();
},
or by changing Dropdown button type from RXString to String
return Obx(
() => Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: DropdownButton<String>( // updated
onChanged: (newValue) {
controller.selected.value = newValue!; //updated
},
value: controller.selected.value, //updated
items: [
for (var value in controller.tierList)
DropdownMenuItem(
value: value,
child: Text(
value, //updated
),
),
]),
),
),
);
Use round brackets to update .obs and change RxString to String
authController.selected(newValue.toString());
onChanged: (newValue){
// controller.selected=newValue!;
controller.selected.value =newValue!;
},

Keep dropdown open when active checkbox

I have a code that is responsible for filtering by certain categories (I shortened it for ease of reading). When opening the filter window, the user sees these category names ('Select a brand', 'Select a operation system', 'Select a color' etc).
Next, the user can open the category (initially, the dropdown list is in the closed position.), and select the parameters from the drop-down list (and click the apply button). The next time you open the filter window, the checkboxes in front of the parameters remain, but the drop-down list collapses.
Tell me how to do it: if in any category there are options marked with a checkmark, so that the drop-down list will be open the next time the window with filters is opened.
class FilterDialog extends StatefulWidget {
final void Function(Map<String, List<String>?>) onApplyFilters;
final Map<String, List<String>?> initialState;
const FilterDialog({
Key? key,
required this.onApplyFilters,
this.initialState = const {},
}) : super(key: key);
#override
State<FilterDialog> createState() => _FilterDialogState();
}
class _FilterDialogState extends State<FilterDialog> {
// Temporary storage of filters.
Map<String, List<String>?> filters = {};
bool needRefresh = false;
// Variable for the ability to hide all elements of filtering by any parameter.
bool isClickedBrand = false;
List manufacturer = [];
#override
void initState() {
super.initState();
filters = widget.initialState;
}
// A function to be able to select an element to filter.
void _handleCheckFilter(bool checked, String key, String value) {
final currentFilters = filters[key] ?? [];
if (checked) {
currentFilters.add(value);
} else {
currentFilters.remove(value);
}
setState(() {
filters[key] = currentFilters;
});
}
// Building a dialog box with filters.
#override
Widget build(BuildContext context) {
return SimpleDialog(
// Window title.
title: const Text('Filters',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.w600,
)),
contentPadding: const EdgeInsets.all(16),
// Defining parameters for filtering.
children: [
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Here and in subsequent Column, there will be a definition of parameters for filtering,
// a title, the ability to hide/show the items of list
Column(children: [
InkWell(
onTap: () async {
manufacturer = await getManufacturerOptions();
setState(() {
isClickedBrand = !isClickedBrand;
});
},
child: Row(children: [
Text('Select a brand'.toString(),
style: const TextStyle(
fontSize: 18,
)),
const Spacer(),
isClickedBrand
? const Icon(Icons.arrow_circle_up)
: const Icon(Icons.arrow_circle_down)
])),
!isClickedBrand
? Container()
: Column(
children: manufacturer
.map(
(el) => CustomCheckboxTile(
value: filters['manufacturer']?.contains(el) ??
false,
label: el,
onChange: (check) =>
_handleCheckFilter(check, 'manufacturer', el),
),
)
.toList())
]),
const SizedBox(
height: 5,
),
// Building a button to apply parameters.
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
widget.onApplyFilters(filters);
needRefresh = true;
},
child:
const Text('APPLY', style: TextStyle(color: Colors.black)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.grey),
)),
// Building a button to reset parameters.
const SizedBox(
height: 5,
),
ElevatedButton(
onPressed: () async {
setState(() {
filters.clear();
});
widget.onApplyFilters(filters);
},
child: const Text('RESET FILTERS',
style: TextStyle(color: Colors.black)),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.grey),
)),
],
),
],
);
}
}
For example: the user clicks on the filter box, selects the brands to search for, and clicks the apply button. My task is that the next time the user opens the filter window, the categories with active checkboxes (in this example, the brand) are in an expanded state
The concept is, you need to check filter data with while opening dialog, To simplify the process I am using ExpansionTile. You can check this demo and customize the behavior and look.
Run on dartPad, Click fab to open dialog and touch outside the dialog to close this.
class ExTExpample extends StatefulWidget {
ExTExpample({Key? key}) : super(key: key);
#override
State<ExTExpample> createState() => _ExTExpampleState();
}
class _ExTExpampleState extends State<ExTExpample> {
// you can use map or model class or both,
List<String> filter_data = [];
List<String> brands = ["Apple", "SamSung"];
List<String> os = ["iOS", "Android"];
_showFilter() async {
await showDialog(
context: context,
builder: (c) {
// you can replace [AlertDialog]
return AlertDialog(
content: StatefulBuilder(
builder: (context, setSBState) => SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ExpansionTile(
title: const Text("Brand"),
/// check any of its's item is checked or not
initiallyExpanded: () {
// you can do different aproach
for (final f in brands) {
if (filter_data.contains(f)) return true;
}
return false;
}(),
children: [
...brands.map(
(brandName) => CheckboxListTile(
value: filter_data.contains(brandName),
title: Text(brandName),
onChanged: (v) {
if (filter_data.contains(brandName)) {
filter_data.remove(brandName);
} else {
filter_data.add(brandName);
}
setSBState(() {});
//you need to reflect the main ui, also call `setState((){})`
},
),
),
],
),
ExpansionTile(
title: const Text("select OS"),
/// check any of its's item is checked or not
initiallyExpanded: () {
// you can do different aproach
for (final f in os) {
if (filter_data.contains(f)) return true;
}
return false;
}(),
children: [
...os.map(
(osName) => CheckboxListTile(
value: filter_data.contains(osName),
title: Text(osName),
onChanged: (v) {
if (filter_data.contains(osName)) {
filter_data.remove(osName);
} else {
filter_data.add(osName);
}
setSBState(() {});
//you need to reflect the main ui, also call `setState((){})`
},
),
),
],
),
],
),
),
),
);
},
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FloatingActionButton(
onPressed: () {
_showFilter();
},
),
),
);
}
}
Whenever you open filter the isClickedBrand is False so it won't showed you a list. So the solution is :
After selecting option from list, change the state of isClickedBrand state. I mean if it's true then it will show the list otherwise show container.
Hope you get my point.

Open Flutter dropdown programmatically

I am trying to build a searchable dropdown which will load value from service on every button click. So, for that I have encapsulated DropDownButton and TextField in Stack Widget.
On keypress we get response from api, so far so good. But after getting data from api dropdown was not opening. After digging a bit I came to know it was not opening because we need to manually tap it to open, but since its in stack and second children is TextField I can't tap it.
But opening DropDownButton button programmatically is not possible.
So I tried second solution from https://stackoverflow.com/a/59499191/10423593 but it didn't work.
Below is my code without solution from stackoverflow.
import 'package:flutter/material.dart';
import 'package:giphy/services/gifs_service.dart';
import 'package:giphy/shared/autocomplete.dart';
class TestDropDown extends StatefulWidget {
// const TestDropDown({Key? key}) : super(key: key);
#override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
final GifyService _service = GifyService();
final TextEditingController _gifSearchController = TextEditingController();
List<SearchData> _dropDownResult = <SearchData>[];
GlobalKey key = GlobalKey();
// T? _findChildWidgetFromKey<T extends Widget>(
// BuildContext? context, T childWidget) {
// T? detector;
// context!.visitChildElements((element) {
// if (element.widget == childWidget) {
// detector = element.widget as T;
// }
// });
// return detector;
// }
Widget _buildDropDown(List<SearchData> searchData) => DropdownButton<String>(
isExpanded: true,
key: key,
onChanged: (String? value) {},
items: searchData
.map(
(e) => DropdownMenuItem<String>(
child: Text(e.name ?? ''),
value: e.name ?? '',
),
)
.toList(),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_buildDropDown(_dropDownResult),
Container(child: () {
if (_dropDownResult.length > 0) {
_buildDropDown(_dropDownResult);
}
}()),
TextField(
controller: _gifSearchController,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(width: 0.5),
borderRadius: BorderRadius.circular(21),
),
),
onChanged: (String value) async {
AutoComplete result = await _service.getSearchKeywords(value);
setState(() {
_dropDownResult = result.data;
});
},
),
],
),
);
}
}
If you are looking to autocomplete the text field then you can use
autocomplete_textfield package.
Or if you want to build it on your own then you can try a different approach by using container instead of dropdown menu.
After trying some packages. I wasn't able to do flutter autocomplete based on api calls.
So decided to try some other approach and used Overlay in flutter
import 'dart:async';
import 'package:flutter/material.dart';
class TestDropDown extends StatefulWidget {
#override
_TestDropDownState createState() => _TestDropDownState();
}
class _TestDropDownState extends State<TestDropDown> {
late OverlayEntry _overlayEntry;
Timer? _debounce;
_showOverlay(BuildContext context) {
OverlayState? state = Overlay.of(context);
final RenderBox? box = key.currentContext!.findRenderObject() as RenderBox;
Size size = box!.size;
Offset position = box.localToGlobal(Offset.zero);
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
top: size.height + position.dy,
left: position.dx,
child: Card(
child: Container(
height: 200,
width: size.width,
decoration: BoxDecoration(
border: Border.all(),
),
child: Column(
children: [
Container(
width: size.width,
child: IconButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
icon: Icon(Icons.close),
alignment: Alignment.topRight,
),
),
Expanded(
child: ListView(
children: []..addAll(List.generate(
200, (index) => Text(index.toString()))),
),
),
],
)),
),
),
);
state!.insert(_overlayEntry);
}
final key = GlobalKey();
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(50),
child: Column(
children: [
TextField(
key: key,
onChanged: (String searchText) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
print(searchText);
});
},
),
ElevatedButton(
onPressed: () {
_showOverlay(context);
},
child: Text('Press Me'),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_overlayEntry.mounted) {
_overlayEntry.remove();
}
},
),
);
}
}
try using: https://pub.dev/packages/dropdown_search
Step 1: you request all data
Step 2: using the library you can searching item

How to call specific validator instead of calling all the validators in same time by using `form.validate()`?

This is the code :
if (form.validate()) {
// Text forms was validated.
if (mounted) {
setState(() {
_isButtonEnabled = true;
});
}
form.save();
}
I want to call specific validator for specific TextFormField
Joao's answer is correct, but it's also over-engineered. You can instead declare a GlobalKey for each field that you want to validate independently:
GlobalKey<FormFieldState> fieldKey = GlobalKey();
Assign it to the TextFormField that you want to validate:
TextFormField(
key: fieldKey,
...
And then use the key to validate that field:
bool valid = fieldKey.currentState.validate();
You will need to wrap each field you want to validate individually with a Form Widget and it's own key:
class TextFormFieldValidation extends StatefulWidget {
#override
_TextFormFieldValidationState createState() => _TextFormFieldValidationState();
}
class _TextFormFieldValidationState extends State<TextFormFieldValidation> {
List<GlobalKey<FormState>> _formKeysList= [
GlobalKey<FormState>(),
GlobalKey<FormState>(),
];
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Form(
key: _formKeysList[0],
child: TextFormField(
validator: (value) => 'Bad',
),
),
Form(
key: _formKeysList[1],
child: TextFormField(
validator: (value) => 'Bad',
),
),
RaisedButton(
child: Text('Validate 1'),
onPressed: () => _formKeysList[0].currentState.validate(),
),
RaisedButton(
child: Text('Validate 2'),
onPressed: () => _formKeysList[1].currentState.validate(),
),
RaisedButton(
child: Text('Reset'),
onPressed: () => _formKeysList.forEach((key) => key.currentState.reset()),
),
],
);
}
}

Flutter ListView widgets resets icon when scrolling items out of screen

I have a ListView with custom statefull widgets as items.
These items has an onTap event that switch the icon from check_box_outline_blank to check_box, and the other way around.
However when I select an item in the list and scroll down and back up again the item has reset the icon from check_box to check_box_outline_blank.
How can I prevent this from happening?
new Expanded(
child: ListView.builder(
itemCount: _list.length,
itemBuilder: (BuildContext context, int index) {
return listTile(_pveList[index], index)
},
)
)
//Adds extra space(height:100) after the last tile
StatefulWidget listTile(String text, int index){
return (index < _pveList.length - 1)?new SelectableListItem(text: text,
onTap: (){
_handleOnTap(text);
},
): new Column(
children: <Widget>[
new SelectableListItem(text: text,
onTap: (){
_handleOnTap(text);
},
),
SizedBox(
height: 100,
)
],
);
}
The tile widget(SelectableListItem):
import 'package:flutter/material.dart';
class SelectableListItem extends StatefulWidget {
SelectableListItem({Key key, this.text, this.onTap})
: super(key: key);
final String text;
final GestureTapCallback onTap;
#override
_SelectableListItemState createState() => _SelectableListItemState();
}
class _SelectableListItemState extends State<SelectableListItem> {
bool isSelected = false;
Icon _checked = new Icon(Icons.check_box_outline_blank);
handleOnTap() {
isSelected = !isSelected;
if (isSelected) {
setState(() {
_checked = new Icon(Icons.check_box);
});
} else {
setState(() {
_checked = new Icon(Icons.check_box_outline_blank);
});
}
return widget.onTap();
}
#override
Widget build(context) {
// Pass the text down to another widget
return new GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => handleOnTap(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
new Padding(
padding: EdgeInsets.all(22),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
new Text(widget.text, style: TextStyle(fontSize: 18)),
_checked
],
),
),
new Container(
width: double.infinity,
height: 1,
color: Color(0x1a222222),
)
],
),
);
}
}
Updated
I believe the OP wants to achieve a standard Checkbox with a label text aside.
CheckboxListTile widget provides such functionality.
Usage example:
bool selected = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: <Widget>[
CheckboxListTile(
onChanged: (val) {
setState(() {
selected = !selected;
});
},
value: selected,
title: Text("Test"),
)
]),
);
}
If you want to have the checkbox located on the left side of the screen - set controlAffinity property accordingly:
CheckboxListTile(
// ...,
controlAffinity: ListTileControlAffinity.leading,
)
Original answer
#1
You should never use functions to build the widget tree.
See this answer for more details.
#2
I'd recommend to use ValueNotifier, declared in SelectableListItem class.
It allows you to change value of a variable in StatefulWidget without the usual warnings for non-final variables.
The value inside of ValueNotifier gets preserved too, just like any other variables that are declared in StatefulWidget class.
e.g.
class SelectableListItem extends StatefulWidget {
// ...
final ValueNotifier<bool> isSelected = ValueNotifier<bool>(false);
// ...
}
class _SelectableListItemState extends State<SelectableListItem> {
handleOnTap() {
setState(() {
widget.isSelected.value = !widget.isSelected.value;
});
return widget.onTap();
}
// ... build() { ...
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(widget.text, style: TextStyle(fontSize: 18)),
Icon(widget.isSelected.value ? Icons.check_box : Icons.check_box_outline_blank),
]
),
// ... } ...
}
ValueNotifier<bool> also eliminates the need to store the icon widget in a separate variable. Instead, you can use it in inline boolean condition, just like it's shown in the code.
PS: I did not test the code, however general instructions are valid. Let me know if this helped.