I like to check if an email already exists in the database on my backend. Therefore I tried to use a state var which should be changed after the async call returns. I found the following threads which contains accepted answeres.
Flutter - Async Validator of TextFormField
Flutter firebase validation of form field inputs
I tried these answeres and also some variations but it still doesn't work for me. I just simulate the backend call. Setting _emailExist to true is printed but I don't see any error. If I click the button twice, the error message is shown correctly.
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
LoginPage({Key key}) : super(key: key);
#override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
bool _emailExist = false;
#override
initState() {
super.initState();
}
checkEmail(String name) {
// Simulare async call
Future.delayed(Duration(seconds: 2)).then((val) {
setState(() {
_emailExist = true;
});
print(_emailExist);
});
return _emailExist;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Test"),
),
body: Container(
child: SingleChildScrollView(
child: Form(
key: _loginFormKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (value) =>
checkEmail(value) ? "Email already taken" : null,
),
RaisedButton(
child: Text("Login"),
onPressed: () {
if (_loginFormKey.currentState.validate()) {}
},
)
],
),
))));
}
}
TextFormField expects a synchronous function as a validator (that's an ordinary function that does some task and then returns its result).
checkEmail is exactly this, a synchronous function. It sets up a Future which will, in two seconds, set _emailExist to true. But crucially, it doesn't then wait around for two seconds. It immediately returns the current value of _emailExist (which is false the first time it runs). Two seconds later, your Future resolves and sets _emailExist to true. That's why, when you run it the second time, it works as expected (_checkEmail again returns the current value of _emailExist but this is now true).
One way around this is to provide onChanged and decoration arguments to achieve the same effect:
TextFormField(
onChanged: _handleChange,
decoration: InputDecoration(
errorText: _emailExist ? "Email already taken" : null,
),
),
Now, you can make your asynchronous backend call as the text field changes, and then update state depending on the response:
void _handleChange(String val) {
Future.delayed(Duration(seconds: 2)).then(() {
setState(() {
_emailExist = true;
});
print(_emailExist);
});
}
It's a good idea to debounce this function so you're not sending off a request with every letter the user types!
Here's a good video and some docs on asynchronous coding in Flutter/Dart.
Related
I'm totally new to Flutter/Dart, I've done all the layouts for my application, and now it's time to make my application's API calls. I'm trying to manage the forms as cleanly as possible.
I created a class that manages TextFields data (values and errors), if my API returns an error I would like the screen to update without having to call setState(() {}), is this possible?
In addition, many of my application's screens use values that the user enters in real time, if that happened I would have to call the setState(() {}) methodmany times.
Any idea how to do this with the excess calls to the setState(() {}) method?
I created a test project for demo, these are my files:
File path: /main.dart
import 'package:flutter/material.dart';
import 'login_form_data.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
setState(() {}); // Don't want to call setState
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('email'),
labelText: 'E-mail',
),
onChanged: (value) => _loginFormData.setValue('email', value),
),
TextField(
decoration: InputDecoration(
errorText: _loginFormData.firstError('password'),
labelText: 'Password',
),
obscureText: true,
onChanged: (value) =>
_loginFormData.setValue('password', value),
),
ElevatedButton(
onPressed: _submitLoginForm,
child: const Text('Login'),
)
],
),
),
),
);
}
}
File path: /login_form_data.dart
import 'form/form_data.dart';
import 'form/form_field.dart';
class LoginFormData extends FormData {
#override
Map<String, FormField> fields = {
'email': FormField(),
'password': FormField(),
'simple_account': FormField(
value: true,
),
};
LoginFormData();
}
File path: /form/form_data.dart
class FormData {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
File path: /form/form_field.dart
class FormField {
dynamic value;
List errors = [];
FormField({
this.value,
});
}
You are essentially looking for a State Management solution.
There are multiple solutions (you can read about them here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options)
State Management allows you to declare when you want your widgets to change state instead of having to imperatively call a setState method.
Flutter recommends Provider as a beginner solution, and you can find many tutorials online.
With that being said, let me show you how to achieve this result with a very basic solution: Change Notifier
Quoting flutter documentation :
” A class that can be extended or mixed in that provides a change
notification API using VoidCallback for notifications.”
We are going to make FormData a Change notifier, and them we are going to make your app listen to changes on the instance, and rebuild itself based on them.
Step 1:
Based on the code you posted, I can tell that you will interact with LoginFormData based on the methods setValue and setError from the parent class FormData. So we are going to make FormData inherit ChangeNotifer, and make a call to notifyListeners() on these two methods.
class FormData extends ChangeNotifier {
final Map<String, dynamic> fields = {};
dynamic getValue(
String key, {
String? defaultValue,
}) {
return fields[key]?.value ?? defaultValue;
}
void setValue(
String key,
String value,
) {
fields[key].value = value;
notifyListeners();
}
void setError(
String key,
String error,
) {
fields[key]?.errors.add(error);
notifyListeners();
}
dynamic firstError(
String key,
) {
return fields[key]?.errors.length > 0 ? fields[key]?.errors[0] : null;
}
FormData();
}
Now, every time you call either setValue or setError, the instance of FormData will notify the listeners.
Step2:
Now we have to setup a widget in your app to listen to these changes. Since your app is still small, it’s easy to find a place to put this listener. But as your app grows, you will see that it gets harder to do this, and that’s where packages like Provider come in handy.
We are going to wrap your Padding widget that is the first on the body of your scaffold, with a AnimatedBuilder. Despite of the misleading name, animated builder is not limited to animations. It is a widget that receives any listenable object as a parameter, and rebuilds itself every time it gets notified, passing down the updated version of the listenable.
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final LoginFormData _loginFormData = LoginFormData();
void _submitLoginForm() {
// Validate and then make a call to the login api
// If the api returns any erros inject then in the LoginFormData class
_loginFormData.setError('email', 'Invalid e-mail');
//setState(() {}); No longer necessary
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: AnimatedBuilder(
animation: _loginFormData,
builder: (context, child) {
return Padding(
padding: const EdgeInsets.all(30),
child: Center(
child: Column(
//... The rest of your widgets
),
),
);
}
),
);
}
}
I have a Flutter application and I want to add a page that appears when opening the application asking the user to answer a question such as how many countries in the world - the answer is already stored in the application, so that if the answer is correct, the answer is stored and the application opens and this page does not appear again,
But if the answer is wrong, the user remains On this page, he cannot open the application until he writes the correct answer
Any suggestions or examples that would be helpful?
Update: I have created the following verification page that checks if the entered text is equal to the stored text,I used flutter_secure_storage to store the text if it is true Now iwant to know how i can add the shared_preferences to my code?
class check extends StatefulWidget {
#override
_checkState createState() => _checkState();
}
class _checkState extends State<check> {
final formKey = GlobalKey<FormState>();
final verifierController = TextEditingController();
String storedvalue = '200';
#override
void initState() {
super.initState();
init();
}
Future init() async {
final realcode = await UserSecureStorage.getCodestored() ?? '';
setState(() {
this.verifierController.text = realcode;
});
}
Codecheck() async {
await UserSecureStorage.setUsername(verifierController.text);
if (storedvalue == verifierController.text) {
Navigator.of(context).pushReplacementNamed('/homeScreen');
}
else {
Navigator.of(context).pushReplacementNamed('/checkScreen');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(10),
child: Center(
child: Stack(
children: [
Align(
child: Text(
'how many countries are there in the world?',
.........
),
Align(
child: TextFormField(
.......
controller: verifierController,
)),
Align(
child: RaisedButton(
.........
onPressed: () async {
Codecheck();
},
..........
you would check the user's answer, if it's correct, you save a boolean in the shared preferences and then navigate to the app home page and every time you open the app you check on this boolean from the shared preferences, if it's true then don't show the question and open the home page directly, if not, then show the question again
In a very simple way, you can use the SharedPreferences plugin to store the answer to your question permanently, for example
You can store a "question" key that will have the value "how many countries are there in the world?" (optional).
You also store an "answer" key with the value "324" (the exact number of countries in the world)
Then you create an "answer_found" key which will be a boolean and will update if yes or no the user answers the question correctly.
Then when the application starts, you will first query the "answer_found" key to see if its value is True.
If this value is True, you do not display the questionnaire page, if it is false or null, you display the questionnaire page.
When the user will enter the answer, simply compare there his answer to the one contained in the "answer" key in the preferences. If it is correct, simply update the key "answer_found" to become true. In the opposite case do nothing (or what you want)
UPDATE :
As you asked, here is an extract of the code.
I made it as simple as possible (although it is a bit barbaric) so that you can understand the mechanism as well as possible and that you can adapt it as you want.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SharedPreferences.getInstance().then((preferences) async {
//Optional key (and can put want you want)
//Store the question in preferences
await preferences.setString('question', 'how many countries are there in the world?');
//Store the answer
await preferences.setInt('answer', 324);
});
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool? questionAnswered;
#override
void initState() {
super.initState();
_getQuestionState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: home,
);
}
Widget get home {
if (questionAnswered == null) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
} else if (questionAnswered!) {
return const HomePage();
} else {
return const QuestionPage();
}
}
Future<void> _getQuestionState() async {
final preferences = await SharedPreferences.getInstance();
//obtaining the present value of 'answer_found' to know if the question has been answered (with a correct answer)
final isQuestionAnswered = preferences.getBool('answer_found') ??
false; //if the value is null, we set it to false (to avoid a NullException)
setState(() => questionAnswered = isQuestionAnswered);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: const Center(
child: Text('WELCOME'),
),
);
}
class QuestionPage extends StatefulWidget {
const QuestionPage({Key? key}) : super(key: key);
#override
State<QuestionPage> createState() => _QuestionPageState();
}
class _QuestionPageState extends State<QuestionPage> {
late final TextEditingController _answerTextController;
#override
void initState() {
super.initState();
_answerTextController = TextEditingController();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text('how many countries are there in the world?'),
TextField(
controller: _answerTextController,
decoration: const InputDecoration(hintText: 'Enter answer'),
),
ElevatedButton(
onPressed: () async {
//Convert the user's response to be in an integer type (because we want to make a comparison with an integer)
//The user's response will be null, if the user has not entered an integer
final userAnswer = int.tryParse(_answerTextController.text);
if (userAnswer != null) {
final preferences = await SharedPreferences.getInstance();
final storedAnswer = preferences.getInt('answer')!;
if (userAnswer == storedAnswer) {
preferences.setBool('answer_found', true);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const HomePage()));
} else {
//Notify user that his answer was wrong or do some stuff (what you want)
}
} else {
//Do some stuff
}
},
child: const Text('Validate')),
],
),
),
);
}
}
I have never used the flutter_secure_storage plugin, but following the code snippet I made for you, I think you could readapt it to work with flutter_secure storage, the reasoning and logic is the same.
P.S: You don't have to use shared_preferences and flutter_secure_storage at the same time. You can simply use flutter_secure_storage which, unlike shared_preferences, offers you a secure storage space (as its name indicates) and you only have to implement the same logic.
I hope you will have the splash screen as the first screen to load in your application.
now, when the first screen will load.
make this first screen a stateful.
this will allow you to use the "initstate" method. and you know that the initstate method will be called first to execute the code.
initstate(){
/// in this case I am using "get_storage" to store the data in local.
GetStorage prefs = GetStorage("ApplicationName");
bool isAnswered = prefs.read("isAnsweredQuestion") ?? false;
if(isAnswered){
/// redirect to the other screen.
}else{
/// redirect to the screen where you have the questions
/// or open the dialog having questions.
}
super.initstate();
};
the initstate method will execute for the first time when the application loads and execute the splash screen and it will check for the local data store.
if the user is opening the application for the first time then the local data will be null and we have used the null-check operator to handle that.
If the user already answered the question then we will get "true" stored in the local. and we will redirect the user to the other screen in that case.
I've been following some of the beginner flutter tutorials on their website and was doing this tutorial for basic interactivity, specifically the part where a parent widget is used to manage the state of a child widget. There is a ParentWidget and _ParentWidgetState class the code for which is as follows:
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
#override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
TapboxB is a class which is a child of ParentWidget, the code for which is as follows:
class TapboxB extends StatelessWidget {
TapboxB({this.active: false, #required this.onChanged});
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Column(
//aligns column in the centre vertically
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
//sets text depending on _active boolean
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 20.0, color: Colors.white),
),
Text(
'Tapbox B',
style: TextStyle(fontSize: 14.0, color: Colors.white),
),
],
),
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
//sets colour depending on _active boolean
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
The _handleTap method is called when the widget is clicked, which calls the onChanged callback, which toggles the active variable. In the definition for onChanged the type is ValueChanged<bool> which is documented as a "signature for callbacks that report that an underlying value has changed." If I change this however to ValueSetter<bool> the app works in the exact same way and nothing seems to change. So my question is what is the difference between the two of these? Is one better in this particular scenario?
I searched the documentation for you, using just flutter ValueChanged ValueSetter, and quickly found this:
void ValueSetter (T Value)
Signature for callbacks that report that a value has been set.
This is the same signature as ValueChanged, but is used when the callback is called even if the underlying value has not changed. For example, service extensions use this callback because they call the callback whenever the extension is called with a value, regardless of whether the given value is new or not.
typedef ValueSetter<T> = void Function(T value);
So they're just typedefs to the same underlying type, but one has a different semantic meaning, presumably used as self-documenting code despite the same underlying signature.
If you don't need the latter meaning of ValueSetter, then use ValueChanged, as the API already said.
usually I can disable/grey-out a button until a TextFormField meets certain parameters in flutter by something like this:
TextFormField(
controller: _controller
value: (value)
)
SubmitButton(
onPressed: _controller.text.isNotEmpty ? _submit : null;
)
But when compiled as a website the Button seems no longer aware of the controller value...
I have tried targeting in several different ways, e.g. _controller.value.text.isEmpty and _controller.text.isEmpty...
I'm guessing I'm missing something or this method just isn't possible for web ... Is there any other way to get the same result?
To be honest, your code shouldn't work in flutter mobile either, but may be works because of screen keyboard causes widget rebuild when showing or hiding.
To fix this issue we have to use stateful widget with state variable like canSubmit and update it in textField's listener onChange with setState method. Then every time the text changes, our stateful widget will update the submit button..
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
bool canSubmit;
#override
void initState() {
canSubmit = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (value) {
setState(() {
canSubmit = value.isNotEmpty;
});
},
),
RaisedButton(
onPressed: canSubmit ? _submit : null,
child: Text('Submit'),
)
],
),
),
);
}
void _submit() {
print('Submitted');
}
}
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.