Creating a Step Progress Indicator Widget in Flutter
In modern applications, guiding users through multi-step processes is crucial for a smooth and intuitive user experience. A Step Progress Indicator is a common UI component used to visualize the user's current position within a sequence of tasks, such as onboarding flows, multi-page forms, or e-commerce checkout processes.
This article will guide you through creating a customizable Step Progress Indicator widget in Flutter, covering its core components, implementation strategy, and a complete code example.
Understanding the Core Components
A typical step progress indicator consists of several key elements:
- Steps: Individual units representing each stage of the process. Each step usually has a visual identifier (like a number or icon) and a descriptive label.
- Connectors (Dividers): Lines or other visual elements that link one step to the next, indicating the flow.
- States: Each step can be in one of three states, requiring distinct visual styles:
- Active (Current): The step the user is currently on.
- Completed: Steps that the user has successfully finished.
- Inactive (Upcoming): Steps that are yet to be reached.
Flutter Implementation Strategy
We'll build our Step Progress Indicator as a StatefulWidget to manage the current step. The layout will primarily use a Row widget to arrange the steps and their connecting dividers horizontally. Each individual step will be a Column containing an icon/number and its text label.
Key considerations for our implementation:
- **Flexibility:** The widget should accept a list of step titles and the current step index as parameters.
- **Customization:** Allow for custom colors for active, inactive, and completed states, as well as adjustable sizes.
- **Clear Visuals:** Distinct styling for each step state to provide immediate feedback to the user.
Step-by-Step Code Walkthrough
Let's break down the creation of our StepProgressIndicator widget.
1. Define the StepProgressIndicator Widget
This widget will be a StatefulWidget to maintain its internal state, primarily responsible for rendering based on the provided steps and currentStep. It also defines customizable properties like colors and sizes.
import 'package:flutter/material.dart';
class StepProgressIndicator extends StatefulWidget {
final List<String> steps;
final int currentStep;
final Color activeColor;
final Color inactiveColor;
final Color completedColor;
final double stepRadius; // Radius of the circular step indicator
final double lineHeight; // Height of the connecting line
const StepProgressIndicator({
Key? key,
required this.steps,
required this.currentStep,
this.activeColor = Colors.blue,
this.inactiveColor = Colors.grey,
this.completedColor = Colors.green,
this.stepRadius = 18.0,
this.lineHeight = 4.0,
}) : assert(currentStep >= 0 && currentStep <= steps.length),
super(key: key);
@override
_StepProgressIndicatorState createState() => _StepProgressIndicatorState();
}
2. Implement the State Class
The _StepProgressIndicatorState will contain the core logic for building the individual steps and dividers.
class _StepProgressIndicatorState extends State {
// Method to build an individual step (circle + text label)
Widget _buildStep(int index, String stepTitle) {
bool isCompleted = index < widget.currentStep;
bool isActive = index == widget.currentStep;
bool isInactive = index > widget.currentStep;
Color stepColor;
Widget stepIcon;
TextStyle stepTextStyle;
if (isCompleted) {
stepColor = widget.completedColor;
stepIcon = Icon(Icons.check, color: Colors.white, size: widget.stepRadius * 1.2);
stepTextStyle = TextStyle(color: widget.completedColor, fontWeight: FontWeight.bold);
} else if (isActive) {
stepColor = widget.activeColor;
stepIcon = Text(
'${index + 1}',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: widget.stepRadius * 0.9),
);
stepTextStyle = TextStyle(color: widget.activeColor, fontWeight: FontWeight.bold);
} else {
stepColor = widget.inactiveColor;
stepIcon = Text(
'${index + 1}',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: widget.stepRadius * 0.9),
);
stepTextStyle = TextStyle(color: widget.inactiveColor);
}
return Column(
children: [
Container(
width: widget.stepRadius * 2,
height: widget.stepRadius * 2,
decoration: BoxDecoration(
color: stepColor,
shape: BoxShape.circle,
border: Border.all(
color: isActive ? widget.activeColor.withOpacity(0.5) : Colors.transparent, // Highlight active step
width: isActive ? 2.0 : 0.0,
),
),
child: Center(child: stepIcon),
),
const SizedBox(height: 8.0),
SizedBox(
width: 80, // Fixed width for labels to prevent overflow
child: Text(
stepTitle,
textAlign: TextAlign.center,
style: stepTextStyle,
overflow: TextOverflow.ellipsis,
),
),
],
);
}
// Method to build the connecting line between steps
Widget _buildDivider(int index) {
bool isCompleted = index < widget.currentStep;
Color lineColor = isCompleted ? widget.completedColor : widget.inactiveColor;
return Expanded(
child: Container(
height: widget.lineHeight,
color: lineColor,
),
);
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start, // Align step labels at the top
children: List.generate(widget.steps.length * 2 - 1, (index) {
if (index.isEven) {
// This is a step indicator
final stepIndex = index ~/ 2;
return _buildStep(stepIndex, widget.steps[stepIndex]);
} else {
// This is a divider line
final dividerIndex = (index - 1) ~/ 2;
return _buildDivider(dividerIndex);
}
}),
),
);
}
}
In the _buildStep method, we determine the step's state (completed, active, or inactive) and apply appropriate colors, icons (a checkmark for completed, a number for active/inactive), and text styles. We use a Column to stack the circular step indicator and its text label.
The _buildDivider method simply returns an Expanded Container, which acts as a line. Its color changes based on whether the preceding step is completed.
The main build method of _StepProgressIndicatorState generates a list of widgets alternating between steps and dividers, all laid out within a Row. List.generate is used to dynamically create these widgets based on the number of steps.
Complete Example Usage
To demonstrate how to use the StepProgressIndicator, let's create a simple Flutter application that allows you to advance through steps.
import 'package:flutter/material.dart';
// Assuming your StepProgressIndicator widget is in a file named
// 'step_progress_indicator.dart' in the same 'lib' folder or imported appropriately.
// import 'step_progress_indicator.dart'; // Uncomment if in a separate file
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Step Progress Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State {
int _currentStep = 0;
final List<String> _steps = [
'Account',
'Address',
'Payment',
'Confirm',
'Done',
];
void _nextStep() {
setState(() {
if (_currentStep < _steps.length - 1) {
_currentStep++;
} else {
_currentStep = 0; // Reset for demo purposes
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Step Progress Indicator'),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(20.0),
child: StepProgressIndicator(
steps: _steps,
currentStep: _currentStep,
activeColor: Colors.deepPurple,
completedColor: Colors.teal,
inactiveColor: Colors.blueGrey.shade200,
stepRadius: 20.0,
lineHeight: 5.0,
),
),
const Expanded(
child: Center(
child: Text(
'Navigate through the steps using the button below.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _nextStep,
child: const Icon(Icons.navigate_next),
),
);
}
}
Customization and Enhancements
The provided widget serves as a solid foundation. Here are some ways you can further customize and enhance it:
- Theming: Introduce a dedicated theme or inherited widget to manage default colors, fonts, and sizes across your application.
- Animations: Add explicit animations (e.g., using
AnimatedContainerorTweenAnimationBuilder) when a step becomes active or completed for a more dynamic feel. - Vertical Layout: Adapt the widget to support a vertical orientation using a
Columninstead of aRow, and adjust divider logic accordingly. - Tappable Steps: Wrap the step indicators with
GestureDetectororInkWellto allow users to tap on a step and potentially navigate directly to it (e.g., in a multi-page form where skipping steps is allowed). - Custom Icons: Allow passing custom widgets or
IconDatafor step indicators instead of just numbers or checkmarks. - Dynamic Step Count: While our example uses a fixed list, ensure your application logic can handle dynamically changing step lists if needed.
Conclusion
A Step Progress Indicator is a powerful UI element that significantly improves user experience by providing clear visual feedback in multi-step processes. By following this guide, you can create a flexible and customizable widget in Flutter that fits various application needs. With further enhancements, it can become an even more interactive and visually appealing component in your Flutter toolkit.