mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Fixes some widgets(ListView.builder
, GridView.builder
etc.) state-lose issue (#100547)
* ++ * ++ * ++ * ++ * codereview feedback * ++
This commit is contained in:
parent
c920ac2bc4
commit
31191dd29a
@ -339,6 +339,7 @@ class SliverAnimatedList extends StatefulWidget {
|
||||
const SliverAnimatedList({
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.findChildIndexCallback,
|
||||
this.initialItemCount = 0,
|
||||
}) : assert(itemBuilder != null),
|
||||
assert(initialItemCount != null && initialItemCount >= 0),
|
||||
@ -359,6 +360,9 @@ class SliverAnimatedList extends StatefulWidget {
|
||||
/// [SliverAnimatedListState.removeItem] removes an item immediately.
|
||||
final AnimatedListItemBuilder itemBuilder;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
||||
final ChildIndexGetter? findChildIndexCallback;
|
||||
|
||||
/// {@macro flutter.widgets.animatedList.initialItemCount}
|
||||
final int initialItemCount;
|
||||
|
||||
@ -507,7 +511,11 @@ class SliverAnimatedListState extends State<SliverAnimatedList> with TickerProvi
|
||||
}
|
||||
|
||||
SliverChildDelegate _createDelegate() {
|
||||
return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount);
|
||||
return SliverChildBuilderDelegate(
|
||||
_itemBuilder,
|
||||
childCount: _itemsCount,
|
||||
findChildIndexCallback: widget.findChildIndexCallback,
|
||||
);
|
||||
}
|
||||
|
||||
/// Insert an item at [index] and start an animation that will be passed to
|
||||
|
@ -665,9 +665,14 @@ class PageView extends StatefulWidget {
|
||||
/// [itemBuilder] will be called only with indices greater than or equal to
|
||||
/// zero and less than [itemCount].
|
||||
///
|
||||
/// [PageView.builder] by default does not support child reordering. If
|
||||
/// you are planning to change child order at a later time, consider using
|
||||
/// [PageView] or [PageView.custom].
|
||||
/// {@template flutter.widgets.PageView.findChildIndexCallback}
|
||||
/// The [findChildIndexCallback] corresponds to the
|
||||
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
|
||||
/// a child widget may not map to its existing [RenderObject] when the order
|
||||
/// of children returned from the children builder changes.
|
||||
/// This may result in state-loss. This callback needs to be implemented if
|
||||
/// the order of the children may change at a later time.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.allowImplicitScrolling}
|
||||
PageView.builder({
|
||||
@ -679,6 +684,7 @@ class PageView extends StatefulWidget {
|
||||
this.pageSnapping = true,
|
||||
this.onPageChanged,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
int? itemCount,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.allowImplicitScrolling = false,
|
||||
@ -689,7 +695,11 @@ class PageView extends StatefulWidget {
|
||||
}) : assert(allowImplicitScrolling != null),
|
||||
assert(clipBehavior != null),
|
||||
controller = controller ?? _defaultPageController,
|
||||
childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a scrollable list that works page by page with a custom child
|
||||
|
@ -430,6 +430,7 @@ class SliverReorderableList extends StatefulWidget {
|
||||
const SliverReorderableList({
|
||||
Key? key,
|
||||
required this.itemBuilder,
|
||||
this.findChildIndexCallback,
|
||||
required this.itemCount,
|
||||
required this.onReorder,
|
||||
this.onReorderStart,
|
||||
@ -447,6 +448,9 @@ class SliverReorderableList extends StatefulWidget {
|
||||
/// {@macro flutter.widgets.reorderable_list.itemBuilder}
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
|
||||
/// {@macro flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
||||
final ChildIndexGetter? findChildIndexCallback;
|
||||
|
||||
/// {@macro flutter.widgets.reorderable_list.itemCount}
|
||||
final int itemCount;
|
||||
|
||||
@ -908,6 +912,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
|
||||
// by a zero height SizedBox, so that the gap can move around. To make the
|
||||
// list extent stable we add a dummy entry to the end.
|
||||
childCount: widget.itemCount + (_dragInfo != null ? 1 : 0),
|
||||
findChildIndexCallback: widget.findChildIndexCallback,
|
||||
);
|
||||
if (widget.itemExtent != null) {
|
||||
return SliverFixedExtentList(
|
||||
|
@ -1126,6 +1126,8 @@ class ListView extends BoxScrollView {
|
||||
/// efficient, however, is to create the instances on demand using this
|
||||
/// constructor's `itemBuilder` callback.
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
|
||||
///
|
||||
/// The `addAutomaticKeepAlives` argument corresponds to the
|
||||
/// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
|
||||
/// `addRepaintBoundaries` argument corresponds to the
|
||||
@ -1133,10 +1135,6 @@ class ListView extends BoxScrollView {
|
||||
/// `addSemanticIndexes` argument corresponds to the
|
||||
/// [SliverChildBuilderDelegate.addSemanticIndexes] property. None may be
|
||||
/// null.
|
||||
///
|
||||
/// [ListView.builder] by default does not support child reordering. If
|
||||
/// you are planning to change child order at a later time, consider using
|
||||
/// [ListView] or [ListView.custom].
|
||||
ListView.builder({
|
||||
Key? key,
|
||||
Axis scrollDirection = Axis.vertical,
|
||||
@ -1149,6 +1147,7 @@ class ListView extends BoxScrollView {
|
||||
this.itemExtent,
|
||||
this.prototypeItem,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
int? itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
@ -1167,6 +1166,7 @@ class ListView extends BoxScrollView {
|
||||
),
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
@ -1211,6 +1211,8 @@ class ListView extends BoxScrollView {
|
||||
/// view's children are created in advance, or all at once when the [ListView]
|
||||
/// itself is created, it is more efficient to use the [ListView] constructor.
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// This example shows how to create [ListView] whose [ListTile] list items
|
||||
@ -1246,6 +1248,7 @@ class ListView extends BoxScrollView {
|
||||
bool shrinkWrap = false,
|
||||
EdgeInsetsGeometry? padding,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
required IndexedWidgetBuilder separatorBuilder,
|
||||
required int itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
@ -1278,6 +1281,7 @@ class ListView extends BoxScrollView {
|
||||
}
|
||||
return widget;
|
||||
},
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: _computeActualChildCount(itemCount),
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
@ -1797,6 +1801,8 @@ class GridView extends BoxScrollView {
|
||||
/// `itemBuilder` will be called only with indices greater than or equal to
|
||||
/// zero and less than `itemCount`.
|
||||
///
|
||||
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
|
||||
///
|
||||
/// The [gridDelegate] argument must not be null.
|
||||
///
|
||||
/// The `addAutomaticKeepAlives` argument corresponds to the
|
||||
@ -1815,6 +1821,7 @@ class GridView extends BoxScrollView {
|
||||
EdgeInsetsGeometry? padding,
|
||||
required this.gridDelegate,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
ChildIndexGetter? findChildIndexCallback,
|
||||
int? itemCount,
|
||||
bool addAutomaticKeepAlives = true,
|
||||
bool addRepaintBoundaries = true,
|
||||
@ -1828,6 +1835,7 @@ class GridView extends BoxScrollView {
|
||||
}) : assert(gridDelegate != null),
|
||||
childrenDelegate = SliverChildBuilderDelegate(
|
||||
itemBuilder,
|
||||
findChildIndexCallback: findChildIndexCallback,
|
||||
childCount: itemCount,
|
||||
addAutomaticKeepAlives: addAutomaticKeepAlives,
|
||||
addRepaintBoundaries: addRepaintBoundaries,
|
||||
|
@ -194,6 +194,10 @@ abstract class SliverChildDelegate {
|
||||
/// This will be called during `performRebuild` in [SliverMultiBoxAdaptorElement]
|
||||
/// to check if a child has moved to a different position. It should return the
|
||||
/// index of the child element with associated key, null if not found.
|
||||
///
|
||||
/// If not provided, a child widget may not map to its existing [RenderObject]
|
||||
/// when the order of children returned from the children builder changes.
|
||||
/// This may result in state-loss.
|
||||
int? findIndexByKey(Key key) => null;
|
||||
|
||||
@override
|
||||
@ -429,14 +433,16 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
|
||||
/// Defaults to providing an index for each widget.
|
||||
final SemanticIndexCallback semanticIndexCallback;
|
||||
|
||||
/// {@template flutter.widgets.SliverChildBuilderDelegate.findChildIndexCallback}
|
||||
/// Called to find the new index of a child based on its key in case of reordering.
|
||||
///
|
||||
/// If not provided, a child widget may not map to its existing [RenderObject]
|
||||
/// when the order in which children are returned from [builder] changes.
|
||||
/// when the order of children returned from the children builder changes.
|
||||
/// This may result in state-loss.
|
||||
///
|
||||
/// This callback should take an input [Key], and it should return the
|
||||
/// index of the child element with that associated key, or null if not found.
|
||||
/// {@endtemplate}
|
||||
final ChildIndexGetter? findChildIndexCallback;
|
||||
|
||||
@override
|
||||
|
@ -7,6 +7,46 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('SliverAnimatedList.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverAnimatedList(
|
||||
initialItemCount: itemCount,
|
||||
itemBuilder: (BuildContext context, int index, Animation<double> animation) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
testWidgets('AnimatedList', (WidgetTester tester) async {
|
||||
Widget builder(BuildContext context, int index, Animation<double> animation) {
|
||||
return SizedBox(
|
||||
|
@ -12,6 +12,45 @@ import '../rendering/rendering_tester.dart' show TestClipPaintingContext;
|
||||
import 'states.dart';
|
||||
|
||||
void main() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('GridView.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return GridView.builder(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext _, int index) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
testWidgets('Empty GridView', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
|
@ -76,6 +76,79 @@ class _StatefulListViewState extends State<_StatefulListView> {
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('ListView.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return ListView.builder(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext _, int index) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('ListView.separator respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return ListView.separated(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext _, int index) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
separatorBuilder: (BuildContext _, int __) => const Divider(),
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
testWidgets('ListView default control', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
|
@ -12,6 +12,42 @@ import 'semantics_tester.dart';
|
||||
import 'states.dart';
|
||||
|
||||
void main() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('PageView.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return PageView.builder(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext _, int index) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
testWidgets('PageView resize from zero-size viewport should not lose state', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/88956
|
||||
final PageController controller = PageController(
|
||||
|
@ -7,6 +7,46 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/100451
|
||||
testWidgets('SliverReorderableList.builder respects findChildIndexCallback', (WidgetTester tester) async {
|
||||
bool finderCalled = false;
|
||||
int itemCount = 7;
|
||||
late StateSetter stateSetter;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
stateSetter = setState;
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverReorderableList(
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (BuildContext _, int index) => Container(
|
||||
key: Key('$index'),
|
||||
height: 2000.0,
|
||||
),
|
||||
findChildIndexCallback: (Key key) {
|
||||
finderCalled = true;
|
||||
return null;
|
||||
},
|
||||
onReorder: (int oldIndex, int newIndex) { },
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
);
|
||||
expect(finderCalled, false);
|
||||
|
||||
// Trigger update.
|
||||
stateSetter(() => itemCount = 77);
|
||||
await tester.pump();
|
||||
|
||||
expect(finderCalled, true);
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/88191
|
||||
testWidgets('Do not crash when dragging with two fingers simultaneously', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(3, (int index) => index);
|
||||
|
Loading…
Reference in New Issue
Block a user