From e9a81918bacd5d1a274129d2e96c02f04d94472a Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 17:27:44 +0800 Subject: [PATCH 1/9] Activate leak testing --- packages/two_dimensional_scrollables/pubspec.yaml | 1 + .../test/flutter_test_config.dart | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 packages/two_dimensional_scrollables/test/flutter_test_config.dart diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 45fb03ee642a..72cc9ebed52f 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + leak_tracker_flutter_testing: any topics: - scrollable diff --git a/packages/two_dimensional_scrollables/test/flutter_test_config.dart b/packages/two_dimensional_scrollables/test/flutter_test_config.dart new file mode 100644 index 000000000000..d1eb77755d23 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/flutter_test_config.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + LeakTesting.enable(); + LeakTracking.warnForUnsupportedPlatforms = false; + await testMain(); +} From 1a2a5b4645e397e57c4c3a2b3b85a26d91527638 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 17:29:02 +0800 Subject: [PATCH 2/9] fix memory leaks --- .../lib/src/table_view/table.dart | 375 ++++++++++++++---- .../lib/src/tree_view/tree.dart | 132 +++++- 2 files changed, 414 insertions(+), 93 deletions(-) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index dc46613574f0..693a1127640d 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -129,34 +129,34 @@ class TableView extends TwoDimensionalScrollView { /// Creates a [TableView] of widgets that are created on demand. /// - /// This constructor is appropriate for table views with a large - /// number of cells because the [cellbuilder] is called only for those - /// cells that are actually visible. + /// This is appropriate for table views with a large number of cells because + /// the [cellBuilder] is called only for those cells that are actually + /// visible. /// - /// This constructor generates a [TableCellBuilderDelegate] for building - /// children on demand using the required [cellBuilder], - /// [columnBuilder], and [rowBuilder]. + /// This generates a [TableCellBuilderDelegate] for building children on + /// demand using the required [cellBuilder], [columnBuilder], and + /// [rowBuilder]. /// /// For infinite rows and columns, omit providing [columnCount] or [rowCount]. - /// Returning null from the [columnBuilder] or [rowBuilder] will terminate - /// the row or column at that index, representing the end of the table in that + /// Returning null from the [columnBuilder] or [rowBuilder] will terminate the + /// row or column at that index, representing the end of the table in that /// axis. In this scenario, until the potential end of the table in either /// dimension is reached by returning null, the /// [ScrollPosition.maxScrollExtent] will reflect [double.infinity]. This is /// because as the table is built lazily, it will not know the end has been /// reached until the [ScrollPosition] arrives there. This is similar to /// returning null from [ListView.builder] to signify the end of the list. - TableView.builder({ - super.key, - super.primary, - super.mainAxis, - super.horizontalDetails, - super.verticalDetails, - super.cacheExtent, - super.diagonalDragBehavior = DiagonalDragBehavior.none, - super.dragStartBehavior, - super.keyboardDismissBehavior, - super.clipBehavior, + static Widget builder({ + Key? key, + bool? primary, + Axis mainAxis = Axis.vertical, + ScrollableDetails horizontalDetails = const ScrollableDetails.horizontal(), + ScrollableDetails verticalDetails = const ScrollableDetails.vertical(), + double? cacheExtent, + DiagonalDragBehavior diagonalDragBehavior = DiagonalDragBehavior.none, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, + Clip clipBehavior = Clip.hardEdge, int pinnedRowCount = 0, int pinnedColumnCount = 0, int trailingPinnedRowCount = 0, @@ -166,56 +166,53 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, required TableViewCellBuilder cellBuilder, - this.alignment = Alignment.topLeft, - }) : assert(pinnedRowCount >= 0), - assert(trailingPinnedRowCount >= 0), - assert(rowCount == null || rowCount >= 0), - assert( - rowCount == null || - rowCount >= pinnedRowCount + trailingPinnedRowCount, - ), - assert(columnCount == null || columnCount >= 0), - assert(pinnedColumnCount >= 0), - assert(trailingPinnedColumnCount >= 0), - assert( - columnCount == null || - columnCount >= pinnedColumnCount + trailingPinnedColumnCount, - ), - super( - delegate: TableCellBuilderDelegate( - columnCount: columnCount, - rowCount: rowCount, - pinnedColumnCount: pinnedColumnCount, - pinnedRowCount: pinnedRowCount, - trailingPinnedColumnCount: trailingPinnedColumnCount, - trailingPinnedRowCount: trailingPinnedRowCount, - cellBuilder: cellBuilder, - columnBuilder: columnBuilder, - rowBuilder: rowBuilder, - ), - ); + AlignmentGeometry alignment = Alignment.topLeft, + }) { + return _TableViewBuilder( + key: key, + primary: primary, + mainAxis: mainAxis, + horizontalDetails: horizontalDetails, + verticalDetails: verticalDetails, + cacheExtent: cacheExtent, + diagonalDragBehavior: diagonalDragBehavior, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + clipBehavior: clipBehavior, + pinnedRowCount: pinnedRowCount, + pinnedColumnCount: pinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + columnCount: columnCount, + rowCount: rowCount, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + cellBuilder: cellBuilder, + alignment: alignment, + ); + } /// Creates a [TableView] from an explicit two dimensional array of children. /// - /// This constructor is appropriate for list views with a small number of - /// children because constructing the [List] requires doing work for every - /// child that could possibly be displayed in the list view instead of just - /// those children that are actually visible. + /// This is appropriate for list views with a small number of children because + /// constructing the [List] requires doing work for every child that could + /// possibly be displayed in the list view instead of just those children that + /// are actually visible. /// /// The [children] are accessed for each [TableVicinity.column] and /// [TableVicinity.row] of the [TwoDimensionalViewport] as /// `children[vicinity.column][vicinity.row]`. - TableView.list({ - super.key, - super.primary, - super.mainAxis, - super.horizontalDetails, - super.verticalDetails, - super.cacheExtent, - super.diagonalDragBehavior = DiagonalDragBehavior.none, - super.dragStartBehavior, - super.keyboardDismissBehavior, - super.clipBehavior, + static Widget list({ + Key? key, + bool? primary, + Axis mainAxis = Axis.vertical, + ScrollableDetails horizontalDetails = const ScrollableDetails.horizontal(), + ScrollableDetails verticalDetails = const ScrollableDetails.vertical(), + double? cacheExtent, + DiagonalDragBehavior diagonalDragBehavior = DiagonalDragBehavior.none, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, + Clip clipBehavior = Clip.hardEdge, int pinnedRowCount = 0, int pinnedColumnCount = 0, int trailingPinnedRowCount = 0, @@ -223,22 +220,29 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, List> cells = const >[], - this.alignment = Alignment.topLeft, - }) : assert(pinnedRowCount >= 0), - assert(pinnedColumnCount >= 0), - assert(trailingPinnedRowCount >= 0), - assert(trailingPinnedColumnCount >= 0), - super( - delegate: TableCellListDelegate( - pinnedColumnCount: pinnedColumnCount, - pinnedRowCount: pinnedRowCount, - trailingPinnedColumnCount: trailingPinnedColumnCount, - trailingPinnedRowCount: trailingPinnedRowCount, - cells: cells, - columnBuilder: columnBuilder, - rowBuilder: rowBuilder, - ), - ); + AlignmentGeometry alignment = Alignment.topLeft, + }) { + return _TableViewList( + key: key, + primary: primary, + mainAxis: mainAxis, + horizontalDetails: horizontalDetails, + verticalDetails: verticalDetails, + cacheExtent: cacheExtent, + diagonalDragBehavior: diagonalDragBehavior, + dragStartBehavior: dragStartBehavior, + keyboardDismissBehavior: keyboardDismissBehavior, + clipBehavior: clipBehavior, + pinnedRowCount: pinnedRowCount, + pinnedColumnCount: pinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + cells: cells, + alignment: alignment, + ); + } /// The alignment of the table within the viewport when there is extra space. /// @@ -265,6 +269,221 @@ class TableView extends TwoDimensionalScrollView { } } +class _TableViewBuilder extends StatefulWidget { + const _TableViewBuilder({ + super.key, + this.primary, + this.mainAxis = Axis.vertical, + this.horizontalDetails = const ScrollableDetails.horizontal(), + this.verticalDetails = const ScrollableDetails.vertical(), + this.cacheExtent, + this.diagonalDragBehavior = DiagonalDragBehavior.none, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.clipBehavior = Clip.hardEdge, + this.pinnedRowCount = 0, + this.pinnedColumnCount = 0, + this.trailingPinnedRowCount = 0, + this.trailingPinnedColumnCount = 0, + this.columnCount, + this.rowCount, + required this.columnBuilder, + required this.rowBuilder, + required this.cellBuilder, + this.alignment = Alignment.topLeft, + }) : assert(pinnedRowCount >= 0), + assert(trailingPinnedRowCount >= 0), + assert(rowCount == null || rowCount >= 0), + assert( + rowCount == null || + rowCount >= pinnedRowCount + trailingPinnedRowCount, + ), + assert(columnCount == null || columnCount >= 0), + assert(pinnedColumnCount >= 0), + assert(trailingPinnedColumnCount >= 0), + assert( + columnCount == null || + columnCount >= pinnedColumnCount + trailingPinnedColumnCount, + ); + + final bool? primary; + final Axis mainAxis; + final ScrollableDetails horizontalDetails; + final ScrollableDetails verticalDetails; + final double? cacheExtent; + final DiagonalDragBehavior diagonalDragBehavior; + final DragStartBehavior dragStartBehavior; + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + final Clip clipBehavior; + final int pinnedRowCount; + final int pinnedColumnCount; + final int trailingPinnedRowCount; + final int trailingPinnedColumnCount; + final int? columnCount; + final int? rowCount; + final TableSpanBuilder columnBuilder; + final TableSpanBuilder rowBuilder; + final TableViewCellBuilder cellBuilder; + final AlignmentGeometry alignment; + + @override + State<_TableViewBuilder> createState() => __TableViewBuilderState(); +} + +class __TableViewBuilderState extends State<_TableViewBuilder> { + late TableCellBuilderDelegate _delegate; + + @override + void initState() { + super.initState(); + _delegate = TableCellBuilderDelegate( + columnCount: widget.columnCount, + rowCount: widget.rowCount, + pinnedColumnCount: widget.pinnedColumnCount, + pinnedRowCount: widget.pinnedRowCount, + trailingPinnedColumnCount: widget.trailingPinnedColumnCount, + trailingPinnedRowCount: widget.trailingPinnedRowCount, + cellBuilder: widget.cellBuilder, + columnBuilder: widget.columnBuilder, + rowBuilder: widget.rowBuilder, + ); + } + + @override + void didUpdateWidget(_TableViewBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.columnCount != oldWidget.columnCount || + widget.rowCount != oldWidget.rowCount || + widget.pinnedColumnCount != oldWidget.pinnedColumnCount || + widget.pinnedRowCount != oldWidget.pinnedRowCount || + widget.trailingPinnedColumnCount != + oldWidget.trailingPinnedColumnCount || + widget.trailingPinnedRowCount != oldWidget.trailingPinnedRowCount || + widget.cellBuilder != oldWidget.cellBuilder || + widget.columnBuilder != oldWidget.columnBuilder || + widget.rowBuilder != oldWidget.rowBuilder) { + _delegate.dispose(); + _delegate = TableCellBuilderDelegate( + columnCount: widget.columnCount, + rowCount: widget.rowCount, + pinnedColumnCount: widget.pinnedColumnCount, + pinnedRowCount: widget.pinnedRowCount, + trailingPinnedColumnCount: widget.trailingPinnedColumnCount, + trailingPinnedRowCount: widget.trailingPinnedRowCount, + cellBuilder: widget.cellBuilder, + columnBuilder: widget.columnBuilder, + rowBuilder: widget.rowBuilder, + ); + } + } + + @override + void dispose() { + _delegate.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TableView( + primary: widget.primary, + mainAxis: widget.mainAxis, + horizontalDetails: widget.horizontalDetails, + verticalDetails: widget.verticalDetails, + cacheExtent: widget.cacheExtent, + diagonalDragBehavior: widget.diagonalDragBehavior, + dragStartBehavior: widget.dragStartBehavior, + delegate: _delegate, + alignment: widget.alignment, + ); + } +} + +class _TableViewList extends StatefulWidget { + const _TableViewList({ + super.key, + this.primary, + this.mainAxis = Axis.vertical, + this.horizontalDetails = const ScrollableDetails.horizontal(), + this.verticalDetails = const ScrollableDetails.vertical(), + this.cacheExtent, + this.diagonalDragBehavior = DiagonalDragBehavior.none, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.clipBehavior = Clip.hardEdge, + this.pinnedRowCount = 0, + this.pinnedColumnCount = 0, + this.trailingPinnedRowCount = 0, + this.trailingPinnedColumnCount = 0, + required this.columnBuilder, + required this.rowBuilder, + this.cells = const >[], + this.alignment = Alignment.topLeft, + }) : assert(pinnedRowCount >= 0), + assert(pinnedColumnCount >= 0), + assert(trailingPinnedRowCount >= 0), + assert(trailingPinnedColumnCount >= 0); + + final bool? primary; + final Axis mainAxis; + final ScrollableDetails horizontalDetails; + final ScrollableDetails verticalDetails; + final double? cacheExtent; + final DiagonalDragBehavior diagonalDragBehavior; + final DragStartBehavior dragStartBehavior; + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + final Clip clipBehavior; + final int pinnedRowCount; + final int pinnedColumnCount; + final int trailingPinnedRowCount; + final int trailingPinnedColumnCount; + final TableSpanBuilder columnBuilder; + final TableSpanBuilder rowBuilder; + final AlignmentGeometry alignment; + final List> cells; + + @override + State<_TableViewList> createState() => _TableViewListState(); +} + +class _TableViewListState extends State<_TableViewList> { + late TableCellListDelegate _delegate; + + @override + void initState() { + super.initState(); + _delegate = TableCellListDelegate( + pinnedColumnCount: widget.pinnedColumnCount, + pinnedRowCount: widget.pinnedRowCount, + trailingPinnedColumnCount: widget.trailingPinnedColumnCount, + trailingPinnedRowCount: widget.trailingPinnedRowCount, + cells: widget.cells, + columnBuilder: widget.columnBuilder, + rowBuilder: widget.rowBuilder, + ); + } + + @override + void dispose() { + _delegate.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TableView( + primary: widget.primary, + mainAxis: widget.mainAxis, + horizontalDetails: widget.horizontalDetails, + verticalDetails: widget.verticalDetails, + cacheExtent: widget.cacheExtent, + diagonalDragBehavior: widget.diagonalDragBehavior, + dragStartBehavior: widget.dragStartBehavior, + delegate: _delegate, + ); + } +} + /// A widget through which a portion of a Table of [Widget] children are viewed, /// typically in combination with a [TableView]. class TableViewport extends TwoDimensionalViewport { diff --git a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart index 6819c9424018..19805132e74b 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart @@ -925,6 +925,7 @@ class _TreeViewState extends State> case AnimationStatus.dismissed: case AnimationStatus.completed: _currentAnimationForParent[node]!.controller.dispose(); + _currentAnimationForParent[node]!.animation.dispose(); _currentAnimationForParent.remove(node); _updateActiveAnimations(); // If the node is collapsing, we need to unpack the active @@ -958,6 +959,7 @@ class _TreeViewState extends State> widget.toggleAnimationStyle?.curve ?? TreeView.defaultAnimationCurve, ); + _currentAnimationForParent[node]?.animation.dispose(); _currentAnimationForParent[node] = ( controller: controller, animation: newAnimation, @@ -978,8 +980,119 @@ class _TreeViewState extends State> } } -class _TreeView extends TwoDimensionalScrollView { - _TreeView({ +class _TreeView extends StatefulWidget { + const _TreeView({ + this.primary, + this.mainAxis = Axis.vertical, + this.horizontalDetails = const ScrollableDetails.horizontal(), + this.verticalDetails = const ScrollableDetails.vertical(), + this.cacheExtent, + this.diagonalDragBehavior = DiagonalDragBehavior.none, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.clipBehavior = Clip.hardEdge, + required this.nodeBuilder, + required this.rowBuilder, + required this.activeAnimations, + required this.rowDepths, + required this.indentation, + required this.alignment, + required this.rowCount, + this.addAutomaticKeepAlives = true, + }); + + final bool? primary; + + final Axis mainAxis; + + final ScrollableDetails horizontalDetails; + + final ScrollableDetails verticalDetails; + + final double? cacheExtent; + + final DiagonalDragBehavior diagonalDragBehavior; + + final DragStartBehavior dragStartBehavior; + + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + + final Clip clipBehavior; + + final TwoDimensionalIndexedWidgetBuilder nodeBuilder; + final TreeVicinityToRowBuilder rowBuilder; + final Map activeAnimations; + final Map rowDepths; + final double indentation; + final AlignmentGeometry alignment; + final int rowCount; + final bool addAutomaticKeepAlives; + + @override + State<_TreeView> createState() => __TreeViewState(); +} + +class __TreeViewState extends State<_TreeView> { + late TreeRowBuilderDelegate _delegate; + + @override + void initState() { + super.initState(); + _delegate = TreeRowBuilderDelegate( + nodeBuilder: widget.nodeBuilder, + rowBuilder: widget.rowBuilder, + rowCount: widget.rowCount, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + ); + } + + @override + void didUpdateWidget(_TreeView oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.nodeBuilder != widget.nodeBuilder || + oldWidget.rowBuilder != widget.rowBuilder || + oldWidget.rowCount != widget.rowCount || + oldWidget.addAutomaticKeepAlives != widget.addAutomaticKeepAlives) { + _delegate.dispose(); + _delegate = TreeRowBuilderDelegate( + nodeBuilder: widget.nodeBuilder, + rowBuilder: widget.rowBuilder, + rowCount: widget.rowCount, + addAutomaticKeepAlives: widget.addAutomaticKeepAlives, + ); + } + } + + @override + void dispose() { + _delegate.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _TreeViewWidget( + primary: widget.primary, + mainAxis: widget.mainAxis, + horizontalDetails: widget.horizontalDetails, + verticalDetails: widget.verticalDetails, + cacheExtent: widget.cacheExtent, + diagonalDragBehavior: widget.diagonalDragBehavior, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + clipBehavior: widget.clipBehavior, + delegate: _delegate, + activeAnimations: widget.activeAnimations, + rowDepths: widget.rowDepths, + indentation: widget.indentation, + alignment: widget.alignment, + ); + } +} + +class _TreeViewWidget extends TwoDimensionalScrollView { + _TreeViewWidget({ super.primary, super.mainAxis, super.horizontalDetails, @@ -989,24 +1102,13 @@ class _TreeView extends TwoDimensionalScrollView { super.dragStartBehavior, super.keyboardDismissBehavior, super.clipBehavior, - required TwoDimensionalIndexedWidgetBuilder nodeBuilder, - required TreeVicinityToRowBuilder rowBuilder, + required super.delegate, required this.activeAnimations, required this.rowDepths, required this.indentation, required this.alignment, - required int rowCount, - bool addAutomaticKeepAlives = true, }) : assert(verticalDetails.direction == AxisDirection.down), - assert(horizontalDetails.direction == AxisDirection.right), - super( - delegate: TreeRowBuilderDelegate( - nodeBuilder: nodeBuilder, - rowBuilder: rowBuilder, - rowCount: rowCount, - addAutomaticKeepAlives: addAutomaticKeepAlives, - ), - ); + assert(horizontalDetails.direction == AxisDirection.right); final Map activeAnimations; final Map rowDepths; From f6dd90ea4b7f9daa2bed3955262482884c4a6f03 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 17:29:07 +0800 Subject: [PATCH 3/9] Update tests --- .../test/table_view/table_cell_test.dart | 401 +++++++++--------- .../test/table_view/table_span_test.dart | 24 +- .../test/table_view/table_test.dart | 301 ++++++++----- 3 files changed, 403 insertions(+), 323 deletions(-) diff --git a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart index 8313dc5c48b0..e360bbd6341e 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_cell_test.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; const TableSpan span = TableSpan(extent: FixedTableSpanExtent(100)); @@ -172,203 +173,215 @@ void main() { expect(cell, isNull); }); - testWidgets('Merge start cannot exceed current index', ( - WidgetTester tester, - ) async { - // Merge span start is greater than given index, ex: column 10 has merge - // start at 20. - final exceptions = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Row - // +---------+ - // | X err | - // | | - // +---------+ - // | merge | - // | | - // + + - // | | - // | | - // +---------+ - // This cell should only be built for (0, 1) and (0, 2), not (0,0). - var cell = const TableViewCell( - rowMergeStart: 1, - rowMergeSpan: 2, - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 1, - rowCount: 3, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeStart <= currentSpan'), - ); - - await tester.pumpWidget(Container()); - exceptions.clear(); - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Column - // +---------+---------+---------+ - // | X err | merged | - // | | | - // +---------+---------+---------+ - // This cell should only be returned for (1, 0) and (2, 0), not (0,0). - cell = const TableViewCell( - columnMergeStart: 1, - columnMergeSpan: 2, - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 3, - rowCount: 1, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeStart <= currentSpan'), - ); - }); - - testWidgets('Merge cannot exceed table contents', ( - WidgetTester tester, - ) async { - // Merge exceeds table content, ex: at column 10, cell spans 4 columns, - // but table only has 12 columns. - final exceptions = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Row - var cell = const TableViewCell( - rowMergeStart: 0, - rowMergeSpan: 10, // Exceeds the number of rows - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 1, - rowCount: 3, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeEnd < spanCount'), - ); + testWidgets( + 'Merge start cannot exceed current index', + // The build throws an assertion error which prevents the table from + // properly disposing the elements. + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + // Merge span start is greater than given index, ex: column 10 has merge + // start at 20. + final exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + // +---------+ + // | X err | + // | | + // +---------+ + // | merge | + // | | + // + + + // | | + // | | + // +---------+ + // This cell should only be built for (0, 1) and (0, 2), not (0,0). + var cell = const TableViewCell( + rowMergeStart: 1, + rowMergeSpan: 2, + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeStart <= currentSpan'), + ); - await tester.pumpWidget(Container()); - exceptions.clear(); - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Column - cell = const TableViewCell( - columnMergeStart: 0, - columnMergeSpan: 10, // Exceeds the number of columns - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 3, - rowCount: 1, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeEnd < spanCount'), - ); - }); + await tester.pumpWidget(const SizedBox()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + // +---------+---------+---------+ + // | X err | merged | + // | | | + // +---------+---------+---------+ + // This cell should only be returned for (1, 0) and (2, 0), not (0,0). + cell = const TableViewCell( + columnMergeStart: 1, + columnMergeSpan: 2, + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeStart <= currentSpan'), + ); + }, + ); + + testWidgets( + 'Merge cannot exceed table contents', + // The build throws an assertion error which prevents the table from + // properly disposing the elements. + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + // Merge exceeds table content, ex: at column 10, cell spans 4 columns, + // but table only has 12 columns. + final exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + var cell = const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 10, // Exceeds the number of rows + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < spanCount'), + ); - testWidgets('Merge cannot contain pinned and unpinned cells', ( - WidgetTester tester, - ) async { - // Merge spans pinned and unpinned cells, ex: column 0 is pinned, 0-2 - // expected merge. - final exceptions = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Row - var cell = const TableViewCell( - rowMergeStart: 0, - rowMergeSpan: 3, - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 1, - rowCount: 3, - pinnedRowCount: 1, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeEnd < pinnedSpanCount'), - ); + await tester.pumpWidget(const SizedBox()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + cell = const TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 10, // Exceeds the number of columns + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < spanCount'), + ); + }, + ); + + testWidgets( + 'Merge cannot contain pinned and unpinned cells', + // The build throws an assertion error which prevents the table from + // properly disposing the elements. + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + // Merge spans pinned and unpinned cells, ex: column 0 is pinned, 0-2 + // expected merge. + final exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Row + var cell = const TableViewCell( + rowMergeStart: 0, + rowMergeSpan: 3, + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 1, + rowCount: 3, + pinnedRowCount: 1, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < pinnedSpanCount'), + ); - await tester.pumpWidget(Container()); - exceptions.clear(); - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - // Column - cell = const TableViewCell( - columnMergeStart: 0, - columnMergeSpan: 3, - child: SizedBox.shrink(), - ); - await tester.pumpWidget( - TableView.builder( - cellBuilder: (_, __) => cell, - columnBuilder: (_) => span, - rowBuilder: (_) => span, - columnCount: 3, - rowCount: 1, - pinnedColumnCount: 1, - ), - ); - FlutterError.onError = oldHandler; - expect(exceptions.length, 2); - expect( - exceptions.first.toString(), - contains('spanMergeEnd < pinnedSpanCount'), - ); - }); + await tester.pumpWidget(const SizedBox()); + exceptions.clear(); + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + // Column + cell = const TableViewCell( + columnMergeStart: 0, + columnMergeSpan: 3, + child: SizedBox.shrink(), + ); + await tester.pumpWidget( + TableView.builder( + cellBuilder: (_, __) => cell, + columnBuilder: (_) => span, + rowBuilder: (_) => span, + columnCount: 3, + rowCount: 1, + pinnedColumnCount: 1, + ), + ); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(2)); + expect( + exceptions.first.toString(), + contains('spanMergeEnd < pinnedSpanCount'), + ); + }, + ); }); group('layout', () { diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index ad3a9b8dca44..2ccdc02e0b92 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -212,7 +212,7 @@ void main() { testWidgets('Vertical main axis, vertical reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( verticalDetails: ScrollableDetails.vertical( controller: verticalController, reverse: true, @@ -318,7 +318,7 @@ void main() { testWidgets('Vertical main axis, horizontal reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( verticalDetails: ScrollableDetails.vertical( controller: verticalController, ), @@ -424,7 +424,7 @@ void main() { testWidgets('Vertical main axis, both reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( verticalDetails: ScrollableDetails.vertical( controller: verticalController, reverse: true, @@ -531,7 +531,7 @@ void main() { testWidgets('Horizontal main axis, vertical reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical( controller: verticalController, @@ -638,7 +638,7 @@ void main() { testWidgets('Horizontal main axis, horizontal reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical( controller: verticalController, @@ -745,7 +745,7 @@ void main() { testWidgets('Horizontal main axis, both reversed', ( WidgetTester tester, ) async { - final table = TableView.builder( + final Widget table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical( controller: verticalController, @@ -854,7 +854,7 @@ void main() { 'paints borders correctly when cross axis is reversed (TableView)', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final tableView = TableView.builder( + final Widget tableView = TableView.builder( horizontalDetails: const ScrollableDetails.horizontal(reverse: true), rowCount: 1, columnCount: 1, @@ -906,7 +906,7 @@ void main() { 'paints borders correctly when vertical scrolling is reversed (TableView)', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final tableView = TableView.builder( + final Widget tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), rowCount: 1, columnCount: 1, @@ -958,7 +958,7 @@ void main() { 'TableView row decoration rect is correct when vertical axis is reversed and padding is used', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final tableView = TableView.builder( + final Widget tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), rowCount: 1, columnCount: 1, @@ -1037,7 +1037,7 @@ void main() { const TableVicinity(row: 3, column: 2): (2, 2), }; - TableView buildScenario1({ + Widget buildScenario1({ bool reverseVertical = false, bool reverseHorizontal = false, }) { @@ -1099,7 +1099,7 @@ void main() { const TableVicinity(row: 2, column: 3): (2, 2), }; - TableView buildScenario2({ + Widget buildScenario2({ bool reverseVertical = false, bool reverseHorizontal = false, }) { @@ -1176,7 +1176,7 @@ void main() { const TableVicinity(row: 2, column: 3): (2, 2), }; - TableView buildScenario3({ + Widget buildScenario3({ Axis mainAxis = Axis.vertical, bool reverseVertical = false, bool reverseHorizontal = false, diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index a89029c843b4..bfa87102587c 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; const TableSpan span = TableSpan(extent: FixedTableSpanExtent(100)); @@ -41,14 +42,21 @@ TableSpan getMouseTrackingSpan( void main() { group('TableView.builder', () { - test('creates correct delegate', () { - final tableView = TableView.builder( + testWidgets('creates correct delegate', (WidgetTester tester) async { + final Widget widget = TableView.builder( columnCount: 3, rowCount: 2, rowBuilder: (_) => span, columnBuilder: (_) => span, cellBuilder: (_, __) => cell, ); + + await tester.pumpWidget(widget); + + final TableView tableView = tester.widget( + find.byType(TableView), + ); + final delegate = tableView.delegate as TableCellBuilderDelegate; expect(delegate.pinnedRowCount, 0); expect(delegate.pinnedRowCount, 0); @@ -60,7 +68,7 @@ void main() { }); test('asserts correct counts', () { - TableView? tableView; + Widget? tableView; expect( () { tableView = TableView.builder( @@ -191,7 +199,7 @@ void main() { horizontalController.dispose(); }); - TableView getTableView({ + Widget getTableView({ int? columnCount, int? rowCount, TableSpanBuilder? columnBuilder, @@ -2030,106 +2038,118 @@ void main() { ); }); - testWidgets('merged column that exceeds metrics will assert', ( - WidgetTester tester, - ) async { - final exceptions = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - const ({int start, int span}) columnConfig = (start: 1, span: 10); - final mergedColumns = List.generate(10, (int index) => index + 1); - await tester.pumpWidget( - MaterialApp( - home: getTableView( - columnBuilder: (int index) { - // There will only be 8 columns, but the merge is set up for 10. - if (index == 8) { - return null; - } - return largeSpan; - }, - cellBuilder: (_, TableVicinity vicinity) { - // Merged column - if (mergedColumns.contains(vicinity.column) && - vicinity.row == 0) { + testWidgets( + 'merged column that exceeds metrics will assert', + // The build throws an assertion error which prevents the table from + // properly disposing the elements. + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + final exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + const ({int start, int span}) columnConfig = (start: 1, span: 10); + final mergedColumns = List.generate( + 10, + (int index) => index + 1, + ); + await tester.pumpWidget( + MaterialApp( + home: getTableView( + columnBuilder: (int index) { + // There will only be 8 columns, but the merge is set up for 10. + if (index == 8) { + return null; + } + return largeSpan; + }, + cellBuilder: (_, TableVicinity vicinity) { + // Merged column + if (mergedColumns.contains(vicinity.column) && + vicinity.row == 0) { + return TableViewCell( + columnMergeStart: columnConfig.start, + columnMergeSpan: columnConfig.span, + child: const Text('R0:C1'), + ); + } return TableViewCell( - columnMergeStart: columnConfig.start, - columnMergeSpan: columnConfig.span, - child: const Text('R0:C1'), + child: Text('R${vicinity.row}:C${vicinity.column}'), ); - } - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + }, + ), ), - ), - ); - await tester.pumpWidget(Container()); - FlutterError.onError = oldHandler; - expect(exceptions.length, 3); - expect( - exceptions.first.toString(), - contains( - 'The merged cell containing (row: 0, column: 1) is ' - 'missing TableSpan information necessary for layout. The ' - 'columnBuilder returned null, signifying the end, at column 8 but ' - 'the merged cell is configured to end with column 10.', - ), - ); - }); + ); + await tester.pumpWidget(const SizedBox()); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(3)); + expect( + exceptions.first.toString(), + contains( + 'The merged cell containing (row: 0, column: 1) is ' + 'missing TableSpan information necessary for layout. The ' + 'columnBuilder returned null, signifying the end, at column 8 but ' + 'the merged cell is configured to end with column 10.', + ), + ); + }, + ); - testWidgets('merged row that exceeds metrics will assert', ( - WidgetTester tester, - ) async { - final exceptions = []; - final FlutterExceptionHandler? oldHandler = FlutterError.onError; - FlutterError.onError = (FlutterErrorDetails details) { - exceptions.add(details.exception); - }; - const ({int start, int span}) rowConfig = (start: 0, span: 10); - final mergedRows = List.generate(10, (int index) => index); - await tester.pumpWidget( - MaterialApp( - home: getTableView( - rowBuilder: (int index) { - // There will only be 8 rows, but the merge is set up for 9. - if (index == 8) { - return null; - } - return largeSpan; - }, - cellBuilder: (_, TableVicinity vicinity) { - // Merged column - if (mergedRows.contains(vicinity.row) && vicinity.column == 0) { + testWidgets( + 'merged row that exceeds metrics will assert', + // The build throws an assertion error which prevents the table from + // properly disposing the elements. + experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), + (WidgetTester tester) async { + final exceptions = []; + final FlutterExceptionHandler? oldHandler = FlutterError.onError; + FlutterError.onError = (FlutterErrorDetails details) { + exceptions.add(details.exception); + }; + const ({int start, int span}) rowConfig = (start: 0, span: 10); + final mergedRows = List.generate(10, (int index) => index); + await tester.pumpWidget( + MaterialApp( + home: getTableView( + rowBuilder: (int index) { + // There will only be 8 rows, but the merge is set up for 9. + if (index == 8) { + return null; + } + return largeSpan; + }, + cellBuilder: (_, TableVicinity vicinity) { + // Merged column + if (mergedRows.contains(vicinity.row) && + vicinity.column == 0) { + return TableViewCell( + rowMergeStart: rowConfig.start, + rowMergeSpan: rowConfig.span, + child: const Text('R0:C0'), + ); + } return TableViewCell( - rowMergeStart: rowConfig.start, - rowMergeSpan: rowConfig.span, - child: const Text('R0:C0'), + child: Text('R${vicinity.row}:C${vicinity.column}'), ); - } - return TableViewCell( - child: Text('R${vicinity.row}:C${vicinity.column}'), - ); - }, + }, + ), ), - ), - ); - await tester.pumpWidget(Container()); - FlutterError.onError = oldHandler; - expect(exceptions.length, 3); - expect( - exceptions.first.toString(), - contains( - 'The merged cell containing (row: 0, column: 0) is ' - 'missing TableSpan information necessary for layout. The ' - 'rowBuilder returned null, signifying the end, at row 8 but ' - 'the merged cell is configured to end with row 9.', - ), - ); - }); + ); + await tester.pumpWidget(const SizedBox()); + FlutterError.onError = oldHandler; + expect(exceptions, hasLength(3)); + expect( + exceptions.first.toString(), + contains( + 'The merged cell containing (row: 0, column: 0) is ' + 'missing TableSpan information necessary for layout. The ' + 'rowBuilder returned null, signifying the end, at row 8 but ' + 'the merged cell is configured to end with row 9.', + ), + ); + }, + ); testWidgets('Binary search correctly finds first/last non-pinned cells', ( WidgetTester tester, @@ -2163,8 +2183,8 @@ void main() { }); group('TableView.list', () { - test('creates correct delegate', () { - final tableView = TableView.list( + testWidgets('creates correct delegate', (WidgetTester tester) async { + final Widget widget = TableView.list( rowBuilder: (_) => span, columnBuilder: (_) => span, cells: const >[ @@ -2172,6 +2192,13 @@ void main() { [cell, cell, cell], ], ); + + await tester.pumpWidget(widget); + + final TableView tableView = tester.widget( + find.byType(TableView), + ); + final delegate = tableView.delegate as TableCellListDelegate; expect(delegate.pinnedRowCount, 0); expect(delegate.pinnedRowCount, 0); @@ -2183,7 +2210,7 @@ void main() { }); test('asserts correct counts', () { - TableView? tableView; + Widget? tableView; expect( () { tableView = TableView.list( @@ -2232,7 +2259,7 @@ void main() { ) async { final childKeys = {}; const span = TableSpan(extent: FixedTableSpanExtent(200)); - final tableView = TableView.builder( + final Widget tableView = TableView.builder( rowCount: 5, columnCount: 5, columnBuilder: (_) => span, @@ -2297,7 +2324,7 @@ void main() { extent: FixedTableSpanExtent(200), padding: TableSpanPadding(leading: 30.0, trailing: 40.0), ); - var tableView = TableView.builder( + Widget tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (_) => columnSpan, @@ -2397,7 +2424,7 @@ void main() { testWidgets('TableSpan gesture hit testing', (WidgetTester tester) async { var tapCounter = 0; // Rows - var tableView = TableView.builder( + Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span, @@ -2573,7 +2600,11 @@ void main() { final rowExtent = TestTableSpanExtent(); final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( rowCount: 10, columnCount: 10, columnBuilder: (_) => TableSpan(extent: columnExtent), @@ -2625,7 +2656,7 @@ void main() { ) async { // Huge padding, first span layout // Column-wise - var tableView = TableView.builder( + Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => const TableSpan( @@ -2696,7 +2727,7 @@ void main() { ) async { // Check with gradually accrued paddings // Column-wise - var tableView = TableView.builder( + Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => @@ -2818,7 +2849,11 @@ void main() { testWidgets('regular layout - no pinning', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span, @@ -2897,7 +2932,11 @@ void main() { // Just pinned rows final verticalController = ScrollController(); final horizontalController = ScrollController(); - var tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + Widget tableView = TableView.builder( rowCount: 50, pinnedRowCount: 1, columnCount: 50, @@ -3139,7 +3178,11 @@ void main() { testWidgets('only paints visible cells', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span, @@ -3206,7 +3249,7 @@ void main() { testWidgets('paints decorations in correct order', ( WidgetTester tester, ) async { - var tableView = TableView.builder( + Widget tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (int index) => TableSpan( @@ -3483,7 +3526,7 @@ void main() { WidgetTester tester, ) async { // Both reversed - Regression test for https://github.com/flutter/flutter/issues/135386 - var tableView = TableView.builder( + Widget tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), horizontalDetails: const ScrollableDetails.horizontal(reverse: true), rowCount: 2, @@ -3578,7 +3621,7 @@ void main() { testWidgets('mouse handling', (WidgetTester tester) async { var enterCounter = 0; var exitCounter = 0; - final tableView = TableView.builder( + final Widget tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span, @@ -3729,7 +3772,11 @@ void main() { testWidgets('Normal axes', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( verticalDetails: ScrollableDetails.vertical( controller: verticalController, ), @@ -3839,7 +3886,11 @@ void main() { testWidgets('Vertical reversed', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( verticalDetails: ScrollableDetails.vertical( reverse: true, controller: verticalController, @@ -3950,7 +4001,11 @@ void main() { testWidgets('Horizontal reversed', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( verticalDetails: ScrollableDetails.vertical( controller: verticalController, ), @@ -4061,7 +4116,11 @@ void main() { testWidgets('Both reversed', (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); - final tableView = TableView.builder( + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + final Widget tableView = TableView.builder( verticalDetails: ScrollableDetails.vertical( reverse: true, controller: verticalController, @@ -4177,13 +4236,17 @@ void main() { (WidgetTester tester) async { final verticalController = ScrollController(); final horizontalController = ScrollController(); + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); final mergedCell = { const TableVicinity(row: 2, column: 2), const TableVicinity(row: 3, column: 2), const TableVicinity(row: 2, column: 3), const TableVicinity(row: 3, column: 3), }; - final tableView = TableView.builder( + final Widget tableView = TableView.builder( columnCount: 10, rowCount: 10, columnBuilder: (_) => @@ -4472,6 +4535,10 @@ void main() { ) async { final horizontalController = ScrollController(); final verticalController = ScrollController(); + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); Widget getTableView({ int? columnCount = 10, From b5eff12f6131f4206959c2593372c274e45a10fe Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 17:31:11 +0800 Subject: [PATCH 4/9] Update version number # Conflicts: # packages/two_dimensional_scrollables/CHANGELOG.md --- packages/two_dimensional_scrollables/CHANGELOG.md | 3 ++- packages/two_dimensional_scrollables/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 0b1efec9e9e4..d5eef5a0044d 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.5.3 * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. +* Fixes memory leaks. ## 0.5.2 diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 72cc9ebed52f..120867520aee 100644 --- a/packages/two_dimensional_scrollables/pubspec.yaml +++ b/packages/two_dimensional_scrollables/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_scrollables description: Widgets that scroll using the two dimensional scrolling foundation. -version: 0.5.2 +version: 0.5.3 repository: https://github.com/flutter/packages/tree/main/packages/two_dimensional_scrollables issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+two_dimensional_scrollables%22+ From 90f11e69942afaaca089c95b491fcf11693329fc Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 18:04:52 +0800 Subject: [PATCH 5/9] Add memory leak test in example --- .../example/pubspec.yaml | 5 +++-- .../example/test/flutter_test_config.dart | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 packages/two_dimensional_scrollables/example/test/flutter_test_config.dart diff --git a/packages/two_dimensional_scrollables/example/pubspec.yaml b/packages/two_dimensional_scrollables/example/pubspec.yaml index c206590b70ce..41199501d219 100644 --- a/packages/two_dimensional_scrollables/example/pubspec.yaml +++ b/packages/two_dimensional_scrollables/example/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_examples -description: 'A sample application that uses TableView and TreeView' -publish_to: 'none' +description: "A sample application that uses TableView and TreeView" +publish_to: "none" # The following defines the version and build number for your application. version: 2.0.0 @@ -18,6 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + leak_tracker_flutter_testing: any # The following section is specific to Flutter packages. flutter: diff --git a/packages/two_dimensional_scrollables/example/test/flutter_test_config.dart b/packages/two_dimensional_scrollables/example/test/flutter_test_config.dart new file mode 100644 index 000000000000..d1eb77755d23 --- /dev/null +++ b/packages/two_dimensional_scrollables/example/test/flutter_test_config.dart @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; + +Future testExecutable(FutureOr Function() testMain) async { + LeakTesting.enable(); + LeakTracking.warnForUnsupportedPlatforms = false; + await testMain(); +} From ac4466b7df1f0d3f2ecf8575f079b49a1508b642 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 18:05:19 +0800 Subject: [PATCH 6/9] Gemini comments --- .../lib/src/table_view/table.dart | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 693a1127640d..3dedbb3ee6cf 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -393,6 +393,8 @@ class __TableViewBuilderState extends State<_TableViewBuilder> { cacheExtent: widget.cacheExtent, diagonalDragBehavior: widget.diagonalDragBehavior, dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + clipBehavior: widget.clipBehavior, delegate: _delegate, alignment: widget.alignment, ); @@ -463,6 +465,30 @@ class _TableViewListState extends State<_TableViewList> { ); } + @override + void didUpdateWidget(_TableViewList oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.pinnedColumnCount != widget.pinnedColumnCount || + oldWidget.pinnedRowCount != widget.pinnedRowCount || + oldWidget.trailingPinnedColumnCount != + widget.trailingPinnedColumnCount || + oldWidget.trailingPinnedRowCount != widget.trailingPinnedRowCount || + oldWidget.cells != widget.cells || + oldWidget.columnBuilder != widget.columnBuilder || + oldWidget.rowBuilder != widget.rowBuilder) { + _delegate.dispose(); + _delegate = TableCellListDelegate( + pinnedColumnCount: widget.pinnedColumnCount, + pinnedRowCount: widget.pinnedRowCount, + trailingPinnedColumnCount: widget.trailingPinnedColumnCount, + trailingPinnedRowCount: widget.trailingPinnedRowCount, + cells: widget.cells, + columnBuilder: widget.columnBuilder, + rowBuilder: widget.rowBuilder, + ); + } + } + @override void dispose() { _delegate.dispose(); @@ -479,7 +505,10 @@ class _TableViewListState extends State<_TableViewList> { cacheExtent: widget.cacheExtent, diagonalDragBehavior: widget.diagonalDragBehavior, dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + clipBehavior: widget.clipBehavior, delegate: _delegate, + alignment: widget.alignment, ); } } From 5defacb025d8a3ec125123027c6da02c5cb535b1 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 May 2026 18:10:50 +0800 Subject: [PATCH 7/9] Fix tests --- .../example/lib/table_view/simple_table.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart b/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart index 855d35a17fe0..b07e9b1f55ee 100644 --- a/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart +++ b/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart @@ -51,6 +51,7 @@ class _TableExampleState extends State { ), ) : TableView.builder( + key: ValueKey(_selectionMode), verticalDetails: ScrollableDetails.vertical( controller: _verticalController, ), From 8a912ebc7d397edc81981cb5c02a9d221ced3a21 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 4 Jun 2026 16:13:59 +0800 Subject: [PATCH 8/9] Fix more memory leaks in tests --- .../test/tree_view/render_tree_test.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/two_dimensional_scrollables/test/tree_view/render_tree_test.dart b/packages/two_dimensional_scrollables/test/tree_view/render_tree_test.dart index 08c41d65a85d..cd65d2d556cf 100644 --- a/packages/two_dimensional_scrollables/test/tree_view/render_tree_test.dart +++ b/packages/two_dimensional_scrollables/test/tree_view/render_tree_test.dart @@ -241,7 +241,7 @@ void main() { expect(horizontalController.position.maxScrollExtent, 90.0); // Collapse a node. The horizontal extent should change to zero, - // and the position should corrrect. + // and the position should correct. treeController.toggleNode(treeController.getNodeFor('Third')!); await tester.pumpAndSettle(); expect(horizontalController.position.pixels, 0.0); @@ -715,6 +715,7 @@ void main() { var rows = 10; late StateSetter setState; final controller = ScrollController(); + addTearDown(controller.dispose); await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -772,6 +773,7 @@ void main() { ) async { final treeController = TreeViewController(); final scrollController = ScrollController(); + addTearDown(scrollController.dispose); // Setup a TreeView with one expanded root node and one child node. final treeNodes = >[ From f3b09825bd48485ac390afd4da8bf3fd80a816f1 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Mon, 8 Jun 2026 18:41:47 +0800 Subject: [PATCH 9/9] Fix breaking changes --- .../example/pubspec.yaml | 4 +- .../lib/src/table_view/table.dart | 613 ++++++++++-------- .../test/table_view/table_span_test.dart | 24 +- .../test/table_view/table_test.dart | 34 +- 4 files changed, 368 insertions(+), 307 deletions(-) diff --git a/packages/two_dimensional_scrollables/example/pubspec.yaml b/packages/two_dimensional_scrollables/example/pubspec.yaml index 41199501d219..cf96f3919584 100644 --- a/packages/two_dimensional_scrollables/example/pubspec.yaml +++ b/packages/two_dimensional_scrollables/example/pubspec.yaml @@ -1,6 +1,6 @@ name: two_dimensional_examples -description: "A sample application that uses TableView and TreeView" -publish_to: "none" +description: 'A sample application that uses TableView and TreeView' +publish_to: 'none' # The following defines the version and build number for your application. version: 2.0.0 diff --git a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart index 4ec0d0cdfbe4..a4e3f203a691 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -105,58 +105,68 @@ import 'table_span.dart'; /// /// * [TableSpan], describes the configuration for a row or column in the /// TableView. -/// * [TwoDimensionalScrollView], the super class that is extended by TableView. +/// * [TwoDimensionalScrollView], a scroll view that can scroll in two +/// dimensions. /// * [GridView], another scrolling widget that can be used to create tables /// that scroll in one dimension. -class TableView extends TwoDimensionalScrollView { +class TableView extends StatefulWidget { /// Creates a [TableView] that scrolls in both dimensions. /// /// A non-null [delegate] must be provided. const TableView({ super.key, - super.primary, - super.mainAxis, - super.horizontalDetails, - super.verticalDetails, - super.cacheExtent, - required TableCellDelegateMixin super.delegate, - super.diagonalDragBehavior = DiagonalDragBehavior.none, - super.dragStartBehavior, - super.keyboardDismissBehavior, - super.clipBehavior, + this.primary, + this.mainAxis = Axis.vertical, + this.verticalDetails = const ScrollableDetails.vertical(), + this.horizontalDetails = const ScrollableDetails.horizontal(), + @Deprecated( + 'Use scrollCacheExtent instead. ' + 'This feature was deprecated after v3.41.0-0.0.pre.', + ) + this.cacheExtent, + required TableCellDelegateMixin this.delegate, + this.diagonalDragBehavior = DiagonalDragBehavior.none, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.clipBehavior = Clip.hardEdge, this.alignment = Alignment.topLeft, - }); + }) : _buildDelegateParameters = null; /// Creates a [TableView] of widgets that are created on demand. /// - /// This is appropriate for table views with a large number of cells because - /// the [cellBuilder] is called only for those cells that are actually - /// visible. + /// This constructor is appropriate for table views with a large + /// number of cells because the [cellBuilder] is called only for those + /// cells that are actually visible. /// - /// This generates a [TableCellBuilderDelegate] for building children on - /// demand using the required [cellBuilder], [columnBuilder], and - /// [rowBuilder]. + /// This constructor generates a [TableCellBuilderDelegate] for building + /// children on demand using the required [cellBuilder], + /// [columnBuilder], and [rowBuilder]. /// /// For infinite rows and columns, omit providing [columnCount] or [rowCount]. - /// Returning null from the [columnBuilder] or [rowBuilder] will terminate the - /// row or column at that index, representing the end of the table in that + /// Returning null from the [columnBuilder] or [rowBuilder] will terminate + /// the row or column at that index, representing the end of the table in that /// axis. In this scenario, until the potential end of the table in either /// dimension is reached by returning null, the /// [ScrollPosition.maxScrollExtent] will reflect [double.infinity]. This is /// because as the table is built lazily, it will not know the end has been /// reached until the [ScrollPosition] arrives there. This is similar to /// returning null from [ListView.builder] to signify the end of the list. - static Widget builder({ - Key? key, - bool? primary, - Axis mainAxis = Axis.vertical, - ScrollableDetails horizontalDetails = const ScrollableDetails.horizontal(), - ScrollableDetails verticalDetails = const ScrollableDetails.vertical(), - double? cacheExtent, - DiagonalDragBehavior diagonalDragBehavior = DiagonalDragBehavior.none, - DragStartBehavior dragStartBehavior = DragStartBehavior.start, - ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, - Clip clipBehavior = Clip.hardEdge, + TableView.builder({ + super.key, + + this.primary, + this.mainAxis = Axis.vertical, + this.verticalDetails = const ScrollableDetails.vertical(), + this.horizontalDetails = const ScrollableDetails.horizontal(), + @Deprecated( + 'Use scrollCacheExtent instead. ' + 'This feature was deprecated after v3.41.0-0.0.pre.', + ) + this.cacheExtent, + this.diagonalDragBehavior = DiagonalDragBehavior.none, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior, + this.clipBehavior = Clip.hardEdge, int pinnedRowCount = 0, int pinnedColumnCount = 0, int trailingPinnedRowCount = 0, @@ -166,219 +176,228 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, required TableViewCellBuilder cellBuilder, - AlignmentGeometry alignment = Alignment.topLeft, - }) { - return _TableViewBuilder( - key: key, - primary: primary, - mainAxis: mainAxis, - horizontalDetails: horizontalDetails, - verticalDetails: verticalDetails, - cacheExtent: cacheExtent, - diagonalDragBehavior: diagonalDragBehavior, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - clipBehavior: clipBehavior, - pinnedRowCount: pinnedRowCount, - pinnedColumnCount: pinnedColumnCount, - trailingPinnedRowCount: trailingPinnedRowCount, - trailingPinnedColumnCount: trailingPinnedColumnCount, - columnCount: columnCount, - rowCount: rowCount, - columnBuilder: columnBuilder, - rowBuilder: rowBuilder, - cellBuilder: cellBuilder, - alignment: alignment, - ); - } + this.alignment = Alignment.topLeft, + }) : assert(pinnedRowCount >= 0), + assert(trailingPinnedRowCount >= 0), + assert(rowCount == null || rowCount >= 0), + assert(rowCount == null || rowCount >= pinnedRowCount + trailingPinnedRowCount), + assert(columnCount == null || columnCount >= 0), + assert(pinnedColumnCount >= 0), + assert(trailingPinnedColumnCount >= 0), + assert(columnCount == null || columnCount >= pinnedColumnCount + trailingPinnedColumnCount), + delegate = null, + _buildDelegateParameters = _TableCellBuilderDelegateParameters( + columnCount: columnCount, + rowCount: rowCount, + pinnedColumnCount: pinnedColumnCount, + pinnedRowCount: pinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + cellBuilder: cellBuilder, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + ); /// Creates a [TableView] from an explicit two dimensional array of children. /// - /// This is appropriate for list views with a small number of children because - /// constructing the [List] requires doing work for every child that could - /// possibly be displayed in the list view instead of just those children that - /// are actually visible. + /// This constructor is appropriate for list views with a small number of + /// children because constructing the [List] requires doing work for every + /// child that could possibly be displayed in the list view instead of just + /// those children that are actually visible. /// /// The [children] are accessed for each [TableVicinity.column] and /// [TableVicinity.row] of the [TwoDimensionalViewport] as /// `children[vicinity.column][vicinity.row]`. - static Widget list({ - Key? key, - bool? primary, - Axis mainAxis = Axis.vertical, - ScrollableDetails horizontalDetails = const ScrollableDetails.horizontal(), - ScrollableDetails verticalDetails = const ScrollableDetails.vertical(), - double? cacheExtent, - DiagonalDragBehavior diagonalDragBehavior = DiagonalDragBehavior.none, - DragStartBehavior dragStartBehavior = DragStartBehavior.start, - ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, - Clip clipBehavior = Clip.hardEdge, - int pinnedRowCount = 0, - int pinnedColumnCount = 0, - int trailingPinnedRowCount = 0, - int trailingPinnedColumnCount = 0, - required TableSpanBuilder columnBuilder, - required TableSpanBuilder rowBuilder, - List> cells = const >[], - AlignmentGeometry alignment = Alignment.topLeft, - }) { - return _TableViewList( - key: key, - primary: primary, - mainAxis: mainAxis, - horizontalDetails: horizontalDetails, - verticalDetails: verticalDetails, - cacheExtent: cacheExtent, - diagonalDragBehavior: diagonalDragBehavior, - dragStartBehavior: dragStartBehavior, - keyboardDismissBehavior: keyboardDismissBehavior, - clipBehavior: clipBehavior, - pinnedRowCount: pinnedRowCount, - pinnedColumnCount: pinnedColumnCount, - trailingPinnedRowCount: trailingPinnedRowCount, - trailingPinnedColumnCount: trailingPinnedColumnCount, - columnBuilder: columnBuilder, - rowBuilder: rowBuilder, - cells: cells, - alignment: alignment, - ); - } - - /// The alignment of the table within the viewport when there is extra space. - /// - /// Defaults to [Alignment.topLeft]. - final AlignmentGeometry alignment; - - @override - TableViewport buildViewport( - BuildContext context, - ViewportOffset verticalOffset, - ViewportOffset horizontalOffset, - ) { - return TableViewport( - verticalOffset: verticalOffset, - verticalAxisDirection: verticalDetails.direction, - horizontalOffset: horizontalOffset, - horizontalAxisDirection: horizontalDetails.direction, - delegate: delegate as TableCellDelegateMixin, - mainAxis: mainAxis, - cacheExtent: cacheExtent, - clipBehavior: clipBehavior, - alignment: alignment, - ); - } -} - -class _TableViewBuilder extends StatefulWidget { - const _TableViewBuilder({ + TableView.list({ super.key, this.primary, this.mainAxis = Axis.vertical, - this.horizontalDetails = const ScrollableDetails.horizontal(), this.verticalDetails = const ScrollableDetails.vertical(), + this.horizontalDetails = const ScrollableDetails.horizontal(), + @Deprecated( + 'Use scrollCacheExtent instead. ' + 'This feature was deprecated after v3.41.0-0.0.pre.', + ) this.cacheExtent, this.diagonalDragBehavior = DiagonalDragBehavior.none, this.dragStartBehavior = DragStartBehavior.start, this.keyboardDismissBehavior, this.clipBehavior = Clip.hardEdge, - this.pinnedRowCount = 0, - this.pinnedColumnCount = 0, - this.trailingPinnedRowCount = 0, - this.trailingPinnedColumnCount = 0, - this.columnCount, - this.rowCount, - required this.columnBuilder, - required this.rowBuilder, - required this.cellBuilder, + int pinnedRowCount = 0, + int pinnedColumnCount = 0, + int trailingPinnedRowCount = 0, + int trailingPinnedColumnCount = 0, + required TableSpanBuilder columnBuilder, + required TableSpanBuilder rowBuilder, + List> cells = const >[], this.alignment = Alignment.topLeft, }) : assert(pinnedRowCount >= 0), - assert(trailingPinnedRowCount >= 0), - assert(rowCount == null || rowCount >= 0), - assert(rowCount == null || rowCount >= pinnedRowCount + trailingPinnedRowCount), - assert(columnCount == null || columnCount >= 0), assert(pinnedColumnCount >= 0), + assert(trailingPinnedRowCount >= 0), assert(trailingPinnedColumnCount >= 0), - assert(columnCount == null || columnCount >= pinnedColumnCount + trailingPinnedColumnCount); - + delegate = null, + + _buildDelegateParameters = _TableCellListDelegateParameters( + pinnedColumnCount: pinnedColumnCount, + pinnedRowCount: pinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + cells: cells, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + ); + + /// {@macro flutter.widgets.scroll_view.primary} final bool? primary; + + /// The main axis of the two. + /// + /// Used to determine how to apply [primary] when true. + /// + /// This value should also be provided to the subclass of + /// [TwoDimensionalViewport], where it is used to determine paint order of + /// children. final Axis mainAxis; + + /// The configuration of the horizontal Scrollable. + /// + /// These [ScrollableDetails] can be used to set the [AxisDirection], + /// [ScrollController], [ScrollPhysics] and more for the horizontal axis. final ScrollableDetails horizontalDetails; + + /// The configuration of the vertical Scrollable. + /// + /// These [ScrollableDetails] can be used to set the [AxisDirection], + /// [ScrollController], [ScrollPhysics] and more for the vertical axis. final ScrollableDetails verticalDetails; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + @Deprecated( + 'Use scrollCacheExtent instead. ' + 'This feature was deprecated after v3.41.0-0.0.pre.', + ) final double? cacheExtent; + + /// The alignment of the table within the viewport when there is extra space. + /// + /// Defaults to [Alignment.topLeft]. + + /// A delegate that provides the children for the [TwoDimensionalScrollView]. + final TableCellDelegateMixin? delegate; + + final _TableCellDelegateParameters? _buildDelegateParameters; + + /// Whether scrolling gestures should lock to one axes, allow free movement + /// in both axes, or be evaluated on a weighted scale. + /// + /// Defaults to [DiagonalDragBehavior.none], locking axes to receive input one + /// at a time. final DiagonalDragBehavior diagonalDragBehavior; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} final DragStartBehavior dragStartBehavior; + + /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} + /// + /// If [keyboardDismissBehavior] is null then it will fallback to the inherited + /// [ScrollBehavior.getKeyboardDismissBehavior]. final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. final Clip clipBehavior; - final int pinnedRowCount; - final int pinnedColumnCount; - final int trailingPinnedRowCount; - final int trailingPinnedColumnCount; - final int? columnCount; - final int? rowCount; - final TableSpanBuilder columnBuilder; - final TableSpanBuilder rowBuilder; - final TableViewCellBuilder cellBuilder; + + /// The alignment of the table within the viewport when there is extra space. + /// + /// Defaults to [Alignment.topLeft]. final AlignmentGeometry alignment; @override - State<_TableViewBuilder> createState() => __TableViewBuilderState(); + State createState() => _TableViewState(); } -class __TableViewBuilderState extends State<_TableViewBuilder> { - late TableCellBuilderDelegate _delegate; +class _TableViewState extends State { + late TableCellDelegateMixin _delegate; + + TableCellDelegateMixin _buildDelegate() { + if (widget.delegate != null) { + return widget.delegate!; + } + return switch (widget._buildDelegateParameters) { + _TableCellBuilderDelegateParameters( + :final int? columnCount, + :final int? rowCount, + :final int pinnedColumnCount, + :final int pinnedRowCount, + :final int trailingPinnedColumnCount, + :final int trailingPinnedRowCount, + :final TableViewCellBuilder cellBuilder, + :final TableSpanBuilder columnBuilder, + :final TableSpanBuilder rowBuilder, + ) => + TableCellBuilderDelegate( + columnCount: columnCount, + rowCount: rowCount, + pinnedColumnCount: pinnedColumnCount, + pinnedRowCount: pinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + cellBuilder: cellBuilder, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + ), + _TableCellListDelegateParameters( + :final int pinnedColumnCount, + :final int pinnedRowCount, + :final int trailingPinnedColumnCount, + :final int trailingPinnedRowCount, + :final List> cells, + :final TableSpanBuilder columnBuilder, + :final TableSpanBuilder rowBuilder, + ) => + TableCellListDelegate( + pinnedColumnCount: pinnedColumnCount, + pinnedRowCount: pinnedRowCount, + trailingPinnedColumnCount: trailingPinnedColumnCount, + trailingPinnedRowCount: trailingPinnedRowCount, + cells: cells, + columnBuilder: columnBuilder, + rowBuilder: rowBuilder, + ), + null => throw ArgumentError('Either a delegate or delegate parameters must be provided.'), + }; + } @override void initState() { super.initState(); - _delegate = TableCellBuilderDelegate( - columnCount: widget.columnCount, - rowCount: widget.rowCount, - pinnedColumnCount: widget.pinnedColumnCount, - pinnedRowCount: widget.pinnedRowCount, - trailingPinnedColumnCount: widget.trailingPinnedColumnCount, - trailingPinnedRowCount: widget.trailingPinnedRowCount, - cellBuilder: widget.cellBuilder, - columnBuilder: widget.columnBuilder, - rowBuilder: widget.rowBuilder, - ); + _delegate = _buildDelegate(); } @override - void didUpdateWidget(_TableViewBuilder oldWidget) { + void didUpdateWidget(TableView oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.columnCount != oldWidget.columnCount || - widget.rowCount != oldWidget.rowCount || - widget.pinnedColumnCount != oldWidget.pinnedColumnCount || - widget.pinnedRowCount != oldWidget.pinnedRowCount || - widget.trailingPinnedColumnCount != oldWidget.trailingPinnedColumnCount || - widget.trailingPinnedRowCount != oldWidget.trailingPinnedRowCount || - widget.cellBuilder != oldWidget.cellBuilder || - widget.columnBuilder != oldWidget.columnBuilder || - widget.rowBuilder != oldWidget.rowBuilder) { - _delegate.dispose(); - _delegate = TableCellBuilderDelegate( - columnCount: widget.columnCount, - rowCount: widget.rowCount, - pinnedColumnCount: widget.pinnedColumnCount, - pinnedRowCount: widget.pinnedRowCount, - trailingPinnedColumnCount: widget.trailingPinnedColumnCount, - trailingPinnedRowCount: widget.trailingPinnedRowCount, - cellBuilder: widget.cellBuilder, - columnBuilder: widget.columnBuilder, - rowBuilder: widget.rowBuilder, - ); + if (widget.delegate != oldWidget.delegate || + widget._buildDelegateParameters != oldWidget._buildDelegateParameters) { + if (oldWidget.delegate == null) { + _delegate.dispose(); + } + _delegate = _buildDelegate(); } } @override void dispose() { - _delegate.dispose(); + if (widget.delegate == null) { + _delegate.dispose(); + } super.dispose(); } @override Widget build(BuildContext context) { - return TableView( + return _TableView( primary: widget.primary, mainAxis: widget.mainAxis, horizontalDetails: widget.horizontalDetails, @@ -394,113 +413,151 @@ class __TableViewBuilderState extends State<_TableViewBuilder> { } } -class _TableViewList extends StatefulWidget { - const _TableViewList({ - super.key, - this.primary, - this.mainAxis = Axis.vertical, - this.horizontalDetails = const ScrollableDetails.horizontal(), - this.verticalDetails = const ScrollableDetails.vertical(), - this.cacheExtent, - this.diagonalDragBehavior = DiagonalDragBehavior.none, - this.dragStartBehavior = DragStartBehavior.start, - this.keyboardDismissBehavior, - this.clipBehavior = Clip.hardEdge, - this.pinnedRowCount = 0, - this.pinnedColumnCount = 0, - this.trailingPinnedRowCount = 0, - this.trailingPinnedColumnCount = 0, +@immutable +sealed class _TableCellDelegateParameters { + const _TableCellDelegateParameters(); +} + +class _TableCellBuilderDelegateParameters extends _TableCellDelegateParameters { + const _TableCellBuilderDelegateParameters({ + required this.columnCount, + required this.rowCount, + required this.pinnedColumnCount, + required this.pinnedRowCount, + required this.trailingPinnedColumnCount, + required this.trailingPinnedRowCount, + required this.cellBuilder, required this.columnBuilder, required this.rowBuilder, - this.cells = const >[], - this.alignment = Alignment.topLeft, - }) : assert(pinnedRowCount >= 0), - assert(pinnedColumnCount >= 0), - assert(trailingPinnedRowCount >= 0), - assert(trailingPinnedColumnCount >= 0); + }); - final bool? primary; - final Axis mainAxis; - final ScrollableDetails horizontalDetails; - final ScrollableDetails verticalDetails; - final double? cacheExtent; - final DiagonalDragBehavior diagonalDragBehavior; - final DragStartBehavior dragStartBehavior; - final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; - final Clip clipBehavior; - final int pinnedRowCount; + final int? columnCount; + final int? rowCount; final int pinnedColumnCount; - final int trailingPinnedRowCount; + final int pinnedRowCount; final int trailingPinnedColumnCount; + final int trailingPinnedRowCount; + final TableViewCellBuilder cellBuilder; final TableSpanBuilder columnBuilder; final TableSpanBuilder rowBuilder; - final AlignmentGeometry alignment; - final List> cells; @override - State<_TableViewList> createState() => _TableViewListState(); -} - -class _TableViewListState extends State<_TableViewList> { - late TableCellListDelegate _delegate; + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _TableCellBuilderDelegateParameters && + other.columnCount == columnCount && + other.rowCount == rowCount && + other.pinnedColumnCount == pinnedColumnCount && + other.pinnedRowCount == pinnedRowCount && + other.trailingPinnedColumnCount == trailingPinnedColumnCount && + other.trailingPinnedRowCount == trailingPinnedRowCount && + other.cellBuilder == cellBuilder && + other.columnBuilder == columnBuilder && + other.rowBuilder == rowBuilder; + } @override - void initState() { - super.initState(); - _delegate = TableCellListDelegate( - pinnedColumnCount: widget.pinnedColumnCount, - pinnedRowCount: widget.pinnedRowCount, - trailingPinnedColumnCount: widget.trailingPinnedColumnCount, - trailingPinnedRowCount: widget.trailingPinnedRowCount, - cells: widget.cells, - columnBuilder: widget.columnBuilder, - rowBuilder: widget.rowBuilder, + int get hashCode { + return Object.hash( + columnCount, + rowCount, + pinnedColumnCount, + pinnedRowCount, + trailingPinnedColumnCount, + trailingPinnedRowCount, + cellBuilder, + columnBuilder, + rowBuilder, ); } +} + +class _TableCellListDelegateParameters extends _TableCellDelegateParameters { + const _TableCellListDelegateParameters({ + required this.pinnedColumnCount, + required this.pinnedRowCount, + required this.trailingPinnedColumnCount, + required this.trailingPinnedRowCount, + required this.cells, + required this.columnBuilder, + required this.rowBuilder, + }); + + final int pinnedRowCount; + final int pinnedColumnCount; + final int trailingPinnedRowCount; + final int trailingPinnedColumnCount; + final TableSpanBuilder columnBuilder; + final TableSpanBuilder rowBuilder; + final List> cells; @override - void didUpdateWidget(_TableViewList oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.pinnedColumnCount != widget.pinnedColumnCount || - oldWidget.pinnedRowCount != widget.pinnedRowCount || - oldWidget.trailingPinnedColumnCount != widget.trailingPinnedColumnCount || - oldWidget.trailingPinnedRowCount != widget.trailingPinnedRowCount || - oldWidget.cells != widget.cells || - oldWidget.columnBuilder != widget.columnBuilder || - oldWidget.rowBuilder != widget.rowBuilder) { - _delegate.dispose(); - _delegate = TableCellListDelegate( - pinnedColumnCount: widget.pinnedColumnCount, - pinnedRowCount: widget.pinnedRowCount, - trailingPinnedColumnCount: widget.trailingPinnedColumnCount, - trailingPinnedRowCount: widget.trailingPinnedRowCount, - cells: widget.cells, - columnBuilder: widget.columnBuilder, - rowBuilder: widget.rowBuilder, - ); + bool operator ==(Object other) { + if (identical(this, other)) { + return true; } + return other is _TableCellListDelegateParameters && + other.pinnedColumnCount == pinnedColumnCount && + other.pinnedRowCount == pinnedRowCount && + other.trailingPinnedColumnCount == trailingPinnedColumnCount && + other.trailingPinnedRowCount == trailingPinnedRowCount && + listEquals(other.cells, cells) && + other.columnBuilder == columnBuilder && + other.rowBuilder == rowBuilder; } @override - void dispose() { - _delegate.dispose(); - super.dispose(); + int get hashCode { + return Object.hash( + pinnedColumnCount, + pinnedRowCount, + trailingPinnedColumnCount, + trailingPinnedRowCount, + Object.hashAll(cells), + columnBuilder, + rowBuilder, + ); } +} + +class _TableView extends TwoDimensionalScrollView { + const _TableView({ + super.primary, + super.mainAxis, + super.horizontalDetails, + super.verticalDetails, + super.cacheExtent, + required TableCellDelegateMixin super.delegate, + super.diagonalDragBehavior = DiagonalDragBehavior.none, + super.dragStartBehavior, + super.keyboardDismissBehavior, + super.clipBehavior, + this.alignment = Alignment.topLeft, + }); + + /// The alignment of the table within the viewport when there is extra space. + /// + /// Defaults to [Alignment.topLeft]. + final AlignmentGeometry alignment; @override - Widget build(BuildContext context) { - return TableView( - primary: widget.primary, - mainAxis: widget.mainAxis, - horizontalDetails: widget.horizontalDetails, - verticalDetails: widget.verticalDetails, - cacheExtent: widget.cacheExtent, - diagonalDragBehavior: widget.diagonalDragBehavior, - dragStartBehavior: widget.dragStartBehavior, - keyboardDismissBehavior: widget.keyboardDismissBehavior, - clipBehavior: widget.clipBehavior, - delegate: _delegate, - alignment: widget.alignment, + TableViewport buildViewport( + BuildContext context, + ViewportOffset verticalOffset, + ViewportOffset horizontalOffset, + ) { + return TableViewport( + verticalOffset: verticalOffset, + verticalAxisDirection: verticalDetails.direction, + horizontalOffset: horizontalOffset, + horizontalAxisDirection: horizontalDetails.direction, + delegate: delegate as TableCellDelegateMixin, + mainAxis: mainAxis, + cacheExtent: cacheExtent, + clipBehavior: clipBehavior, + alignment: alignment, ); } } diff --git a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart index 8b498933dc15..d6ecb97d35af 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_span_test.dart @@ -186,7 +186,7 @@ void main() { } testWidgets('Vertical main axis, vertical reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( verticalDetails: ScrollableDetails.vertical(controller: verticalController, reverse: true), horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController), rowCount: 10, @@ -280,7 +280,7 @@ void main() { }); testWidgets('Vertical main axis, horizontal reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( verticalDetails: ScrollableDetails.vertical(controller: verticalController), horizontalDetails: ScrollableDetails.horizontal( controller: horizontalController, @@ -374,7 +374,7 @@ void main() { }); testWidgets('Vertical main axis, both reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( verticalDetails: ScrollableDetails.vertical(controller: verticalController, reverse: true), horizontalDetails: ScrollableDetails.horizontal( controller: horizontalController, @@ -474,7 +474,7 @@ void main() { }); testWidgets('Horizontal main axis, vertical reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical(controller: verticalController, reverse: true), horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController), @@ -569,7 +569,7 @@ void main() { }); testWidgets('Horizontal main axis, horizontal reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical(controller: verticalController), horizontalDetails: ScrollableDetails.horizontal( @@ -664,7 +664,7 @@ void main() { }); testWidgets('Horizontal main axis, both reversed', (WidgetTester tester) async { - final Widget table = TableView.builder( + final table = TableView.builder( mainAxis: Axis.horizontal, verticalDetails: ScrollableDetails.vertical(controller: verticalController, reverse: true), horizontalDetails: ScrollableDetails.horizontal( @@ -768,7 +768,7 @@ void main() { WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final Widget tableView = TableView.builder( + final tableView = TableView.builder( horizontalDetails: const ScrollableDetails.horizontal(reverse: true), rowCount: 1, columnCount: 1, @@ -809,7 +809,7 @@ void main() { WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final Widget tableView = TableView.builder( + final tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), rowCount: 1, columnCount: 1, @@ -850,7 +850,7 @@ void main() { 'TableView row decoration rect is correct when vertical axis is reversed and padding is used', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/177117 - final Widget tableView = TableView.builder( + final tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), rowCount: 1, columnCount: 1, @@ -923,7 +923,7 @@ void main() { const TableVicinity(row: 3, column: 2): (2, 2), }; - Widget buildScenario1({bool reverseVertical = false, bool reverseHorizontal = false}) { + TableView buildScenario1({bool reverseVertical = false, bool reverseHorizontal = false}) { return TableView.builder( verticalDetails: ScrollableDetails.vertical(reverse: reverseVertical), horizontalDetails: ScrollableDetails.horizontal(reverse: reverseHorizontal), @@ -978,7 +978,7 @@ void main() { const TableVicinity(row: 2, column: 3): (2, 2), }; - Widget buildScenario2({bool reverseVertical = false, bool reverseHorizontal = false}) { + TableView buildScenario2({bool reverseVertical = false, bool reverseHorizontal = false}) { return TableView.builder( verticalDetails: ScrollableDetails.vertical(reverse: reverseVertical), horizontalDetails: ScrollableDetails.horizontal(reverse: reverseHorizontal), @@ -1048,7 +1048,7 @@ void main() { const TableVicinity(row: 2, column: 3): (2, 2), }; - Widget buildScenario3({ + TableView buildScenario3({ Axis mainAxis = Axis.vertical, bool reverseVertical = false, bool reverseHorizontal = false, diff --git a/packages/two_dimensional_scrollables/test/table_view/table_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index a7a3da8e9fe4..8a507699b96a 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -42,7 +42,7 @@ TableSpan getMouseTrackingSpan( void main() { group('TableView.builder', () { testWidgets('creates correct delegate', (WidgetTester tester) async { - final Widget widget = TableView.builder( + final widget = TableView.builder( columnCount: 3, rowCount: 2, rowBuilder: (_) => span, @@ -52,7 +52,9 @@ void main() { await tester.pumpWidget(widget); - final TableView tableView = tester.widget(find.byType(TableView)); + final TwoDimensionalScrollView tableView = tester.widget( + find.byWidgetPredicate((widget) => widget is TwoDimensionalScrollView), + ); final delegate = tableView.delegate as TableCellBuilderDelegate; expect(delegate.pinnedRowCount, 0); @@ -65,7 +67,7 @@ void main() { }); test('asserts correct counts', () { - Widget? tableView; + TableView? tableView; expect( () { tableView = TableView.builder( @@ -196,7 +198,7 @@ void main() { horizontalController.dispose(); }); - Widget getTableView({ + TableView getTableView({ int? columnCount, int? rowCount, TableSpanBuilder? columnBuilder, @@ -1940,7 +1942,7 @@ void main() { group('TableView.list', () { testWidgets('creates correct delegate', (WidgetTester tester) async { - final Widget widget = TableView.list( + final widget = TableView.list( rowBuilder: (_) => span, columnBuilder: (_) => span, cells: const >[ @@ -1951,7 +1953,9 @@ void main() { await tester.pumpWidget(widget); - final TableView tableView = tester.widget(find.byType(TableView)); + final TwoDimensionalScrollView tableView = tester.widget( + find.byWidgetPredicate((widget) => widget is TwoDimensionalScrollView), + ); final delegate = tableView.delegate as TableCellListDelegate; expect(delegate.pinnedRowCount, 0); @@ -1964,7 +1968,7 @@ void main() { }); test('asserts correct counts', () { - Widget? tableView; + TableView? tableView; expect( () { tableView = TableView.list( @@ -2011,7 +2015,7 @@ void main() { testWidgets('parent data and table vicinities', (WidgetTester tester) async { final childKeys = {}; const span = TableSpan(extent: FixedTableSpanExtent(200)); - final Widget tableView = TableView.builder( + final tableView = TableView.builder( rowCount: 5, columnCount: 5, columnBuilder: (_) => span, @@ -2071,7 +2075,7 @@ void main() { extent: FixedTableSpanExtent(200), padding: TableSpanPadding(leading: 30.0, trailing: 40.0), ); - Widget tableView = TableView.builder( + var tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (_) => columnSpan, @@ -2164,7 +2168,7 @@ void main() { testWidgets('TableSpan gesture hit testing', (WidgetTester tester) async { var tapCounter = 0; // Rows - Widget tableView = TableView.builder( + var tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span, @@ -2385,7 +2389,7 @@ void main() { testWidgets('First row/column layout based on padding', (WidgetTester tester) async { // Huge padding, first span layout // Column-wise - Widget tableView = TableView.builder( + var tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => const TableSpan( @@ -2454,7 +2458,7 @@ void main() { testWidgets('lazy layout accounts for gradually accrued padding', (WidgetTester tester) async { // Check with gradually accrued paddings // Column-wise - Widget tableView = TableView.builder( + var tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => const TableSpan(extent: FixedTableSpanExtent(200)), @@ -2947,7 +2951,7 @@ void main() { }); testWidgets('paints decorations in correct order', (WidgetTester tester) async { - Widget tableView = TableView.builder( + var tableView = TableView.builder( rowCount: 2, columnCount: 2, columnBuilder: (int index) => TableSpan( @@ -3191,7 +3195,7 @@ void main() { WidgetTester tester, ) async { // Both reversed - Regression test for https://github.com/flutter/flutter/issues/135386 - Widget tableView = TableView.builder( + var tableView = TableView.builder( verticalDetails: const ScrollableDetails.vertical(reverse: true), horizontalDetails: const ScrollableDetails.horizontal(reverse: true), rowCount: 2, @@ -3274,7 +3278,7 @@ void main() { testWidgets('mouse handling', (WidgetTester tester) async { var enterCounter = 0; var exitCounter = 0; - final Widget tableView = TableView.builder( + final tableView = TableView.builder( rowCount: 50, columnCount: 50, columnBuilder: (_) => span,