user9047282
user9047282

Reputation:

Flutter - Async Validator of TextFormField

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.

I have found a solution.

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

Answers (5)

Picard
Picard

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

Noryn Basaya
Noryn Basaya

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

petomuro
petomuro

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

snippetkid
snippetkid

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).

enter image description here

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

Your Friend Ken
Your Friend Ken

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

Related Questions