2

I like to check if an email already exists in the database on my backend. Therefore I tried to use a state var which should be changed after the async call returns. I found the following threads which contains accepted answeres.

Flutter - Async Validator of TextFormField

Flutter firebase validation of form field inputs

I tried these answeres and also some variations but it still doesn't work for me. I just simulate the backend call. Setting _emailExist to true is printed but I don't see any error. If I click the button twice, the error message is shown correctly.

import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  LoginPage({Key key}) : super(key: key);

  @override
  _LoginPageState createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final GlobalKey<FormState> _loginFormKey = GlobalKey<FormState>();
  bool _emailExist = false;

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

  checkEmail(String name) {

    // Simulare async call
    Future.delayed(Duration(seconds: 2)).then((val) {
      setState(() {
        _emailExist = true;
      });
      print(_emailExist);
    });
    return _emailExist;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Test"),
        ),
        body: Container(
            child: SingleChildScrollView(
                child: Form(
          key: _loginFormKey,
          child: Column(
            children: <Widget>[
              TextFormField(
                validator: (value) =>
                checkEmail(value) ? "Email already taken" : null,
              ),
              RaisedButton(
                child: Text("Login"),
                onPressed: () {
                  if (_loginFormKey.currentState.validate()) {}
                },
              )
            ],
          ),
        ))));
  }
}
MarcS82
  • 2,065
  • 7
  • 29
  • 46

1 Answers1

3

TextFormField expects a synchronous function as a validator (that's an ordinary function that does some task and then returns its result).

checkEmail is exactly this, a synchronous function. It sets up a Future which will, in two seconds, set _emailExist to true. But crucially, it doesn't then wait around for two seconds. It immediately returns the current value of _emailExist (which is false the first time it runs). Two seconds later, your Future resolves and sets _emailExist to true. That's why, when you run it the second time, it works as expected (_checkEmail again returns the current value of _emailExist but this is now true).

One way around this is to provide onChanged and decoration arguments to achieve the same effect:

TextFormField(
  onChanged: _handleChange,
  decoration: InputDecoration(
    errorText: _emailExist ? "Email already taken" : null,
  ),
),

Now, you can make your asynchronous backend call as the text field changes, and then update state depending on the response:

void _handleChange(String val) {
  Future.delayed(Duration(seconds: 2)).then(() {
    setState(() {
      _emailExist = true;
    });
    print(_emailExist);
  });
}

It's a good idea to debounce this function so you're not sending off a request with every letter the user types!

Here's a good video and some docs on asynchronous coding in Flutter/Dart.

Xander
  • 1,606
  • 15
  • 30