Building Multi-Step Stepper Form Widgets in Flutter
Multi-step forms are a common UI pattern used to break down complex data entry processes into smaller, manageable sections. This approach improves user experience by reducing cognitive load, providing clear progress indicators, and making forms less intimidating. In Flutter, the Stepper widget is a powerful tool designed specifically for this purpose, allowing developers to create elegant multi-step forms with ease.
Understanding the Flutter Stepper Widget
The Stepper widget in Flutter provides a visual representation of a sequence of steps. Each step can contain its own content, often including form fields. Users can navigate between steps, and the widget can manage the current step's state and validate input before proceeding.
Key Properties of Stepper:
steps: A list ofStepwidgets, defining each stage of the process.currentStep: An integer representing the index of the currently active step.onStepContinue: A callback function triggered when the "Continue" button is pressed. This is where you typically implement validation and navigate to the next step.onStepCancel: A callback function triggered when the "Cancel" (or "Back") button is pressed. Used for navigating to the previous step.onStepTapped: A callback function triggered when a step's header is tapped, allowing direct navigation to a specific step.type: Defines the orientation of the stepper (StepperType.verticalorStepperType.horizontal).
Key Properties of Step:
title: AWidget, usually aTextwidget, displayed as the step's title.content: AWidgetthat holds the main content of the step, such as form fields.isActive: A boolean indicating if the step is currently active.state: AStepStateenum (indexed,editing,complete,disabled,error) to convey the step's status.
Basic Implementation of a Stepper Form
Let's start by building a simple multi-step form that collects basic user information. We will manage the current step using a state variable and handle navigation.
Step 1: Set up the Stepper Widget
We'll create a StatefulWidget to manage the state of our stepper, including the currentStep index and data collected from each step.
import 'package:flutter/material.dart';
class MultiStepStepperForm extends StatefulWidget {
const MultiStepStepperForm({super.key});
@override
State createState() => _MultiStepStepperFormState();
}
class _MultiStepStepperFormState extends State {
int _currentStep = 0;
String? _name;
String? _email;
String? _address;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multi-Step Stepper Form'),
),
body: Stepper(
type: StepperType.vertical,
currentStep: _currentStep,
onStepContinue: () {
final isLastStep = _currentStep == getSteps().length - 1;
if (isLastStep) {
// Logic to submit the form
print('Form Submitted!');
print('Name: $_name');
print('Email: $_email');
print('Address: $_address');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form Submitted Successfully!')),
);
} else {
setState(() {
_currentStep += 1;
});
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() {
_currentStep -= 1;
});
}
},
onStepTapped: (step) {
setState(() {
_currentStep = step;
});
},
steps: getSteps(),
),
);
}
List getSteps() {
return [
Step(
title: const Text('Account Info'),
content: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Name'),
onChanged: (value) => setState(() => _name = value),
),
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
onChanged: (value) => setState(() => _email = value),
),
],
),
isActive: _currentStep >= 0,
state: _currentStep > 0 ? StepState.complete : StepState.indexed,
),
Step(
title: const Text('Personal Address'),
content: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Address'),
onChanged: (value) => setState(() => _address = value),
),
],
),
isActive: _currentStep >= 1,
state: _currentStep > 1 ? StepState.complete : StepState.indexed,
),
Step(
title: const Text('Confirmation'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${_name ?? ''}'),
Text('Email: ${_email ?? ''}'),
Text('Address: ${_address ?? ''}'),
const SizedBox(height: 16),
const Text('Please review your information before submitting.'),
],
),
isActive: _currentStep >= 2,
state: _currentStep == 2 ? StepState.editing : StepState.indexed,
),
];
}
}
In this example, we define three steps using the getSteps() method. Each step contains TextFormField widgets to collect user input. The onStepContinue, onStepCancel, and onStepTapped callbacks update the _currentStep state variable, which in turn rebuilds the Stepper to show the active step.
Adding Form Validation
For robust forms, validation is essential. Flutter's Form widget combined with GlobalKey allows us to validate individual steps before proceeding.
Step 2: Integrate Form and Validation
We will modify the getSteps() method to wrap each step's content in a Form widget and add validators to the TextFormField widgets. We'll also need a list of GlobalKey, one for each step.
import 'package:flutter/material.dart';
class MultiStepStepperFormWithValidation extends StatefulWidget {
const MultiStepStepperFormWithValidation({super.key});
@override
State createState() => _MultiStepStepperFormWithValidationState();
}
class _MultiStepStepperFormWithValidationState extends State {
int _currentStep = 0;
String? _name;
String? _email;
String? _address;
// List of GlobalKeys for form validation, one for each step
final List> _formKeys = [
GlobalKey(), // Key for Step 1
GlobalKey(), // Key for Step 2
GlobalKey(), // Key for Step 3 (optional, as it's just confirmation)
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Multi-Step Stepper Form (with Validation)'),
),
body: Stepper(
type: StepperType.vertical,
currentStep: _currentStep,
onStepContinue: () {
// Validate the current step's form
if (_formKeys[_currentStep].currentState!.validate()) {
_formKeys[_currentStep].currentState!.save(); // Save the form data
final isLastStep = _currentStep == getSteps().length - 1;
if (isLastStep) {
// Logic to submit the form
print('Form Submitted!');
print('Name: $_name');
print('Email: $_email');
print('Address: $_address');
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Form Submitted Successfully!')),
);
} else {
setState(() {
_currentStep += 1;
});
}
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() {
_currentStep -= 1;
});
}
},
onStepTapped: (step) {
// Allow direct navigation only if previous steps are valid (optional, depends on UX)
// For simplicity, we'll allow tapping to any step for now, but
// a production app might restrict this.
setState(() {
_currentStep = step;
});
},
steps: getSteps(),
),
);
}
List getSteps() {
return [
Step(
title: const Text('Account Info'),
content: Form(
key: _formKeys[0], // Assign key for validation
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Name'),
initialValue: _name,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your name.';
}
return null;
},
onSaved: (value) => _name = value,
),
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
initialValue: _email,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your email.';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'Please enter a valid email address.';
}
return null;
},
onSaved: (value) => _email = value,
),
],
),
),
isActive: _currentStep >= 0,
state: _currentStep > 0 ? StepState.complete : StepState.indexed,
),
Step(
title: const Text('Personal Address'),
content: Form(
key: _formKeys[1], // Assign key for validation
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'Address'),
initialValue: _address,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your address.';
}
return null;
},
onSaved: (value) => _address = value,
),
],
),
),
isActive: _currentStep >= 1,
state: _currentStep > 1 ? StepState.complete : StepState.indexed,
),
Step(
title: const Text('Confirmation'),
content: Form( // Wrap in Form even if no validation, for consistency
key: _formKeys[2],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${_name ?? ''}'),
Text('Email: ${_email ?? ''}'),
Text('Address: ${_address ?? ''}'),
const SizedBox(height: 16),
const Text('Please review your information before submitting.'),
],
),
),
isActive: _currentStep >= 2,
state: _currentStep == 2 ? StepState.editing : StepState.indexed,
),
];
}
}
In this enhanced version:
-
A
Listis used to uniquely identify each step's form.> _formKeys -
Each step's
contentis wrapped in aFormwidget, and its respectiveGlobalKeyis assigned. -
TextFormFieldwidgets now include avalidatorfunction to check input validity and anonSavedcallback to store the value once validated. -
Inside
onStepContinue, we call_formKeys[_currentStep].currentState!.validate(). The stepper will only proceed to the next step if the current step's form is valid. If valid,_formKeys[_currentStep].currentState!.save()is called to trigger theonSavedcallbacks and update the state variables. -
The
stateproperty of eachStepis updated dynamically to show whether a step is complete, editing, or indexed, providing better visual feedback.
Conclusion
Building multi-step stepper forms in Flutter using the Stepper widget is an effective way to improve user experience for complex data entry. By combining Stepper with Flutter's Form and validation capabilities, developers can create interactive, validated forms that guide users through a logical flow. This pattern not only makes forms easier to complete but also ensures data integrity before submission. Further enhancements can include custom stepper controls, animations, and integration with more advanced state management solutions like Provider or BLoC for larger applications.