image

15 Dec 2025

9K

35K

Building Dynamic Stepper Widgets in Flutter

Stepper widgets are a common UI pattern used to guide users through a series of sequential steps or a multi-stage process. In Flutter, the built-in Stepper widget provides a convenient way to implement such functionality. While the basic Stepper is straightforward, building one that dynamically adapts its steps based on data, user input, or business logic offers greater flexibility and reusability.

This article will guide you through creating a dynamic stepper in Flutter, covering how to manage step data, update the UI, and handle navigation efficiently.

Understanding Flutter's Stepper Widget

Before diving into dynamic behavior, let's briefly review the core properties of Flutter's Stepper widget:

  • steps: A List<Step> that defines all the steps in the stepper. Each Step object requires a title and content, and can optionally have subtitle, isActive, and state properties.
  • currentStep: An integer indicating the currently active step (0-indexed).
  • onStepContinue: A callback function triggered when the "Continue" button is pressed.
  • onStepCancel: A callback function triggered when the "Cancel" button is pressed.
  • onStepTapped: A callback function triggered when a step header is tapped, allowing direct navigation to a specific step.
  • type: Determines the orientation of the stepper, either StepperType.vertical (default) or StepperType.horizontal.
  • controlsBuilder: A builder function to customize the navigation buttons (continue/cancel).

The key to making a stepper dynamic lies in how we populate the steps list and how we manage the currentStep in response to user interactions.

Making the Stepper Dynamic

A dynamic stepper means that the number of steps, their titles, content, or even the widgets they display, are not hardcoded but are generated from a data source. This data could come from:

  • An API response.
  • A local database.
  • User configurations.
  • A predefined list that can change based on conditions.

Here's the general approach:

  1. Define a Data Structure: Create a list of objects or maps that represent your steps. Each item in this list should contain all the necessary information for a single step (title, content, and potentially a widget to display).
  2. Use a StatefulWidget: The stepper's state, specifically currentStep, needs to be managed, so a StatefulWidget is essential.
  3. Generate List<Step> Dynamically: Map your data structure to a List<Step> that the Stepper widget can consume.
  4. Update State on Navigation: Implement onStepContinue, onStepCancel, and onStepTapped to update the _currentStep variable within setState().

Implementation Example

Let's build a dynamic stepper that simulates a multi-step form where the steps are defined by a list of maps, and each step presents a different input field.

1. Set up the Project

Create a new Flutter project if you don't have one:


flutter create dynamic_stepper_demo
cd dynamic_stepper_demo

2. Create the Dynamic Stepper Widget

We'll create a StatefulWidget called DynamicStepperScreen to house our stepper logic.


import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dynamic Stepper Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DynamicStepperScreen(),
    );
  }
}

class DynamicStepperScreen extends StatefulWidget {
  @override
  _DynamicStepperScreenState createState() => _DynamicStepperScreenState();
}

class _DynamicStepperScreenState extends State {
  int _currentStep = 0; // State variable to keep track of the current step

  // Simulate dynamic data for steps.
  // In a real application, this might come from an API or complex logic.
  final List> _stepData = [
    {
      'title': 'Personal Info',
      'content': 'Please enter your personal details below.',
      'widget': TextField(
          decoration: InputDecoration(
              labelText: 'Full Name', hintText: 'John Doe')),
    },
    {
      'title': 'Contact Information',
      'content': 'Provide your contact details.',
      'widget': TextField(
          decoration: InputDecoration(
              labelText: 'Email', hintText: '[email protected]')),
    },
    {
      'title': 'Address Details',
      'content': 'Where do you live?',
      'widget': TextField(
          decoration: InputDecoration(
              labelText: 'Street Address', hintText: '123 Main St')),
    },
    {
      'title': 'Review and Submit',
      'content': 'Please review all your details before submission.',
      'widget': Text(
          'Ensure all information is correct before finalizing.'), // A simple message for the last step
    },
  ];

  // A helper method to generate the List from our _stepData
  List _getSteps() {
    return _stepData.map((stepInfo) {
      int index = _stepData.indexOf(stepInfo);
      return Step(
        title: Text(stepInfo['title']),
        content: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(stepInfo['content']),
            SizedBox(height: 16),
            stepInfo['widget'], // The actual input widget for the step
          ],
        ),
        isActive: _currentStep >= index, // Step is active if it's the current or a future step
        state: _currentStep > index
            ? StepState.complete // If currentStep is past this step, it's complete
            : (_currentStep == index ? StepState.editing : StepState.indexed), // Otherwise, editing or indexed
      );
    }).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Dynamic Stepper'),
      ),
      body: Stepper(
        type: StepperType.vertical, // Can be StepperType.horizontal
        currentStep: _currentStep,
        steps: _getSteps(), // Generate steps dynamically
        onStepContinue: () {
          // Logic for when the "Continue" button is pressed
          if (_currentStep < _stepData.length - 1) {
            setState(() {
              _currentStep += 1;
            });
          } else {
            // Last step, typically submit the form or perform final action
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text('Form Submitted!')),
            );
            // Optionally reset or navigate away
            // setState(() {
            //   _currentStep = 0;
            // });
          }
        },
        onStepCancel: () {
          // Logic for when the "Cancel" (Back) button is pressed
          if (_currentStep > 0) {
            setState(() {
              _currentStep -= 1;
            });
          }
        },
        onStepTapped: (step) {
          // Logic for when a step header is tapped
          setState(() {
            _currentStep = step;
          });
        },
        // Optional: Customize the controls (buttons)
        controlsBuilder: (BuildContext context, ControlsDetails details) {
          return Padding(
            padding: const EdgeInsets.only(top: 16.0),
            child: Row(
              children: [
                Expanded(
                  child: ElevatedButton(
                    onPressed: details.onStepContinue,
                    child: Text(_currentStep == _stepData.length - 1 ? 'SUBMIT' : 'NEXT'),
                  ),
                ),
                SizedBox(width: 10),
                if (_currentStep != 0) // Don't show "BACK" on the first step
                  Expanded(
                    child: OutlinedButton(
                      onPressed: details.onStepCancel,
                      child: const Text('BACK'),
                    ),
                  ),
              ],
            ),
          );
        },
      ),
    );
  }
}

Explanation of the Code:

  • _stepData: This List<Map<String, dynamic>> serves as our dynamic data source. Each map represents a step and contains a title, content, and a widget. The `widget` key holds the specific UI element (e.g., a TextField or a Text widget) for that step.
  • _getSteps(): This method iterates through _stepData using map() and transforms each data item into a Flutter Step object.
    • isActive is set to _currentStep >= index, meaning the current step and any subsequent steps are visually active.
    • state uses a conditional check to show StepState.complete for steps already passed, StepState.editing for the current step, and StepState.indexed for future steps.
  • onStepContinue: Increments _currentStep. If it's the last step, it triggers a "Form Submitted!" message (you would integrate your form submission logic here).
  • onStepCancel: Decrements _currentStep, preventing it from going below 0.
  • onStepTapped: Allows direct navigation to a tapped step by updating _currentStep to the index of the tapped step.
  • controlsBuilder: We've customized the default buttons. The "NEXT" button becomes "SUBMIT" on the final step, and the "BACK" button is hidden on the first step.

Further Customization and Considerations

  • Validation: For real-world forms, you would add validation logic within onStepContinue. If validation fails, prevent _currentStep from advancing and display an error. You could also set StepState.error for specific steps.
  • Data Collection: In this example, TextFields are present but their values aren't explicitly collected. You would typically use TextEditingControllers for each input field and store their values in a state management solution or a dedicated data model.
  • Different Widget Types: The 'widget' key in _stepData can hold any Flutter widget, allowing for highly flexible step content (e.g., dropdowns, sliders, image pickers).
  • Horizontal Stepper: Simply change type: StepperType.vertical to type: StepperType.horizontal for a horizontal layout.
  • Custom Stepper Headers: You can customize the entire step display by not just setting the title and subtitle, but by wrapping them in custom widgets.

Conclusion

Building dynamic stepper widgets in Flutter enhances the user experience by providing clear, sequential guidance through complex processes. By leveraging Flutter's StatefulWidget and dynamically generating the List<Step> from a data source, you can create highly adaptable and reusable stepper components. This approach ensures your UI remains flexible, easily scalable, and maintainable as your application's requirements evolve.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is