I'm currently dynamically creating a custom form widget (Row) and am wondering what the best way to validate each form was. By attempting to use a global FormState key, nothing works as the key is shared by each form instance (I assume).
This creates issues such as the keyboard immediately dropping out upon focusing on a textfield.
Was wondering if anyone has a solution to this or could point me in the right direction. Thanks!
Root Widget:
class ExerciseTable extends ConsumerWidget {
final Exercise exercise;
ExerciseTable({#required this.exercise});
#override
Widget build(BuildContext context, ScopedReader watch) {
final _exerciseTableController = watch(exerciseTableControllerProvider);
/*
* Logic of where I build the form rows dynamically
*/
List<Widget> _buildFormRows() {
List<Widget> rows = [];
int sets = int.parse(exercise.sets);
for (int i = 1; i < sets; i++) {
rows.add(
_BuildExerciseRow(
set: i.toString(),
),
);
}
return rows;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
exercise.exerciseName,
style: Theme.of(context).textTheme.headline6,
),
const SizedBox(height: 8),
YoutubePlayerTile(
url: exercise.exerciseURL,
),
const SizedBox(height: 8),
_BuildRowHeader(),
Column(children: _buildFormRows())
],
);
}
}
Individual Form Row Widget:
/**
* * Form Rows
*/
class _BuildExerciseRow extends StatelessWidget {
final String set;
final _formKey = GlobalKey<FormState>();
final _kgTextEditingController = TextEditingController();
final _repsTextEditingController = TextEditingController();
_BuildExerciseRow({this.set});
#override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 5),
child: Form(
key: _formKey,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Text(set),
const SizedBox(width: 15),
Text("-"),
const SizedBox(width: 15),
_BuildInputTextField(
controller: _kgTextEditingController,
validator: (value) {
if (value.isEmpty) {
return "Please enter some text";
} else
return "";
}),
// const SizedBox(width: 15),
_BuildInputTextField(
controller: _repsTextEditingController,
),
TickBox(onTap: () => _formKey.currentState.validate())
],
),
),
);
}
}
class _BuildInputTextField extends StatelessWidget {
// final int keyValue;
final String Function(String) validator;
final TextEditingController controller;
_BuildInputTextField({this.validator, #required this.controller});
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 2),
height: 39,
width: 80,
child: TextFormField(
validator: validator,
inputFormatters: [
LengthLimitingTextInputFormatter(6),
],
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(bottom: 5, left: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(width: 1),
),
),
onChanged: (value) => {}),
);
}
}
Custom TextFormField Widget:
class _BuildInputTextField extends StatelessWidget {
// final int keyValue;
final String Function(String) validator;
final TextEditingController controller;
_BuildInputTextField({this.validator, #required this.controller});
#override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 2),
height: 39,
width: 80,
child: TextFormField(
validator: validator,
inputFormatters: [
LengthLimitingTextInputFormatter(6),
],
controller: controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(bottom: 5, left: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(width: 1),
),
),
onChanged: (value) => {}),
);
}
}
try changing your validator callback as
validator: (value) {
if (value.isEmpty) {
return "Please enter some text";
} else
return null;
}),
Fixed!
It was super simple, I forgot to convert _BuildExerciseRow to a Stateful Widget. (In my case, a Consumer Widget from Riverpod which extends StatefulWidget).
Related
I have a form builded with Bloc package.
There are two options with textfields in it.
Switching between option i've made also with bloc and Visibility widget.
When I choose an option widget rebuilds, name and price values deletes.
What is the best way to save this values between choosing options?
Here is my Bloc code
class FormBloc extends Bloc<FormEvent, MyFormState> {
FormBloc() : super(MyFormState()) {
on<RadioButtonFormEvent>(_setRadioButtonState);
}
void _setRadioButtonState(
RadioButtonFormEvent event, Emitter<MyFormState> emit) async {
emit(RadioButtonFormState(
buttonIndex: event.buttonIndex,
buttonName: event.buttonName,
));
}
}
class MyFormState {}
class RadioButtonFormState extends MyFormState {
final int buttonIndex;
final String buttonName;
RadioButtonFormState({
required this.buttonIndex,
required this.buttonName,
});
}
abstract class FormEvent extends Equatable {}
class RadioButtonFormEvent extends FormEvent {
final int buttonIndex;
final String buttonName;
RadioButtonFormEvent({
required this.buttonIndex,
required this.buttonName,
});
#override
List<Object?> get props => [buttonIndex, buttonName,];
}
Here is Form code
class FormInput extends StatelessWidget {
const FormInput({super.key});
#override
Widget build(BuildContext context) {
final formBlocWatcher = context.watch<FormBloc>().state;
final nameController = TextEditingController();
final priceController = TextEditingController();
final formOneController = TextEditingController();
final formTwoController = TextEditingController();
final formThreeController = TextEditingController();
String formOptionController = '';
bool optionOneIsActive = true;
bool optionTwoIsActive = false;
if (formBlocWatcher is RadioButtonFormState) {
switch (formBlocWatcher.buttonIndex) {
case 0:
formOptionController = formBlocWatcher.buttonName;
break;
case 1:
optionTwoIsActive = true;
optionOneIsActive = false;
formOptionController = formBlocWatcher.buttonName;
break;
}
}
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 15,
left: 15,
right: 15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(hintText: 'Name'),
),
const SizedBox(height: 10),
TextField(
controller: priceController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Price'),
),
const SizedBox(height: 15),
OptionsWidget(),
Visibility(
visible: optionOneIsActive,
child: TextField(
controller: formOneController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form one'),
)),
Visibility(
visible: optionTwoIsActive,
child: Column(
children: [
TextField(
controller: formTwoController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form two'),
),
TextField(
controller: formThreeController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(hintText: 'Form three'),
),
],
)),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
BlocProvider.of<ProductsListBloc>(context).add(
AddProductEvent(
productName: nameController.text,
productPrice: int.parse(priceController.text),
productDescOne: formOneController.text,
productDescTwo: formTwoController.text,
productDescThree: formThreeController.text,
formOption: formOptionController,
),
);
},
child: Text('Create New'),
),
],
),
);
}
}
class OptionsWidget extends StatelessWidget {
OptionsWidget({super.key});
int value = 0;
Widget CustomRadioButton(String text, int index, BuildContext context) {
final formBloc = BlocProvider.of<FormBloc>(context);
final blocWatch = context.watch<FormBloc>().state;
if (blocWatch is RadioButtonFormState) {
value = blocWatch.buttonIndex;
}
return OutlinedButton(
onPressed: () {
formBloc.add(RadioButtonFormEvent(
buttonIndex: index,
buttonName: text,
));
},
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
side: BorderSide(color: (value == index) ? Colors.blue : Colors.grey),
),
child: Text(
text,
style: TextStyle(
color: (value == index) ? Colors.blue : Colors.grey,
),
));
}
#override
Widget build(BuildContext context) {
return Row(
children: [
CustomRadioButton("option one", 0, context),
const SizedBox(width: 15),
CustomRadioButton("option two", 1, context),
],
);
}
}
Your FormInput class should be extends from StatefulWidget, not StatelessWidget.
After this change, you should remove all TextEditingController assignments from the build() method and move them into initState().
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,
],
),
],
),
),
),
);
}
}
I have separated my TextFormField Widget from the editing page to clear the clutter.
This is inside my CustomFormField:
class CustomFormField extends StatefulWidget {
String? val;
bool? isNumberPadRequired;
CustomFormField({
Key? key,
this.val = '',
this.isNumberPadRequired = false,
}) : super(key: key);
#override
_CustomFormFieldState createState() => _CustomFormFieldState();
}
class _CustomFormFieldState extends State<CustomFormField> {
#override
Widget build(BuildContext context) {
return Container(
height: 40,
child: Center(
child: TextFormField(
initialValue: widget.val!,
onSaved: (val) {
print(val);
if (val!.isNotEmpty) {
setState(() {
widget.val = val;
});
}
},
keyboardType: widget.isNumberPadRequired!
? TextInputType.number
: TextInputType.text,
textAlign: TextAlign.left,
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
vertical: 25.0,
horizontal: 10.0,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: CustomColors.secondary,
width: 2,
),
),
),
),
),
);
}
}
And inside my EditProfilePage I have some string values like name, email and number with the current values:
String email = user.email; // john#john.com
String name = user.name; // John Doe
And inside the Form:
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildName(),
....
],
),
);
Widget _buildName() {
return FormFieldWrapper(
label: "Name",
child: CustomFormField(
val: firstName, // newly typed name: Jane Monroe
),
);
}
But when I try to call the _handleUpdate():
_updateProfile() async {
(_formKey.currentState as FormState).save();
print(name);
}
I am getting the old values i.e John Doe.
You should add the String Function argument to your custom textfield. When call saves method you should call this argument, for the update name or whatever variable.
Example;
class CustomFormField extends StatefulWidget {
String? val;
bool? isNumberPadRequired;
final Fuction(String savedValue) onSaved;
CustomFormField({
Key? key,
this.val = '',
required this.onSaved,
this.isNumberPadRequired = false,
}) : super(key: key);
#override
_CustomFormFieldState createState() => _CustomFormFieldState();
}
class _CustomFormFieldState extends State<CustomFormField> {
#override
Widget build(BuildContext context) {
return Container(
height: 40,
child: Center(
child: TextFormField(
initialValue: widget.val!,
onSaved: (val) {
print(val);
if (val!.isNotEmpty) {
widget.onSaved(val);
}
},
keyboardType: widget.isNumberPadRequired!
? TextInputType.number
: TextInputType.text,
textAlign: TextAlign.left,
decoration: InputDecoration(
isDense: true,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
vertical: 25.0,
horizontal: 10.0,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: CustomColors.secondary,
width: 2,
),
),
),
),
),
);
}
}
and should implement inside Form widget;
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildName(),
....
],
),
);
Widget _buildName() {
return FormFieldWrapper(
label: "Name",
child: CustomFormField(
val: firstName, // newly typed name: Jane Monroe
onSaved: (String savedValue){
name = savedValue;
},
),
);
}
class LoginPage extends StatelessWidget {
final _username = "admin";
final _password = "123";
final usernameController = TextEditingController();
final passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return Scaffold(body: Container(
child: Column(children: [
Padding(
padding: EdgeInsets.all(20.0),
child: TextField(
controller: usernameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Username"),
autofocus: true;)),
Padding(
padding: EdgeInsets.all(20.0),
child: TextField(
controller: passwordController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Password"),
obscureText: true;)),
how do i compare the values inputted and the default credentials before proceeding to HomePage?
ElevatedButton(
onPressed: (){Navigator.push(context,
MaterialPageRoute(builder: (context) => HomePage()));})
])
)
);
}
What I wanted to happen is once the user entered the correct default credentials (ex. Uname=admin, pass=123) the login button will proceed to my HomePage(). Else it will give me a message to try it again. Again, I dont want to use firebase authentication just yet. If there's a way I could do this,
import 'package:flutter/material.dart';
import 'homepage.dart';
#override
class LoginPageState extends StatefulWidget {
static const String id = 'Home';
LoginPage createState() => LoginPage();}
class LoginPage extends State<LoginPageState>{
final _formKey = GlobalKey<FormState>();
final _username = "admin";
final _password = "123";
final usernameController = TextEditingController();
final passwordController = TextEditingController();
#override
void dispose() {
usernameController.dispose();
passwordController.dispose();
super.dispose();
}
void _submit() {
if(_formKey.currentState.validate()){
if(_username == usernameController.text && _password == passwordController.text) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => HomePage()));
} else {
// whatever you want to do
// you can show a dialog box
}
}
}
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(children: [
//USERNAME
Padding(
padding: EdgeInsets.fromLTRB(20.0, 90.0, 20.0, 20.0),
child:TextFormField(
autofocus: true,
controller: usernameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Username"),
validator: (value){
if (value.isEmpty){
return "Username required";}
return null;
},
),
),
//PASSWORD
Padding(
padding: EdgeInsets.all(20.0),
child: TextFormField(
obscureText: true,
controller: passwordController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Password"),
validator: (value){
if (value.isEmpty){
return "Password required";}
return null;
})),
//LOGIN
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.green[300])),
child: Text("LOGIN",
style: TextStyle(color: Colors.black)),
onPressed: _submit,
)
]
)
);
}
}
it is a good practise to dispose TextEditingController when it is no longer needed. Check https://api.flutter.dev/flutter/widgets/TextEditingController-class.html for more info.
I think this should solve your problem.
Simply create two variable username="admin" and password="123".
Get the input from the user and compare with username and password.
If both match simply redirect to the Homepage
Else assign a bool variable like invalid true (Don't forget to use setState((){}))
And just above/below the login button create a
if(invalid == true)
Text("Try Again")
Anyhow, I finally got this. Thanks to #adrsh23 for the answer! So here's how I did it:
First I have the scaffolded login page. This does not contain my login form just yet
import 'package:flutter/material.dart';
import 'LoginPageState.dart';
class LoginPage extends StatelessWidget{
Widget build(BuildContext context){
return Scaffold(
body: Container(
child: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(20.0, 90.0, 20.0, 20.0),
child: Container(
color: Colors.green[300],
padding: EdgeInsets.symmetric(horizontal: 50.0, vertical: 20.0),
child: Text("Sample Flutter Login"))),
LoginPageState(),]
)
)
)
);
}
}
And here, I created a stateful widget that already contains my form:
import 'package:flutter/material.dart';
import 'homepage.dart';
#override
class LoginPageState extends StatefulWidget {
LoginPage createState() => LoginPage();}
class LoginPage extends State<LoginPageState>{
final _formKey = GlobalKey<FormState>();
final _username = "admin";
final _password = "123";
final usernameController = TextEditingController();
final passwordController = TextEditingController();
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(children: [
//USERNAME
Padding(
padding: EdgeInsets.fromLTRB(20.0, 90.0, 20.0, 20.0),
child:TextFormField(
autofocus: true,
controller: usernameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Username"),
validator: (value){
if (value.isEmpty || value != _username){
return "Input correct username";}
return null;
})),
//PASSWORD
Padding(
padding: EdgeInsets.all(20.0),
child: TextFormField(
obscureText: true,
controller: passwordController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: "Password"),
validator: (value){
if (value.isEmpty || value != _password){
return "Input correct password";}
return null;
})),
//LOGIN
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(Colors.green[300])),
child: Text("LOGIN",
style: TextStyle(color: Colors.black)),
onPressed: ( ) {
if(_formKey.currentState.validate()){
Navigator.push(
context, MaterialPageRoute(builder: (context) => HomePage()));
}
},
)
]
)
);
}
}
I used this for a basic flutter login that does not have firebase auth, instead using a default credentials.
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.