5

I have an asynchronous function in Flutter which takes the value of the validator as an argument:

validatePhone(number) {
  bool _isValid;

  Requests.get("http://apilayer.net/api/validate?value=$number", json: true)
      .then((val) {
    if (val['valid']) {
      // setState(() {  <- also tried setting state here
      _isValid = true;
      // });
    } else {
      // setState(() {
      _isValid = false;
      // });
    }
  });

  return _isValid;
}

and

TextFormField(
  validator: (value) {
    if (value.isEmpty) {
      return 'Please your phone number';
    } else {
      if (validatePhone(value)) {
        return 'Your phone number is not valid';
      }
    }
  },
),

but it does not work it always returns null or the initial value set in validatePhone. Any idea how I can make this work?

6
  • I'm new to Fluuter (3 days so be indulgent) but I think you need to update _isValid inside the setstate() method. This method will rebuild the widget and modifiy the view. Commented Mar 26, 2019 at 15:21
  • @Maxouille appreciate your honesty :D but setState() I also tried and it didn't work neither. Commented Mar 26, 2019 at 15:22
  • 1
    Okay srry. Interested in the answer so :) Commented Mar 26, 2019 at 15:23
  • 1
    Have you tried awaiting your code when you validate the phone number? If not the code execution may just continue without having actually validated if the string is a phone number. Commented Mar 26, 2019 at 15:36
  • 1
    If you want to check input asynchronously then you can use stream and block to do so. Commented Mar 26, 2019 at 15:47

2 Answers 2

6

As it was said in the comments, it is not possible to have async validators as validator is expected to return a String and not a `Future'.

However, there are a number of things that's wrong in your code. First of all, validatePhone returns before _isValid is set, which is why you're getting a null value, because it was never set to anything. Your request completes after validatePhone returns and setting _isValid is useless at that point.

Let's try to fix validatePhone:

Future<bool> validatePhone(number) async {
  bool _isValid;

  final val = await Requests.get(
          "http://apilayer.net/api/validate?value=$number",
          json: true);

  if (val['valid']) {
    // setState(() {
      _isValid = true;
    // });
  } else {
    // setState(() {
      _isValid = false;
    // });
  }

  return _isValid;
}

as you see, it's return value had to become Future<bool>, not bool. There is no way to fix this. If validator was allowed to return Future, then it could work.

You're going to have to implement your validation logic in a custom painful way.

Edit: here comes a custom painful way :)

String lastValidatedNumber;
String lastRejectedNumber;

// this will be called upon user interaction or re-initiation as commented below
String validatePhone(String number) {
  if (lastValidatedNumber == number) {
    return null;
  } else if (lastRejectedNumber == number) {
    return "Phone number is invalid";
  } else {
    initiateAsyncPhoneValidation(number);
    return "Validation in progress";
  }
}

Future<void> initiateAsyncPhoneValidation(String number) async {
  final val = await Requests.get(
          "http://apilayer.net/api/validate?value=$number",
          json: true);

  if (val['valid']) {
    lastValidatedNumber = number;
  } else {
    lastRejectedNumber = number;
  }
  _formKey.currentState.validate(); // this will re-initiate the validation
}

You need to have a form key:

final _formKey = GlobalKey<FormState>();

And your form should auto validate:

    child: Form(
      key: _formKey,
      autovalidate: true,
      child: TextFormField(
        validator: validatePhone
      )
    )

I'm not 100% sure if this would work, but it's worth a shot.

Sign up to request clarification or add additional context in comments.

17 Comments

so you're fixing my code but it still doesn't work, yeah? Maybe you could provide the custom painful way you are talking about :)
I just wanted to show you what happens if you try to fix the code, and where you hit the wall with this approach. Good luck!
Appreciate it but that's not an answer to this question
I guess the answer is "validators can't be async". If you can explain your situation and why you really need an async validator, perhaps a better answer can follow.
Right, that solution is missing the _formKey.currentState.validate();, which is why you have to submit once more. If you autovalidate, at every change a validation is triggered, which in turn asynchronously trigger a validation again after it has the answer. If you can tell me what happened with my answer I can try to help.
|
6

Check out flutter_form_bloc, it support async validators and you can set the debounce time, in addition to offering other advantages.

You can use TextFieldBloc without a FormBloc, but it is much more powerful if you use inside a FormBloc

...
    _phoneFieldBloc = TextFieldBloc(
      asyncValidatorDebounceTime: Duration(milliseconds: 300),
      asyncValidators: [_validatePhone],
    );
...
...
     TextFieldBlocBuilder(
       textFieldBloc: _phoneFieldBloc,
       suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
       decoration: InputDecoration(labelText: 'Phone number'),
       keyboardType: TextInputType.phone,
     ),
...

.

Example #1 - Without FormBloc

dependencies:
  form_bloc: ^0.5.2
  flutter_form_bloc: ^0.4.3
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:form_bloc/form_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';

void main() => runApp(MaterialApp(home: HomeScreen()));

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

  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  TextFieldBloc _phoneFieldBloc;

  StreamSubscription<TextFieldBlocState> _textFieldBlocSubscription;

  @override
  void initState() {
    super.initState();
    _phoneFieldBloc = TextFieldBloc(
      asyncValidatorDebounceTime: Duration(milliseconds: 300),
      asyncValidators: [_validatePhone],
    );

    _textFieldBlocSubscription = _phoneFieldBloc.state.listen((state) {
      if (state.isValid) {
        // Print the value of the _textFieldBloc when has a valid value
        print(state.value);
      }
    });
  }

  @override
  void dispose() {
    _phoneFieldBloc.dispose();
    _textFieldBlocSubscription.cancel();
    super.dispose();
  }

  Future<String> _validatePhone(String number) async {
    // Fake phone async validator
    await Future<void>.delayed(Duration(milliseconds: 200));
    if (number.length > 4 && number.length < 9) {
      return 'Your phone number is not valid';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: TextFieldBlocBuilder(
          textFieldBloc: _phoneFieldBloc,
          suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
          decoration: InputDecoration(labelText: 'Phone number'),
          keyboardType: TextInputType.phone,
        ),
      ),
    );
  }
}

Example #2 - With FormBloc

dependencies:
  form_bloc: ^0.5.2
  flutter_form_bloc: ^0.4.3
import 'package:flutter/material.dart';
import 'package:form_bloc/form_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';

void main() => runApp(MaterialApp(home: HomeScreen()));

class SimpleFormBloc extends FormBloc<String, String> {
  final phoneField = TextFieldBloc(
    asyncValidatorDebounceTime: Duration(milliseconds: 600),
  );

  final emailField = TextFieldBloc(
    validators: [Validators.email],
    asyncValidatorDebounceTime: Duration(milliseconds: 300),
  );

  @override
  List<FieldBloc> get fieldBlocs => [phoneField, emailField];

  SimpleFormBloc() {
    phoneField.addAsyncValidators([_isValidPhone]);
    emailField.addAsyncValidators([_isEmailAvailable]);
  }

  Future<String> _isValidPhone(String number) async {
    // Fake phone async validator
    await Future<void>.delayed(Duration(milliseconds: 200));
    if (number.length > 4 && number.length < 9) {
      return 'Your phone number is not valid';
    }
    return null;
  }

  Future<String> _isEmailAvailable(String email) async {
    // Fake email async validator
    await Future<void>.delayed(Duration(milliseconds: 200));
    if (email == '[email protected]') {
      return 'That email is taken. Try another.';
    } else {
      return null;
    }
  }

  @override
  Stream<FormBlocState<String, String>> onSubmitting() async* {
    // Form logic...
    try {
      // Get the fields values:
      print(phoneField.value);
      print(emailField.value);
      await Future<void>.delayed(Duration(seconds: 2));
      yield currentState.toSuccess();
    } catch (e) {
      yield currentState.toFailure(
          'Fake error, please continue testing the async validation.');
    }
  }
}

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

  _HomeScreenState createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  SimpleFormBloc _simpleFormBloc;

  @override
  void initState() {
    super.initState();
    _simpleFormBloc = SimpleFormBloc();
  }

  @override
  void dispose() {
    _simpleFormBloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FormBlocListener(
        formBloc: _simpleFormBloc,
        onSubmitting: (context, state) {
          // Show the progress dialog
          showDialog(
            context: context,
            barrierDismissible: false,
            builder: (_) => WillPopScope(
              onWillPop: () async => false,
              child: Center(
                child: Card(
                  child: Container(
                    width: 80,
                    height: 80,
                    padding: EdgeInsets.all(12.0),
                    child: CircularProgressIndicator(),
                  ),
                ),
              ),
            ),
          );
        },
        onSuccess: (context, state) {
          // Hide the progress dialog
          Navigator.of(context).pop();
          // Navigate to success screen
          Navigator.of(context).pushReplacement(
              MaterialPageRoute(builder: (_) => SuccessScreen()));
        },
        onFailure: (context, state) {
          // Hide the progress dialog
          Navigator.of(context).pop();
          // Show snackbar with the error
          Scaffold.of(context).showSnackBar(
            SnackBar(
              content: Text(state.failureResponse),
              backgroundColor: Colors.red[300],
            ),
          );
        },
        child: ListView(
          children: <Widget>[
            TextFieldBlocBuilder(
              textFieldBloc: _simpleFormBloc.phoneField,
              suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
              decoration: InputDecoration(labelText: 'Phone number'),
              keyboardType: TextInputType.phone,
            ),
            TextFieldBlocBuilder(
              textFieldBloc: _simpleFormBloc.emailField,
              suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
              decoration: InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: _simpleFormBloc.submit,
                child: Center(child: Text('SUBMIT')),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class SuccessScreen extends StatelessWidget {
  const SuccessScreen({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green[300],
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              Icon(
                Icons.sentiment_satisfied,
                size: 100,
              ),
              RaisedButton(
                color: Colors.green[100],
                child: Text('Go to home'),
                onPressed: () => Navigator.of(context).pushReplacement(
                    MaterialPageRoute(builder: (_) => HomeScreen())),
              )
            ],
          ),
        ),
      ),
    );
  }
}

2 Comments

CAUTION: this library is not maintained anymore and can not be run with flutter 2.0 and above
The example without formbloc isnt working for me. Is this still possible?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.