- Flutter Development Tutorial (Beginner to Intermediate)
- Part 1: Introduction to Flutter and Environment Setup
- Part 2: Dart Basics for Flutter
- Part 3: Understanding Widgets and Building UI
- Part 4: Navigation and Routing
- Part 5: Working with Forms and Input Handling
- Part 6: Lists, Grids, and Dynamic Content
- Part 7: Advanced State Management
15. Forms and Validation
Forms are critical for collecting user input, and Flutter provides a powerful way to build and validate forms. Using the Form widget along with input fields like TextFormField allows you to collect, manage, and validate data easily.
15.1 Building a Simple Form
Flutter’s Form widget helps organize and manage multiple form fields. You can validate input and display error messages when invalid data is entered.
Example: Basic Form with Validation
class SimpleForm extends StatefulWidget {
@override
_SimpleFormState createState() => _SimpleFormState();
}
class _SimpleFormState extends State<SimpleForm> {
final _formKey = GlobalKey<FormState>();
String _name = '';
void _submitForm() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Form submitted: $_name');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Simple Form')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name';
}
return null;
},
onSaved: (value) {
_name = value!;
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
),
),
);
}
}
How it Works:
- Form Validation: The validator function checks whether the user has entered valid input, returning an error message if not.
- Form Submission: On submission, FormState.validate() is called to check the form’s validity, and onSaved stores the input data.
15.2 Advanced Validation with Regular Expressions
For more complex validation (e.g., validating emails or phone numbers), you can use regular expressions.
Example: Email Validation
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter an email';
} else if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Please enter a valid email';
}
return null;
},
)
15.3 Managing Multiple Input Fields
If a form has multiple fields (e.g., username, password, etc.), use the same Form widget to manage them efficiently.
Example: Login Form
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
String _username = '';
String _password = '';
void _submitForm() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Login submitted: $_username, $_password');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Login Form')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Username'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a username';
}
return null;
},
onSaved: (value) {
_username = value!;
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a password';
}
return null;
},
onSaved: (value) {
_password = value!;
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
child: Text('Login'),
),
],
),
),
),
);
}
}
Best Practices for Forms:
- Use form validation to ensure users enter valid data before submitting.
- Regular expressions can be helpful for pattern matching, such as validating emails or passwords.
- Use FocusNode to manage focus between multiple fields, allowing for better navigation between inputs using the keyboard.
16. Handling Navigation and Routing
Navigation allows users to move between different screens or pages in the app. Flutter provides various methods for routing, ranging from simple navigation to complex deep linking.
16.1 Basic Navigation with Navigator.push()
Flutter uses a navigation stack where each page is a route pushed onto the stack. You can use Navigator.push() to navigate between screens.
Example: Basic Screen Navigation
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Text('Go to Second Screen'),
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
),
);
}
}
How it Works:
Navigator.push() pushes a new route onto the stack, and Navigator.pop() removes it, taking the user back to the previous screen.
16.2 Named Routes
For apps with multiple screens, named routes provide a more scalable solution. You define routes in the MaterialApp widget and navigate by name.
Step 1: Define Named Routes
void main() {
runApp(MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/second': (context) => SecondScreen(),
},
));
}
Step 2: Navigate Using Named Routes
Navigator.pushNamed(context, '/second');
16.3 Passing Data Between Screens
You can pass arguments between screens using either constructor parameters or route arguments.
Example: Passing Data Using Route Arguments
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('First Screen')),
body: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen(data: 'Hello from First Screen')),
);
},
child: Text('Go to Second Screen'),
),
);
}
}
class SecondScreen extends StatelessWidget {
final String data;
SecondScreen({required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(child: Text(data)),
);
}
}
Best Practices for Navigation:
- Use named routes for better scalability in apps with multiple screens.
- For deep linking, use the onGenerateRoute callback in MaterialApp.
- Pass data between screens using route arguments or constructor parameters to maintain separation of concerns.