How can I validate the value in textformfield using restful call in Flutter?

Viewed 4504

I would like to check whether the postcode that was entered in form is valid or not. I am using this to validate.

I would like to show the user, as bellow, if their input incorrect postcode.

enter image description here

Widget formUI() {
  return new Column(
    children: <Widget>[
      new TextFormField(
          decoration: new InputDecoration(hintText: 'Email ID'),
          keyboardType: TextInputType.emailAddress,
          maxLength: 32,
          validator: Validation.validateEmail,
          onSaved: (String val) {
            email = val;
          }),
      new TextFormField(
        decoration: new InputDecoration(hintText: 'Password'),
        maxLength: 32,
        validator: Validation.validateName,
        onSaved: (String val) {
          password = val;
          print("name is now $password from $val");
        },
      ),
      //I WANT TO VALIDATE THE POSTCODE
      new TextFormField(
        decoration: new InputDecoration(hintText: 'Postcode'),
        maxLength: 32,
        validator: checkThePostCode(),
        onSaved: (String val) {
          postcode = val;
          print("name is now $postcode from $val");
        },
      ),
      new SizedBox(height: 15.0),
      new RaisedButton(
        onPressed: () {
          if (_sendToServer()) {
            _showSnackBar("Successfully Signed-up!");
          }
        },
        child: new Text('Sign up'),
      ),
    ],
  );
}

And, I have the bellow code that make API call. I would like to show the error response on the form eg "Invalid postcode"

Future<String> checkThePostCode() async {
  final response =
      await http.get('https://api.postcodes.io/postcodes/$postcode');

  if (response.statusCode != 200) {
    var map = ErrorMessage.fromJson(json.decode(response.body));
    postcodeValidator = map.error;
    return map.error;
  }
  return null;
}
2 Answers

The problem you are facing with the validator property is that you have to return synchronously a String that will be displayed if the postalcode is not correct.

In your case this is impossible as you will need to wait for a network call to complete.
One solution to this is to attach a TextEditingController to your TextFormField.

Every time a value is inputted I make a network request to the API. (This is not ideal, you could have a debounce function here to not spam the API too much)

The submit button is disabled as long as all the fields are not valid.

Proof of concept code:

class PostcodeValidator extends StatefulWidget {
  @override
  _PostcodeValidatorState createState() => _PostcodeValidatorState();
}

class _PostcodeValidatorState extends State<PostcodeValidator> {
  String email;
  String password;
  String postcode;
  bool hasValidPostcode = false;
  bool get canSubmitForm => hasValidPostcode;
  String postcodeErrorMessage;

  final TextEditingController _postcodeController = TextEditingController();

  void checkPostalCode(String postcode) async {
    final response = await http.get('https://api.postcodes.io/postcodes/$postcode');

    if (response.statusCode != 200) {
      setState(() {
        hasValidPostcode = false;
        postcodeErrorMessage = json.decode(response.body)["error"];
      });
    }

    if (response.statusCode == 200) {
      setState(() {
        hasValidPostcode = true;
      });
    }

    return null;
  }

  @override
  void initState() {
    super.initState();
    _postcodeController.addListener(() {
      final postcodeInput = _postcodeController.value.text;
      if (postcodeInput.isNotEmpty) {
        checkPostalCode(postcodeInput);
      }
    });
  }

  @override
  Widget build(BuildContext context) => Column(
    children: <Widget>[
      TextFormField(
        controller: _postcodeController,
        decoration: InputDecoration(hintText: 'Postcode'),
        maxLength: 32,
        autovalidate: true,
        validator: (_) => hasValidPostcode ? null : postcodeErrorMessage,
        onSaved: (String val) {
          postcode = val;
          print("name is now $postcode from $val");
        },
      ),
      RaisedButton(
        onPressed: canSubmitForm ? _sendToServer : null,
        child: new Text('Sign up'),
      ),
    ],
  );

  void _sendToServer() {
    print("Submitting");
  }
}

You can use flutter_form_bloc, it support async validators, and you can set the debounce time to not spam the API, in addition to offering other advantages.

So when you create a TextFieldBloc inside your SignUpFormBloc you can set the debounce time:

class SignUpFormBloc extends FormBloc<String, String> {
  ...
  final postcodeField = TextFieldBloc(
    asyncValidatorDebounceTime: Duration(milliseconds: 1000),
  );
  ...
}

Then in the constructor you can add your async validator:

...
  SignUpFormBloc() {
    postcodeField.addAsyncValidators([_isValidPostcode]);
  }
...

if you don't want auto validation when typing, and only when submit the form, you can set autoValitade to false in the super constructor:

...
  SignUpFormBloc() : super(autoValidate: false) {
    postcodeField.addAsyncValidators([_isValidPostcode]);
  }
...

Or if you want auto validation and only check the async validator when submit the form you can check this answer: https://stackoverflow.com/a/58295440/11768068

This is an example that you can run:

dependencies:
  form_bloc: ^0.5.2
  flutter_form_bloc: ^0.4.4
  http: ^0.12.0+2
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:form_bloc/form_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
import 'package:http/http.dart' as http;

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

class SignUpFormBloc extends FormBloc<String, String> {
  final emailField = TextFieldBloc(
    validators: [Validators.email],
  );

  final passwordField = TextFieldBloc(
    validators: [Validators.passwordMin6Chars],
  );

  final postcodeField = TextFieldBloc(
    asyncValidatorDebounceTime: Duration(milliseconds: 500),
  );

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

  SignUpFormBloc() {
    postcodeField.addAsyncValidators([_isValidPostcode]);
  }

  Future<String> _isValidPostcode(String postcode) async {
    try {
      final response =
          await http.get('https://api.postcodes.io/postcodes/$postcode');

      print(response.body);
      if (response.statusCode != 200) {
        return json.decode(response.body)['error'];
      }
    } catch (e) {
      return 'There is no Internet connection';
    }

    return null;
  }

  @override
  Stream<FormBlocState<String, String>> onSubmitting() async* {
    // Form logic...
    try {
      // Get the fields values:
      print(postcodeField.value);
      print(emailField.value);
      print(postcodeField.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 SignUpScreen extends StatefulWidget {
  SignUpScreen({Key key}) : super(key: key);

  _SignUpScreenState createState() => _SignUpScreenState();
}

class _SignUpScreenState extends State<SignUpScreen> {
  SignUpFormBloc _simpleFormBloc;

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sign up')),
      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.emailField,
              decoration: InputDecoration(labelText: 'Email ID'),
              maxLength: 32,
              keyboardType: TextInputType.emailAddress,
            ),
            TextFieldBlocBuilder(
              textFieldBloc: _simpleFormBloc.passwordField,
              decoration: InputDecoration(labelText: 'Password'),
              maxLength: 32,
            ),
            TextFieldBlocBuilder(
              textFieldBloc: _simpleFormBloc.postcodeField,
              suffixButton: SuffixButton.circularIndicatorWhenIsAsyncValidating,
              decoration: InputDecoration(labelText: 'Postcode'),
              maxLength: 32,
              errorBuilder: (context, error) {
                // Here you can map your error codes
                // if you want to use Localizations
                switch (error) {
                  case ValidatorsError.requiredTextFieldBloc:
                    return 'Please enter a postcode.';
                    break;
                  default:
                    return error;
                }
              },
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: _simpleFormBloc.submit,
                child: Center(child: Text('SIGN UP')),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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: (_) => SignUpScreen())),
              )
            ],
          ),
        ),
      ),
    );
  }
}


Related