You can display validation messages from an API response in a Flutter
TextFormField by storing the error string in a state variable and passing it to the field's
errorText property. When the asynchronous API call completes, update this variable using
setState, which triggers a rebuild and renders the message below the input field immediately.
Understanding the TextFormField Error Mechanism
The
TextFormField widget in Flutter has a built-in mechanism for displaying validation errors via the
errorText parameter. This parameter expects a nullable String. If you provide a non-null value, Flutter automatically displays that text in red beneath the input field, mimicking the behavior of standard form validators. However, unlike synchronous validators that run on every keystroke, API-based validation is asynchronous. You cannot return a string directly from an async function during the build phase. Instead, you must manage the error state manually.
To implement this, you need a
StatefulWidget. Inside your state class, declare a private variable, typically named
_apiError or similar, initialized to null. This variable acts as the bridge between your network logic and the UI. The
TextFormField will read this variable. When it is null, no error is shown. When it contains a string, the error appears. This separation allows you to keep the validation logic separate from the presentation layer while maintaining a clean user experience.
Implementing State Management for API Errors
The core challenge is updating the UI after the HTTP request finishes. In Flutter, this requires calling
setState. Below is a concrete example of how to structure your widget. We assume you are using a service class to handle the network request, but the principle applies to any async method.
class MyFormState extends State<MyForm> {
final TextEditingController _controller = TextEditingController();
String? _apiError; // Stores the error message
Future<void> submitForm() async {
// Clear previous errors
setState(() {
_apiError = null;
});
try {
final response = await ApiService.validateUsername(_controller.text);
// Assuming the API returns a boolean or specific status
if (!response.isValid) {
setState(() {
_apiError = response.message; // e.g., "Username already taken"
});
} else {
// Success logic
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Success!')),
);
}
} catch (e) {
setState(() {
_apiError = 'Network error. Please try again.';
});
}
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Username',
errorText: _apiError, // This links the state to the UI
),
);
}
}
In this code,
submitForm is called when the user taps a button. It first clears any existing error to ensure a fresh state. Then, it performs the async call. If the API indicates failure, the error message from the JSON response is assigned to
_apiError. Finally,
setState is called, which rebuilds the widget tree and updates the
errorText in the
InputDecoration.
Handling Loading States
While showing errors, it is crucial to prevent the user from submitting multiple times. You should introduce a boolean flag, such as
_isLoading, within your state. Set it to true before the API call and false afterward. Disable the submit button while
_isLoading is true. This prevents race conditions where multiple requests might overwrite each other's error messages or leave the UI in an inconsistent state.
For more details on managing API client structures that feed into these forms, you might find
Creating API Client Codes in OpenAPI Generator helpful for generating robust service classes.
Integrating with Existing Validators
Often, you want to combine local validation (like checking if the field is empty) with remote validation (checking if the username exists). The
validator property of
TextFormField runs synchronously. You can chain them by returning the local error first, then triggering the async check.
However, a cleaner approach is to handle local validation in the button's
onPressed callback before calling the API. If the field is empty, show a snackbar or set a local error message. Only proceed to the API call if local validation passes. This keeps the
errorText property dedicated to server-side feedback, reducing complexity in the validation logic.
If you are dealing with complex form states that do not update correctly due to provider issues, reviewing
Riverpod's Notifier: UI Not Updating Despite Correct State Changes may help troubleshoot state synchronization problems in larger applications.
Common Pitfalls and Best Practices
One common mistake is forgetting to clear the error message before making a new request. If the user fixes their input but forgets to tap submit again, the old error remains visible. Always reset
_apiError to null at the start of the submission process or whenever the text changes significantly.
Another pitfall is handling exceptions incorrectly. Network failures often throw generic exceptions rather than returning structured error codes. Wrap your API call in a try-catch block. Map these generic exceptions to user-friendly strings. Do not expose raw stack traces or technical error codes to the end-user. Instead, provide a generic message like "Something went wrong" while logging the detailed error for debugging purposes.
Ensure that your API response parsing is robust. If the API returns a malformed JSON object, your app might crash when trying to extract the message string. Use safe navigation operators (
?.) or default values when parsing the response body to prevent crashes during validation.
Finally, consider the visual hierarchy. The error text displayed by
errorText is small and red. Ensure it does not overlap with other content. If your form is long, scrolling to the error field after submission improves accessibility. You can achieve this by using a
ScrollController to scroll to the specific field once the error state is updated.
Frequently Asked Questions
How do I clear the error message when the user starts typing again?
You can clear the error by listening to the
TextEditingController's listener. Add a listener to your controller that sets
_apiError to null and calls
setState whenever the text changes. This provides immediate feedback that the system is ready to re-validate the input.
Can I use a custom validator for both local and remote checks?
No, the
validator callback only accepts synchronous functions. For remote validation, you must handle the async logic separately in your submit handler and update the
errorText property manually via state management, as shown in the main implementation steps.
What happens if the API takes too long to respond?
If the API hangs, the UI will freeze until the response arrives. To mitigate this, show a loading indicator (like a circular progress indicator) inside the submit button. Also, implement timeouts in your HTTP client configuration to fail fast if the server does not respond within a reasonable timeframe, such as 10 seconds.
How do I handle multiple fields with different API validations?
Maintain a separate error state variable for each field that requires API validation, such as
_emailError and
_usernameError. Update the respective variable in
setState based on which field triggered the validation. This ensures that errors for one field do not interfere with others.
Is it safe to store error messages in the widget state?
Yes, for simple forms, storing errors in
StatefulWidget is perfectly safe and efficient. For very large applications with complex form data, consider using a state management solution like Riverpod or BLoC to centralize the validation logic and error handling across multiple screens.