diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 5dce14a15900..c82ad2ca9de9 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:collection' show HashMap, SplayTreeMap; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -1035,6 +1036,184 @@ class SliverList extends SliverMultiBoxAdaptorWidget { required super.delegate, }); + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// This constructor is appropriate for sliver lists with a large (or + /// infinite) number of children because the builder is called only for those + /// children that are actually visible. + /// + /// Providing a non-null `itemCount` improves the ability of the [SliverGrid] + /// to estimate the maximum scroll extent. + /// + /// `itemBuilder` will be called only with indices greater than or equal to + /// zero and less than `itemCount`. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// + /// {@tool snippet} + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverList.builder( + /// itemBuilder: (BuildContext context, int index) { + /// return Container( + /// alignment: Alignment.center, + /// color: Colors.lightBlue[100 * (index % 9)], + /// child: Text('list item $index'), + /// ); + /// }, + /// ) + /// ``` + /// {@end-tool} + SliverList.builder({ + super.key, + required NullableIndexedWidgetBuilder itemBuilder, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + + /// A sliver that places multiple box children, separated by box widgets, in a linear array along the main + /// axis. + /// + /// This constructor is appropriate for sliver lists with a large (or + /// infinite) number of children because the builder is called only for those + /// children that are actually visible. + /// + /// Providing a non-null `itemCount` improves the ability of the [SliverGrid] + /// to estimate the maximum scroll extent. + /// + /// `itemBuilder` will be called only with indices greater than or equal to + /// zero and less than `itemCount`. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// + /// The `separatorBuilder` is similar to `itemBuilder`, except it is the widget + /// that gets placed between itemBuilder(context, index) and itemBuilder(context, index + 1). + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// {@tool snippet} + /// + /// This example shows how to create a [SliverList] whose [Container] items + /// are separated by [Divider]s. + /// + /// ```dart + /// SliverList.separated( + /// itemBuilder: (BuildContext context, int index) { + /// return Container( + /// alignment: Alignment.center, + /// color: Colors.lightBlue[100 * (index % 9)], + /// child: Text('list item $index'), + /// ); + /// }, + /// separatorBuilder: (BuildContext context, int index) => const Divider(), + /// ) + /// ``` + /// {@end-tool} + SliverList.separated({ + super.key, + required NullableIndexedWidgetBuilder itemBuilder, + ChildIndexGetter? findChildIndexCallback, + required NullableIndexedWidgetBuilder separatorBuilder, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : assert(itemBuilder != null), + assert(separatorBuilder != null), + super(delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + final int itemIndex = index ~/ 2; + final Widget? widget; + if (index.isEven) { + widget = itemBuilder(context, itemIndex); + } else { + widget = separatorBuilder(context, itemIndex); + assert(() { + if (widget == null) { + throw FlutterError('separatorBuilder cannot return null.'); + } + return true; + }()); + } + return widget; + }, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount == null ? null : math.max(0, itemCount * 2 - 1), + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + semanticIndexCallback: (Widget _, int index) { + return index.isEven ? index ~/ 2 : null; + }, + )); + + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// This constructor uses a list of [Widget]s to build the sliver. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// + /// {@tool snippet} + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverList.list( + /// children: const [ + /// Text('Hello'), + /// Text('World!'), + /// ], + /// ); + /// ``` + /// {@end-tool} + SliverList.list({ + super.key, + required List children, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + @override SliverMultiBoxAdaptorElement createElement() => SliverMultiBoxAdaptorElement(this, replaceMovedChildren: true); @@ -1098,6 +1277,116 @@ class SliverFixedExtentList extends SliverMultiBoxAdaptorWidget { required this.itemExtent, }); + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// [SliverFixedExtentList] places its children in a linear array along the main + /// axis starting at offset zero and without gaps. Each child is forced to have + /// the [itemExtent] in the main axis and the + /// [SliverConstraints.crossAxisExtent] in the cross axis. + /// + /// This constructor is appropriate for sliver lists with a large (or + /// infinite) number of children whose extent is already determined. + /// + /// Providing a non-null `itemCount` improves the ability of the [SliverGrid] + /// to estimate the maximum scroll extent. + /// + /// `itemBuilder` will be called only with indices greater than or equal to + /// zero and less than `itemCount`. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// The `itemExtent` argument is the extent of each item. + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// {@tool snippet} + /// + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverFixedExtentList.builder( + /// itemExtent: 50.0, + /// itemBuilder: (BuildContext context, int index) { + /// return Container( + /// alignment: Alignment.center, + /// color: Colors.lightBlue[100 * (index % 9)], + /// child: Text('list item $index'), + /// ); + /// }, + /// ) + /// ``` + /// {@end-tool} + SliverFixedExtentList.builder({ + super.key, + required NullableIndexedWidgetBuilder itemBuilder, + required this.itemExtent, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// [SliverFixedExtentList] places its children in a linear array along the main + /// axis starting at offset zero and without gaps. Each child is forced to have + /// the [itemExtent] in the main axis and the + /// [SliverConstraints.crossAxisExtent] in the cross axis. + /// + /// This constructor uses a list of [Widget]s to build the sliver. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// + /// {@tool snippet} + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverFixedExtentList.list( + /// itemExtent: 50.0, + /// children: const [ + /// Text('Hello'), + /// Text('World!'), + /// ], + /// ); + /// ``` + /// {@end-tool} + SliverFixedExtentList.list({ + super.key, + required List children, + required this.itemExtent, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + /// The extent the children are forced to have in the main axis. final double itemExtent; diff --git a/packages/flutter/lib/src/widgets/sliver_prototype_extent_list.dart b/packages/flutter/lib/src/widgets/sliver_prototype_extent_list.dart index 5fa4533b2c22..93826086b7b8 100644 --- a/packages/flutter/lib/src/widgets/sliver_prototype_extent_list.dart +++ b/packages/flutter/lib/src/widgets/sliver_prototype_extent_list.dart @@ -40,6 +40,109 @@ class SliverPrototypeExtentList extends SliverMultiBoxAdaptorWidget { required this.prototypeItem, }) : assert(prototypeItem != null); + /// A sliver that places its box children in a linear array and constrains them + /// to have the same extent as a prototype item along the main axis. + /// + /// This constructor is appropriate for sliver lists with a large (or + /// infinite) number of children whose extent is already determined. + /// + /// Providing a non-null `itemCount` improves the ability of the [SliverGrid] + /// to estimate the maximum scroll extent. + /// + /// `itemBuilder` will be called only with indices greater than or equal to + /// zero and less than `itemCount`. + /// + /// {@macro flutter.widgets.ListView.builder.itemBuilder} + /// + /// The `prototypeItem` argument is used to determine the extent of each item. + /// + /// {@macro flutter.widgets.PageView.findChildIndexCallback} + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// + /// {@tool snippet} + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverPrototypeExtentList.builder( + /// prototypeItem: Container( + /// alignment: Alignment.center, + /// child: const Text('list item prototype'), + /// ), + /// itemBuilder: (BuildContext context, int index) { + /// return Container( + /// alignment: Alignment.center, + /// color: Colors.lightBlue[100 * (index % 9)], + /// child: Text('list item $index'), + /// ); + /// }, + /// ) + /// ``` + /// {@end-tool} + SliverPrototypeExtentList.builder({ + super.key, + required NullableIndexedWidgetBuilder itemBuilder, + required this.prototypeItem, + ChildIndexGetter? findChildIndexCallback, + int? itemCount, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildBuilderDelegate( + itemBuilder, + findChildIndexCallback: findChildIndexCallback, + childCount: itemCount, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + + /// A sliver that places multiple box children in a linear array along the main + /// axis. + /// + /// This constructor uses a list of [Widget]s to build the sliver. + /// + /// The `addAutomaticKeepAlives` argument corresponds to the + /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The + /// `addRepaintBoundaries` argument corresponds to the + /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The + /// `addSemanticIndexes` argument corresponds to the + /// [SliverChildBuilderDelegate.addSemanticIndexes] property. + /// + /// {@tool snippet} + /// This example, which would be inserted into a [CustomScrollView.slivers] + /// list, shows an infinite number of items in varying shades of blue: + /// + /// ```dart + /// SliverPrototypeExtentList.list( + /// prototypeItem: const Text('Hello'), + /// children: const [ + /// Text('Hello'), + /// Text('World!'), + /// ], + /// ); + /// ``` + /// {@end-tool} + SliverPrototypeExtentList.list({ + super.key, + required List children, + required this.prototypeItem, + bool addAutomaticKeepAlives = true, + bool addRepaintBoundaries = true, + bool addSemanticIndexes = true, + }) : super(delegate: SliverChildListDelegate( + children, + addAutomaticKeepAlives: addAutomaticKeepAlives, + addRepaintBoundaries: addRepaintBoundaries, + addSemanticIndexes: addSemanticIndexes, + )); + /// Defines the main axis extent of all of this sliver's children. /// /// The [prototypeItem] is laid out before the rest of the sliver's children diff --git a/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart index 247df56c4cac..48b000f5eb47 100644 --- a/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart +++ b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; class TestItem extends StatelessWidget { @@ -41,6 +40,61 @@ Widget buildFrame({ int? count, double? width, double? height, Axis? scrollDirec } void main() { + testWidgets('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + slivers: [ + SliverPrototypeExtentList.builder( + itemBuilder: (BuildContext context, int index) => TestItem(item: index), + prototypeItem: const TestItem(item: -1, height: 100.0), + itemCount: 20, + ), + ], + ), + ), + ), + ); + + // The viewport is 600 pixels high, lazily created items are 100 pixels high. + for (int i = 0; i < 6; i += 1) { + final Finder item = find.widgetWithText(Container, 'Item $i'); + expect(item, findsOneWidget); + expect(tester.getTopLeft(item).dy, i * 100.0); + expect(tester.getSize(item).height, 100.0); + } + for (int i = 7; i < 20; i += 1) { + expect(find.text('Item $i'), findsNothing); + } + }); + + testWidgets('SliverPrototypeExtentList.builder test', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: CustomScrollView( + slivers: [ + SliverPrototypeExtentList.list( + prototypeItem: const TestItem(item: -1, height: 100.0), + children: [0, 1, 2, 3, 4, 5, 6, 7].map((int index) => TestItem(item: index)).toList(), + ), + ], + ), + ), + ), + ); + + // The viewport is 600 pixels high, lazily created items are 100 pixels high. + for (int i = 0; i < 6; i += 1) { + final Finder item = find.widgetWithText(Container, 'Item $i'); + expect(item, findsOneWidget); + expect(tester.getTopLeft(item).dy, i * 100.0); + expect(tester.getSize(item).height, 100.0); + } + expect(find.text('Item 7'), findsNothing); + }); + testWidgets('SliverPrototypeExtentList vertical scrolling basics', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(count: 20, height: 100.0)); diff --git a/packages/flutter/test/widgets/slivers_test.dart b/packages/flutter/test/widgets/slivers_test.dart index 0067cc409a9e..bd9231afd2cc 100644 --- a/packages/flutter/test/widgets/slivers_test.dart +++ b/packages/flutter/test/widgets/slivers_test.dart @@ -992,6 +992,264 @@ void main() { expect(secondTapped, 1); }); + testWidgets('SliverList.builder can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverList.builder( + itemCount: 2, + itemBuilder: (BuildContext context, int index) { + return Material( + color: index.isEven ? Colors.yellow : Colors.red, + child: InkWell( + onTap: () { + index.isEven ? firstTapped++ : secondTapped++; + }, + child: Text('Index $index'), + ), + ); + }, + ), + ], + ), + ), + )); + + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + + testWidgets('SliverList.builder can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverList.builder( + itemCount: 2, + itemBuilder: (BuildContext context, int index) { + return Material( + color: index.isEven ? Colors.yellow : Colors.red, + child: InkWell( + onTap: () { + index.isEven ? firstTapped++ : secondTapped++; + }, + child: Text('Index $index'), + ), + ); + }, + ), + ], + ), + ), + )); + + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + + testWidgets('SliverList.separated can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverList.separated( + itemCount: 2, + itemBuilder: (BuildContext context, int index) { + return Material( + color: index.isEven ? Colors.yellow : Colors.red, + child: InkWell( + onTap: () { + index.isEven ? firstTapped++ : secondTapped++; + }, + child: Text('Index $index'), + ), + ); + }, + separatorBuilder: (BuildContext context, int index) => Text('Separator $index'), + ), + ], + ), + ), + )); + + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + + testWidgets('SliverList.separated has correct number of children', (WidgetTester tester) async { + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverList.separated( + itemCount: 2, + itemBuilder: (BuildContext context, int index) => const Text('item'), + separatorBuilder: (BuildContext context, int index) => const Text('separator'), + ), + ], + ), + ), + )); + expect(find.text('item'), findsNWidgets(2)); + expect(find.text('separator'), findsNWidgets(1)); + }); + + testWidgets('SliverList.list can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverList.list( + children: [ + Material( + color: Colors.yellow, + child: InkWell( + onTap: () => firstTapped++, + child: const Text('Index 0'), + ), + ), + Material( + color: Colors.red, + child: InkWell( + onTap: () => secondTapped++, + child: const Text('Index 1'), + ), + ), + ], + ), + ], + ), + ), + )); + + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + + testWidgets('SliverFixedExtentList.builder can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverFixedExtentList.builder( + itemCount: 2, + itemExtent: 100, + itemBuilder: (BuildContext context, int index) { + return Material( + color: index.isEven ? Colors.yellow : Colors.red, + child: InkWell( + onTap: () { + index.isEven ? firstTapped++ : secondTapped++; + }, + child: Text('Index $index'), + ), + ); + }, + ), + ], + ), + ), + )); + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + + testWidgets('SliverList.list can build children', (WidgetTester tester) async { + int firstTapped = 0; + int secondTapped = 0; + final Key key = UniqueKey(); + await tester.pumpWidget(MaterialApp( + home: Scaffold( + key: key, + body: CustomScrollView( + slivers: [ + SliverFixedExtentList.list( + itemExtent: 100, + children: [ + Material( + color: Colors.yellow, + child: InkWell( + onTap: () => firstTapped++, + child: const Text('Index 0'), + ), + ), + Material( + color: Colors.red, + child: InkWell( + onTap: () => secondTapped++, + child: const Text('Index 1'), + ), + ), + ], + ), + ], + ), + ), + )); + + // Verify correct hit testing + await tester.tap(find.text('Index 0')); + expect(firstTapped, 1); + expect(secondTapped, 0); + firstTapped = 0; + await tester.tap(find.text('Index 1')); + expect(firstTapped, 0); + expect(secondTapped, 1); + }); + testWidgets('SliverGrid.builder can build children', (WidgetTester tester) async { int firstTapped = 0; int secondTapped = 0;