Back to Posts

Flutter Form Widgets: Handling User Input

14 min read

Form widgets are essential for handling user input and data collection in Flutter applications. This comprehensive guide covers everything from basic form widgets to advanced form handling patterns and best practices.

1. Basic Form Widgets

TextFormField

The TextFormField is one of the most commonly used form widgets, providing text input with built-in validation capabilities.

class TextFormFieldExample extends StatefulWidget {
  @override
  _TextFormFieldExampleState createState() => _TextFormFieldExampleState();
}

class _TextFormFieldExampleState extends State<TextFormFieldExample> {
  final _formKey = GlobalKey<FormState>();
  final _textController = TextEditingController();

  @override
  void dispose() {
    _textController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _textController,
            decoration: InputDecoration(
              labelText: 'Enter text',
              hintText: 'Type something...',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.text_fields),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter some text';
              }
              if (value.length < 3) {
                return 'Text must be at least 3 characters long';
              }
              return null;
            },
          ),
          SizedBox(height: 16),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // Handle form submission
                print('Text: ${_textController.text}');
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

CheckboxListTile

CheckboxListTile provides a checkbox with a label, perfect for terms and conditions or multiple selection scenarios.

class CheckboxListTileExample extends StatefulWidget {
  @override
  _CheckboxListTileExampleState createState() => _CheckboxListTileExampleState();
}

class _CheckboxListTileExampleState extends State<CheckboxListTileExample> {
  bool _isChecked = false;

  @override
  Widget build(BuildContext context) {
    return CheckboxListTile(
      title: Text('Accept Terms and Conditions'),
      subtitle: Text('Please read and accept our terms and conditions'),
      value: _isChecked,
      onChanged: (value) {
        setState(() {
          _isChecked = value!;
        });
      },
      secondary: Icon(Icons.description),
      controlAffinity: ListTileControlAffinity.leading,
    );
  }
}

2. Advanced Form Widgets

RadioListTile

RadioListTile is ideal for single-selection scenarios, such as choosing a payment method or delivery option.

class RadioListTileExample extends StatefulWidget {
  @override
  _RadioListTileExampleState createState() => _RadioListTileExampleState();
}

class _RadioListTileExampleState extends State<RadioListTileExample> {
  String _selectedOption = 'Option 1';

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile(
          title: Text('Option 1'),
          subtitle: Text('Basic plan'),
          value: 'Option 1',
          groupValue: _selectedOption,
          onChanged: (value) {
            setState(() {
              _selectedOption = value.toString();
            });
          },
          secondary: Icon(Icons.star_border),
        ),
        RadioListTile(
          title: Text('Option 2'),
          subtitle: Text('Premium plan'),
          value: 'Option 2',
          groupValue: _selectedOption,
          onChanged: (value) {
            setState(() {
              _selectedOption = value.toString();
            });
          },
          secondary: Icon(Icons.star),
        ),
        RadioListTile(
          title: Text('Option 3'),
          subtitle: Text('Enterprise plan'),
          value: 'Option 3',
          groupValue: _selectedOption,
          onChanged: (value) {
            setState(() {
              _selectedOption = value.toString();
            });
          },
          secondary: Icon(Icons.star_half),
        ),
      ],
    );
  }
}

Slider

The Slider widget is perfect for selecting a value within a range, such as volume control or price range selection.

class SliderExample extends StatefulWidget {
  @override
  _SliderExampleState createState() => _SliderExampleState();
}

class _SliderExampleState extends State<SliderExample> {
  double _sliderValue = 0.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Slider(
          value: _sliderValue,
          min: 0.0,
          max: 100.0,
          divisions: 10,
          label: _sliderValue.round().toString(),
          onChanged: (value) {
            setState(() {
              _sliderValue = value;
            });
          },
          activeColor: Theme.of(context).primaryColor,
          inactiveColor: Theme.of(context).primaryColor.withOpacity(0.3),
        ),
        Text(
          'Value: ${_sliderValue.round()}',
          style: Theme.of(context).textTheme.titleMedium,
        ),
      ],
    );
  }
}

3. Custom Form Widgets

Custom Form Field

Creating custom form fields helps maintain consistency across your application and reduces code duplication.

class CustomFormField extends StatelessWidget {
  final String label;
  final String? Function(String?)? validator;
  final TextEditingController controller;
  final bool obscureText;
  final IconData? prefixIcon;
  final String? hintText;

  const CustomFormField({
    required this.label,
    required this.validator,
    required this.controller,
    this.obscureText = false,
    this.prefixIcon,
    this.hintText,
  });

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: controller,
      obscureText: obscureText,
      decoration: InputDecoration(
        labelText: label,
        hintText: hintText,
        border: OutlineInputBorder(),
        errorBorder: OutlineInputBorder(
          borderSide: BorderSide(color: Colors.red),
        ),
        prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
      ),
      validator: validator,
    );
  }
}

Custom Form with Validation

A complete form example with multiple fields and validation.

class CustomForm extends StatefulWidget {
  @override
  _CustomFormState createState() => _CustomFormState();
}

class _CustomFormState extends State<CustomForm> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  final _passwordController = TextEditingController();
  final _emailController = TextEditingController();

  @override
  void dispose() {
    _usernameController.dispose();
    _passwordController.dispose();
    _emailController.dispose();
    super.dispose();
  }

  String? _validateEmail(String? value) {
    if (value == null || value.isEmpty) {
      return 'Please enter an email address';
    }
    if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
      return 'Please enter a valid email address';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          CustomFormField(
            label: 'Username',
            controller: _usernameController,
            prefixIcon: Icons.person,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter a username';
              }
              if (value.length < 3) {
                return 'Username must be at least 3 characters long';
              }
              return null;
            },
          ),
          SizedBox(height: 16),
          CustomFormField(
            label: 'Email',
            controller: _emailController,
            prefixIcon: Icons.email,
            validator: _validateEmail,
          ),
          SizedBox(height: 16),
          CustomFormField(
            label: 'Password',
            controller: _passwordController,
            obscureText: true,
            prefixIcon: Icons.lock,
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter a password';
              }
              if (value.length < 6) {
                return 'Password must be at least 6 characters long';
              }
              return null;
            },
          ),
          SizedBox(height: 24),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                // Handle form submission
                print('Form submitted successfully');
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}

Best Practices for Form Handling

  1. Always Validate Input: Implement proper validation for all form fields to ensure data integrity.
  2. Use Appropriate Widgets: Choose the right widget for each input type (e.g., TextFormField for text, CheckboxListTile for boolean values).
  3. Provide Clear Feedback: Show error messages and validation feedback to users.
  4. Handle Form State: Use FormState to manage form validation and submission.
  5. Clean Up Resources: Always dispose of controllers when they're no longer needed.
  6. Consider Accessibility: Ensure your forms are accessible by providing proper labels and semantic information.
  7. Implement Loading States: Show loading indicators during form submission.
  8. Handle Errors Gracefully: Provide clear error messages and recovery options.

Conclusion

Flutter provides a rich set of form widgets that make it easy to create beautiful and functional forms. By following best practices and using the appropriate widgets for each use case, you can create forms that are both user-friendly and maintainable. Remember to validate input, provide clear feedback, and handle form state properly to ensure a great user experience.