fix: findChildIndexCallback to take seperators into account for seperated named constructor in ListView and SliverList (#174491)

FindChildIndexCallback to take seperators into account for seperated
named constructor in ListView and SliverList
fixes: #174261

## Migration guide

https://github.com/flutter/website/pull/12636

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart
index 7786e3e..d37cb17 100644
--- a/packages/flutter/lib/src/widgets/scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/scroll_view.dart
@@ -1419,6 +1419,20 @@
   ///
   /// {@macro flutter.widgets.PageView.findChildIndexCallback}
   ///
+  /// {@template flutter.widgets.ListView.separated.findItemIndexCallback}
+  /// The [findItemIndexCallback] returns item indices (excluding separators),
+  /// unlike the deprecated [findChildIndexCallback] which returns child indices
+  /// (including both items and separators).
+  ///
+  /// For example, in a list with 3 items and 2 separators:
+  /// * Item indices: 0, 1, 2
+  /// * Child indices: 0 (item), 1 (separator), 2 (item), 3 (separator), 4 (item)
+  ///
+  /// This callback should be implemented if the order of items may change at a
+  /// later time. If null, reordering items may result in state-loss as widgets
+  /// may not map to their existing [RenderObject]s.
+  /// {@endtemplate}
+  ///
   /// {@tool snippet}
   ///
   /// This example shows how to create [ListView] whose [ListTile] list items
@@ -1454,7 +1468,16 @@
     super.shrinkWrap,
     super.padding,
     required NullableIndexedWidgetBuilder itemBuilder,
+    @Deprecated(
+      'Use findItemIndexCallback instead. '
+      'findChildIndexCallback returns child indices (which include separators), '
+      'while findItemIndexCallback returns item indices (which do not). '
+      'If you were multiplying results by 2 to account for separators, '
+      'you can remove that workaround when migrating to findItemIndexCallback. '
+      'This feature was deprecated after v3.37.0-1.0.pre.',
+    )
     ChildIndexGetter? findChildIndexCallback,
+    ChildIndexGetter? findItemIndexCallback,
     required IndexedWidgetBuilder separatorBuilder,
     required int itemCount,
     bool addAutomaticKeepAlives = true,
@@ -1467,6 +1490,11 @@
     super.clipBehavior,
     super.hitTestBehavior,
   }) : assert(itemCount >= 0),
+       assert(
+         findItemIndexCallback == null || findChildIndexCallback == null,
+         'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
+         'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
+       ),
        itemExtent = null,
        itemExtentBuilder = null,
        prototypeItem = null,
@@ -1478,7 +1506,12 @@
            }
            return separatorBuilder(context, itemIndex);
          },
-         findChildIndexCallback: findChildIndexCallback,
+         findChildIndexCallback: findItemIndexCallback != null
+             ? (Key key) {
+                 final int? itemIndex = findItemIndexCallback(key);
+                 return itemIndex == null ? null : itemIndex * 2;
+               }
+             : findChildIndexCallback,
          childCount: _computeActualChildCount(itemCount),
          addAutomaticKeepAlives: addAutomaticKeepAlives,
          addRepaintBoundaries: addRepaintBoundaries,
diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart
index 3fd4cd0..3412e89 100644
--- a/packages/flutter/lib/src/widgets/sliver.dart
+++ b/packages/flutter/lib/src/widgets/sliver.dart
@@ -246,6 +246,7 @@
   ///
   /// {@macro flutter.widgets.PageView.findChildIndexCallback}
   ///
+  /// {@macro flutter.widgets.ListView.separated.findItemIndexCallback}
   ///
   /// The `separatorBuilder` is similar to `itemBuilder`, except it is the widget
   /// that gets placed between itemBuilder(context, index) and itemBuilder(context, index + 1).
@@ -278,13 +279,27 @@
   SliverList.separated({
     super.key,
     required NullableIndexedWidgetBuilder itemBuilder,
+    @Deprecated(
+      'Use findItemIndexCallback instead. '
+      'findChildIndexCallback returns child indices (which include separators), '
+      'while findItemIndexCallback returns item indices (which do not). '
+      'If you were multiplying results by 2 to account for separators, '
+      'you can remove that workaround when migrating to findItemIndexCallback. '
+      'This feature was deprecated after v3.37.0-1.0.pre.',
+    )
     ChildIndexGetter? findChildIndexCallback,
+    ChildIndexGetter? findItemIndexCallback,
     required NullableIndexedWidgetBuilder separatorBuilder,
     int? itemCount,
     bool addAutomaticKeepAlives = true,
     bool addRepaintBoundaries = true,
     bool addSemanticIndexes = true,
-  }) : super(
+  }) : assert(
+         findItemIndexCallback == null || findChildIndexCallback == null,
+         'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
+         'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
+       ),
+       super(
          delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              final int itemIndex = index ~/ 2;
@@ -302,7 +317,12 @@
              }
              return widget;
            },
-           findChildIndexCallback: findChildIndexCallback,
+           findChildIndexCallback: findItemIndexCallback != null
+               ? (Key key) {
+                   final int? itemIndex = findItemIndexCallback(key);
+                   return itemIndex == null ? null : itemIndex * 2;
+                 }
+               : findChildIndexCallback,
            childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1),
            addAutomaticKeepAlives: addAutomaticKeepAlives,
            addRepaintBoundaries: addRepaintBoundaries,
diff --git a/packages/flutter/test/widgets/scroll_view_test.dart b/packages/flutter/test/widgets/scroll_view_test.dart
index ea27384..0dfd43c3 100644
--- a/packages/flutter/test/widgets/scroll_view_test.dart
+++ b/packages/flutter/test/widgets/scroll_view_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:math';
+
 import 'package:flutter/gestures.dart' show DragStartBehavior;
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart' show LogicalKeyboardKey;
@@ -9,6 +11,23 @@
 
 import 'states.dart';
 
+class ItemWidget extends StatefulWidget {
+  const ItemWidget({super.key, required this.value});
+  final String value;
+
+  @override
+  State<StatefulWidget> createState() => _ItemWidgetState();
+}
+
+class _ItemWidgetState extends State<ItemWidget> {
+  int randomInt = Random().nextInt(1000);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text('${widget.value}: $randomInt');
+  }
+}
+
 class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
   @override
   bool isSupported(Locale locale) => true;
@@ -1794,4 +1813,102 @@
 
     expect(tester.testTextInput.isVisible, isFalse);
   });
+
+  testWidgets('ListView.separated findItemIndexCallback preserves state correctly', (
+    WidgetTester tester,
+  ) async {
+    final List<String> items = <String>['A', 'B', 'C'];
+
+    Widget buildFrame(List<String> itemList) {
+      return MaterialApp(
+        home: Material(
+          child: ListView.separated(
+            itemCount: itemList.length,
+            findItemIndexCallback: (Key key) {
+              final ValueKey<String> valueKey = key as ValueKey<String>;
+              return itemList.indexOf(valueKey.value);
+            },
+            itemBuilder: (BuildContext context, int index) {
+              return ItemWidget(key: ValueKey<String>(itemList[index]), value: itemList[index]);
+            },
+            separatorBuilder: (BuildContext context, int index) => const Divider(),
+          ),
+        ),
+      );
+    }
+
+    // Build initial frame
+    await tester.pumpWidget(buildFrame(items));
+
+    final Finder texts = find.byType(Text);
+    expect(texts, findsNWidgets(3));
+
+    // Store all text in list
+    final List<String?> textValues = List<String?>.generate(3, (int index) {
+      return (tester.widget(texts.at(index)) as Text).data;
+    });
+
+    await tester.pumpWidget(buildFrame(items));
+    await tester.pump();
+
+    final Finder updatedTexts = find.byType(Text);
+    expect(updatedTexts, findsNWidgets(3));
+
+    final List<String?> updatedTextValues = List<String?>.generate(3, (int index) {
+      return (tester.widget(updatedTexts.at(index)) as Text).data;
+    });
+
+    expect(textValues, updatedTextValues);
+  });
+
+  testWidgets('SliverList.separated findItemIndexCallback preserves state correctly', (
+    WidgetTester tester,
+  ) async {
+    final List<String> items = <String>['A', 'B', 'C'];
+
+    Widget buildFrame(List<String> itemList) {
+      return MaterialApp(
+        home: Material(
+          child: CustomScrollView(
+            slivers: <Widget>[
+              SliverList.separated(
+                itemCount: itemList.length,
+                findItemIndexCallback: (Key key) {
+                  final ValueKey<String> valueKey = key as ValueKey<String>;
+                  return itemList.indexOf(valueKey.value);
+                },
+                itemBuilder: (BuildContext context, int index) {
+                  return ItemWidget(key: ValueKey<String>(itemList[index]), value: itemList[index]);
+                },
+                separatorBuilder: (BuildContext context, int index) => const Divider(),
+              ),
+            ],
+          ),
+        ),
+      );
+    }
+
+    // Build initial frame
+    await tester.pumpWidget(buildFrame(items));
+
+    final Finder texts = find.byType(Text);
+    expect(texts, findsNWidgets(3));
+
+    // Store all text in list
+    final List<String?> textValues = List<String?>.generate(3, (int index) {
+      return (tester.widget(texts.at(index)) as Text).data;
+    });
+
+    await tester.pumpWidget(buildFrame(items));
+    await tester.pump();
+
+    final Finder updatedTexts = find.byType(Text);
+    expect(updatedTexts, findsNWidgets(3));
+
+    final List<String?> updatedTextValues = List<String?>.generate(3, (int index) {
+      return (tester.widget(updatedTexts.at(index)) as Text).data;
+    });
+
+    expect(textValues, updatedTextValues);
+  });
 }