Flutter Stepper Widget: Create Multi-Step Forms and Wizards
•14 min read
The Stepper widget in Flutter is a powerful component for creating multi-step processes like forms, wizards, or onboarding flows. This comprehensive guide will show you how to implement and customize the Stepper widget effectively.
Basic Stepper Implementation
1. Simple Stepper Example
class BasicStepper extends StatefulWidget { @override _BasicStepperState createState() => _BasicStepperState(); } class _BasicStepperState extends State<BasicStepper> { int _currentStep = 0; @override Widget build(BuildContext context) { return Stepper( currentStep: _currentStep, onStepTapped: (step) => setState(() => _currentStep = step), onStepContinue: () { if (_currentStep != 2) { setState(() => _currentStep += 1); } }, onStepCancel: () { if (_currentStep != 0) { setState(() => _currentStep -= 1); } }, steps: [ Step( title: Text('Step 1'), content: Text('This is the content of Step 1'), isActive: _currentStep >= 0, ), Step( title: Text('Step 2'), content: Text('This is the content of Step 2'), isActive: _currentStep >= 1, ), Step( title: Text('Step 3'), content: Text('This is the content of Step 3'), isActive: _currentStep >= 2, ), ], ); } }
Multi-Step Form Implementation
1. Form with Validation
class MultiStepForm extends StatefulWidget { @override _MultiStepFormState createState() => _MultiStepFormState(); } class _MultiStepFormState extends State<MultiStepForm> { int _currentStep = 0; final _formKey = GlobalKey<FormState>(); // Form controllers final _nameController = TextEditingController(); final _emailController = TextEditingController(); final _phoneController = TextEditingController(); final _addressController = TextEditingController(); @override Widget build(BuildContext context) { return Form( key: _formKey, child: Stepper( type: StepperType.vertical, currentStep: _currentStep, onStepTapped: (step) => setState(() => _currentStep = step), onStepContinue: () { bool isLastStep = (_currentStep == getSteps().length - 1); if (isLastStep) { // Handle form submission if (_formKey.currentState!.validate()) { // Process data print('Form submitted'); } } else { setState(() => _currentStep += 1); } }, onStepCancel: () { if (_currentStep != 0) { setState(() => _currentStep -= 1); } }, steps: getSteps(), ), ); } List<Step> getSteps() { return [ Step( title: Text('Personal Information'), content: Column( children: [ TextFormField( controller: _nameController, decoration: InputDecoration(labelText: 'Full Name'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your name'; } return null; }, ), TextFormField( controller: _emailController, decoration: InputDecoration(labelText: 'Email'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!value.contains('@')) { return 'Please enter a valid email'; } return null; }, ), ], ), isActive: _currentStep >= 0, ), Step( title: Text('Contact Details'), content: Column( children: [ TextFormField( controller: _phoneController, decoration: InputDecoration(labelText: 'Phone Number'), validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your phone number'; } return null; }, ), TextFormField( controller: _addressController, decoration: InputDecoration(labelText: 'Address'), maxLines: 3, validator: (value) { if (value == null || value.isEmpty) { return 'Please enter your address'; } return null; }, ), ], ), isActive: _currentStep >= 1, ), Step( title: Text('Review'), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Name: ${_nameController.text}'), Text('Email: ${_emailController.text}'), Text('Phone: ${_phoneController.text}'), Text('Address: ${_addressController.text}'), ], ), isActive: _currentStep >= 2, ), ]; } @override void dispose() { _nameController.dispose(); _emailController.dispose(); _phoneController.dispose(); _addressController.dispose(); super.dispose(); } }
Customizing Stepper Appearance
1. Custom Stepper Theme
class CustomStyledStepper extends StatelessWidget { @override Widget build(BuildContext context) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( primary: Colors.blue, onPrimary: Colors.white, surface: Colors.blue, onSurface: Colors.black87, ), ), child: Stepper( // Your stepper configuration ), ); } }
2. Custom Step Icons
Step( title: Text('Custom Step'), content: Text('Step content'), state: StepState.complete, isActive: true, icon: Icon( Icons.check_circle, color: Colors.green, ), )
Horizontal Stepper Implementation
class HorizontalStepper extends StatefulWidget { @override _HorizontalStepperState createState() => _HorizontalStepperState(); } class _HorizontalStepperState extends State<HorizontalStepper> { int _currentStep = 0; @override Widget build(BuildContext context) { return Stepper( type: StepperType.horizontal, currentStep: _currentStep, onStepTapped: (step) => setState(() => _currentStep = step), onStepContinue: () { if (_currentStep != 2) { setState(() => _currentStep += 1); } }, onStepCancel: () { if (_currentStep != 0) { setState(() => _currentStep -= 1); } }, steps: [ Step( title: Text('Step 1'), content: Container( alignment: Alignment.centerLeft, child: Text('Horizontal step 1 content'), ), isActive: _currentStep >= 0, ), Step( title: Text('Step 2'), content: Container( alignment: Alignment.centerLeft, child: Text('Horizontal step 2 content'), ), isActive: _currentStep >= 1, ), Step( title: Text('Step 3'), content: Container( alignment: Alignment.centerLeft, child: Text('Horizontal step 3 content'), ), isActive: _currentStep >= 2, ), ], ); } }
Best Practices
-
State Management
- Keep track of step state properly
- Validate each step before proceeding
- Handle form data appropriately
-
User Experience
- Provide clear instructions
- Show progress indication
- Allow step navigation when appropriate
-
Error Handling
- Validate input at each step
- Show clear error messages
- Allow users to correct mistakes
-
Accessibility
- Ensure proper focus management
- Provide clear labels
- Support screen readers
Common Issues and Solutions
1. Step Validation
bool validateStep(int step) { switch (step) { case 0: return _nameController.text.isNotEmpty && _emailController.text.contains('@'); case 1: return _phoneController.text.isNotEmpty && _addressController.text.isNotEmpty; default: return true; } } void onStepContinue() { if (validateStep(_currentStep)) { if (_currentStep < getSteps().length - 1) { setState(() => _currentStep += 1); } } else { // Show error message ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Please fill all required fields')), ); } }
2. Handling Back Navigation
Future<bool> onWillPop() async { if (_currentStep > 0) { setState(() => _currentStep -= 1); return false; } return true; } // Usage in Widget WillPopScope( onWillPop: onWillPop, child: Stepper( // Your stepper configuration ), )
Conclusion
The Stepper widget is a versatile component for creating guided user experiences in Flutter. Key takeaways:
- Use appropriate validation at each step
- Maintain good state management
- Consider user experience and accessibility
- Handle errors gracefully
- Customize appearance when needed
By following these guidelines and examples, you can create intuitive multi-step processes that enhance your app's user experience.