Back to Posts

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

  1. State Management

    • Keep track of step state properly
    • Validate each step before proceeding
    • Handle form data appropriately
  2. User Experience

    • Provide clear instructions
    • Show progress indication
    • Allow step navigation when appropriate
  3. Error Handling

    • Validate input at each step
    • Show clear error messages
    • Allow users to correct mistakes
  4. 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.