4

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?

supersize
  • 13,764
  • 18
  • 74
  • 133
  • 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. – Maxouille Mar 26 '19 at 15:21
  • @Maxouille appreciate your honesty :D but `setState()` I also tried and it didn't work neither. – supersize Mar 26 '19 at 15:22
  • 1
    Okay srry. Interested in the answer so :) – Maxouille Mar 26 '19 at 15:23
  • 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. – RhysD Mar 26 '19 at 15:36
  • 1
    If you want to check input asynchronously then you can use stream and block to do so. – Viren V Varasadiya Mar 26 '19 at 15:47
  • this issue is related https://github.com/flutter/flutter/issues/9688 – mirkancal Aug 03 '19 at 09:25

2 Answers2

5

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 == 'name@domain.com') {
      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())),
              )
            ],
          ),
        ),
      ),
    );
  }
}

GiancarloCode
  • 902
  • 8
  • 8
4

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.

Gazihan Alankus
  • 11,256
  • 7
  • 46
  • 57
  • 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 :) – supersize Mar 26 '19 at 16:39
  • 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! – Gazihan Alankus Mar 26 '19 at 18:51
  • Appreciate it but that's not an answer to this question – supersize Mar 27 '19 at 07:46
  • 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. – Gazihan Alankus Mar 27 '19 at 08:00
  • `Requests` is async, so when I need to make a request during form submission and validate the response, how does this not fit into `why you really need an async validator`. – supersize Mar 27 '19 at 08:05
  • Ok now I understand why, sorry I did not read your code in detail. I edited the answer to add some code, see if it works. – Gazihan Alankus Mar 27 '19 at 08:40
  • Thanks, but I didn't manage to make it work with your example. But this accepted answer worked for me: https://stackoverflow.com/questions/52584520/flutter-firebase-validation-of-form-field-inputs – supersize Mar 27 '19 at 10:32
  • Actually it doesn't it will only validate correctly on the next submission, not on the current :( – supersize Mar 27 '19 at 10:38
  • 2
    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. – Gazihan Alankus Mar 27 '19 at 10:50
  • The problem with your answer is probably just that you could bring it in a complete context and write out the whole classes. `initiateAsyncPhoneValidation` should receive an argument `number` so thats missing. But also where to put the validator then. – supersize Mar 27 '19 at 11:36
  • That's right, I added the `number` parameter now. You'll use it as such: `TextFormField(validator: validatePhone)`, did I get your question right? – Gazihan Alankus Mar 27 '19 at 12:27
  • So can you edit the part with `Form()` in your answer to the one from your comment? Should it look like this: `TextFormField(validator: (val) { validatePhone(val) }, autovalidate: true, key: _formKey)`? – supersize Mar 27 '19 at 12:31
  • I edited the answer. `_formKey` goes in the `Form()`, which should be an ancestor of `TextFormField`. You can add that if it's not there. Also I added the `String` type for `number` in the functions in case it may be an issue. – Gazihan Alankus Mar 27 '19 at 12:57
  • Doesn't work I'm getting `Failed assertion: boolean expression must not be null`. Also in your example `validator: validatePhone` must look like this: `child: TextFormField( validator: (value) { validatePhone(value) } )` to be complete. – supersize Mar 27 '19 at 13:15
  • If you can tell me which line that assertion is in that may be helpful. It's probably your `if (val['valid'])`. Also, you are not right about the usage of validator. If you really want to use it like that, you have to add a return there as such: `validator: (value) { return validatePhone(value); }`. However, `validator: validatePhone` is just fine, I'm staring at my own app's code that's similar. – Gazihan Alankus Mar 27 '19 at 13:27
  • But `validatePhone` takes an argument which is passed from the validator right? Yeah its the line you were pressuming `val['valid']` is causing this. – supersize Mar 27 '19 at 13:36
  • Yes, the system passes the parameter when it calls the function. You are just giving the reference of the function as a parameter. Now you need to check `val['valid']` correctly. – Gazihan Alankus Mar 27 '19 at 13:48