Skip to main content

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

  1. Appropriate Step Count: Use 3-7 steps for optimal user experience
  2. Consistent Sizing: Use consistent radius across similar interfaces
  3. Clear Labeling: Ensure step numbers are clearly visible
  4. Progress Feedback: Update currentPosition as users progress
  5. Accessibility: Provide semantic labels for screen readers
  6. 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