setState() called during Widget Build - flutter

One of the requirements of my current project is a multi-page sign-up form, with each page performing its own independent validation. The way I ended up deciding to implement this is by splitting it up into several smaller forms, which you can pass some functions in as parameters to be executed when the form validation and/or value change runs. Below is an example of one of the "mini-forms" I created to implement this. This one collects information about the user's name.
import "package:flutter/material.dart";
class SignUpNameForm extends StatefulWidget {
final Function(String) onFirstNameChange;
final Function(String) onLastNameChange;
final Function(bool) onFirstNameValidationStatusUpdate;
final Function(bool) onLastNameValidationStatusUpdate;
final String firstNameInitialValue;
final String lastNameInitialValue;
SignUpNameForm(
{this.onFirstNameChange,
this.onLastNameChange,
this.onFirstNameValidationStatusUpdate,
this.onLastNameValidationStatusUpdate,
this.firstNameInitialValue,
this.lastNameInitialValue});
#override
_SignUpNameFormState createState() => _SignUpNameFormState();
}
class _SignUpNameFormState extends State<SignUpNameForm> {
final _formKey = GlobalKey<FormState>();
TextEditingController _firstName;
TextEditingController _lastName;
bool _editedFirstNameField;
bool _editedLastNameField;
#override
Widget build(BuildContext context) {
_firstName.text = widget.firstNameInitialValue;
_lastName.text = widget.lastNameInitialValue;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _firstName,
autovalidate: true,
decoration: InputDecoration(
hintText: "First Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedFirstNameField = true;
widget.onFirstNameChange(value);
},
validator: (String value) {
String error;
if (_editedFirstNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onFirstNameValidationStatusUpdate(isValid);
}
return error;
},
),
TextFormField(
controller: _lastName,
autovalidate: true,
decoration: InputDecoration(
hintText: "Last Name",
),
keyboardType: TextInputType.text,
onChanged: (String value) {
_editedLastNameField = true;
widget.onLastNameChange(value);
},
validator: (String value) {
String error;
if (_editedLastNameField) {
error = value.isEmpty ? "This field is required" : null;
bool isValid = error == null;
widget.onLastNameValidationStatusUpdate(isValid);
}
return error;
},
),
],
),
);
}
#override
void initState() {
super.initState();
_firstName = new TextEditingController();
_lastName = new TextEditingController();
_editedFirstNameField = false;
_editedLastNameField = false;
}
#override
void dispose() {
super.dispose();
_firstName.dispose();
_lastName.dispose();
}
}
Then, in my widget that displays the form components, I do something like this.
class _SignUpFormState extends State<SignUpForm> {
String _firstName;
bool _firstNameIsValid;
String _lastName;
bool _lastNameIsValid;
int current;
final maxLength = 4;
final minLength = 0;
#override
Widget build(BuildContext context) {
// create the widget
return Container(
child: _showFormSection(),
);
}
/// Show the appropriate form section
Widget _showFormSection() {
Widget form;
switch (current) {
case 0:
form = _showNameFormSection();
break;
case 1:
form = _showEmailForm();
break;
case 2:
form = _showPasswordForm();
break;
case 3:
form = _showDobForm();
break;
}
return form;
}
// shows the name section of the form.
Widget _showNameFormSection() {
return Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
child: SignUpNameForm(
firstNameInitialValue: _firstName,
lastNameInitialValue: _lastName,
onFirstNameChange: (String value) {
setState(() {
_firstName = value;
});
},
onFirstNameValidationStatusUpdate: (bool value) {
setState(() {
_firstNameIsValid = value;
});
},
onLastNameChange: (String value) {
setState(() {
_lastName = value;
});
},
onLastNameValidationStatusUpdate: (bool value) {
setState(() {
_lastNameIsValid = value;
});
},
),
),
Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
FlatButton(
child: Text("Next"),
onPressed: (_firstNameIsValid && _lastNameIsValid)
? _showNextSection
: null,
),
],
),
)
],
),
);
}
/// Shows the next section in the form
void _showNextSection() {
print("Current Section: " + current.toString());
setState(() {
if (current >= maxLength) {
current = maxLength;
} else {
current++;
}
});
print("Next Section: " + current.toString());
}
/// Show the previous section of the form
void _showPreviousSection() {
print("Current Section:" + current.toString());
setState(() {
if (current <= minLength) {
current = minLength;
} else {
current--;
}
print("Previous Section: " + current.toString());
});
}
#override
void initState() {
super.initState();
current = 0;
_firstName = "";
_firstNameIsValid = false;
_lastName = "";
_lastNameIsValid = false;
// other initializations
}
}
As you can see here, I pass in functions to extract the values of the user's name, as well as the status of the validation, and use that to determine whether or not I should enable the "next" button in the form handler widget. This is now causing a problem, specifically because the functions I pass into the "mini-form" invokes initState() while the widget is being rebuilt.
How might I go about handling this? Or, is there a better way I can go about implementing this multi-Page form that is cleaner?
Thanks.

build() should be fast and idempotent. You should not be calling setState() inside a build. Imagine build() is being called 60 times a second (although it won't be thanks to optimizations) and you'll have the proper mindset.

Related

How to limit text field with a value in flutter?

I want to limit the value that user input not to over a expected value in flutter text field.
Example,
If there is a value come from API is 10 and want to limit the input not over 10, if client type 11 or something over 10, want to show alert or make user not to type.
How to control this?
TextFormField(
style: TextStyle(fontSize: 20),
onChanged: (value) {
if (value != "") {
int _checkValue = int.parse(value);
if (_checkValue >
Provider.of<SaleProvider>(context, listen: false)
.remainNewQuantity(
this.currentProductItemSelected.id)) {
return 'error';
} else {
setState(() {
this.qty = int.parse(value);
updateByQty();
});
}
} else {
setState(() {
});
}
},
),
This is my trying, but can't do that I want.
Please check below method. I think this will resolve your issue. If still not work, please let me know
Widget getTextField({required int maxValue}) {
return TextFormField(
controller: _textController,
keyboardType: TextInputType.number,
onChanged: (text) {
if (int.parse(text) > maxValue) {
// show popup here.
_textController.text = validText;
_textController.selection = TextSelection.fromPosition(TextPosition(offset: _textController.text.length));
}else{
validText = text;
}
},
);
}
As an exercise for my own learning I gave it a go and came up with the following approach; creating a bespoke widget which admittedly looks like a lot of code for something so simple... but it appears to work as expected I think and one could modify, expand and integrate it with other elements in various ways.
Usage: const MaxIntField(max: 100),
Implementation:
class MaxIntField extends StatefulWidget {
const MaxIntField({Key? key, this.max = 1}) : super(key: key);
final int max;
#override
State<MaxIntField> createState() => _MaxIntFieldState();
}
class _MaxIntFieldState extends State<MaxIntField> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.value.copyWith(text: '0');
_controller.addListener(() {
if (_controller.text.isNotEmpty && _controller.text != '0') {
int intVal = int.parse(_controller.text);
if (intVal > widget.max) {
setState(() {
_controller.value =
_controller.value.copyWith(text: widget.max.toString());
_showMyDialog();
});
} else if (_controller.text != intVal.toString()) {
//remove leading '0'
setState(() {
_controller.value =
_controller.value.copyWith(text: intVal.toString());
});
}
}
});
}
// assuming using Material
_showMyDialog() async {
showDialog<String>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('AlertDialog Title'),
content: Text('This field is limited to ${widget.max}'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
);
}
}

Flutter form with provider

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.

Flutter Forms: Get the list of fields in error

Can we get the list of fields in error from Flutter forms after validation ? This will help developers use focus-nodes to redirect the attention to the field in error.
I don't think it is possible to get this kind of information from a Form object or a FormState.
But here is a way around to obtain the result you want (focus on the field in error) :
class _MyWidgetState extends State<MyWidget> {
FocusNode _fieldToFocus;
List<FocusNode> _focusNodes;
final _formKey = GlobalKey<FormState>();
final _numberOfFields = 3;
String _emptyFieldValidator(String val, FocusNode focusNode) {
if (val.isEmpty) {
_fieldToFocus ??= focusNode;
return 'This field cannot be empty';
}
return null;
}
#override
void initState() {
super.initState();
_focusNodes =
List<FocusNode>.generate(_numberOfFields, (index) => FocusNode());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(actions: [
IconButton(
icon: Icon(Icons.check),
onPressed: () {
if (_formKey.currentState.validate()) {
print('Valid form');
} else {
_fieldToFocus?.requestFocus();
_fieldToFocus = null;
}
},
),
]),
body: Form(
key: _formKey,
child: Column(children: [
...List<TextFormField>.generate(
_numberOfFields,
(index) => TextFormField(
decoration: InputDecoration(hintText: "Field $index"),
focusNode: _focusNodes[index],
validator: (val) => _emptyFieldValidator(val, _focusNodes[index]),
),
),
]),
),
);
}
}
You simply need to create a FocusNode for each one of your fields, thanks to that you will be abla to call requestFocus on a precise field (in your case a field considered as invalid). Then in the validator property of your form field, as it is the method called by the FormState.validate(), you need to set a temporary variable which will contains the right FocusNode. In my example I only set the variable _fieldToFocus if it was not already assigned using the ??= operator. After requesting the focus on the node I set _fieldToFocus back to null so it will still works for another validation.
You can try the full test code I have used on DartPad.
Sorry if I have derived a bit from your question but I still hope this will help you.
Expanding on Guillaume's answer, I've wrapped the functionality into a reusable class.
You can view a working example on DartPad here: https://www.dartpad.dev/61c4ccddbf29a343c971ee75e60d1038
import 'package:flutter/material.dart';
class FormValidationManager {
final _fieldStates = Map<String, FormFieldValidationState>();
FocusNode getFocusNodeForField(key) {
_ensureExists(key);
return _fieldStates[key].focusNode;
}
FormFieldValidator<T> wrapValidator<T>(String key, FormFieldValidator<T> validator) {
_ensureExists(key);
return (input) {
final result = validator(input);
_fieldStates[key].hasError = (result?.isNotEmpty ?? false);
return result;
};
}
List<FormFieldValidationState> get erroredFields =>
_fieldStates.entries.where((s) => s.value.hasError).map((s) => s.value).toList();
void _ensureExists(String key) {
_fieldStates[key] ??= FormFieldValidationState(key: key);
}
void dispose() {
_fieldStates.entries.forEach((s) {
s.value.focusNode.dispose();
});
}
}
class FormFieldValidationState {
final String key;
bool hasError;
FocusNode focusNode;
FormFieldValidationState({#required this.key})
: hasError = false,
focusNode = FocusNode();
}
To use it, create your forms as usual, but add a FormValidationManager to your state class, and then use that instance to wrap your validation methods.
Usage:
class _MyWidgetState extends State<MyWidget> {
final _formKey = GlobalKey<FormState>();
final _formValidationManager = FormValidationManager();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field1'),
validator: _formValidationManager.wrapValidator('field1', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
TextFormField(
focusNode: _formValidationManager.getFocusNodeForField('field2'),
validator: _formValidationManager.wrapValidator('field2', (value) {
if (value.isEmpty) {
return 'Please enter a value';
}
return null;
})),
ElevatedButton(
onPressed: () {
if (!_formKey.currentState.validate()) {
_formValidationManager.erroredFields.first.focusNode.requestFocus();
}
},
child: Text('SUBMIT'))
],
),
);
}
#override
void dispose() {
_formValidationManager.dispose();
super.dispose();
}
}

Disabled button when textInputField is invalid on change text Flutter

I'm new on Flutter.
I'm trying to disabled button while I compile textFormField and it is invalid.
My problem is that it works only if I click on "confirm" on keyboard and not while I compile my input.
So would like disable button while I write the input.
I've done a pastebin for example:
https://pastebin.com/q5WuwrCm
class AddCartButton extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return AddCartButtonState();
}
}
class AddCartButtonState extends State<AddCartButton>{
TextEditingController myController = TextEditingController();
bool isValid = false;
#override
Widget build(BuildContext context) {
void _addToCart(){
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("QUANTITY"),
content: Column(
children: <Widget>[
TextFormField(
controller: myController,
decoration: new InputDecoration(labelText: "quantity"),
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: <TextInputFormatter>[],
autovalidate: true,
validator: (value) {
if (value.isEmpty) {
isValid = false;
return "the quantity cannot be empty";
} else if (double.tryParse(value) == null) {
isValid = false;
return "the quantity must be valid number";
} else {
isValid = true;
return null;
}
}
)
],
),
actions: <Widget>[
FlatButton(
disabledTextColor: Colors.grey,
child: Text("add"),
onPressed: isValid ? () { print("is valid"); }: null
)
],
);
},
);
}
}
}
You can use addListener() function on your TextEditingController myController.
myController.addListener((){
//With this, you can "listen" all the changes on your text while
//you are typing on input
print("value: ${myController.text}");
//use setState to rebuild the widget
setState(() {
//you can check here if your text is valid or no
//_isValidText() is just an invented function that returns
//a boolean representing if the text is valid or not
if(_isValidText(myController.text)) isValid = true;
else isValid = false;
});
});
If I understood what you want to achieve the following code works for you. The conditions I set are the same as your validation rules:
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
TextEditingController _textEditingController = TextEditingController();
Function _submitFunction;
// We need a reusable timer and it needs to not be null so that we can cancel it from th start.
Timer _timer = Timer(Duration(milliseconds: 1), () => print('initialTimer'));
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Auto disable button'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
controller: _textEditingController,
onChanged: (text) {
setState(() {
_submitFunction = null;
});
restartTimer();
},
),
RaisedButton(
child: Text('Submit'),
onPressed: _submitFunction,
),
],
),
),
)
);
}
void submitForm(){
print(_textEditingController.text);
}
void enableButton(){
if(_textEditingController.text != ''){
setState(() {
_submitFunction = submitForm;
});
}
}
void restartTimer(){
_timer.cancel();
_timer = Timer(Duration(milliseconds: 1500), enableButton);
}
}
You can use this method .
bool isEnable = false;
void validateButton() {
bool isValid = true;
isValid = userEmail.isNotEmpty &&
userPassword.isNotEmpty &&
validateEmail(userEmail) &&
userPassword.length >= 8;
setState(() {
isEnable = isValid;
});
}
now in your Textfield onChanged method you have to call this function
like This
onChanged: (email) {
userEmail = email;
setState(() {});
validateButton();
},
and in your Login Button
isEnable?ActiveButton():DisableButton()

How to clear error message in TextFormField in Flutter

In my code I validate phone number. If phone number is incorrect - I show error message. But, when user starts to edit number I want to hide this error message.
I've found the solution with currentState.reset(), but it seems not the good one. I have to handle issues with saving text and cursor position. And I still have one small artifact. Normally when I press and hold backspace - it deletes symbols one by one. If I do it when error message is shown - then error message disappears and only one symbol is deleted.
Does anybody know the right solution for this case?
final TextEditingController controller = TextEditingController();
final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10, 15}\$");
bool isError = false;
TextSelection currentPosition;
return Column(
children: <Widget>[
Form(
key: _textKey,
child: TextFormField(
controller: controller,
validator: (str) {
isError = true;
if (str.isEmpty) {
return err_empty_field;
} else if (!_phoneRegex.hasMatch(str)) {
return err_invalid_phone;
}
isError = false;
},
),
onChanged: () {
if (controller.selection.start < 0 &&
controller.text.length > 0) {
TextSelection position =
controller.text.length > currentPosition.start
? currentPosition
: TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
controller.selection = position;
}
if (isError) {
isError = false;
currentPosition = controller.selection;
if (currentPosition.start > controller.text.length) {
currentPosition = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
}
String currentText = controller.text;
_textKey.currentState.reset();
controller.text = currentText;
controller.selection = currentPosition;
}
},
),
RaisedButton(
onPressed: () {
_textKey.currentState.validate();
},
child: Text(login),
)
],
);
EDIT (Nov 2020)
autovalidate was deprecated after v1.19.0.
Instead use autovalidateMode:
Form(
autovalidateMode: AutovalidateMode.onUserInteraction`.
...
)
Original post
here is a suitable solution to this problem.
You don't actually need to use onChanged or any tips causing side-effects, I solved it by creating a class property which is initialized to false:
bool _autovalidate = false;
The Form Widget has a named property autovalidate. You should pass it the previous boolean:
Form(
key: _textKey,
autovalidate: _autovalidate,
...
)
And in your Submit button onPressed() method, you should update the _autovalidate boolean to be true if the form is invalid, this will make the form to auto validate the TextFormField on every onChanged call:
RaisedButton(
onPressed: () {
if (_textKey.currentState.validate()) {
print('valid');
} else {
print('invalid');
setState(() => _autoValidate = true);
}
},
child: Text(login),
)
I hope it helped Somebody.
January 2021
...
AutovalidateMode _autoValidate = AutovalidateMode.disabled;
Form(
key: _textKey,
autovalidateMode: _autovalidate,
...
)
RaisedButton(
onPressed: () {
if (_textKey.currentState.validate()) {
print('valid');
} else {
print('invalid');
setState(() => _autoValidate = AutovalidateMode.always);
}
},
child: Text("login"),
)
The problem here is errorText is automatically managed by the validator field of the TextFormField. At the same time, the simple solution is to handle the errorText manually.
Step 1: Create
String field, _errorText initialised to null. The field will hold the error message that needs to be shown.
Boolean field, _error initialised to false. The filed is true if there is an error otherwise false.
Step 2:
Assign _errorText to TextFormField
Step 3 (Important):
Make sure that TextFormField validator returns a null value.
Handle the validation here and assign the proper error message to _errorText.
Update _error state correspondingly.
Step 4 (Important):
Reset _errorText and _error. This will remove the error from field soon as you start editing.
Step 5:
Trigger field validation in the onFieldSubmitted and manage your code flow...
import 'package:flutter/material.dart';
class WorkGround extends StatefulWidget {
#override
_WorkGroundState createState() => _WorkGroundState();
}
class _WorkGroundState extends State<WorkGround> {
final _formKey = GlobalKey<FormState>();
final _usernameFocusNode = FocusNode();
final _phoneNumberFocusNode = FocusNode();
/*
* Step 1.
* */
String _userNameErrorText;
bool _userNameError = false;
String _phoneNumberErrorText;
bool _phoneNumberError = false;
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
focusNode: _usernameFocusNode,
decoration: InputDecoration(
labelText: 'Username',
/*
* Step 2
* */
errorText: _userNameErrorText, // Handling error manually
),
textInputAction: TextInputAction.next,
/*
* Step 3
* */
validator: (value) {
setState(() {
if(value.isEmpty) {
_userNameError = true;
_userNameErrorText = 'Enter Username';
}
});
return null; // Return null to handle error manually.
},
/*
* Step 4
* */
onChanged: (value) {
setState(() {
_userNameError = false;
_userNameErrorText = null; // Resets the error
});
},
/*
* Step 5
* */
onFieldSubmitted: (value) {
_formKey.currentState.validate(); // Trigger validation
if(!_userNameError) {
FocusScope.of(context).requestFocus(_phoneNumberFocusNode);
}
},
),
TextFormField(
focusNode: _phoneNumberFocusNode,
decoration: InputDecoration(
labelText: 'Phone Number',
/*
* Step 2
* */
errorText: _phoneNumberErrorText, // Handling error manually
),
textInputAction: TextInputAction.done,
/*
* Step 3
* */
validator: (value) {
setState(() {
if(value.isEmpty) {
_phoneNumberError = true;
_phoneNumberErrorText = 'Enter Phone number';
} else if( value.length < 10) {
_phoneNumberError = true;
_phoneNumberErrorText = 'Invalid Phone number';
}
});
return null; // Return null to handle error manually.
},
/*
* Step 4
* */
onChanged: (value) {
setState(() {
_phoneNumberError = false;
_phoneNumberErrorText = null; // Resets the error
});
},
/*
* Step 5
* */
onFieldSubmitted: (value) {
_formKey.currentState.validate(); // Trigger validation
if(!_phoneNumberError) {
// submit form or whatever your code flow is...
}
},
),
],
),
),
);
}
}
I have achieved your both below functionality:
1) Hide error message when editing
2) validate input field when login button pressed
Note: i have commented phone number regex and put validation for
string length < 10 digit for testing.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState(){
super.initState();
}
final TextEditingController controller = TextEditingController();
// final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10, 15}\$");
bool isError = false;
bool isWriting = false;
bool isLoginPressed = false;
int counter = 0;
String myErrorString = "";
TextSelection currentPosition;
final _textKey = GlobalKey<FormState>();
#override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('MapSample'),
),
body: Container(
child: Column(
children: <Widget>[
Form(
key: _textKey,
child: TextFormField(
controller: controller,
validator: (str) {
myErrorString = "";
if(isLoginPressed){
isError = true;
if (str.isEmpty) {
myErrorString = 'err_empty_field';
return myErrorString;
}
else if (str.length < 10) {
myErrorString = 'err_invalid_phone';
validateMe();
return myErrorString;
}
/*else if (!_phoneRegex.hasMatch(str)) {
myErrorString = 'err_invalid_phone';
validateMe();
return myErrorString;
}*/
isError = false;
myErrorString = "";
}else{
myErrorString = "";
}
},
),
onChanged: () {
counter++;
if(counter == 9){
counter = 0;
isLoginPressed = false;
}
if(isLoginPressed){
}else{
isWriting = true;
isLoginPressed = false;
myErrorString = "";
_textKey.currentState.validate();
}
},
),
RaisedButton(
onPressed: () {
counter = 1;
isWriting = false;
isLoginPressed = true;
_textKey.currentState.validate();
},
child: Text('login'),
)
],
),
),
);
}
void validateMe() {
if(isLoginPressed){
currentPosition = TextSelection.fromPosition(
TextPosition(offset: controller.text.length));
String currentText = controller.text;
_textKey.currentState.reset();
controller.text = currentText;
controller.selection = currentPosition;
isWriting = false;
isLoginPressed = true;
}
}
}
I've found working and easier way
final _textKey = GlobalKey<FormState>();
final TextEditingController _controller = TextEditingController();
Widget _getPhoneInputForm() {
final RegExp _phoneRegex = RegExp(r"^\+{1}\d{10,17}");
bool isError = false;
bool isButtonPressed = false;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(horizontal: 36.0),
child: Form(
key: _textKey,
child: TextFormField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: hint_enter_phone,
contentPadding: EdgeInsets.all(24.0),
fillColor: Colors.blueGrey.withOpacity(0.3),
filled: true,
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(16.0)),
borderSide: BorderSide(color: Colors.blueGrey))),
controller: _controller,
validator: (str) {
if (!isButtonPressed) {
return null;
}
isError = true;
if (str.isEmpty) {
return err_empty_field;
} else if (!_phoneRegex.hasMatch(str)) {
return err_invalid_phone;
}
isError = false;
},
onFieldSubmitted: (str) {
if (_textKey.currentState.validate()) _phoneLogin();
},
),
onChanged: () {
isButtonPressed = false;
if (isError) {
_textKey.currentState.validate();
}
},
),
),
RaisedButton(
color: Colors.teal,
textColor: Colors.white,
onPressed: () {
isButtonPressed = true;
if (_textKey.currentState.validate()) _phoneLogin();
},
child: Text(login),
)
],
);
}
This is an exemple , i think its not necessary to do onchange() , the function validate name do the work ...
String validateName(String value) {
String patttern = r'(^[a-zA-Z ]*$)';
RegExp regExp = new RegExp(patttern);
if (value.length == 0) {
return "Name is Required";
} else if (!regExp.hasMatch(value)) {
return "Name must be a-z and A-Z";
}
return null;
}
TextFormField(
controller: _lastname, validator: validateName ,
//initialValue: widget.contact.last_name,
decoration:
InputDecoration(labelText: 'Last name'),
),
void Save() {
if (_keyForm.currentState.validate()) {
// No any error in validation
_keyForm.currentState.save();
................
}
i have found that using a combination of FocusNode and AtuoValidateMode.onUserInteraction does the trick.
class _TextAutoValidateModeExampleState extends State<TextAutoValidateModeExample> {
FocusNode node = FocusNode();
#override
Widget build(BuildContext context) {
return Container(
child: TextFormField(
focusNode: node,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) {
if(node.hasFocus) return null;
if (value!.isEmpty) return "value cannot be empty";
if (!value.isEmail) return "not a valid email";
},
),
);
}
}
// Call this method inside onChanged() and when its focusnode hasFocus
void formReset(GlobalKey<FormState> formKey, TextEditingController controller) {
String stringValue = controller.text;
TextPosition textPosition = controller.selection.base;
formKey.currentState.reset();
controller.text = stringValue;
controller.selection = TextSelection.fromPosition(textPosition);
}
this format worked for me, Hope it helps someone....
validator: (value){
bool emailValid = RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value);
isError = true;
if(value.isEmpty){
return "Provide an email";
}else if(!emailValid){
return "Enter a valid email";
}
isError = false;
return null;
},