
Flutter: How to use the Stepper Widget
Whenever you're dealing with large forms inside of your Flutter applications, the use of a Stepper
Widget may be worth using. To quote the Material Design specification for Steppers:
Steppers convey progress through numbered steps.
How many times have you had to fill out a form and it feels like you've been scrolling forever? I've been in that spot more times than I can count. From the point of view of the developer, this isn't an easy task either. Forms are an eternal struggle, but a necessity for just about every mobile application.
$ flutter create flutter_step && cd flutter_step
$ code .
In order to understand the Stepper
, let's take a look at what it's expecting:
Stepper({
Key key,
@required this.steps,
this.physics,
this.type = StepperType.vertical,
this.currentStep = 0,
this.onStepTapped,
this.onStepContinue,
this.onStepCancel,
this.controlsBuilder,
})
The only required thing here is a list of steps
. We can create a new Step
like this:
Step(
title: const Text('New Account'),
isActive: true,
state: StepState.complete,
content: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Email Address'),
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
),
],
),
),
Therefore, all we need to do is provide a List<Step>
to our Stepper
. Without jumping too far forward, we'll house this inside of an AccountPage
at lib/pages/account_page.dart
:
class AccountPage extends StatefulWidget {
@override
_AccountPageState createState() => new _AccountPageState();
}
class _AccountPageState extends State<AccountPage> {
List<Step> steps = [
Step(
title: const Text('New Account'),
isActive: true,
state: StepState.complete,
content: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Email Address'),
),
TextFormField(
decoration: InputDecoration(labelText: 'Password'),
),
],
),
),
Step(
isActive: false,
state: StepState.editing,
title: const Text('Address'),
content: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Home Address'),
),
TextFormField(
decoration: InputDecoration(labelText: 'Postcode'),
),
],
),
),
Step(
state: StepState.error,
title: const Text('Avatar'),
subtitle: const Text("Error!"),
content: Column(
children: <Widget>[
CircleAvatar(
backgroundColor: Colors.red,
)
],
),
),
];
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('Create an account'),
),
body: Column(children: <Widget>[
Expanded(
child: Stepper(
steps: steps,
),
),
]));
}
}
This gives us a Stepper
with three steps
, but we currently don't have the ability to go backwards or forwards. We'll need to hook into onStepContinue
, onStepCancel
and onStepTapped
to allow a user to cycle through it.

Let's add a next
, cancel
and goTo
respectively:
// omitted
int currentStep = 0;
bool complete = false;
next() {
currentStep + 1 != steps.length
? goTo(currentStep + 1)
: setState(() => complete = true);
}
cancel() {
if (currentStep > 0) {
goTo(currentStep - 1);
}
}
goTo(int step) {
setState(() => currentStep = step);
}
We can add this to our Stepper
:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('Create an account'),
),
body: Column(children: <Widget>[
Expanded(
child: Stepper(
steps: steps,
currentStep: currentStep,
onStepContinue: next,
onStepTapped: (step) => goTo(step),
onStepCancel: cancel,
),
),
]));
}
Awesome! We've now got the ability to cycle through our Stepper
using the CONTINUE
and CANCEL
buttons.

As there's also the ability to change how the Stepper
looks (i.e. vertical
or horizontal
), let's add a Floating Action Button (FAB) that cycles this too.
We'll need to define a stepperType
and then a switchStepType
function that goes between two states:
StepperType stepperType = StepperType.horizontal;
switchStepType() {
setState(() => stepperType == StepperType.horizontal
? stepperType = StepperType.vertical
: stepperType = StepperType.horizontal);
}
// Scaffold
floatingActionButton: FloatingActionButton(
child: Icon(Icons.list),
onPressed: switchStepType,
),
Update the Stepper
to add the type
property equal to our stepperType
variable:
Stepper(
steps: steps,
type: stepperType,
currentStep: currentStep,
onStepContinue: next,
onStepTapped: (step) => goTo(step),
onStepCancel: cancel,
),
That gives us the following result(s) when we click our FAB:

Finally, wouldn't it be good if we could show an AlertDialog
when we've completed all steps?
There's many ways that we can do this, but for our simple example we'll simply replace the Stepper
with an AlertDialog
on completion:
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('Create an account'),
),
body: Column(
children: <Widget>[
complete ? Expanded(
child: Center(
child: AlertDialog(
title: new Text("Profile Created"),
content: new Text(
"Tada!",
),
actions: <Widget>[
new FlatButton(
child: new Text("Close"),
onPressed: () {
setState(() => complete = false);
},
),
],
),
),
)
: Expanded(
child: Stepper(
type: stepperType,
steps: steps,
currentStep: currentStep,
onStepContinue: next,
onStepTapped: (step) => goTo(step),
onStepCancel: cancel,
),
),
],
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.list),
onPressed: switchStepType,
),
);
}
Here's our final result:

Summary
I hope you found this useful! Now go out there and make some awesome steppers!
If you liked this article, you may also like the below video which looks at customising our Stepper button(s):