Forms are one of the most important elements in mobile applications. From login screens to checkout flows, registration pages to feedback forms, they are the backbone of interaction between users and the system. While validation ensures that data is correct, the next step is handling form submission effectively.
Form submission is more than just sending data to a backend server. It involves creating a smooth and intuitive user experience, handling loading states, error feedback, and ensuring successful completion.
In this guide, we will explore in detail how to:
- Submit form data to a backend.
- Show success and error states clearly.
- Follow best practices for user experience (UX) in form submission.
By the end, you will have a complete understanding of how to implement robust form submission in your Flutter applications.
What Does Form Submission Mean?
Form submission is the process of:
- Collecting user input from fields.
- Validating the input.
- Sending it to a backend server (via APIs).
- Handling responses (success or failure).
- Updating the UI to reflect the result.
A simple login form, for example, collects an email and password. When the user taps the submit button:
- Flutter validates the input.
- The app sends the email and password to the backend API.
- If the credentials are correct, the user logs in successfully.
- If they are incorrect, an error message is shown.
Submitting Form Data to Backend
Once the form is validated, the next step is to send the data to a backend server. This is typically done using HTTP requests. In Flutter, the most common way is through the http package.
Example: Sending Form Data
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final response = await http.post(
Uri.parse("https://example.com/api/login"),
headers: {"Content-Type": "application/json"},
body: json.encode({
"email": _emailController.text,
"password": _passwordController.text,
}),
);
if (response.statusCode == 200) {
print("Login successful");
} else {
setState(() {
_errorMessage = "Invalid email or password";
});
}
} catch (error) {
setState(() {
_errorMessage = "Something went wrong. Please try again.";
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: "Email"),
validator: (value) =>
value == null || value.isEmpty ? "Email required" : null,
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(labelText: "Password"),
validator: (value) =>
value == null || value.isEmpty ? "Password required" : null,
),
SizedBox(height: 20),
if (_errorMessage != null)
Text(_errorMessage!, style: TextStyle(color: Colors.red)),
SizedBox(height: 10),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text("Login"),
)
],
),
),
),
);
}
}
Explanation
- Validation: Runs before submission (
_formKey.currentState!.validate()). - Loading State: A boolean
_isLoadingshows a spinner during submission. - Error Handling:
_errorMessagestores errors returned by the backend. - Success State: When status code is 200, login is successful.
This is a simple yet complete example of handling submission.
Showing Success and Error States
An essential part of form submission is giving feedback to the user. They need to know whether their submission worked or failed.
Error States
Errors can occur for many reasons:
- Validation errors: The user forgot to fill a field.
- Authentication errors: Invalid credentials.
- Network errors: Poor connection or no internet.
- Server errors: The backend fails to process the request.
In all these cases, users need to see clear, human-readable error messages.
Example: Showing Error Message
if (_errorMessage != null)
Text(_errorMessage!, style: TextStyle(color: Colors.red));
This displays the error below the submit button.
Success States
When the form submission succeeds, you may:
- Navigate to another screen.
- Show a success message.
- Display a confirmation dialog.
Example: Success Feedback with Snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Login successful!")),
);
This shows a temporary message at the bottom of the screen.
Example: Navigate to Home Screen
Navigator.pushReplacementNamed(context, "/home");
Users expect to move forward after successful actions. Navigating them is an excellent way to confirm success.
Best Practices for UX in Form Submission
Form submission isn’t just about code; it’s about experience. Users should never feel confused or left in the dark. Below are best practices to create a smooth, user-friendly process.
1. Disable Submit Button While Loading
Prevent duplicate submissions by disabling the button during an API call.
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text("Submit"),
)
2. Show Progress Feedback
Always let users know something is happening with a spinner or loading indicator. Silence creates confusion.
3. Provide Clear Error Messages
- Bad: “Invalid input.”
- Good: “Password must be at least 6 characters.”
The more specific the error, the easier it is for users to fix.
4. Keep Users on the Same Page for Errors
Do not navigate away if there’s an error. Stay on the form page and highlight issues.
5. Use Inline Validation
Whenever possible, validate fields as users type instead of waiting until submission. This reduces frustration.
6. Confirm Success
After success:
- Use a Snackbar, Toast, or Dialog.
- Navigate to the next logical page.
- Provide a sense of closure to the action.
7. Handle No Internet Gracefully
Users may not have a connection. Always show meaningful messages like:
"Unable to connect. Please check your internet connection."
8. Save Drafts if Necessary
For long forms (like feedback or surveys), allow users to save partial data. This prevents data loss.
9. Keep Submit Buttons Accessible
On mobile screens, the submit button should remain visible even when the keyboard is open. Use resizeToAvoidBottomInset: true with Scaffold.
10. Test on Real Devices
Emulators don’t always show real-world behavior (e.g., slow networks, back button behavior). Always test submission on physical devices.
Real-World Example: Registration Form with Submission
class RegistrationForm extends StatefulWidget {
@override
_RegistrationFormState createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
Future<void> _submitForm() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final response = await http.post(
Uri.parse("https://example.com/api/register"),
headers: {"Content-Type": "application/json"},
body: json.encode({
"name": _nameController.text,
"email": _emailController.text,
"password": _passwordController.text,
}),
);
if (response.statusCode == 201) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Registration successful")),
);
Navigator.pushReplacementNamed(context, "/home");
} else {
setState(() {
_errorMessage = "Registration failed. Try again.";
});
}
} catch (error) {
setState(() {
_errorMessage = "Network error. Please try later.";
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: EdgeInsets.all(20),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: "Name"),
validator: (value) =>
value == null || value.isEmpty ? "Name required" : null,
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: "Email"),
validator: (value) =>
value == null || value.isEmpty ? "Email required" : null,
),
TextFormField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(labelText: "Password"),
validator: (value) =>
value == null || value.isEmpty ? "Password required" : null,
),
if (_errorMessage != null)
Text(_errorMessage!, style: TextStyle(color: Colors.red)),
SizedBox(height: 20),
ElevatedButton(
onPressed: _isLoading ? null : _submitForm,
child: _isLoading
? CircularProgressIndicator(color: Colors.white)
: Text("Register"),
)
],
),
),
),
);
}
}
Leave a Reply