[Stepper] adds stepIconBuilder property (#122816)

[Stepper] adds stepIconBuilder property
diff --git a/packages/flutter/lib/src/material/stepper.dart b/packages/flutter/lib/src/material/stepper.dart
index 4348a18..8978fe6 100644
--- a/packages/flutter/lib/src/material/stepper.dart
+++ b/packages/flutter/lib/src/material/stepper.dart
@@ -106,6 +106,10 @@
 ///  * [WidgetBuilder], which is similar but only takes a [BuildContext].
 typedef ControlsWidgetBuilder = Widget Function(BuildContext context, ControlsDetails details);
 
+/// A builder that creates the icon widget for the [Step] at [stepIndex], given
+/// [stepState].
+typedef StepIconBuilder = Widget? Function(int stepIndex, StepState stepState);
+
 const TextStyle _kStepStyle = TextStyle(
   fontSize: 12.0,
   color: Colors.white,
@@ -207,6 +211,7 @@
     this.controlsBuilder,
     this.elevation,
     this.margin,
+    this.stepIconBuilder,
   }) : assert(0 <= currentStep && currentStep < steps.length);
 
   /// The steps of the stepper whose titles, subtitles, icons always get shown.
@@ -303,9 +308,17 @@
   /// The elevation of this stepper's [Material] when [type] is [StepperType.horizontal].
   final double? elevation;
 
-  /// custom margin on vertical stepper.
+  /// Custom margin on vertical stepper.
   final EdgeInsetsGeometry? margin;
 
+  /// Callback for creating custom icons for the [steps].
+  ///
+  /// When overriding icon for [StepState.error], please return
+  /// a widget whose width and height are 14 pixels or less to avoid overflow.
+  ///
+  /// If null, the default icons will be used for respective [StepState].
+  final StepIconBuilder? stepIconBuilder;
+
   @override
   State<Stepper> createState() => _StepperState();
 }
@@ -373,6 +386,10 @@
   Widget _buildCircleChild(int index, bool oldState) {
     final StepState state = oldState ? _oldStates[index]! : widget.steps[index].state;
     final bool isDarkActive = _isDark() && widget.steps[index].isActive;
+    final Widget? icon = widget.stepIconBuilder?.call(index, state);
+    if (icon != null) {
+      return icon;
+    }
     switch (state) {
       case StepState.indexed:
       case StepState.disabled:
diff --git a/packages/flutter/test/material/stepper_test.dart b/packages/flutter/test/material/stepper_test.dart
index e1472be..03f2c5c 100644
--- a/packages/flutter/test/material/stepper_test.dart
+++ b/packages/flutter/test/material/stepper_test.dart
@@ -1259,6 +1259,48 @@
         tester.widget<Text>(find.text('Label ${index + 2}'));
     expect(bodyMediumStyle, nextLabelTextWidget.style);
   });
+
+  testWidgets('Stepper stepIconBuilder test', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      MaterialApp(
+        home: Material(
+          child: Stepper(
+            stepIconBuilder: (int index, StepState state) {
+              if (state == StepState.complete) {
+                return const FlutterLogo(size: 18);
+              }
+              return null;
+            },
+            steps: const <Step>[
+              Step(
+                title: Text('A'),
+                state: StepState.complete,
+                content: SizedBox(width: 100.0, height: 100.0),
+              ),
+              Step(
+                title: Text('B'),
+                state: StepState.editing,
+                content: SizedBox(width: 100.0, height: 100.0),
+              ),
+              Step(
+                title: Text('C'),
+                state: StepState.error,
+                content: SizedBox(width: 100.0, height: 100.0),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+
+    /// Finds the overridden widget for StepState.complete
+    expect(find.byType(FlutterLogo), findsOneWidget);
+
+    /// StepState.editing and StepState.error should have a default icon
+    expect(find.byIcon(Icons.edit), findsOneWidget);
+    expect(find.text('!'), findsOneWidget);
+  });
+
 }
 
 class _TappableColorWidget extends StatefulWidget {