Reputation:
In my app the user has to insert a name in the textformfield. While the user is writing a query should be made to the database, that controls if the name already exists. This query returns the number of how many times the name exists. Till now I am able to make it when I press a button.
This is the function that returns the count of the names:
checkRecipe(String name) async{
await db.create();
int count = await db.checkRecipe(name);
print("Count: "+count.toString());
if(count > 0) return "Exists";
}
And this is the TextFormField, which should be validated async:
TextField(
controller: recipeDescription,
decoration: InputDecoration(
hintText: "Beschreibe dein Rezept..."
),
keyboardType: TextInputType.multiline,
maxLines: null,
maxLength: 75,
validator: (text) async{ //Returns an error
int count = await checkRecipe(text);
if (count > 0) return "Exists";
},
)
The error of the code is:
The argument type Future can't be assigned to the parameter type String
I do know what the error means. But I do not know how to work around could look like. It would be awesome if somebody could help me.
My Code looks now like this:
//My TextFormField validator
validator: (value) => checkRecipe(value) ? "Name already taken" : null,
//the function
checkRecipe<bool>(String name) {
bool _recExist = false;
db.create().then((nothing){
db.checkRecipe(name).then((val){
if(val > 0) {
setState(() {
_recExist = true;
});
} else {
setState(() {
_recExist = false;
});
}
});
});
return _recExist;
}
Upvotes: 13
Views: 8764
Reputation: 4102
Bit late to the party but maybe this will be helpful for someone. I solved this issue in three steps:
I. Created input field with validator and onChanged functions:
TextFormField(
[...]
validator: _validateBrandName,
onChanged: _validateExistsOnChange,
);
II. onChanged contains async part:
Future<void> _validateExistsOnChange(String? brandName) async {
setState(() {
_isBrandCheckInProgress = true;
});
List<Brand> brands = await _asyncGetBrandsAction(brandRepository);
bool brandExists = brands.any((brand) => brand.name == brandName);
setState(() {
_isBrandCheckInProgress = false;
_isBrandAlreadyAdded = brandExists;
});
}
III. _validateBrandName checks if async check is in progress, and also if it returned positive result already finished
String? _validateBrandName(String? value) {
if (value == null || value.isEmpty) {
return 'Brand name is required!';
}
if (_isBrandAlreadyAdded) {
return 'This brand is already added!';
}
if (_isBrandCheckInProgress) {
return 'Please wait. Checking DB!';
}
return null;
}
So if your user hits submit button during the check - he will get and error that "DB checking is in progress", so he needs to wait and hit this submit button again and then when async check hopefully is done the regular non-async check takes place verifying final result from async and any other (like non empty value).
_submitBrandItem() {
if (_formKey.currentState!.validate()) {
widget.onAddNewBrand(newBrand);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Brand added to the list of brands'),
duration: Duration(seconds: 3),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid data entered')),
);
}
}
Upvotes: 0
Reputation: 741
This an old thread but I would like to share my solution for this question.
Since validator does not allow async, using future inside validator does not work. What I have done is I declared a variable and check if the variable is valid or not inside validator. to clarify my solution, here is my code.
bool userExist = true; // or bool userExist;
GlobalKey<FormState> formState = GlobalKey();
Form(
key: formState,
child: Column(
children: [
TextFormField(
validator: (value) {
if (userExist) {
return 'user already exists';
}
return null;
},
),
ElevatedButton(
onPressed: () async {
userExist =
await yourFutureFunction(); // this must be always above formState.currentState!.validate()
bool isFormValid = formState.currentState!
.validate(); // invoking validate() will trigger the TextFormField validator
// if (formState.currentState!.validate()) {
//
// }
if (isFormValid) {
// your code here if the form is valid
}
},
child: const Text('Submit'),
),
],
),
);
Upvotes: 0
Reputation: 84
Try something like this:
import 'dart:async';
import 'package:flutter/material.dart';
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<StatefulWidget> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _recipeController = TextEditingController();
bool? _isRecipeValid;
bool _validateAndSave() {
final FormState? form = _formKey.currentState;
if (form?.validate() ?? false) {
form?.save();
return true;
} else {
return false;
}
}
Future<bool> _checkRecipe() async {
// Change to any number to test the functionality
const int returnValue = 1;
// This is just a simulation of async stuff
// Do whatever you want here instead
final int count = await Future.delayed(const Duration(seconds: 1), () => returnValue);
return count > 0 ? true : false;
}
Future<void> _validateAndSubmit() async {
_isRecipeValid = await _checkRecipe();
if (_validateAndSave()) {
// If validation succeed, do whatever you want here
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _recipeItems())))));
}
List<Widget> _recipeItems() {
return [
TextFormField(
controller: _recipeController,
decoration:
const InputDecoration(hintText: 'Beschreibe dein Rezept...'),
keyboardType: TextInputType.multiline,
maxLines: null,
maxLength: 75,
validator: (_) => _isRecipeValid ?? false ? 'Exists' : null),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _validateAndSubmit,
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
child: const Text('Save')),
];
}
}
Upvotes: 0
Reputation: 293
I wanted a same behavior for one of our apps and ended up writing a widget (which I recently published to pub.dev).
AsyncTextFormField(
controller: controller,
validationDebounce: Duration(milliseconds: 500),
validator: isValidPasscode,
hintText: 'Enter the Passcode')
You can pass in a Future<bool>
function for the validator
and set an interval before the text is sent to server.
The code is available on github.
Upvotes: 5
Reputation: 8872
Perhaps you could run your async
check using the onChange
handler and set a local variable to store the result.
Something like:
TextFormField(
controller: recipeDescription,
decoration: InputDecoration(hintText: "Beschreibe dein Rezept..."),
keyboardType: TextInputType.multiline,
maxLines: null,
maxLength: 75,
onChanged: (text) async {
final check = await checkRecipe(text);
setState(() => hasRecipe = check);
},
validator: (_) => (hasRecipe) ? "Exists" : null,
)
Upvotes: 9