Manually setting Flutter Validation Error

Viewed 12171

After validating a form and sending a request from flutter to the server backend: I want to set any potential error message from the server to be displayed in the original form. Preferably exactly like a validation error.

For instance:

Widget build(BuildContext context) {
...
  TextFormField(
    onFieldSubmitted: (value) => _signIn(),
    validator: (input) {
      if (input.length < 6)
        return 'Your password is too short';
      return null;
    },
    onSaved: (input) => _password = input,
    decoration: InputDecoration(
      labelText: 'Password',
    ),
    obscureText: true,
  )
...
}

Future<void> _signIn() async {
  final formState = _formKey.currentState;
  if (!formState.validate()) return;
  formState.save();

  try {
    ... // do fancy request stuff
  } catch (e) {
    // this is where I want to set the "validation" error
  }
}
4 Answers

It's actually super simple and the validation error still works aswell.

String? _errorMsg;

Widget build(BuildContext context) {
...
  TextFormField(
    onFieldSubmitted: (value) => _signIn(),
    validator: (input) {
      if (input.length < 6)
        // will set the errorText directly, no need for a variable here
        return 'Your password is too short';
      return null;
    },
    onSaved: (input) => _password = input,
    decoration: InputDecoration(
      labelText: 'Password',
      errorText: _errorMsg,
    ),
    obscureText: true,
  )
...
}

Future<void> _signIn() async {
  setState(() {
    _errorMsg = null; // clear any existing errors
  });

  final formState = _formKey.currentState;
  if (!formState.validate()) return;
  formState.save();

  try {
    ... // do fancy request stuff
  } catch (e) {
    setState(() {
      _errorMsg = 'Wrong password.';
    });
  }
}

I suppose, I could think of a solution, but I think it's kind of ugly. I could have an "error" variable, that is set when the request fails. I would then call formState.validate() a second time, in there: check the error variable and return it if it's not null.

You can use flutter_form_bloc and use addError method of TextFieldBloc.

usernameField.addError('That username is taken. Try another.');

Keep in mind that you can also use asynchronous validators.

This is a complete example:

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^0.21.0
  form_bloc: ^0.5.0
  flutter_form_bloc: ^0.4.1+1
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_bloc/flutter_form_bloc.dart';
import 'package:form_bloc/form_bloc.dart';

void main() {
  runApp(MaterialApp(home: SignUpForm()));
}

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

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

  @override
  Stream<FormBlocState<String, String>> onSubmitting() async* {
    // Form logic...
    try {
      await _signUp(
        throwException: true,
        username: usernameField.value,
        password: passwordField.value,
      );
      yield currentState.toSuccess();
    } catch (e) {
      // When get the error from the backend you can
      // add the error to the field:
      usernameField.addError('That username is taken. Try another.');

      yield currentState
          .toFailure('The error was added to the username field.');
    }
  }

  Future<void> _signUp({
    @required bool throwException,
    @required String username,
    @required String password,
  }) async {
    print(username);
    print(password);
    await Future<void>.delayed(Duration(seconds: 2));
    if (throwException) throw Exception();
  }
}

class SignUpForm extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<SignUpFormBloc>(
      builder: (context) => SignUpFormBloc(),
      child: Builder(
        builder: (context) {
          final formBloc = BlocProvider.of<SignUpFormBloc>(context);

          return Scaffold(
            appBar: AppBar(title: Text('Sign Up Form')),
            body: FormBlocListener<SignUpFormBloc, String, String>(
              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: formBloc.usernameField,
                    decoration: InputDecoration(labelText: 'Username'),
                  ),
                  TextFieldBlocBuilder(
                    textFieldBloc: formBloc.passwordField,
                    decoration: InputDecoration(labelText: 'Password'),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: RaisedButton(
                      onPressed: formBloc.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('Sign out'),
                onPressed: () => Navigator.of(context).pushReplacement(
                    MaterialPageRoute(builder: (_) => SignUpForm())),
              )
            ],
          ),
        ),
      ),
    );
  }
}

A simple solution:

Make a key for the widgets state:

GlobalKey<_CustomErrorTextField> _passwordTextFieldState = GlobalKey();

Set the Error message using the key:

_passwordTextFieldState.currentState.updateError(errorMsg);

Reset the error after 2 seconds:

Future.delayed(Duration(seconds: 2), () {
   // Runs after duration sec
   _passwordTextFieldState.currentState.updateError(null);
});

Set up the widget (be sure to set the key):

CustomErrorTextField(
   key: _passwordTextFieldState,
   label: "Password",
   currentPassword: password,
   validator: yourValidator,
   callback: passwordCallback,
   obscureText: hidePassword.value - a bool value show/hide password
)

Here is the Widget:

class CustomErrorTextField extends StatefulWidget {

  CustomErrorTextField({
    Key key,
    this.label,
    this.currentPassword,
    this.validator,
    this.callback,
    this.obscureText = false
  }): super(key: key);

  final String label;
  final String currentPassword;
  final FormFieldValidator<String> validator;
  final Function callback;
  final obscureText;

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

class _CustomErrorTextField extends State<CustomErrorTextField> {

  String errorMsg;

  updateError(String errorMsg){
    setState(() {
      this.errorMsg = errorMsg;
    });
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      decoration: InputDecoration(
          labelText: widget.label,
          errorText: errorMsg
      ),
      initialValue: widget.currentPassword,
      keyboardType: TextInputType.visiblePassword,
      validator: widget.validator,
      onSaved: (String val) {
        widget.callback(val);
      },
      obscureText: widget.obscureText,
    );
  }
}
Related