Trying to get TextFormField data from another widget - flutter

I am building an app that generates form fields from JSON data.
the Form widget is declared in the custom_form.dart
child: Form(
key: _globalKey,
child: Column(
children: [
SizedBox(
height: 10,
),
Text("Register Form: "),
SizedBox(
height: 3,
),
for (var i = 0; i < this.widget.widgetList.length; i++)
generateWidgetFromType(this.widget.widgetList[i]),
I'm using that generateWidgetFrom Type to generate the widgets
Widget generateWidgetFromType(Mywidget mywidget) {
switch (mywidget.type) {
case "CheckBox":
return GenerateCheckBox(mywidget);
case "Input":
return CustomGeneratedWidget(mywidget,
onSaved: (value) => widget.test = value);
case "DatePicker":
return GenerateDatePicker(mywidget);
}
return Text("This type is not supported yet");
}
The TextFormField is declared in the CustomGeneratedWidget
child: TextFormField(
controller: textEditingController,
obscureText: isPasswordField(this.widget.mywidget.key),
keyboardType: customType(this.widget.mywidget.key),
decoration: InputDecoration(
labelText: this.widget.mywidget.label,
border: OutlineInputBorder()),
validator: (value) {
I want to retrieve data from TextFormField in the CustomGeneratedWidget when I click on the Add button in the custom_form.dart

One way might be to add a method called getValue() on to your CustomGeneratedWidget class:
class CustomGeneratedWidget extends Mywidget {
TextEditingController textEditingController = TextEditingController();
...
String getValue() {
return textEditingController.text;
}
...
#override
Widget build(BuildContext context) {
...
}
}
I'm guessing, based on what you're building, that you'll want to eventually get data not just from TextFormField / CustomGeneratedWidget instances, but also instances of GenerateCheckBox and GenerateDatePicker as well. With that being the case, if it were me, I would add an abstract method to Mywidget that you override in its subclasses, perhaps called getValue(). So, Mywidget might look like:
abstract class Mywidget extends Widget {
...
// this method is purposefully unimplemented, and is therefore an "abstract" method
dynamic getValue();
...
}
In your subclasses, such as CustomGeneratedWidget, you would "override" this method by redefining it / implementing it, and therefore making it "concrete":
class CustomGeneratedWidget extends Mywidget {
TextEditingController textEditingController = TextEditingController();
...
#override
String getValue() {
return textEditingController.text;
}
#override
Widget build(BuildContext context) {
return Container(
child: TextFormField(
controller: textEditingController
...
)
);
}
}
Back in custom_form.dart, in your handler for your 'Add' button, you could go through the list of created Mywidget instances and call getValue() on each.

Related

onChanged method with setState of TextField does not update the result?

I am experimenting with flutter and I have a very simple code as follows:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
static double d = 0;
static TextEditingController editingController = TextEditingController();
#override
void initState() {
editingController.addListener(() {
setState(() {});
});
super.initState();
}
#override
void dispose() {
editingController.dispose();
super.dispose();
}
Calc calc = Calc(d: d, e: editingController);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Padding(
padding: const EdgeInsets.all(25),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextField(
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
if (value.isNotEmpty) {
d = double.parse(value);
} else if (value.isEmpty) {
d = 0;
}
});
},
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'(^(\d{1,})\.?(\d{0,2}))'),
),
],
),
TextField(
keyboardType: TextInputType.number,
controller: editingController,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'(^(\d{1,})\.?(\d{0,2}))'),
),
],
),
Text(
'First Text Field Value + 2 = \n${calc.dString()}',
style: TextStyle(fontSize: 30, color: Colors.purpleAccent),
),
Text(
calc.eString(),
style: TextStyle(fontSize: 30, color: Colors.deepOrangeAccent),
),
],
),
),
),
);
}
}
class Calc {
final double d;
final TextEditingController e;
Calc({this.d, this.e});
String dString() {
double result = d + 2;
return result.toStringAsFixed(0);
}
String eString() {
return e.text;
}
}
As we can see I am passing both the text fields' values into another class for some math and getting the results. These results are displayed using the Text widgets.
For the 1st TextField, I have used onChange method, and for the 2nd TextField, I have used TextEditingController.
I get return value for 2nd TextField from the Calc class, but not for the 1st TextField!
I think I am missing something basic and I did not find any solution so far. Can you please help me what am I missing here.
1st of all, you are creating just a single object of Calc,
yes as you can see your 2nd textField update perfectly because it's using TextEditingController but for the 1st one, it just call once and become 2 because of dString(), while on 1st run d becomes 0 passed on Calc.
if you want to use Calc to update text, you can simply put it inside build method like this , i dont suggest it, you can use callBackMethod to handle this or use another TextEditingController.
Hope you get it now
#override
Widget build(BuildContext context) {
Calc calc = Calc(d: d, e: editingController);
return Scaffold(
body: Center(
Your Calc Object is not being affect by setState() call. To run be able to get value of the calcobject, run it in you onChanged() function.

Is there any workaround for using keepalive mixin alongside the GetX package?

I created a slider-based stepper form using TabBarView which validate the input before switching. It works, but when I go back, the state was reset. This behavior leads me to an empty form when I try to collect the data at the end of the tab.
I have googled for few hours and have been tried switching the current GetView<MyController> to the classic StatefulWidget with AutomaticKeepAliveMixin with no luck, so I revert it.
I'm a bit stuck, I wonder if there is any other way to achieve this, the GetX way, if possible.
visual explanation
`
create_account_form_slider.dart
class CreateAccountFormSlider extends GetView<CreateAccountController> {
#override
Widget build(BuildContext context) {
return Expanded(
child: TabBarView(
physics: const NeverScrollableScrollPhysics(),
controller: controller.tabController,
children: [
_buildEmailForm(),
_buildNameForm(),
_buildPasswordForm(),
],
),
);
}
Widget _buildEmailForm() {
return Form(
key: controller.emailFormKey,
child: Column(
children: [
Spacer(), // Necessary to push the input to the bottom constraint, Align class doesn't work.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
margin: EdgeInsets.zero,
label: 'create_account_form_email'.tr,
hintText: 'janedoe#example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
),
),
],
),
);
}
... each form has similar structure (almost identical), so i will not include it here
create_account_controller.dart
class CreateAccountController extends GetxController
with SingleGetTickerProviderMixin {
final tabIndex = 0.obs;
final emailFormKey = GlobalKey<FormState>();
FormState get emailForm => emailFormKey.currentState;
final emailFocusNode = FocusNode();
final email = ''.obs;
TabController tabController;
#override
void onInit() {
_initTabController();
super.onInit();
}
#override
void onClose() {
_disposeFocusNodes();
_disposeTabController();
super.onClose();
}
/// Initialize tab controller and add a listener.
void _initTabController() {
tabController = TabController(vsync: this, length: 3);
tabController.addListener(_tabListener);
}
/// Listen on tab change and update `tabIndex`
void _tabListener() => tabIndex(tabController.index);
/// Dispose tab controller and remove its listener.
void _disposeTabController() {
tabController.removeListener(_tabListener);
tabController.dispose();
}
/// Dispose all the focus nodes.
void _disposeFocusNodes() {
emailFocusNode.dispose();
}
/// Animate to the next slide.
void _nextSlide() => tabController.animateTo(tabIndex() + 1);
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
_nextSlide();
return focusScope.requestFocus(nameFocusNode);
}
...
}
/// A function that checks the validity of the given value.
///
/// When the email is empty, show required error message and when the email
/// is invalid, show the invalid message.
String emailValidator(String val) {
if (val.isEmpty) return 'create_account_form_error_email_required'.tr;
if (!val.isEmail) return 'create_account_form_error_email_invalid'.tr;
return null;
}
/// Submit data to the server.
void _submit() {
print('TODO: implement submit');
print(email());
}
}
I made it by saving the form and adding an initialValue on my custom FormInput widget then put the observable variable onto each related FormInput. No need to use keepalive mixin.
create_account_controller.dart
/// Animate to the next slide or submit if current tab is the last tab.
void next() {
if (tabIndex().isEqual(0) && emailForm.validate()) {
// save the form so the value persisted into the .obs variable
emailForm.save();
// slide to next form
_nextSlide();
// TODO: wouldn't it be nice if we use autofocus since we only have one input each form?
return focusScope.requestFocus(nameFocusNode);
}
...
}
create_account_form_slider.dart
Obx( // wrap the input inside an Obx to rebuild with the new value
() => Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: FormInput(
focusNode: controller.emailFocusNode,
label: 'create_account_form_email'.tr,
hintText: 'janedoe#example.com',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.emailAddress,
validator: controller.emailValidator,
onFieldSubmitted: (_) => controller.next(),
initialValue: controller.email(), // use initial value to keep current value when user go back from the next slide
onSaved: controller.email, // persist current value into the .obs variable
),
),
),
FYI: The FormInput is just a regular TextInput, only decoration is modified. This should work with the regular flutter TextInput.
if you want to use AutomaticKeepAliveMixin in GetX like StatefulWidget. You can add the parameter 'permanent: true' in Get.put like this
Get.put<HomeController>(
HomeController(),
permanent: true,
);
Full code on HomeBinding like this
import 'package:get/get.dart';
import '../controllers/home_controller.dart';
class HomeBinding extends Bindings {
#override
void dependencies() {
Get.put<HomeController>(
HomeController(),
permanent: true,
);
}
}

How to compare value within 2 TextFormField

I wish to validate if the values within 2 TextFormFields matches.
I could validate them individually.
But how could I capture both those values to validate by comparing?
import 'package:flutter/material.dart';
class RegisterForm extends StatefulWidget {
#override
_RegisterFormState createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Stack(
children: [
Container(
padding: EdgeInsets.all(20),
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
TextFormField(
decoration: InputDecoration(
hintText: 'Confirm Password'
),
validator: (value) {
if(value.isEmpty){
return 'is empty';
}
return value;
},
),
RaisedButton(
onPressed: (){
if (_formKey.currentState.validate()) {
print('ok');
} else {
print('not ok');
}
},
),
],
),
)
],
),
);
}
}
One possible solution as follows.
I could store them as values within _RegisterFormState and retrieve them within the validate blocks. But is there a cleaner way to achieve this?
class _RegisterFormState extends State<RegisterForm> {
final _formKey = GlobalKey<FormState>();
String password;
String confirmPassword;
.....
TextFormField(
decoration: InputDecoration(
hintText: 'Password'
),
validator: (value) {
// I want to compare this value against the TextFormField below.
if(value.isEmpty){
setState(() {
password = value;
});
performValidation(password, confirmPassword); // some custom validation method
return 'is empty';
}
return value;
},
),
.....
}
P.S: If there would be a better way to do it via a state management tool, I am using Provider. Not looking for Bloc solutions.
There are few steps I have performed to achieve this. You can refer that to achieve your goal.
Create a stateful widget and return a InputBox from there
add a property named as callback and set it's datatype as
ValueSetter callback;
assign this callback to onChanged Event of the input box
onChanged: (text) {
widget.callback(text);
},
Use your custom widget in the class where you want to use the input box
while using your widget pass a callback to it
InputWithLabel(
callback: (value) {
password = value;
},
),
InputWithLabel(
callback: (value) {
confirmPassword = value;
},
),
and at last, we have to compare those values,
you can bind a key to your form add use it in saved event of it. You can wrap it in Button's callback
if (InputValidation.validatePasswordandConfirm(password, cpassword)) {
// your after form code
}
To have it a real time comparison mark one of the input's callback in setState(){} and create a new property in your Custom Text Widget named as compareTxt;
and on validator check for compare text and return the error message
validator: (text) {
if (widget.comaparetext != text) {
return 'Password does not match';
}
I had the same problem but I was using custom TextFormField widgets.
So this is how I achieved it:
Problem:
I had two TextFormField widgets in a Form and I wanted to compare the values
of those two TextFormWidgets, to check whether the value of one filed is greater
then the other.
Solution:
I defined two controllers globally, because the TextFormWidgets were in a separate class.
final _startingRollNumberController = TextEditingController();
final _endingRollNumberController = TextEditingController();
Passed these controllers to the instances of TextFormField in the Form class.
CustomTextField('Starting R#', _startingRollNumberController),
CustomTextField('Ending R#', _endingRollNumberController),
And validated them in the TextFormField class.
class CustomTextField extends StatelessWidget {
// *===== CustomTextField class =====* //
CustomTextField(this._hintText, this._controller);
final String _hintText;
final _controller;
#override
Widget build(BuildContext context) {
return TextFormField(
validator: (value) {
final _startingRollNumber = _startingRollNumberController.text;
final _endingRollNumber = _endingRollNumberController.text;
// Starting roll-number needs to be smaller then the ending roll-number.
if (!_startingRollNumber.isEmpty && !_endingRollNumber.isEmpty) {
if (int.parse(_startingRollNumber) >= int.parse(_endingRollNumber)) {
return 'starting roll number must be smaller.';
}
}
},
);
}
}
This may not be the best approach but this is how I achieved it. There is
also, this flutter_form_builder package which can be used to achieve this easily using the form key.
A related question: Flutter: Best way to get all values in a form

Flutter/Dart - How to Save State when Navigating back to a Form with Textfields?

Currently, when a user fills in a TextField the text is lost if they navigate away and then return. How can I get the text to stay in the field upon their return?
Here's the stateful widget I'm using;
class _EditPageState extends State<EditPage> {
final _formKey = GlobalKey<FormState>();
String audiotitle;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Form(
key: _formKey,
child: Container(
child: TextField(
decoration: new InputDecoration(
hintText: widget.oldaudiotitle,
),
keyboardType: TextInputType.text,
onChanged: (titleText) {
setState(() {
this.audiotitle = titleText;
});
},
),
),
),
);
}
}
What am I doing wrong here?
you have two ways :
store the Data of the text field and set the data in init method
(use sharedpreferences or any other database as per your requirements)
TextEditingController controller = TextEditingController();
#override
void initState() {
// TODO: implement initState
// retrive the Data
if(data != null) {
controller = new TextEditingController(text: data);
}
}
or if the first screen is navigating in the second Screen than just pop that screen
Navigator.pop(context);

Flutter custom FormField : validate and save methods are not called

I am quite new to Flutter, and I am struggling a bit to create a custom Form Field. The issue is that neither the validator nor the onSaved method from my custom FormField are called. I really am clueless on why they get ignored when I trigger a formKey.currentState.validate() or formKey.currentState.save().
This is a pretty simple widget for now, with an input text and a button.
The button will fetch the current location of the user, and update the text field with the current address.
When the user inputs an address in the text field, it will fetch the location for that address on focus lost (I have also integration with Google Maps, but I simplified it to isolate the issue).
Here is the constructor of my form field :
class LocationFormField extends FormField<LocationData> {
LocationFormField(
{FormFieldSetter<LocationData> onSaved,
FormFieldValidator<LocationData> validator,
LocationData initialValue,
bool autovalidate = false})
: super(
onSaved: onSaved,
validator: validator,
initialValue: initialValue,
autovalidate: autovalidate,
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
#override
FormFieldState<LocationData> createState() {
return _LocationFormFieldState();
}
}
As I need to handle state in my custom FormField, I build it in the FormFieldState object. The location state is updated when the button is pressed :
class _LocationFormFieldState extends FormFieldState<LocationData> {
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
focusNode: _addressInputFocusNode,
controller: _addressInputController,
decoration: InputDecoration(labelText: 'Address'),
),
SizedBox(height: 10.0),
FlatButton(
color: Colors.deepPurpleAccent,
textColor: Colors.white,
child: Text('Locate me !'),
onPressed: _updateLocation,
),
],
);
}
void _updateLocation() async {
print('current value: ${this.value}');
final double latitude = 45.632;
final double longitude = 17.457;
final String formattedAddress = await _getAddress(latitude, longitude);
print(formattedAddress);
if (formattedAddress != null) {
final LocationData locationData = LocationData(
address: formattedAddress,
latitude: latitude,
longitude: longitude);
_addressInputController.text = locationData.address;
// save data in form
this.didChange(locationData);
print('New location: ' + locationData.toString());
print('current value: ${this.value}');
}
}
This is how I instantiate it in my app. Nothing special here; I put it in a Form with a form key. There is another TextFormField to verify that this one is working fine:
main.dart
Widget _buildLocationField() {
return LocationFormField(
initialValue: null,
validator: (LocationData value) {
print('validator location');
if (value.address == null || value.address.isEmpty) {
return 'No valid location found';
}
},
onSaved: (LocationData value) {
print('location saved: $value');
_formData['location'] = value;
},
); // LocationFormField
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Container(
margin: EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: targetPadding / 2),
child: Column(
children: <Widget>[
_buildTitleTextField(),
SizedBox(
height: 10.0,
),
_buildLocationField(),
SizedBox(
height: 10.0,
),
_buildSubmitButton(),
],
),
),
),
),
),
);
}
The submit method triggered by the form submit button will just try to validate then save the form.
Just printing the data saved in the form:
void _submitForm() {
print('formdata : $_formData');
if (!_formKey.currentState.validate()) {
return;
}
_formKey.currentState.save();
print('formdata : $_formData');
}
But _formData['location'] always returns null, and the validator is never called (no 'validator location' or 'location saved' printed in logs).
I created a sample repo to reproduce this issue. You can try running the project, click first on the Locate me ! button, then the Save button at https://github.com/manumura/flutter-location-form-field
Answer 1: Put the build method for the Builder
Replace the FormField's builder
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
with your custom builder function
builder: (FormFieldState<LocationData> state) {
return Column(
children: <Widget>[
TextField(
focusNode: _addressInputFocusNode,
controller: _addressInputController,
decoration: InputDecoration(labelText: 'Address'),
),
SizedBox(height: 10.0),
FlatButton(
color: Colors.deepPurpleAccent,
textColor: Colors.white,
child: Text('Locate me !'),
onPressed: _updateLocation,
),
],
});
Answer 2: Pseudo CustomFormFieldState
You can't extend the FormFieldState because overriding the "build" function causes errors (explained below)
However you can create a Widget that takes the FormFieldState as a parameter to make it a separate class to act like it extends the FormFieldState (which seems a bit cleaner to me then the above method)
class CustomFormField extends FormField<List<String>> {
CustomFormField({
List<String> initialValue,
FormFieldSetter<List<String>> onSaved,
FormFieldValidator<List<String>> validator,
}) : super(
autovalidate: false,
onSaved: onSaved,
validator: validator,
initialValue: initialValue ?? List(),
builder: (state) {
return CustomFormFieldState(state);
});
}
class CustomFormFieldState extends StatelessWidget {
FormFieldState<List<String>> state;
CustomFormFieldState(this.state);
#override
Widget build(BuildContext context) {
return Container(), //The Widget(s) to build your form field
}
}
Explanation
The reason why extending the FormFieldState doesn't work is because overriding the build method in the FormFieldState object causes the FormFieldState to not be registered with the Form itself.
Below is a list of functions that I followed to get my explanation
1) Your _LocationFormFieldState overrides the build method which means the build method of the FormFieldState never executes
#override
Widget build(BuildContext context)
2) The build method the FormFieldState registers itself to the current FormState
///function in FormFieldState
Widget build(BuildContext context) {
// Only autovalidate if the widget is also enabled
if (widget.autovalidate && widget.enabled)
_validate();
Form.of(context)?._register(this);
return widget.builder(this);
}
3) The FormState then saves the FormFieldState in a List
void _register(FormFieldState<dynamic> field) {
_fields.add(field);
}
4) Then when the FormState saves/validates it loops through the list of FormFieldStates
/// Saves every [FormField] that is a descendant of this [Form].
void save() {
for (FormFieldState<dynamic> field in _fields)
field.save();
}
By overriding the build method you cause the FormField to not be registered with the Form, which is why saving and loading the Form doesn't call the methods of your custom FormField.
If the FormState._register() method was public instead of private you could call this method in your _LocationFormFieldState.build method to register your app to the form, but sadly since it is a private function you cannot.
Also note that if you were to call the super.build() function in your CustomFormFieldState's build method it leads to a StackOverflow
#override
Widget build(BuildContext context) {
super.build(context); //leads to StackOverflow!
return _buildFormField(); //anything you want
}
This is happening because you have overridden the build() method in _LocationFormFieldState. When you override this method, out of the box mechanism for registering the custom form field & validation with the form is also overridden. Therefore, the field is not getting registered and the onSaved()and validate() methods are not called automatically.
In your _LocationFormFieldState class, copy the contents of the Widget build(BuildContext context) methods into a new method. Let's call it Widget _constructWidget(). Since, we are in a stateful class, the context object will be implicitly present.
Next, remove the Widget build(BuildContext context) entirely from the _LocationFormFieldState class. Since we have removed the override, the superclass build method will be called, which will do the registration of this custom form field with the parent form for us.
Now, in the LocationFormFieldState constructor, replace:
builder: (FormFieldState<LocationData> state) {
return state.build(state.context);
});
With
builder: (FormFieldState<LocationData> state) {
return (state as _LocationFormFieldState)._constructWidget();
});
Here, we typecasted the FormFieldState to _LocationFormFieldState using the as operator and called our custom _constructWidget() method which returns the widget tree (which was previously in the overridden build())
Had the same problem. For me it worked when I replaced
return state.build(state.context);
With actual code from the build method and removed the build method override from the state.
For all viewing this question late like me, here's an actual simple and clean solution:
Put all the TextFields you want to validate and save inside a Form Widget and then use a GlobalKey to save and validate all fields together:
final _formKey = GlobalKey<FormState>();
//our save function that gets triggered when pressing a button for example
void save() {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
//add your code here
}
}
//code inside buildMethod:
Form(
key: _formKey,
child: TextFormField(
onSaved: (String value) { //code that gets executed with _formkey.currentState.save()
setState(() {
text = value;
});
},
),
)
This solution won't trigger unwanted rebuilds of the StatefulWidget because the values only get updated once the user triggers via the button.