flutter validate form asynchronously

I needed to do this for username validation recently (to check if a username already exists in firebase) and this is how I achieved async validation on a TextFormField ( without installation of any additional packages). I have a "users" collection where the document name is the unique username ( Firebase can't have duplicate document names in a collection but watch out for case sensitivity)

//In my state class
class _MyFormState extends State<MyForm> {
  final _usernameFormFieldKey = GlobalKey<FormFieldState>();

  //Create a focus node
  FocusNode _usernameFocusNode;

  //Create a controller
  final TextEditingController _usernameController = new TextEditingController();

  bool _isUsernameTaken = false;
  String _usernameErrorString;

 @override
  void initState() {
    super.initState();

    _usernameFocusNode = FocusNode();

    //set up focus node listeners
    _usernameFocusNode.addListener(_onUsernameFocusChange);

  }

  @override
  void dispose() {
    _usernameFocusNode.dispose();

    _usernameController.dispose();

    super.dispose();
  }
}

Then in my TextFormField widget

  TextFormField(
      keyboardType: TextInputType.text,
      focusNode: _usernameFocusNode,
      textInputAction: TextInputAction.next,
      controller: _usernameController,
      key: _usernameFormFieldKey,
      onEditingComplete: _usernameEditingComplete,
      validator: (value) => _isUsernameTaken ? "Username already taken" : _usernameErrorString,)

Listen for focus changes on the widget i.e when it loses focus. You can also do something similar for "onEditingComplete" method

void _onUsernameFocusChange() {
    if (!_usernameFocusNode.hasFocus) {

      
      String message = UsernameValidator.validate(_usernameController.text.trim());

      //First make sure username is in valid format, if it is then check firebase
      if (message == null) {
        Firestore.instance.collection("my_users").document(_usernameController.text.trim()).get().then((doc) {

          if (doc.exists) {
            setState(() {
              _isUsernameTaken = true;
              _usernameErrorString = null;
            });
          } else {
            setState(() {
              _isUsernameTaken = false;
              _usernameErrorString = null;
            });
          }
          _usernameFormFieldKey.currentState.validate();
        }).catchError((onError) {
          setState(() {
            _isUsernameTaken = false;
            _usernameErrorString = "Having trouble verifying username. Please try again";
          });
          _usernameFormFieldKey.currentState.validate();
        });
      } else {
        setState(() {
          _usernameErrorString = message;
        });
        _usernameFormFieldKey.currentState.validate();
      }
    }
  }

For completeness, this is my username validator class

class UsernameValidator {
  static String validate(String value) {
    final regexUsername = RegExp(r"^[a-zA-Z0-9_]{3,20}$");

    String trimmedValue = value.trim();

    if (trimmedValue.isEmpty) {
      return "Username can't be empty";
    }
    if (trimmedValue.length < 3) {
      return "Username min is 3 characters";
    }

    if (!regexUsername.hasMatch(trimmedValue)) {
      return "Usernames should be a maximum of 20 characters with letters, numbers or underscores only. Thanks!";
    }

    return null;
  }
}

I had the same problem while using Firebase's Realtime Database but I found a pretty good solution similar to Zroq's solution. This function creates a simple popup form to have the user input a name. Essentially, I was trying to see if a particular name for a specific user was already in the database and show a validation error if true. I created a local variable called 'duplicate' that is changed anytime the user clicks the ok button to finish. Then I can call the validator again if there is an error, and the validator will display it.

void add(BuildContext context, String email) {
String _name;
bool duplicate = false;
showDialog(
    context: context,
    builder: (_) {
      final key = GlobalKey<FormState>();
      return GestureDetector(
        onTap: () => FocusScope.of(context).requestFocus(new FocusNode()),
        child: AlertDialog(
          title: Text("Add a Workspace"),
          content: Form(
            key: key,
            child: TextFormField(
              autocorrect: true,
              autofocus: false,
              decoration: const InputDecoration(
                labelText: 'Title',
              ),
              enableInteractiveSelection: true,
              textCapitalization: TextCapitalization.sentences,
              onSaved: (value) => _name = value.trim(),
              validator: (value) {
                final validCharacters =
                    RegExp(r'^[a-zA-Z0-9]+( [a-zA-Z0-9]+)*$');
                if (!validCharacters.hasMatch(value.trim())) {
                  return 'Alphanumeric characters only.';
                } else if (duplicate) {
                  return 'Workspace already exists for this user';
                }
                return null;
              },
            ),
          ),
          actions: <Widget>[
            FlatButton(
              child: const Text("Ok"),

              onPressed: () async {
                duplicate = false;
                if (key.currentState.validate()) {
                  key.currentState.save();
                  if (await addToDatabase(_name, email) == false) {
                    duplicate = true;
                    key.currentState.validate();
                  } else {
                    Navigator.of(context).pop(true);
                  }
                }
              },
            ),
            FlatButton(
              child: const Text('Cancel'),

              onPressed: () {
                Navigator.of(context).pop(false);
              },
            ),
          ],
        ),
      );
    });
  }

At this time I think that you can't associate a Future to a validator.

What you can do is this verifying the data on a button click or in another way and set the state on the validator response var.

 @override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
    body: Form(
        key: _formKey,
        child: Column(children: [
          new TextFormField(
              validator: (value) {
                return usernameValidator;
              },
              decoration: InputDecoration(hintText: 'Username')),
          RaisedButton(
            onPressed: () async {
              var response = await checkUser();

              setState(() {
                this.usernameValidator = response;
              });

              if (_formKey.currentState.validate()) {}
            },
            child: Text('Submit'),
          )
        ])));
}