FormKey current state in flutter equals null - flutter

So i'm having a form like this:
Form(
key: formKey,
child: Column(
children: [
TextFormField(
validator: _validateForm,
cursorColor: Colors.black,
controller: _numberLocalsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
left: 15, bottom: 11, top: 11, right: 15),
hintText: "numero di locali"),
),
TextFormField(
validator: _validateForm,
cursorColor: Colors.black,
controller: _numberRoomsController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
left: 15, bottom: 11, top: 11, right: 15),
hintText: "numero stanze da letto"),
),
TextFormField(
validator: _validateForm,
cursorColor: Colors.black,
keyboardType: TextInputType.number,
controller: _numberbathroomsController,
decoration: InputDecoration(
contentPadding: EdgeInsets.only(
left: 15, bottom: 11, top: 11, right: 15),
hintText: "n° bagni"),
),
],
)),
and i initialized the formKey outside the build method like this :
class _FilterPageState extends State<FilterPage> {
final formKey = GlobalKey<FormState>();}
The idea is that there's a button that's clicked that just does the following :
final isValid = formKey.currentState.validate();
if (isValid) {
Navigator.pop(context, filterModel);
}
Now I get the error
Validate() was called on null
The formkey current context only has a value the first time i open the form. but when the navigator pops and i try to access the form again it gives the error.

Salma,
I'm still not clear on where / why you're seeing the null error, since I don't have access to your full code.
So I'll just post my own code sample below and you can compare implementations and hopefully that leads you to an answer.
From my test code below, it should be possible to Navigator.pop from the form page back to a previous page and then Navigator.push back to the form page and use the formKey again, without a null error.
FormStateTestPage
Just a base page with a Scaffold
BasePage
StatefulWidget where you can click a button to Navigator.push to the form page (FilterPage).
FilterPage
the Form is here
with the code sample below the Form is using a locally defined form key (localKey)
you can push to this FilterPage, pop back to BasePage, and push again to FilterPage and there should be no null errors
if you need access to form state outside of FilterPage, then you must declare a form key above FilterPage and provide it as a constructor argument
for outside access, replace the foreignKey: null with foreignKey: foreignKey (which is defined in _BasePageState). Then you can access form state from BasePage.
the code below is capturing the return value from the Navigator.push / Navigator.pop to FilterPage & back. That is how the form value is being shown in BasePage. The key line is: nukeCodes = await Navigator.push<String>
if nukeCodes is not null after popping back to BasePage, a setState is called, rebuilding BasePage & displaying the nukeCodes value.
class FormStateTestPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Form State Test'),
),
body: BasePage(),
);
}
}
class BasePage extends StatefulWidget {
#override
_BasePageState createState() => _BasePageState();
}
class _BasePageState extends State<BasePage> {
Key foreignKey = GlobalKey<FormState>();
String nukeCodes;
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Nuke Codes:'),
Text(nukeCodes ?? 'awaiting'),
Center(
child: RaisedButton(
child: Text('Go to Form'),
onPressed: () async {
nukeCodes = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => FilterPage(foreignKey: null)
) // pass null to have FilterPage use its own, self-defined formKey
// pass in foreignKey to use a formKey defined ABOVE FilterPage
);
if (nukeCodes != null && nukeCodes.isNotEmpty) {
print('Codes returned');
setState(() {});
}
}
),
),
],
);
}
}
class FilterPage extends StatefulWidget {
final GlobalKey<FormState> foreignKey;
FilterPage({this.foreignKey});
#override
_FilterPageState createState() => _FilterPageState();
}
class _FilterPageState extends State<FilterPage> {
final localKey = GlobalKey<FormState>();
GlobalKey<FormState> formKey;
#override
void initState() {
super.initState();
if (widget.foreignKey != null) {
formKey = widget.foreignKey;
print('Form using foreignKey');
}
else {
formKey = localKey;
print('Form using localKey');
}
}
#override
Widget build(BuildContext context) {
String codes;
return Scaffold(
body: SafeArea(
child: Form(
key: formKey,
//key: widget.formKey,
child: ListView(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'Enter Nuclear Codes'
),
onSaved: (val) => codes = val,
validator: (value) {
if (value.isEmpty) {
return 'A value is required';
}
return null;
},
),
RaisedButton(
child: Text('Submit'),
onPressed: () {
if (formKey.currentState.validate()) {
formKey.currentState.save();
Navigator.pop(context, codes);
}
},
)
],
),
),
),
);
}
}
Anyways, hopefully you can compare/contrast your code base with above and find out what's causing the null error.
Best of luck.

Related

How to validate the TextFormField as we type in the input in Flutter

I have created a login screen with textformfield for email id and password using flutter. Also, I have added the validation to check these fields. The code is as below;
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
theme: ThemeData(
brightness: Brightness.dark,
),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _formKey = GlobalKey<FormState>();
var isLoading = false;
void _submit() {
final isValid = _formKey.currentState.validate();
if (!isValid) {
return;
}
_formKey.currentState.save();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Form Validation"),
leading: Icon(Icons.filter_vintage),
),
//body
body: Padding(
padding: const EdgeInsets.all(16.0),
//form
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
Text(
"Form-Validation In Flutter ",
style: TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold),
),
//styling
SizedBox(
height: MediaQuery.of(context).size.width * 0.1,
),
TextFormField(
decoration: InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (value) {
//Validator
},
validator: (value) {
if (value.isEmpty ||
!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+#[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(value)) {
return 'Enter a valid email!';
}
return null;
},
),
//box styling
SizedBox(
height: MediaQuery.of(context).size.width * 0.1,
),
//text input
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (value) {},
obscureText: true,
validator: (value) {
if (value.isEmpty) {
return 'Enter a valid password!';
}
return null;
},
),
SizedBox(
height: MediaQuery.of(context).size.width * 0.1,
),
RaisedButton(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 15.0,
),
child: Text(
"Submit",
style: TextStyle(
fontSize: 24.0,
),
),
onPressed: () => _submit(),
)
],
),
),
),
);
}
}
The issue I am facing is, I want to validate the fields as soon as the user starts typing the input(dynamically) rather than clicking on the submit button to wait for the validation to happen. I did a lot of research yet could not find a solution. Thanks in advance for any help!
Flutter Form Validation with TextFormField
Here's an alternative implementation of the _TextSubmitWidgetState that uses a Form:
class _TextSubmitWidgetState extends State<TextSubmitForm> {
// declare a GlobalKey
final _formKey = GlobalKey<FormState>();
// declare a variable to keep track of the input text
String _name = '';
void _submit() {
// validate all the form fields
if (_formKey.currentState!.validate()) {
// on success, notify the parent widget
widget.onSubmit(_name);
}
}
#override
Widget build(BuildContext context) {
// build a Form widget using the _formKey created above.
return Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
decoration: const InputDecoration(
labelText: 'Enter your name',
),
// use the validator to return an error string (or null) based on the input text
validator: (text) {
if (text == null || text.isEmpty) {
return 'Can\'t be empty';
}
if (text.length < 4) {
return 'Too short';
}
return null;
},
// update the state variable when the text changes
onChanged: (text) => setState(() => _name = text),
),
ElevatedButton(
// only enable the button if the text is not empty
onPressed: _name.isNotEmpty ? _submit : null,
child: Text(
'Submit',
style: Theme.of(context).textTheme.headline6,
),
),
],
),
);
}
}
source : https://codewithandrea.com/articles/flutter-text-field-form-validation/
May be this can help someone. Inside the TextFormField use this line of code:
autovalidateMode: AutovalidateMode.onUserInteraction
use autovalidateMode in your Form widget
Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: FormUI(),
),

How to add an input cross that erases the entered value?

I have a field for entering a phone number. The task is that as soon as one character has been entered, a cross appears on the right, which erases everything that the user has entered. It should only appear when at least one character has been entered into the field. If not, then it is invisible. How can this be implemented?
class PhoneScreen extends StatefulWidget {
const PhoneScreen({Key? key}) : super(key: key);
#override
State<PhoneScreen> createState() => _PhoneScreenState();
}
class _PhoneScreenState extends State<PhoneScreen> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Padding(
padding: EdgeInsets.fromLTRB(20, MediaQuery.of(context).size.height * 0.2, 20, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new TextField(
decoration: new InputDecoration(labelText: "Enter your number"),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
),
],
)
)
)
);
}
}
try the code below.
These are the steps with explanation:
Create a TextEditingController and link the controller to the input, now we can get value of the input using _inputController.text
Next add a boolean value to check if we can show the icon.
Change the boolean value in the onChanged function
With the suffixIcon property in the InputDecoration we add the IconButton and call two lines to reset input value and set the _showIcon value to false
PS: remember the setState calls the build method, so you can avoid unnecessary rebuilds by creating a custom input widget and reuse it
EDIT:
fixed keyboard does not work after you erase the entered value
class PhoneScreen extends StatefulWidget {
const PhoneScreen({Key? key}) : super(key: key);
#override
State<PhoneScreen> createState() => _PhoneScreenState();
}
class _PhoneScreenState extends State<PhoneScreen> {
final TextEditingController _inputController = TextEditingController();
bool _showIcon = false;
// Dispose the _inputController to release memory
#override
void dispose() {
_inputController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Padding(
padding: EdgeInsets.fromLTRB(
20, MediaQuery.of(context).size.height * 0.2, 20, 0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
onChanged: (String value) {
setState(() {
_showIcon = value.isNotEmpty;
});
},
controller: _inputController,
decoration: InputDecoration(
labelText: "Enter your number",
suffixIcon: _showIcon
? IconButton(
onPressed: () {
setState(() {
// Use the clear method instead of assigning an empty string
_inputController.clear();
_showIcon = false;
});
},
icon: const Icon(Icons.close),
)
: null,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
),
],
),
),
),
);
}
}

Password show/hide toggle deletes password TextField value (Flutter)

When I click on show/hide toggle, both password and username textfield values gets deleted. I understand that, when setState rebuilds the widget tree, it resets password/username textfield values to initial blank values. I know I can get value of textfield, However I am not able to implement logic, how can I first get current username and password value, save it somewhere and then insert it back at the time widget rebuilds alongwith setState.
import 'package:flutter/material.dart';
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
bool _showPassword = true;
#override
Widget build(BuildContext context) {
final usernameController = TextEditingController();
final passwordController = TextEditingController();
return SafeArea(
child: Scaffold(
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image(image: AssetImage('images/logo.png')),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: usernameController,
decoration: InputDecoration(
hintText: "Enter Username",
labelText: "UserName",
),
),
SizedBox(
height: 20.0,
),
TextField(
obscureText: _showPassword,
controller: passwordController,
decoration: InputDecoration(
hintText: "Enter Password",
labelText: "Password",
suffixIcon: GestureDetector(
onTap: _togglePasswordVisibility,
child: _showPassword
? Icon(Icons.visibility)
: Icon(Icons.visibility_off),
),
),
),
SizedBox(
height: 20.0,
),
RaisedButton(
onPressed: () {
//call api here to authenticate user
},
child: Text("Login"),
),
],
),
),
Column(
children: [
Text("Don't have an account! Get it in 2 easy steps"),
RaisedButton(
onPressed: () {
//take user to registration screen
},
child: Text("Register"),
),
],
),
],
),
),
),
);
}
void _togglePasswordVisibility() {
setState(() {
_showPassword = !_showPassword;
});
}
}
When you use the setState to toggle the visibility, the build method will get called to redraw that widget. Since you are initialising your TextEditingController in the build method, it get initialised again and loose the previous value. To fix this you just remove the initialisation from build method to class level.
class _LoginScreenState extends State<LoginScreen> {
bool _showPassword = true;
final usernameController = TextEditingController();
final passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return SafeArea(
//... Your code
);
}
//... Your code
}

I have a Form where two text inputs are there. when user enter text in one input and goes to other the text vanishes . what to do?

I have a Form where two text inputs are there. when user enter text in one input and goes to other the text vanishes . what to do ?there are four dart files main.dart,new_transaction.dart,
the form module
import 'package:flutter/material.dart';
class NewTransaction extends StatelessWidget {
final titleController = TextEditingController();
final amountController=TextEditingController();
final Function addTx;
NewTransaction(this.addTx);
void submitData()
{
final enterTitle =titleController.text;
final enterAmount=double.parse(amountController.text);
if (enterTitle.isEmpty||enterAmount<=0)
{return;}
addTx(enterTitle,enterAmount);
}
#override
Widget build(BuildContext context) {
return
Card(
elevation: 7,
child: Container(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
TextField(
decoration: InputDecoration(labelText: 'title'),
controller: titleController,
onSubmitted:(_)=>submitData(),
),
TextField(
decoration: InputDecoration(labelText: 'amount'),
controller: amountController,
keyboardType: TextInputType.number,
onSubmitted:(_)=>submitData(),
),
FlatButton(
onPressed: submitData,
child: Text('Add Transaction'),
textColor: Colors.purple,
),
],
),
));
}
}
i am calling this from Main like this
main.dart
Its because NewTransaction is StatelessWidget. When setState is called from its parent titleController and amountController will be recreated. So the value will be empty.
Solution:
Make NewTransaction as StatefulWidget.
Explanation:
StatefulWidget have state for them (Kind of separate runtime memory block storage to store values of the variables). So even if the parent widget rebuilds, this State of the StatefulWidget won't be recreated. It will be just reused with previous persisted values.
But StatelessWidget don't have State (Won't maintain values of the variable). So if parent widget get rebuilds, then this StatelessWidget also rebuild. which means all the variable like titleController and amountController will be deleted and recreated(with empty values).
Try this code.
class _NewTransactionState extends State<NewTransaction> {
String title = "";
int amount = 0;
void submitData() {
if (title == "" || amount <= 0) {
print("Invalid");
//TODO:Handle invalid inputs
return;
}
widget.addTx(title, amount);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Card(
elevation: 7,
child: Container(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
TextField(
decoration: InputDecoration(labelText: 'title'),
onChanged: (value) {
title = value;
},
onSubmitted: (_) => submitData(),
),
TextField(
decoration: InputDecoration(labelText: 'amount'),
keyboardType: TextInputType.number,
onSubmitted: (_) => submitData(),
onChanged: (value) {
amount = int.parse(value);
},
),
FlatButton(
onPressed: submitData,
child: Text('Add Transaction'),
textColor: Colors.purple,
),
],
),
)),
),
);
}
}
Hope this will work for you if not then tell me know in comment.

Custom Validation TextFormField Flutter

I have Form and TextFormField inside it :
new Expanded(
child: TextFormField(
style: new TextStyle(color: Colors.white),
keyboardType: TextInputType.text,
validator: (String value) {
if (value.length <= 5) {
//Show error as a Snackbar
}
},
onSaved: (String value) {},
),
)
On a Buttom press I am checking if all the fields are validate :
if (_formKey.currentState.validate()) {
_submit();
}
Now the issue is when you call validate() and don't return any text in validate() method then it will consider it return true.
I don't want to show error below the textField but as a Snackbar.
Also, I tried setting an extra flag and setting in each validator Method but it gets complex if there are multiple fields in the form.
Can anyone tell me how can I handle this situation where _formKey.currentState.validate() should return false and validator method
inside TextFormField need not to return error text.
You shouldn't be using Form widget and TextFormField for displaying error in TextField.
Do validation by controllers instead
For Example
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() {
return new MyHomePageState();
}
}
class MyHomePageState extends State<MyHomePage> {
final _text = TextEditingController();
bool _validate = false;
#override
void dispose() {
_text.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('TextField Demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Error Showed if Field is Empty on Submit button Pressed'),
TextField(
controller: _text,
decoration: InputDecoration(
labelText: 'Enter the Value',
),
),
RaisedButton(
onPressed: () {
if(_text.text.length<=5){
// open dialog
}
},
child: Text('Submit'),
textColor: Colors.white,
color: Colors.blueAccent,
)
],
),
),
);
}
}