Flutter Forms: Get the list of fields in error
I don't think it is possible to get this kind of information from a Form
object or a FormState
.
But here is a way around to obtain the result you want (focus on the field in error) :
class _MyWidgetState extends State<MyWidget> { FocusNode _fieldToFocus; List<FocusNode> _focusNodes; final _formKey = GlobalKey<FormState>(); final _numberOfFields = 3; String _emptyFieldValidator(String val, FocusNode focusNode) { if (val.isEmpty) { _fieldToFocus ??= focusNode; return 'This field cannot be empty'; } return null; } @override void initState() { super.initState(); _focusNodes = List<FocusNode>.generate(_numberOfFields, (index) => FocusNode()); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(actions: [ IconButton( icon: Icon(Icons.check), onPressed: () { if (_formKey.currentState.validate()) { print('Valid form'); } else { _fieldToFocus?.requestFocus(); _fieldToFocus = null; } }, ), ]), body: Form( key: _formKey, child: Column(children: [ ...List<TextFormField>.generate( _numberOfFields, (index) => TextFormField( decoration: InputDecoration(hintText: "Field $index"), focusNode: _focusNodes[index], validator: (val) => _emptyFieldValidator(val, _focusNodes[index]), ), ), ]), ), ); }}
You simply need to create a FocusNode
for each one of your fields, thanks to that you will be abla to call requestFocus
on a precise field (in your case a field considered as invalid). Then in the validator
property of your form field, as it is the method called by the FormState.validate()
, you need to set a temporary variable which will contains the right FocusNode
. In my example I only set the variable _fieldToFocus
if it was not already assigned using the ??=
operator. After requesting the focus on the node I set _fieldToFocus
back to null
so it will still works for another validation.
You can try the full test code I have used on DartPad.
Sorry if I have derived a bit from your question but I still hope this will help you.
Expanding on Guillaume's answer, I've wrapped the functionality into a reusable class.
You can view a working example on DartPad here: https://www.dartpad.dev/61c4ccddbf29a343c971ee75e60d1038
import 'package:flutter/material.dart';class FormValidationManager { final _fieldStates = Map<String, FormFieldValidationState>(); FocusNode getFocusNodeForField(key) { _ensureExists(key); return _fieldStates[key].focusNode; } FormFieldValidator<T> wrapValidator<T>(String key, FormFieldValidator<T> validator) { _ensureExists(key); return (input) { final result = validator(input); _fieldStates[key].hasError = (result?.isNotEmpty ?? false); return result; }; } List<FormFieldValidationState> get erroredFields => _fieldStates.entries.where((s) => s.value.hasError).map((s) => s.value).toList(); void _ensureExists(String key) { _fieldStates[key] ??= FormFieldValidationState(key: key); } void dispose() { _fieldStates.entries.forEach((s) { s.value.focusNode.dispose(); }); }}class FormFieldValidationState { final String key; bool hasError; FocusNode focusNode; FormFieldValidationState({@required this.key}) : hasError = false, focusNode = FocusNode();}
To use it, create your forms as usual, but add a FormValidationManager
to your state class, and then use that instance to wrap your validation methods.
Usage:
class _MyWidgetState extends State<MyWidget> { final _formKey = GlobalKey<FormState>(); final _formValidationManager = FormValidationManager(); @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( children: [ TextFormField( focusNode: _formValidationManager.getFocusNodeForField('field1'), validator: _formValidationManager.wrapValidator('field1', (value) { if (value.isEmpty) { return 'Please enter a value'; } return null; })), TextFormField( focusNode: _formValidationManager.getFocusNodeForField('field2'), validator: _formValidationManager.wrapValidator('field2', (value) { if (value.isEmpty) { return 'Please enter a value'; } return null; })), ElevatedButton( onPressed: () { if (!_formKey.currentState.validate()) { _formValidationManager.erroredFields.first.focusNode.requestFocus(); } }, child: Text('SUBMIT')) ], ), ); } @override void dispose() { _formValidationManager.dispose(); super.dispose(); }}