NumberStepperAppi
A visual step indicator widget that displays numbered steps with progress indication, perfect for multi-step processes, wizards, and progress tracking.
Features
- Step Visualization: Clear numbered step indicators
- Progress Tracking: Visual indication of completed steps
- Customizable Size: Adjustable circle radius for different layouts
- Connected Steps: Divider lines connecting sequential steps
- Theme Integration: Adapts to app theme colors
- Responsive Design: Scales appropriately for different screen sizes
- Accessibility: Screen reader compatible with step information
Usage
Basic Step Indicator
NumberStepperAppi(
len: 5,
currentPosition: 2,
radius: 20.0,
)
Large Step Indicator
NumberStepperAppi(
len: 4,
currentPosition: 3,
radius: 25.0,
)
Compact Step Indicator
NumberStepperAppi(
len: 6,
currentPosition: 1,
radius: 15.0,
)
Parameters
| Name | Type | Description | Default |
|------|------|-------------|---------||
| len
| int
| Required. Total number of steps to display | - |
| currentPosition
| int
| Required. Current active step position (0-based) | - |
| radius
| double
| Required. Radius of each step circle indicator | - |
Behavior
Step States
- Completed Steps: Steps before
currentPosition
are filled with primary color - Current Step: Step at
currentPosition
has border highlighting - Future Steps: Steps after
currentPosition
are outlined only - Connecting Lines: Dividers between steps show completion status
Visual Elements
- Circle Indicators: Numbered circles for each step
- Step Numbers: Sequential numbering starting from 1
- Progress Lines: Horizontal dividers connecting steps
- Color Coding: Theme-based colors for different states
Layout
- Horizontal Layout: Steps are arranged in a horizontal row
- Centered Alignment: Automatically centers within available space
- Responsive Spacing: Consistent spacing between elements
Best Practices
- Appropriate Step Count: Use 3-7 steps for optimal user experience
- Consistent Sizing: Use consistent radius across similar interfaces
- Clear Labeling: Ensure step numbers are clearly visible
- Progress Feedback: Update
currentPosition
as users progress - Accessibility: Provide semantic labels for screen readers
- Responsive Design: Test on different screen sizes
Examples
Registration Wizard
class RegistrationWizard extends StatefulWidget {
_RegistrationWizardState createState() => _RegistrationWizardState();
}
class _RegistrationWizardState extends State<RegistrationWizard> {
int currentStep = 0;
final int totalSteps = 4;
final List<String> stepTitles = [
'Personal Info',
'Contact Details',
'Verification',
'Complete'
];
void _nextStep() {
if (currentStep < totalSteps - 1) {
setState(() => currentStep++);
}
}
void _previousStep() {
if (currentStep > 0) {
setState(() => currentStep--);
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Registration')),
body: Column(
children: [
Container(
padding: EdgeInsets.all(24),
child: Column(
children: [
NumberStepperAppi(
len: totalSteps,
currentPosition: currentStep,
radius: 20.0,
),
SizedBox(height: 16),
Text(
stepTitles[currentStep],
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
'Step ${currentStep + 1} of $totalSteps',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
Expanded(
child: _buildStepContent(),
),
Container(
padding: EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: currentStep > 0 ? _previousStep : null,
child: Text('Previous'),
),
ElevatedButton(
onPressed: currentStep < totalSteps - 1 ? _nextStep : _completeRegistration,
child: Text(currentStep < totalSteps - 1 ? 'Next' : 'Complete'),
),
],
),
),
],
),
);
}
Widget _buildStepContent() {
switch (currentStep) {
case 0:
return PersonalInfoStep();
case 1:
return ContactDetailsStep();
case 2:
return VerificationStep();
case 3:
return CompletionStep();
default:
return Container();
}
}
void _completeRegistration() {
// Handle registration completion
Navigator.of(context).pushReplacementNamed('/home');
}
}
Order Tracking
class OrderTrackingWidget extends StatelessWidget {
final Order order;
OrderTrackingWidget({required this.order});
Widget build(BuildContext context) {
final steps = [
'Order Placed',
'Processing',
'Shipped',
'Out for Delivery',
'Delivered'
];
final currentStep = _getCurrentStepIndex(order.status);
return Card(
margin: EdgeInsets.all(16),
child: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Order #${order.id}',
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 20),
NumberStepperAppi(
len: steps.length,
currentPosition: currentStep,
radius: 18.0,
),
SizedBox(height: 16),
Text(
steps[currentStep],
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
_getStepDescription(currentStep),
style: Theme.of(context).textTheme.bodyMedium,
),
if (order.estimatedDelivery != null) ...[
SizedBox(height: 12),
Text(
'Estimated Delivery: ${_formatDate(order.estimatedDelivery!)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
),
);
}
int _getCurrentStepIndex(OrderStatus status) {
switch (status) {
case OrderStatus.placed:
return 0;
case OrderStatus.processing:
return 1;
case OrderStatus.shipped:
return 2;
case OrderStatus.outForDelivery:
return 3;
case OrderStatus.delivered:
return 4;
default:
return 0;
}
}
String _getStepDescription(int step) {
switch (step) {
case 0:
return 'Your order has been received and confirmed';
case 1:
return 'We are preparing your items for shipment';
case 2:
return 'Your order is on its way to you';
case 3:
return 'Your order is out for delivery today';
case 4:
return 'Your order has been delivered successfully';
default:
return '';
}
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
}
Course Progress Tracker
class CourseProgressWidget extends StatelessWidget {
final Course course;
final int completedLessons;
CourseProgressWidget({
required this.course,
required this.completedLessons,
});
Widget build(BuildContext context) {
final totalLessons = course.lessons.length;
final progressSteps = (totalLessons / 5).ceil(); // Group lessons into 5 steps
final currentStep = (completedLessons / (totalLessons / progressSteps)).floor();
return Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade50, Colors.white],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
course.title,
style: Theme.of(context).textTheme.headlineSmall,
),
SizedBox(height: 8),
Text(
'$completedLessons of $totalLessons lessons completed',
style: Theme.of(context).textTheme.bodyMedium,
),
SizedBox(height: 20),
NumberStepperAppi(
len: progressSteps,
currentPosition: currentStep,
radius: 22.0,
),
SizedBox(height: 16),
LinearProgressIndicator(
value: completedLessons / totalLessons,
backgroundColor: Colors.grey.shade300,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
),
SizedBox(height: 8),
Text(
'${(completedLessons / totalLessons * 100).toInt()}% Complete',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}
Setup Wizard
class SetupWizard extends StatefulWidget {
_SetupWizardState createState() => _SetupWizardState();
}
class _SetupWizardState extends State<SetupWizard> {
int currentStep = 0;
final int totalSteps = 6;
final List<SetupStep> setupSteps = [
SetupStep('Welcome', 'Get started with your account'),
SetupStep('Profile', 'Set up your profile information'),
SetupStep('Preferences', 'Choose your preferences'),
SetupStep('Notifications', 'Configure notification settings'),
SetupStep('Security', 'Set up security options'),
SetupStep('Complete', 'Setup is complete!'),
];
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
children: [
Text(
'Account Setup',
style: Theme.of(context).textTheme.headlineMedium,
),
SizedBox(height: 24),
NumberStepperAppi(
len: totalSteps,
currentPosition: currentStep,
radius: 16.0,
),
SizedBox(height: 16),
Text(
setupSteps[currentStep].title,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
setupSteps[currentStep].description,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
],
),
),
Expanded(
child: Container(
width: double.infinity,
padding: EdgeInsets.all(24),
child: _buildStepContent(),
),
),
Container(
padding: EdgeInsets.all(24),
child: Row(
children: [
if (currentStep > 0)
Expanded(
child: OutlinedButton(
onPressed: () => setState(() => currentStep--),
child: Text('Back'),
),
),
if (currentStep > 0) SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: _handleNext,
child: Text(
currentStep < totalSteps - 1 ? 'Continue' : 'Finish',
),
),
),
],
),
),
],
),
),
);
}
Widget _buildStepContent() {
// Return different content based on current step
return Center(
child: Text(
'Step ${currentStep + 1} content goes here',
style: Theme.of(context).textTheme.bodyLarge,
),
);
}
void _handleNext() {
if (currentStep < totalSteps - 1) {
setState(() => currentStep++);
} else {
// Complete setup
Navigator.of(context).pushReplacementNamed('/home');
}
}
}
class SetupStep {
final String title;
final String description;
SetupStep(this.title, this.description);
}
Project Milestone Tracker
class ProjectMilestoneWidget extends StatelessWidget {
final Project project;
ProjectMilestoneWidget({required this.project});
Widget build(BuildContext context) {
final milestones = project.milestones;
final completedMilestones = milestones.where((m) => m.isCompleted).length;
return Card(
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
project.name,
style: Theme.of(context).textTheme.headlineSmall,
),
Chip(
label: Text('${completedMilestones}/${milestones.length}'),
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
),
],
),
SizedBox(height: 16),
NumberStepperAppi(
len: milestones.length,
currentPosition: completedMilestones,
radius: 20.0,
),
SizedBox(height: 16),
if (completedMilestones < milestones.length) ...[
Text(
'Next Milestone:',
style: Theme.of(context).textTheme.bodySmall,
),
Text(
milestones[completedMilestones].title,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
if (milestones[completedMilestones].dueDate != null)
Text(
'Due: ${_formatDate(milestones[completedMilestones].dueDate!)}',
style: Theme.of(context).textTheme.bodySmall,
),
] else ...[
Row(
children: [
Icon(Icons.check_circle, color: Colors.green),
SizedBox(width: 8),
Text(
'Project Completed!',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Colors.green,
fontWeight: FontWeight.bold,
),
),
],
),
],
],
),
),
);
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
}
See Also
- LinearProgressIndicatorAppi - For linear progress indicators
- CircleLoaderAppi - For circular progress indicators
- ButtonAppi - For navigation buttons in step processes