Skip to content

Commit 6f3168c

Browse files
authored
Re-add the ability to return null in ListView.builder (#108706)
1 parent 6157c0e commit 6f3168c

File tree

3 files changed

+108
-17
lines changed

3 files changed

+108
-17
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,8 @@ class PageView extends StatefulWidget {
675675
/// [itemBuilder] will be called only with indices greater than or equal to
676676
/// zero and less than [itemCount].
677677
///
678+
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
679+
///
678680
/// {@template flutter.widgets.PageView.findChildIndexCallback}
679681
/// The [findChildIndexCallback] corresponds to the
680682
/// [SliverChildBuilderDelegate.findChildIndexCallback] property. If null,
@@ -693,7 +695,7 @@ class PageView extends StatefulWidget {
693695
this.physics,
694696
this.pageSnapping = true,
695697
this.onPageChanged,
696-
required IndexedWidgetBuilder itemBuilder,
698+
required NullableIndexedWidgetBuilder itemBuilder,
697699
ChildIndexGetter? findChildIndexCallback,
698700
int? itemCount,
699701
this.dragStartBehavior = DragStartBehavior.start,

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

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,13 +1114,22 @@ class ListView extends BoxScrollView {
11141114
/// The `itemBuilder` callback will be called only with indices greater than
11151115
/// or equal to zero and less than `itemCount`.
11161116
///
1117-
/// The `itemBuilder` should always return a non-null widget, and actually
1118-
/// create the widget instances when called. Avoid using a builder that
1119-
/// returns a previously-constructed widget; if the list view's children are
1120-
/// created in advance, or all at once when the [ListView] itself is created,
1121-
/// it is more efficient to use the [ListView] constructor. Even more
1122-
/// efficient, however, is to create the instances on demand using this
1123-
/// constructor's `itemBuilder` callback.
1117+
/// {@template flutter.widgets.ListView.builder.itemBuilder}
1118+
/// It is legal for `itemBuilder` to return `null`. If it does, the scroll view
1119+
/// will stop calling `itemBuilder`, even if it has yet to reach `itemCount`.
1120+
/// By returning `null`, the [ScrollPosition.maxScrollExtent] will not be accurate
1121+
/// unless the user has reached the end of the [ScrollView]. This can also cause the
1122+
/// [Scrollbar] to grow as the user scrolls.
1123+
///
1124+
/// For more accurate [ScrollMetrics], consider specifying `itemCount`.
1125+
/// {@endtemplate}
1126+
///
1127+
/// The `itemBuilder` should always create the widget instances when called.
1128+
/// Avoid using a builder that returns a previously-constructed widget; if the
1129+
/// list view's children are created in advance, or all at once when the
1130+
/// [ListView] itself is created, it is more efficient to use the [ListView]
1131+
/// constructor. Even more efficient, however, is to create the instances on
1132+
/// demand using this constructor's `itemBuilder` callback.
11241133
///
11251134
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
11261135
///
@@ -1142,7 +1151,7 @@ class ListView extends BoxScrollView {
11421151
super.padding,
11431152
this.itemExtent,
11441153
this.prototypeItem,
1145-
required IndexedWidgetBuilder itemBuilder,
1154+
required NullableIndexedWidgetBuilder itemBuilder,
11461155
ChildIndexGetter? findChildIndexCallback,
11471156
int? itemCount,
11481157
bool addAutomaticKeepAlives = true,
@@ -1188,11 +1197,13 @@ class ListView extends BoxScrollView {
11881197
/// The `separatorBuilder` callback will be called with indices greater than
11891198
/// or equal to zero and less than `itemCount - 1`.
11901199
///
1191-
/// The `itemBuilder` and `separatorBuilder` callbacks should always return a
1192-
/// non-null widget, and actually create widget instances when called. Avoid
1193-
/// using a builder that returns a previously-constructed widget; if the list
1194-
/// view's children are created in advance, or all at once when the [ListView]
1195-
/// itself is created, it is more efficient to use the [ListView] constructor.
1200+
/// The `itemBuilder` and `separatorBuilder` callbacks should always
1201+
/// actually create widget instances when called. Avoid using a builder that
1202+
/// returns a previously-constructed widget; if the list view's children are
1203+
/// created in advance, or all at once when the [ListView] itself is created,
1204+
/// it is more efficient to use the [ListView] constructor.
1205+
///
1206+
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
11961207
///
11971208
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
11981209
///
@@ -1230,7 +1241,7 @@ class ListView extends BoxScrollView {
12301241
super.physics,
12311242
super.shrinkWrap,
12321243
super.padding,
1233-
required IndexedWidgetBuilder itemBuilder,
1244+
required NullableIndexedWidgetBuilder itemBuilder,
12341245
ChildIndexGetter? findChildIndexCallback,
12351246
required IndexedWidgetBuilder separatorBuilder,
12361247
required int itemCount,
@@ -1250,7 +1261,7 @@ class ListView extends BoxScrollView {
12501261
childrenDelegate = SliverChildBuilderDelegate(
12511262
(BuildContext context, int index) {
12521263
final int itemIndex = index ~/ 2;
1253-
final Widget widget;
1264+
final Widget? widget;
12541265
if (index.isEven) {
12551266
widget = itemBuilder(context, itemIndex);
12561267
} else {
@@ -1742,6 +1753,8 @@ class GridView extends BoxScrollView {
17421753
/// `itemBuilder` will be called only with indices greater than or equal to
17431754
/// zero and less than `itemCount`.
17441755
///
1756+
/// {@macro flutter.widgets.ListView.builder.itemBuilder}
1757+
///
17451758
/// {@macro flutter.widgets.PageView.findChildIndexCallback}
17461759
///
17471760
/// The [gridDelegate] argument must not be null.
@@ -1761,7 +1774,7 @@ class GridView extends BoxScrollView {
17611774
super.shrinkWrap,
17621775
super.padding,
17631776
required this.gridDelegate,
1764-
required IndexedWidgetBuilder itemBuilder,
1777+
required NullableIndexedWidgetBuilder itemBuilder,
17651778
ChildIndexGetter? findChildIndexCallback,
17661779
int? itemCount,
17671780
bool addAutomaticKeepAlives = true,

packages/flutter/test/widgets/scroll_view_test.dart

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,82 @@ void main() {
143143
expect(textField.focusNode!.hasFocus, isFalse);
144144
});
145145

146+
testWidgets('GridView.builder supports null items', (WidgetTester tester) async {
147+
await tester.pumpWidget(textFieldBoilerplate(
148+
child: GridView.builder(
149+
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
150+
crossAxisCount: 42,
151+
),
152+
itemCount: 42,
153+
itemBuilder: (BuildContext context, int index) {
154+
if (index == 5) {
155+
return null;
156+
}
157+
158+
return const Text('item');
159+
},
160+
),
161+
));
162+
163+
expect(find.text('item'), findsNWidgets(5));
164+
});
165+
166+
testWidgets('ListView.builder supports null items', (WidgetTester tester) async {
167+
await tester.pumpWidget(textFieldBoilerplate(
168+
child: ListView.builder(
169+
itemCount: 42,
170+
itemBuilder: (BuildContext context, int index) {
171+
if (index == 5) {
172+
return null;
173+
}
174+
175+
return const Text('item');
176+
},
177+
),
178+
));
179+
180+
expect(find.text('item'), findsNWidgets(5));
181+
});
182+
183+
testWidgets('PageView supports null items in itemBuilder', (WidgetTester tester) async {
184+
await tester.pumpWidget(textFieldBoilerplate(
185+
child: PageView.builder(
186+
itemCount: 5,
187+
controller: PageController(viewportFraction: 1/5),
188+
itemBuilder: (BuildContext context, int index) {
189+
if (index == 2) {
190+
return null;
191+
}
192+
193+
return const Text('item');
194+
},
195+
),
196+
));
197+
198+
expect(find.text('item'), findsNWidgets(2));
199+
});
200+
201+
testWidgets('ListView.separated supports null items in itemBuilder', (WidgetTester tester) async {
202+
await tester.pumpWidget(textFieldBoilerplate(
203+
child: ListView.separated(
204+
itemCount: 42,
205+
separatorBuilder: (BuildContext context, int index) {
206+
return const Text('separator');
207+
},
208+
itemBuilder: (BuildContext context, int index) {
209+
if (index == 5) {
210+
return null;
211+
}
212+
213+
return const Text('item');
214+
},
215+
),
216+
));
217+
218+
expect(find.text('item'), findsNWidgets(5));
219+
expect(find.text('separator'), findsNWidgets(5));
220+
});
221+
146222
testWidgets('ListView.builder dismiss keyboard onDrag test', (WidgetTester tester) async {
147223
final List<FocusNode> focusNodes = List<FocusNode>.generate(50, (int i) => FocusNode());
148224

0 commit comments

Comments
 (0)