Part 7: Advanced State Management

This entry is part 8 of 8 in the series Flutter Development Tutorial (Beginner to Intermediate)

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.

Related posts<< Part 6: Lists, Grids, and Dynamic Content