Skip to content

Commit 5dcf1d0

Browse files
rkishan516IvoneDjaja
authored andcommitted
fix: findChildIndexCallback to take seperators into account for seperated named constructor in ListView and SliverList (flutter#174491)
FindChildIndexCallback to take seperators into account for seperated named constructor in ListView and SliverList fixes: flutter#174261 ## Migration guide flutter/website#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.
1 parent 5851de8 commit 5dcf1d0

File tree

3 files changed

+173
-3
lines changed

3 files changed

+173
-3
lines changed

packages/flutter/lib/src/widgets/scroll_view.dart

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,20 @@ class ListView extends BoxScrollView {
14191419
///
14201420
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
14211421
///
1422+
/// {@template flutter.widgets.ListView.separated.findItemIndexCallback}
1423+
/// The [findItemIndexCallback] returns item indices (excluding separators),
1424+
/// unlike the deprecated [findChildIndexCallback] which returns child indices
1425+
/// (including both items and separators).
1426+
///
1427+
/// For example, in a list with 3 items and 2 separators:
1428+
/// * Item indices: 0, 1, 2
1429+
/// * Child indices: 0 (item), 1 (separator), 2 (item), 3 (separator), 4 (item)
1430+
///
1431+
/// This callback should be implemented if the order of items may change at a
1432+
/// later time. If null, reordering items may result in state-loss as widgets
1433+
/// may not map to their existing [RenderObject]s.
1434+
/// {@endtemplate}
1435+
///
14221436
/// {@tool snippet}
14231437
///
14241438
/// This example shows how to create [ListView] whose [ListTile] list items
@@ -1454,7 +1468,16 @@ class ListView extends BoxScrollView {
14541468
super.shrinkWrap,
14551469
super.padding,
14561470
required NullableIndexedWidgetBuilder itemBuilder,
1471+
@Deprecated(
1472+
'Use findItemIndexCallback instead. '
1473+
'findChildIndexCallback returns child indices (which include separators), '
1474+
'while findItemIndexCallback returns item indices (which do not). '
1475+
'If you were multiplying results by 2 to account for separators, '
1476+
'you can remove that workaround when migrating to findItemIndexCallback. '
1477+
'This feature was deprecated after v3.37.0-1.0.pre.',
1478+
)
14571479
ChildIndexGetter? findChildIndexCallback,
1480+
ChildIndexGetter? findItemIndexCallback,
14581481
required IndexedWidgetBuilder separatorBuilder,
14591482
required int itemCount,
14601483
bool addAutomaticKeepAlives = true,
@@ -1467,6 +1490,11 @@ class ListView extends BoxScrollView {
14671490
super.clipBehavior,
14681491
super.hitTestBehavior,
14691492
}) : assert(itemCount >= 0),
1493+
assert(
1494+
findItemIndexCallback == null || findChildIndexCallback == null,
1495+
'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
1496+
'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
1497+
),
14701498
itemExtent = null,
14711499
itemExtentBuilder = null,
14721500
prototypeItem = null,
@@ -1478,7 +1506,12 @@ class ListView extends BoxScrollView {
14781506
}
14791507
return separatorBuilder(context, itemIndex);
14801508
},
1481-
findChildIndexCallback: findChildIndexCallback,
1509+
findChildIndexCallback: findItemIndexCallback != null
1510+
? (Key key) {
1511+
final int? itemIndex = findItemIndexCallback(key);
1512+
return itemIndex == null ? null : itemIndex * 2;
1513+
}
1514+
: findChildIndexCallback,
14821515
childCount: _computeActualChildCount(itemCount),
14831516
addAutomaticKeepAlives: addAutomaticKeepAlives,
14841517
addRepaintBoundaries: addRepaintBoundaries,

packages/flutter/lib/src/widgets/sliver.dart

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ class SliverList extends SliverMultiBoxAdaptorWidget {
246246
///
247247
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
248248
///
249+
/// {@macro flutter.widgets.ListView.separated.findItemIndexCallback}
249250
///
250251
/// The `separatorBuilder` is similar to `itemBuilder`, except it is the widget
251252
/// that gets placed between itemBuilder(context, index) and itemBuilder(context, index + 1).
@@ -278,13 +279,27 @@ class SliverList extends SliverMultiBoxAdaptorWidget {
278279
SliverList.separated({
279280
super.key,
280281
required NullableIndexedWidgetBuilder itemBuilder,
282+
@Deprecated(
283+
'Use findItemIndexCallback instead. '
284+
'findChildIndexCallback returns child indices (which include separators), '
285+
'while findItemIndexCallback returns item indices (which do not). '
286+
'If you were multiplying results by 2 to account for separators, '
287+
'you can remove that workaround when migrating to findItemIndexCallback. '
288+
'This feature was deprecated after v3.37.0-1.0.pre.',
289+
)
281290
ChildIndexGetter? findChildIndexCallback,
291+
ChildIndexGetter? findItemIndexCallback,
282292
required NullableIndexedWidgetBuilder separatorBuilder,
283293
int? itemCount,
284294
bool addAutomaticKeepAlives = true,
285295
bool addRepaintBoundaries = true,
286296
bool addSemanticIndexes = true,
287-
}) : super(
297+
}) : assert(
298+
findItemIndexCallback == null || findChildIndexCallback == null,
299+
'Cannot provide both findItemIndexCallback and findChildIndexCallback. '
300+
'Use findItemIndexCallback as findChildIndexCallback is deprecated.',
301+
),
302+
super(
288303
delegate: SliverChildBuilderDelegate(
289304
(BuildContext context, int index) {
290305
final int itemIndex = index ~/ 2;
@@ -302,7 +317,12 @@ class SliverList extends SliverMultiBoxAdaptorWidget {
302317
}
303318
return widget;
304319
},
305-
findChildIndexCallback: findChildIndexCallback,
320+
findChildIndexCallback: findItemIndexCallback != null
321+
? (Key key) {
322+
final int? itemIndex = findItemIndexCallback(key);
323+
return itemIndex == null ? null : itemIndex * 2;
324+
}
325+
: findChildIndexCallback,
306326
childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1),
307327
addAutomaticKeepAlives: addAutomaticKeepAlives,
308328
addRepaintBoundaries: addRepaintBoundaries,

packages/flutter/test/widgets/scroll_view_test.dart

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,32 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:math';
6+
57
import 'package:flutter/gestures.dart' show DragStartBehavior;
68
import 'package:flutter/material.dart';
79
import 'package:flutter/services.dart' show LogicalKeyboardKey;
810
import 'package:flutter_test/flutter_test.dart';
911

1012
import 'states.dart';
1113

14+
class ItemWidget extends StatefulWidget {
15+
const ItemWidget({super.key, required this.value});
16+
final String value;
17+
18+
@override
19+
State<StatefulWidget> createState() => _ItemWidgetState();
20+
}
21+
22+
class _ItemWidgetState extends State<ItemWidget> {
23+
int randomInt = Random().nextInt(1000);
24+
25+
@override
26+
Widget build(BuildContext context) {
27+
return Text('${widget.value}: $randomInt');
28+
}
29+
}
30+
1231
class MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> {
1332
@override
1433
bool isSupported(Locale locale) => true;
@@ -1794,4 +1813,102 @@ void main() {
17941813

17951814
expect(tester.testTextInput.isVisible, isFalse);
17961815
});
1816+
1817+
testWidgets('ListView.separated findItemIndexCallback preserves state correctly', (
1818+
WidgetTester tester,
1819+
) async {
1820+
final List<String> items = <String>['A', 'B', 'C'];
1821+
1822+
Widget buildFrame(List<String> itemList) {
1823+
return MaterialApp(
1824+
home: Material(
1825+
child: ListView.separated(
1826+
itemCount: itemList.length,
1827+
findItemIndexCallback: (Key key) {
1828+
final ValueKey<String> valueKey = key as ValueKey<String>;
1829+
return itemList.indexOf(valueKey.value);
1830+
},
1831+
itemBuilder: (BuildContext context, int index) {
1832+
return ItemWidget(key: ValueKey<String>(itemList[index]), value: itemList[index]);
1833+
},
1834+
separatorBuilder: (BuildContext context, int index) => const Divider(),
1835+
),
1836+
),
1837+
);
1838+
}
1839+
1840+
// Build initial frame
1841+
await tester.pumpWidget(buildFrame(items));
1842+
1843+
final Finder texts = find.byType(Text);
1844+
expect(texts, findsNWidgets(3));
1845+
1846+
// Store all text in list
1847+
final List<String?> textValues = List<String?>.generate(3, (int index) {
1848+
return (tester.widget(texts.at(index)) as Text).data;
1849+
});
1850+
1851+
await tester.pumpWidget(buildFrame(items));
1852+
await tester.pump();
1853+
1854+
final Finder updatedTexts = find.byType(Text);
1855+
expect(updatedTexts, findsNWidgets(3));
1856+
1857+
final List<String?> updatedTextValues = List<String?>.generate(3, (int index) {
1858+
return (tester.widget(updatedTexts.at(index)) as Text).data;
1859+
});
1860+
1861+
expect(textValues, updatedTextValues);
1862+
});
1863+
1864+
testWidgets('SliverList.separated findItemIndexCallback preserves state correctly', (
1865+
WidgetTester tester,
1866+
) async {
1867+
final List<String> items = <String>['A', 'B', 'C'];
1868+
1869+
Widget buildFrame(List<String> itemList) {
1870+
return MaterialApp(
1871+
home: Material(
1872+
child: CustomScrollView(
1873+
slivers: <Widget>[
1874+
SliverList.separated(
1875+
itemCount: itemList.length,
1876+
findItemIndexCallback: (Key key) {
1877+
final ValueKey<String> valueKey = key as ValueKey<String>;
1878+
return itemList.indexOf(valueKey.value);
1879+
},
1880+
itemBuilder: (BuildContext context, int index) {
1881+
return ItemWidget(key: ValueKey<String>(itemList[index]), value: itemList[index]);
1882+
},
1883+
separatorBuilder: (BuildContext context, int index) => const Divider(),
1884+
),
1885+
],
1886+
),
1887+
),
1888+
);
1889+
}
1890+
1891+
// Build initial frame
1892+
await tester.pumpWidget(buildFrame(items));
1893+
1894+
final Finder texts = find.byType(Text);
1895+
expect(texts, findsNWidgets(3));
1896+
1897+
// Store all text in list
1898+
final List<String?> textValues = List<String?>.generate(3, (int index) {
1899+
return (tester.widget(texts.at(index)) as Text).data;
1900+
});
1901+
1902+
await tester.pumpWidget(buildFrame(items));
1903+
await tester.pump();
1904+
1905+
final Finder updatedTexts = find.byType(Text);
1906+
expect(updatedTexts, findsNWidgets(3));
1907+
1908+
final List<String?> updatedTextValues = List<String?>.generate(3, (int index) {
1909+
return (tester.widget(updatedTexts.at(index)) as Text).data;
1910+
});
1911+
1912+
expect(textValues, updatedTextValues);
1913+
});
17971914
}

0 commit comments

Comments
 (0)