What is the difference between TextField and TextFormField in flutter? Both look the same. Which one should i use? Which one do you recommend? I use TextField like this:
const TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Password',
),
)
A TextFormField mainly has the validator argument, while TextField doesn't: it must return null if the input is valid, and it must return a String if there's some error (usually the String itself contains information about the error). This argument allows you to validate the user input in a very easy way, provided that you include the form fields (there are others, in addition to TextFormField) in a Form widget and that you apply a Key to the Form.
If you do everything correctly, you could just invoke formKey.currentState!.validate() and automatically Flutter will invoke the validator argument for each form field that you added to the Form. If everything checks, the validate will return true, and you can proceed with your program logic. Otherwise, it will return false and it will show the String returned by the validator near the form field that contained incorrect data.
This example is taken from the Flutter cookbook on forms:
[...]
final _formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
// Build a Form widget using the _formKey created above.
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextFormField(
// The validator receives the text that the user has entered.
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Processing Data')),
);
}
},
child: const Text('Submit'),
),
),
],
),
);
}
}
You use TextFormField when using Form widget. Combined with Form widget you can make more complex forms + validation for whole form.
TextField is basically a TextFormField but you don't have to include it into the Form widget and validation would work a bit differently.
Related
So I'm relatively new to flutter and I've been trying to dynamically add Sections(TextFormFields) that are represented in a form that has Form.Helper as its child and in the process to get the saveAndValidate method to work i had to use a GlobalKey to be able to access the currentState of its so i can validate and save user input and such, but whenever i try add another Section to the screen it display this error massage
════════ Exception caught by widgets library ═══════════════════════════════════
Multiple widgets used the same GlobalKey.
════════════════════════════════════════════════════════════════════════════════
here is the code I wrote and I'd appreciate any help in solving this error please.
#1- the code for the model I used:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AddCourse with ChangeNotifier {
String? sectionName;
List<String>? sections;
List<dynamic>? addVids;
AddCourse({this.sectionName, this.sections, this.addVids});
/*where we save our values later to push them to firbase/database*/
Map<String, dynamic> toJson() {
final Map<String, dynamic> sectionData = <String, dynamic>{};
sectionData['Section #'] =
sections; // where current section number is saved and is stored dynamicly and updates as user adds more or less sections.
sectionData['Section Name'] =
sectionName; // where the input of the textformfield is saved and to be later pushed to the database and also is stored in a list so it can hold multiple section names as such.
return sectionData;
}
/* this is another model data for a functionality thats not implemented yet*/
Map<dynamic, dynamic> toJson2() {
final Map<dynamic, dynamic> vidData = <dynamic, dynamic>{};
vidData['Videos #'] = addVids;
return vidData;
}
}
#2 this the code for the form I created
import 'package:flutter/material.dart';
import 'package:snippet_coder_utils/FormHelper.dart';
import '../provider/course_add_model.dart';
class CourseCardBody extends StatefulWidget {
const CourseCardBody({
Key? key,
}) : super(key: key);
#override
State<CourseCardBody> createState() => _CourseCardBodyState();
}
class _CourseCardBodyState extends State<CourseCardBody> {
/* this is where i set up my global key that has the type of GlobalKey<FormState>*/
/*State associated with a [Form] widget. such as textformfields/forms/textfields..etc// the use of the (FormState) is to be able to Access the Functions "save"/"validate"/"reset" as to use them with forms/textformfields that you want to validate thier input or save it*/
GlobalKey<FormState> globalkey = GlobalKey();
AddCourse coursesModel = AddCourse();
#override
void initState() {
super.initState();
coursesModel.sections = List<String>.empty(growable: true);
coursesModel.sections?.add("");
// adds empty sections to the list of sections when the add button is used
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Add Courses'),
centerTitle: true,
),
body: ListView.separated(
shrinkWrap: true,
physics: const ScrollPhysics(),
itemBuilder: ((context, index) => Column(
children: [
_uiWidget(index),
Center(
// the submit button here needs some work to only be show once but for now sorry for this annoying button.
child: FormHelper.submitButton('Save', () {
if (validateAndSave()) {
print(coursesModel.toJson());
}
}),
),
],
)),
separatorBuilder: ((context, index) => const Divider()),
itemCount: coursesModel.sections!.length,
),
);
}
Widget _uiWidget(index) {
/* this form here is the parent of form fields/Formhelper widgets as seen below*/
return Form(
/* -- note here--
if we use a UniqueKey()
instead of our globalkey
here and comment the ValidateAndSave() function here
the form will work in terms of adding and removing sections
but we won't be able to either
save content/input of the user in the fields or
either validate
them so that sucks. */
/*this form is where global key is first used*/
key: globalkey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionsContainer(index),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
flex: 1,
fit: FlexFit.loose,
child: FormHelper.inputFieldWidgetWithLabel(
context,
'Add Section$index',
'',
'Section Title',
(onValidate) {
if (onValidate.isEmpty) {
return 'section ${index + 1} name cant be empty';
}
return null;
},
(onSavedVal) {
coursesModel.sections![index++] = index.toString();
onSavedVal = index;
},
onChange: (onChangedval) {
coursesModel.sectionName = onChangedval;
},
initialValue: coursesModel.sectionName ?? "",
borderColor: Colors.black,
borderFocusColor: Colors.black,
fontSize: 14,
labelFontSize: 14,
validationColor: Colors.redAccent,
),
),
Visibility(
visible: index == coursesModel.sections!.length - 1,
child: IconButton(
onPressed: () {
addEmailControl();
},
icon: const Icon(
Icons.add_circle,
color: Colors.greenAccent,
),
),
),
Visibility(
visible: index > 0,
child: SizedBox(
width: 35,
child: IconButton(
onPressed: () {
removeEmailControl(index);
},
icon: const Icon(
Icons.remove_circle,
color: Colors.redAccent,
),
),
),
),
],
),
],
),
),
);
}
Widget _sectionsContainer(index) {
/* the widget used to create the current section displayed on the top left of each textformfields*/
return Column(
children: [
Padding(
padding: const EdgeInsets.all(10),
child: Text(
'Section ${index + 1}',
textAlign: TextAlign.left,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
void addEmailControl() {
setState(() {
coursesModel.sections!.add('');
});
}
void removeEmailControl(index) {
setState(() {
if (coursesModel.sections!.length > 1) {
coursesModel.sections!.removeAt(index);
}
});
}
bool validateAndSave() {
/* we're especially using the <FormState> that is provided by the Globalkey to be able access the currentState of widget/form that has the global key in order to either validate or save the textformfields input or both in the same time*/
// validate each form
if (globalkey.currentState!.validate()) {
// If all data are correct then save data to out variables
// save each form
globalkey.currentState!.save();
return true;
} else {
return false;
}
}
}
I'm trying my best to figure it out on my own as I want to know how to solve this problem properly and where did I go wrong, and any help is very much appreciated thank you!
I suggest to create List<GlobalKey> variable. When you dynamically add or delete sub forms, you add or remove list items accordingly. It is impossible to use same GlobalKey for multiple widgets. So you need to create separate GlobalKeys for each form.
You may create a file of Global variables that may be shared across multiple files to ensure you are using a single instance.
Example globals.dart file
GlobalKey<SomeState> myGlobalKey = GlobalKey<SomeState>();
Example of implementation inside main.dart (or whatever file)
import './[path-to-globals]/globals.dart' // enter the appropriate path for your project
... // some code
Form(
key: myGlobalKey,
... // code
)
... // maybe more code
I’m making a conversation starter app and inside this app, there are different categories of questions a user can choose from. This is how the home page of the app looks like after the user logs in:
The way I’m currently listing all these categories is by saving the category names as the document ID’s under a collection I call ‘users’. Then I use the following snippet of code to get all these document IDs/ categories and add them to a List. I then use a FutureBuilder to convert this List<String> to a List of buttons. The code below can help clarify what I am doing:
Step 1: get all document IDs/category names:
List<String> questionCategories = [];
Future getCategories() async {
await FirebaseFirestore.instance
.collection('users')
.get()
.then((snapshot) => snapshot.docs.forEach(
(document) {
questionCategories.add(document.reference.id);
));
}
Step 2: Use the questionCategories List<String> to create a List of buttons
FutureBuilder(
future: getCategories(),
builder: (context, snapshot) {
return SizedBox(
height: MediaQuery.of(context).size.height - 250,
child: ListView.builder(
itemCount: questionCategories.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: //questionPageInit,
() {
print(collectionList);
Navigator.push(context,
MaterialPageRoute(builder: (context) {
//return ForgotPasswordPage();
return CategoryPage(
categoryName: questionCategories[index],
);
}));
},
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(questionCategories[index],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
))))),
);
},
),
);
},
),
Upon picking a category, questions are displayed one at a time on a question card, wherein below this card a user can switch between the next and previous questions and then shuffle. This page looks like so:
The way I’m getting these questions displayed is by getting a List of all the fields under a document ID and adding it to a List<String>. When the user presses shuffle, next, or previous, I just change a global index variable and set the state again to display a new question based on which question appears to be at that specific index in the List. The following code should help clarify what I am doing:
void printAllQuestionsList(snapshot) {
Map<String, dynamic> data = snapshot.data() as Map<String, dynamic>;
for (String key in data.keys) {
print(key + data[key]!);
questionsList.add(data[key]);
}
}
Future getQuestionList() async {
if (questIndex > 1) {
return;
}
if (widget.categoryName == "ALL") {
await FirebaseFirestore.instance
.collection('users')
.get()
.then(((snapshot) => snapshot.docs.forEach((document) {
print(document.reference.id);
FirebaseFirestore.instance
.collection('users')
.doc(document.reference.id)
.get()
.then((snapshot) => {printAllQuestionsList(snapshot)});
})));
} else {
await FirebaseFirestore.instance
.collection('users')
.doc(widget.categoryName)
.get()
.then((snapshot) => {printQuestionList(snapshot)});
}
}
Inside the widget Build function, I have this snippet of code:
FutureBuilder(
future: getQuestionList(),
builder: ((context, snapshot) {
// return TextField(
// decoration: InputDecoration(
// enabledBorder: OutlineInputBorder(
// borderSide: BorderSide(
// width: 5, //<-- SEE HERE
// color: Colors.greenAccent,
// ),
// borderRadius: BorderRadius.circular(50.0),
// ),
// ),
// );f
return Container(
margin: const EdgeInsets.all(15.0),
padding: const EdgeInsets.all(10.0),
width: 350,
height: 350,
decoration: BoxDecoration(
color: Colors.deepPurple[200],
borderRadius:
BorderRadius.all(Radius.circular(20))
// border: Border.all(color: Colors.blueAccent)
),
child: Align(
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Text(
questionsList[index],
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 32,
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
),
),
));
}))
I have a second page on this app that is used to submit questions to an existing or new Category (if they enter the right password that I set).
I use the following snippet of code to do so:
Future addQuestion(String category, String question) async {
var usersRef = questionCollection.doc(category);
await FirebaseFirestore.instance
.collection('users')
.get()
.then((snapshot) => snapshot.docs.forEach(
(document) {
existingQuestionCategories.add(document.reference.id);
},
));
if (existingQuestionCategories.contains(category)) {
print("Document Exists! ");
questionCollection.doc(category).update({question: question});
} else {
// FieldPath pathfield = FieldPath.fromString(category);
//String category2 = pathfield.category;
print('No such document exists so now about to set document anew!');
print(category);
FirebaseFirestore.instance
.collection("users")
.doc(category)
.set({question: question});
}
}
Here’s how my Firestore database is organized
Users -> Question Categories (Document IDs) -> question Key: question field
This is how I want to set it up:
Users -> Autogenerated ID -> Question Categories as collections -> question key (titled “question”): question key (“the actual question here)
This way under each collection I can also list fields pertaining to the question like if it’s light, medium, or deep that I may be able to add on to later.
I also want to do it this way because sometimes when I try to use my submit question page, the question I type does not get submitted and I think it may be because I’m submitting the question under a document ID and not under a collection.
In summary, my question to you is how do I list all the questions on my home page as a list of collections from my database? Also, how would this change the code I wrote to (1) view the questions on individual cards when clicking a category name and (2) submit new questions to a specific category/collection?
If what I’m trying to do cannot be done in the way I want it done, is there a more efficient way to do this?
I tried searching for how to get a list of collections on Firestore on Flutter but all the answers I found gave me a solution on how to get a List of fields under a document ID. This is why I'm asking this question.
Actually the Firebase SDK for Flutter ( and I'm assuming that for Android/IOS) doesn't have any pre-built methods to get a List of all collections in the firestore database.
But, as I know you can get them with a cloud function written as example with Node.js, refer to this and this.
if you're willing to write a cloud function to achieve this on your flutter project, then it's fine.
However, I can think about a practical solution, if that interest's you:
Create another collection/document where you list your firestore collections, for your precedent collections, I guess you have no option but to add them manually, but if you're creating new collections for the future in your project, you can implement general methods that check for the existence of a collection name, and act based on it.
I just want to make my TextField as required field, in which I am using Email and password to login for the user. Please let me know how can I make it required and if user don't fill it, how can I give him warning.
TextField (
onChanged: (value) {
email=value;
},
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
fillColor: Colors.grey.shade100,
filled: true,
hintText: "Email",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
)
),
),
const SizedBox(
height: 30,
),
TextField(
onChanged: (value) {
password=value;
},
style: const TextStyle(),
obscureText: true,
decoration: InputDecoration(
fillColor: Colors.grey.shade100,
filled: true,
hintText: "Password",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
)
),
),
The esiest way to set a validation logic for the TextField in Flutter is to use TextFormField instead of TextField in combination with Form widget.
It provides you with a callback called validator which is called whenever you call .validate() method in the Form Key.
To learn more about using Form widget in Flutter along with TextFormFiled and validation, check out this video.
Example for a condition in the validator to make the field required:
validator: (String? value) {
if (value == null)
{
return 'This field is required';
}
return null;
},
NOTE:
If the validator callback returned a message, this means the message would be displayed in the errorText for the TextFormField and the .validate() method would return false.
If the validator callback returned null, this means that no errors and the .validate() method would return true.
if user click on submit button then you can check for is email or password field is empty or not empty.
How can I add validation to Radio Button in Flutter? I know there's a package called flutter_form_builder but I don't want to use it. Is there any way to add validation to the radio button? I would like it to validate it using formkey and I can't post code because the whole form is dynamic and I don't have permission to post the code online so any help is appreciated. Can I make a custom radio button?
I know it is a bit late. Just use FormBuilder, for example.
or if you use it inside Form(), this is an example:
FormField(
builder: (FormFieldState<bool> state) {
return Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Where is occurrence happened?'),
state.hasError
? Text(
state.errorText,
style: TextStyle(color: Colors.red),
)
: Container(),
RadioListTile(
...
onChanged: (SomeValueType value) {
...
state.setValue(true);
},
),
RadioListTile(
...
),
],
));
},
validator: (value) {
if (value != true) {
return 'Please choose location';
}
return null;
},
)
in that code, the validator will run when you call FormState.validate(), and then show the ErrorText.
i stored phone numbers in a firebase document(table). What i want is to detect if the number already existed by using validators and display a message under a textbox that the phone number is already exists i had no problem with this , my problem is a have to double tap the button to execute to complete the task.
var _formKey = GlobalKey<FormState>();
var validate;
String validateNumber(String value){
if (value.isEmpty) {
return 'Please enter number';
}if(validate == true){
return "Phone number already exists";
}
return null;
}
addPhoneNumber(phoneNumber) async{
var exist = await db.checkPhoneNumber(phoneNumber); //my query to firestore so far i have no problem with this.
if(exist == true){
validate = true;
print(validate);
}
if(exist == false){
validate = false;
Navigator.of(context).push(_verifyPhone(_phoneNumber.text));
await db.addPhoneNumber(phoneNumber);
}
} //my function to detect if the number exists
#override
Widget build(BuildContext context) {
_get1();
return WillPopScope(
onWillPop: () async {
Navigator.pop(context);
return true;
},
child: Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Form(
key:_formKey,
child:Expanded(
child:Scrollbar(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
TextFormField(
controller: _phoneNumber,
validator: validateNumber, //my validator
),
],
),
),
),
),
FlatButton(
onTap: (){
if(_formKey.currentState.validate()){
addPhoneNumber(phoneNumber)
}
},
)
],
),
),
);
}
}
You need to call the validate() method on your form state. Look here for API reference.
This is not invoked automatically by the framework for you. You need to invoke it inside the onTap() method of your button. Add there a line _formKey.currentState.validate().
When this method is invoked then the form calls the validator for each form field.
---------------------------UPDATE---------------------------------------
Ok, #ppdy, you are one step closer now. It doesn't work yet because your validator only checks if the value is not empty. Just look carefully at what happens when you push the button. It runs the validateNumber and if the value is empty the framework will render your validate message. Then if the value is not empty you run the addPhoneNumber method, but you run it yourself. This is important, notice that it is not get run as part of the text form field validator property function. So you need to handle the output of the await db.checkPhoneNumber(phoneNumber); yourself and render the validation red message in the text form field if the check is false.
For this please note first that the TextFormField class has the decoration property. Type of this property is the InputDecoration. Please read the documentation of this class here. So by setting the errorText property of the decoration you will be able to add the red validation message to the form field manually.
Try to think it all over, play with it, stick this all together and you will have what you want :)