Hopefully someone can help me on this. I have this scenario where my textfield has been set an error text since user click continue without choosing value. Supposedly, when i tap on the textfield, a dialog will show with a list of data that i should choose. When done choosing, the value is set to the textfield and the error text is reset to null. But currently the error text is not reset to null and it's still shown along the value that i chose. I'm thinking maybe it's because i need to rebuild MySearchList class by calling setstate. But i am not sure how to do this. Hope anyone can advise me on what is the best way to do this. Thanks in advance.
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class MySearchList extends StatefulWidget {
MySearchList(
{required this.label,
required this.controller,
required this.dataList,
this.errorText});
final String label;
final TextEditingController controller;
final List<String> dataList;
String? errorText;
#override
_MySearchListState createState() => _MySearchListState();
}
class _MySearchListState extends State<MySearchList> {
#override
Widget build(BuildContext context) {
return Material(
child: TextField(
decoration: InputDecoration(
labelText: widget.label,
errorText: widget.errorText,
),
readOnly: true,
controller: widget.controller,
onTap: () {
showDialog(
context: context,
builder: (BuildContext context) {
return MySearchListDialog(
label: widget.label,
controller: widget.controller,
dataList: widget.dataList,
);
});
}),
);
}
}
// ignore: must_be_immutable
class MySearchListDialog extends StatefulWidget {
MySearchListDialog(
{required this.label,
required this.controller,
required this.dataList,
this.errorText});
final String label;
final TextEditingController controller;
final List<String> dataList;
String? errorText;
#override
_MySearchListDialogState createState() => _MySearchListDialogState();
}
class _MySearchListDialogState extends State<MySearchListDialog> {
TextEditingController _textController = TextEditingController();
List<String> _mainDataList = [];
List<String> _newDataList = [];
#override
void initState() {
super.initState();
_mainDataList = widget.dataList;
_newDataList = List.from(_mainDataList);
}
#override
void dispose() {
_textController.dispose();
super.dispose();
}
onItemChanged(String value) {
setState(() {
_newDataList = _mainDataList
.where((string) => string.toLowerCase().contains(value.toLowerCase()))
.toList();
});
}
#override
Widget build(BuildContext context) {
return Dialog(
child: Container(
constraints: BoxConstraints(
minHeight: 300.0,
maxHeight: 400.0,
),
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 0.0),
child: MyTextField(
hintLabel: "Search",
controller: _textController,
onChanged: onItemChanged,
errorText: widget.errorText,
),
),
Expanded(
child: ListView(
padding: EdgeInsets.all(12.0),
children: _newDataList.map((data) {
return ListTile(
title: Text(data),
onTap: () {
//this setstate will not rebuild class MySearchList
setState(() {
widget.errorText = null; //reset error on textfield
widget.controller.text = data.toString(); //set value to textfield
Navigator.pop(context); //close dialog
});
});
}).toList(),
),
)
],
),
));
}
}
Show your dialog like this:
_MySearchListState
onTap: () async {
var data = await showDialog(
context: context,
builder: (BuildContext context) {
return MySearchListDialog(
label: widget.label,
controller: widget.controller,
dataList: widget.dataList,
);
});
if(null != data){
setState(() {
});
}
})
And close your dialog like this:
_MySearchListDialogState
onTap: () {
//this setstate will not rebuild class MySearchList
setState(() {
widget.errorText = null; //reset error on textfield
widget.controller.text = data.toString(); //set value to textfield
});
Navigator.of(context).pop(data); // <-- Pass data and close dialog
});
Related
in my flutter app I'm using a dynamic form in which the user adds more fields based on their info. this form consists of two textfields and one dropdown. what I want to achieve is as follows.
the issue I'm facing is that when I remove a certain form group it removes from the last index but the value of the form is removed correctly. but the value from the UI removed is the last one. for the textfields I can create controller and manage with the through dispose method. but how can I make it work for the dropdown as well?
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();
});
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],
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: (value) {
widget.onchangeCategory;
if (widget.dataCtrl.containsKey('${widget.key}') &&
value != null) {
widget.dataCtrl['${widget.key}'] = value.toString();
}
})
/// 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;
}
}
screenshot of the problem. check the category list from the text above vs the dropdown value.
Your code is a bit confusing, i tried to reproduce your error and something came to my attention:
The problem seems to be with the dropdown, as the text fields reset correctly.
You initialize your data in the initState in MyForm, however when you change a category for example, this is no longer called. So I moved that out to the build method. Also I have passed a value to the dropdown. This made it work for me:
Widget build(BuildContext context) {
_phoneCtrl = TextEditingController(text: widget.initInfo.phone);
_emailCtrl = TextEditingController(text: widget.initInfo.email);
selected = widget.initInfo.category;
Add this to your DropdownButton
value: selected!.isEmpty ? null : selected,
I new to Flutter and i was trying to find a solution for the below issue for several hours. I have searched and every solution provided does not work form me.
I have page where one of the widgets is the autocomplete text input. I have created this autocomplete widget on different class. I have added this widget as StatefulBuilder within my main widget. it is working fine however, i am not able to access its value so I can store it with other fields.
My code look like
class ItemDetails extends StatefulWidget {
const ItemDetails({Key? key}) : super(key: key);
static const routeName = '/item_details';
#override
State<ItemDetails> createState() => _ItemDetails();
}
class _ItemDetails extends State<ItemDetails> {
late TextEditingController labelController;
late TextEditingController valueController;
late TextEditingController notesController;
bool _submitted = false;
late var args;
String _itemLabel2 = "";
// var labelAutoComp = LabelSugg();
#override
void initState() {
super.initState();
labelController = TextEditingController();
valueController = TextEditingController();
notesController = TextEditingController();
}
#override
void dispose() {
labelController.dispose();
valueController.dispose();
notesController.dispose();
// Hive.close();
super.dispose();
}
String? _labelErrorText(context) {
final text = labelController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
String? _valueErrorText(context) {
final text = valueController.value.text;
if (text.isEmpty) {
// return 'Can\'t be empty';
return AppLocalizations.of(context)!.noEmpty;
}
}
#override
Widget build(BuildContext context) {
try {
args = ModalRoute.of(context)!.settings.arguments as Map;
} on Exception catch (e) {
// print(e);
}
// print(args);
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
LabelSugg(getLabelText: (String val) {
print(val);
_itemLabel2 = val;
}),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.label,
hintText: AppLocalizations.of(context)!.labelHint,
errorText:
_submitted ? _labelErrorText(context) : null,
),
controller: labelController,
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.value,
hintText: AppLocalizations.of(context)!.valueHint,
errorText:
_submitted ? _valueErrorText(context) : null,
),
controller: valueController,
keyboardType: const TextInputType.numberWithOptions(
decimal: true, signed: false),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
TextInputFormatter.withFunction(
(oldValue, newValue) {
try {
final text = newValue.text;
if (text.isNotEmpty) double.parse(text);
return newValue;
} catch (e) {}
return oldValue;
}),
], // Only numbers can be entered
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 5),
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!.notes,
hintText: AppLocalizations.of(context)!.noteHint,
),
controller: notesController,
onChanged: (_) => setState(() {}),
),
]),
// ],
),
Expanded(
child: Align(
alignment: FractionalOffset.bottomCenter,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: () {
setState(() => _submitted = true);
if (_labelErrorText(context) == null &&
_valueErrorText(context) == null) {
//insert
var localLabel = labelController.value.text;
var _localValue = 0.0;
if (valueController.value.text != '') {
_localValue =
double.parse(valueController.value.text);
} else {
_localValue = 0.0;
}
var localNotes = notesController.value.text;
addItemToList(
localLabel, _localValue, localNotes);
Navigator.of(context).pop();
labelController.clear();
valueController.clear();
notesController.clear();
}
},
label: Text(AppLocalizations.of(context)!.add),
icon: const Icon(Icons.save, size: 18),
),
const SizedBox(width: 10),
ElevatedButton.icon(
onPressed: () => {Navigator.pop(context)},
label: Text(AppLocalizations.of(context)!.cancel),
icon: const Icon(Icons.cancel, size: 18),
),
],
)),
),
// )
],
)));
}
void addItemToList(String localLabel, double localValue, String localNotes) {
var _item = YearItems()..yearID = args['year'];
_item.itemLabel = localLabel;
_item.itemValue = localValue;
_item.itemNote = localNotes;
print(_itemLabel2);
final itemsBox = ItemsBoxes.getTransactions();
itemsBox.add(_item);
}
}
my labelAutoComp widget code look like
class LabelSugg extends StatefulWidget {
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController2;
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
getLabel() {
return widget.getLabelText(fieldTextEditingController2.text);
}
#override
Widget build(BuildContext context) {
List<LabelsAc> labelOptions = <LabelsAc>[
LabelsAc(label: AppLocalizations.of(context)!.labelClothes),
LabelsAc(label: AppLocalizations.of(context)!.labelFood),
LabelsAc(label: AppLocalizations.of(context)!.labelPerfumes),
LabelsAc(label: AppLocalizations.of(context)!.labelCapital),
];
return Autocomplete<LabelsAc>(
optionsBuilder: (TextEditingValue textEditingValue) {
return labelOptions
.where((LabelsAc _label) => _label.label
.toLowerCase()
.startsWith(textEditingValue.text.toLowerCase()))
.toList();
},
displayStringForOption: (LabelsAc option) => option.label,
fieldViewBuilder: (BuildContext context,
TextEditingController fieldTextEditingController,
// fieldTextEditingController,
FocusNode fieldFocusNode,
VoidCallback onFieldSubmitted) {
return TextField(
controller: fieldTextEditingController,
focusNode: fieldFocusNode,
style: const TextStyle(fontWeight: FontWeight.bold),
// onChanged: getLabel(),
onChanged: (String val) {
fieldTextEditingController2 = fieldTextEditingController;
getLabel();
});
},
onSelected: (LabelsAc selection) {
fieldTextEditingController2 =
TextEditingController(text: selection.label);
getLabel();
},
optionsViewBuilder: (BuildContext context,
AutocompleteOnSelected<LabelsAc> onSelected,
Iterable<LabelsAc> options) {
return Align(
alignment: Alignment.topLeft,
child: Material(
child: Container(
// width: 350,
// color: Colors.cyan,
child: ListView.builder(
padding: const EdgeInsets.all(10.0),
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final LabelsAc option = options.elementAt(index);
return GestureDetector(
onTap: () {
onSelected(option);
},
child: ListTile(
title: Text(option.label,
style: const TextStyle(color: Colors.black)),
),
);
},
),
),
),
);
},
);
// ),
// );
}
}
class LabelsAc {
LabelsAc({required this.label});
String label;
}
first is redundant when you wrap your class that extend StatefullWidget with StatefullBuilder. LabelSugg is a component Widget. you can use it like other widget.
benefit to separate widget with StatefullWidget class is, we can update the value inside the class without re-build the current page. which is good for performance. that's why developer recomend to separete with class insted compared to make local method.
as you see, when you create LabelSugg extend StatefullWidget class , we will have _LabelSugg . underscore means that: all variable only accessible on current file.
thats why we can't call getLabel() or other variable from different file.
its used for handle the State in 'LabelSugg` widget.
now how to pass the value from LabelSugg is by created variable outside the state. here you are:
class LabelSugg extends StatefulWidget {
// use this to pass any changes when we use LabelSugg
final ValueChanged<String> getLabelText;
const LabelSugg({Key? key, required this.getLabelText}) : super(key: key);
#override
State<LabelSugg> createState() => _LabelSugg();
}
then we can call the onChaged inside _LabelSugg state. because its Statefull widget, we can acces by : widget.getLabelText()
class _LabelSugg extends State<LabelSugg> {
late TextEditingController fieldTextEditingController;
.....
getLabel() {
return widget.getLabelText(fieldTextEditingController.text);
}
then in other class we call LabelSugg like common widget
import 'package:../labelsug.dart';
class ItemDetails extends StatefulWidget {
.....
return Scaffold(
appBar: AppBar(
title: Text(args['title']),
),
body: Container(
padding: const EdgeInsets.all(20),
child: Column(
children: <Widget>[
// now use it like a widget
LabelSug(
getLabelText: (String val){
print(val);
}
:)
I created a list element search. I get the list from the Internet. The data is all displayed, but when I want to use the _runFilter method, the data disappears and nothing happens. 1. How to make data search work? 2. When I open the page, my _foundAddressesInStationList is empty, but I initialize it and the data should be displayed, why is it not displayed?
Thanks in advance.
List<PublicChargingStationModel> stationsList = [];
List<PublicChargingStationModel> _foundAddressesInStationList = [];
#override
void initState() {
_loadStations();
_foundAddressesInStationList = stationsList;
super.initState();
}
void _runFilter(String enteredKeyword) {
List<PublicChargingStationModel> resultAddressesInStationList = [];
if (enteredKeyword.isEmpty) {
resultAddressesInStationList = stationsList;
} else {
resultAddressesInStationList = stationsList
.where((element) => element.address!
.toLowerCase()
.contains(enteredKeyword.toLowerCase()))
.toList();
}
setState(
() {
_foundAddressesInStationList = resultAddressesInStationList;
},
);
}
#override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
final double paddingTop = MediaQuery.of(context).padding.top;
return Container(
width: size.width,
height: size.height,
child: _child(size, paddingTop),
);
}
void _loadStations() async {
final StationCubit stationCubit = BlocProvider.of<StationCubit>(context);
stationsList = await stationCubit.getPublicChargingStation();
}
}
In the init method you are calling your cubit which is asynchronous, but without waiting for the result you initialize the _foundAddressesInStationList with stationList which is empty. So, initialize the _foundAddressesInStationList in the load method like this.
void _loadStations() async {
final StationCubit stationCubit = BlocProvider.of<StationCubit>(context);
stationsList = await stationCubit.getPublicChargingStation();
_foundAddressesInStationList = stationsList;
setState((){});
}
Filter should work fine. Do you have any warnings or errors? Do you have expected values in resultAddressesInStationList while filtering?
Edit:
Alternate Approach for Filter:
stationsList.forEach((element) {
if(element.address!.toLowerCase().contains(enteredKeyword.toLowerCase())) {
resultAddressesInStationList.add(element);
}
});
_foundAddressesInStationList = resultAddressesInStationList;
setState(() {});
The only place you added null check operator is element.address!. "Null check operator used on a null value" means the address should be null or should not exists.
Also try element["address"]
Edit 2:
You Search Widget like this should be:
class MySearchWidget extends StatefulWidget {
MySearchWidget(
{Key? key,
required this.onChanged, this.isListClear})
: super(key: key);
final Function(String?) onChanged;
final Function(bool)? isListClear;
#override
State<MySearchWidget> createState() => _MySearchWidget();
}
class _MySearchWidget extends State<MySearchWidget> {
late FocusNode myFocusNode;
final TextEditingController _textController = TextEditingController();
#override
void initState() {
myFocusNode = FocusNode();
super.initState();
}
#override
void dispose() {
myFocusNode.dispose();
super.dispose();
}
_onBackPressed(context) {}
#override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
_onBackPressed(context);
},
icon: Icon(Icons.arrow_left),
),
Expanded(
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.only(right: 14),
child: TextFormField(
controller: _textController,
focusNode: myFocusNode,
autofocus: true,
onChanged: (value) => widget.onChanged(value),
decoration: InputDecoration(
suffixIcon: _textController.text.isNotEmpty
? GestureDetector(
onTap: () {
_textController.clear();
widget.isListClear!(true);
},
child: Container(
width: 10,
height: 10,
alignment: Alignment.center,
child: Icon(Icons.clear),
),
)
: const SizedBox(),
),
),
),
),
],
);
}
}
If not used like this, the change method of this widget will be called while initializing the widget and it will filter your data for the empty string which is unwanted and will also raise issues.
But the actual error is the null operator. I guess your PublicChargingStationModel class should have the 'address' property as not null.
So you should not use like
resultAddressesInStationList = stationsList
.where((element) => element.address!
.toLowerCase()
.contains(enteredKeyword.toLowerCase()))
.toList();
instead, use it like
resultAddressesInStationList = stationsList
.where((element) => element.address
.toLowerCase()
.contains(enteredKeyword.toLowerCase()))
.toList();
or change your model address property to nullable and keep your filter as it is now.
class PublicChargingStationModel{
final String? address;
PublicChargingStationModel({required this.address});
}
I have built a very simple form in flutter and I am trying to save the value of whatever is typed in the form fields to variables. This way, I can push these variables to firebase. However, nothing in the onSaved() block of the TextFormFields is being run. I have called save() on the current state of the form, but it still doesn't seem to work. Any ideas?
I have attached the code for the page below:
import 'package:flutter/material.dart';
class AddJobPage extends StatefulWidget {
const AddJobPage({Key? key}) : super(key: key);
static Future<void> show(BuildContext context) async {
await Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const AddJobPage(),
fullscreenDialog: true
));
}
#override
_AddJobPageState createState() => _AddJobPageState();
}
class _AddJobPageState extends State<AddJobPage> {
final _formKey = GlobalKey<FormState>();
//These two variables are where we will store the values of the text form fields
//before we push to firestore.
String? _name = '';
int _ratePerHour = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 2,
title: const Text("New Job"),
backgroundColor: Colors.teal.shade700,
actions: [
TextButton(
onPressed: _submit,
child: const Text(
'Save',
style: TextStyle(fontSize: 18, color: Colors.white),
)
)
],
),
body: _buildContents(),
backgroundColor: Colors.grey.shade200,
);
}
void _submit() {
if(_validateAndSave()) {
print("form saved, name: $_name, ratePerHour: $_ratePerHour");
}
}
bool _validateAndSave() {
final form = _formKey.currentState;
if(form!.validate()) {
print("the form was saved here");
form.save;
return true;
}
return false;
}
Widget _buildContents() {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildForm(),
),
),
),
);
}
Widget _buildForm() {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _buildFormChildren(),
)
);
}
List<Widget> _buildFormChildren() {
return [
TextFormField(
decoration: const InputDecoration(labelText: 'Job name'),
onSaved: (value) {
print("code doesn't reach here");
_name = value; //save the value of the text field to _name
}
),
TextFormField(
decoration: const InputDecoration(labelText: 'Rate Per Hour'),
keyboardType: const TextInputType.numberWithOptions(
signed: false,
decimal: false
),
onSaved: (value) {
_ratePerHour = int.tryParse(value!) ?? 0;
}
),
];
}
}
You need to call the save function.
So replace form.save; in your code with form.save();
replace form.save with form.save() and you're done. No need to worry about text controllers since you're using a text form field
you need to add a TextEditingController to get the text from a TextFormField. Then you need to call setstate inside OnChanged, not on Onsaved so you can transfer the text to a variable. This is what your code should look like:
First you initialize a controller like this
final TextEditingController textController = TextEditingController();
String _name = "not set";
then you add the controller to your textformfield like this
TextFormField(
controller: textController ,
onChanged:(value)
{
setState(() {
_name = textController.text;
});
} ,
)
This is a complete example:
class TextFormFieldExample extends StatefulWidget {
const TextFormFieldExample({Key? key}) : super(key: key);
#override
_TextFormFieldExampleState createState() => _TextFormFieldExampleState();
}
class _TextFormFieldExampleState extends State<TextFormFieldExample> {
//Create the controller here
final TextEditingController textController = TextEditingController();
String _name = "not set";
#override
Widget build(BuildContext context) {
print(_name);
return Scaffold(
body: Material(
child: Container(
color: Colors.white,
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child:TextFormField(
controller: textController ,//Attach the controller to the text form here
onChanged:(value)
{
setState(() {
_name = textController.text;//Save the text from the controller to a variable
});
} ,
),
),
),
);
}
}
I'm new with Flutter and provider.
I'm trying to make a form with provider in order to separate my logic in my code but I'm struggling ...
My form in the screen :
class CalculatorScreen extends StatefulWidget{
CalculatorScreen({Key key}) : super(key: key);
#override
_CalculatorScreenState createState() => _CalculatorScreenState();
}
class _CalculatorScreenState extends State<CalculatorScreen> {
final TextEditingController _controllerDistance = TextEditingController();
#override
void dispose(){
_controllerDistance.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() => FocusScope.of(context).requestFocus(FocusNode())),
child: Scaffold(
body : _buildBody(context)
),
);
}
Widget _buildBody(BuildContext context)
{
var _formCalculatorProvider = Provider.of<FormCalculatorNotifier>(context);
return SingleChildScrollView(
child: Column(
children: [
ContainerComponent(
background: AppColors.colorBgLight,
children: [
Form(
key : _formCalculatorProvider.globalFormKey,
autovalidate: _formCalculatorProvider.autovalidate,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
decoration: InputDecoration(
labelText: "Distance",
),
controller: _controllerDistance,
keyboardType : TextInputType.number,
validator: (String value){
return FormValidatorService.isDistanceValid(value);
},
onSaved: (var value) {
_formCalculatorProvider.saveDistance(value);
}
),
],
),
),
ButtonComponent.primary(
context: context,
text: "Send",
onPressed: _formCalculatorProvider.submit,
),
],
)
],
),
);
}
}
And my notifier :
enum FormCalculatorState{
READY,
SUCCESS,
ERROR
}
class FormCalculatorNotifier with ChangeNotifier {
final GlobalKey<FormState> globalFormKey = GlobalKey<FormState>();
FormCalculatorState formState = FormCalculatorState.READY;
bool autovalidate = false;
FormCalculatorModel formData = FormCalculatorModel();
void saveDistance(String value){
print("save");
formData.distance = num.tryParse(value).round();
notifyListeners();
}
void submit(){
if (!globalFormKey.currentState.validate()) {
print("submit");
print(formData);
autovalidate = true;
formState = FormCalculatorState.ERROR;
return;
}
else{
globalFormKey.currentState.save();
}
notifyListeners();
}
Future showErrorNotification(){
// Here I need to know the context
return InfoBarComponent.error(title: AppTextInfobar.ERROR_TITLE, description: AppTextInfobar.ERROR_DESCRIPTION, context: context);
}
How to use my showErrorNotification because I need the context to show my notificationBar ? When I try to add context in the scrren on the submit function I have an error.
Is this the right method?
Did not go through your entire code. But I immediately noticed that notifyListeners is missing in FormCalculatorNotifier class.