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/example/lib/table_view/simple_table.dart b/packages/two_dimensional_scrollables/example/lib/table_view/simple_table.dart index 6a3c4d4a8f89..bb3579c1ca39 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 @@ -49,6 +49,7 @@ class _TableExampleState extends State { ), ) : TableView.builder( + key: ValueKey(_selectionMode), verticalDetails: ScrollableDetails.vertical(controller: _verticalController), cellBuilder: _buildCell, columnCount: 20, diff --git a/packages/two_dimensional_scrollables/example/pubspec.yaml b/packages/two_dimensional_scrollables/example/pubspec.yaml index c206590b70ce..cf96f3919584 100644 --- a/packages/two_dimensional_scrollables/example/pubspec.yaml +++ b/packages/two_dimensional_scrollables/example/pubspec.yaml @@ -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(); +} 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 dab40443d8ea..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,32 +105,37 @@ 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 constructor is appropriate for table views with a large - /// number of cells because the [cellbuilder] is called only for those + /// number of cells because the [cellBuilder] is called only for those /// cells that are actually visible. /// /// This constructor generates a [TableCellBuilderDelegate] for building @@ -148,15 +153,20 @@ class TableView extends TwoDimensionalScrollView { /// 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, + + 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, @@ -175,18 +185,17 @@ class TableView extends TwoDimensionalScrollView { 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, - ), + 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. @@ -201,15 +210,19 @@ class TableView extends TwoDimensionalScrollView { /// `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, + 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, @@ -222,18 +235,308 @@ class TableView extends TwoDimensionalScrollView { 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, - ), + 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; + + /// The alignment of the table within the viewport when there is extra space. + /// + /// Defaults to [Alignment.topLeft]. + final AlignmentGeometry alignment; + + @override + State createState() => _TableViewState(); +} + +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 = _buildDelegate(); + } + + @override + void didUpdateWidget(TableView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.delegate != oldWidget.delegate || + widget._buildDelegateParameters != oldWidget._buildDelegateParameters) { + if (oldWidget.delegate == null) { + _delegate.dispose(); + } + _delegate = _buildDelegate(); + } + } + + @override + void dispose() { + if (widget.delegate == null) { + _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, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + clipBehavior: widget.clipBehavior, + delegate: _delegate, + alignment: widget.alignment, + ); + } +} + +@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, + }); + + 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; + + @override + 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 + 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 + 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 + 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]. 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 79ee107a9a8d..14407dd1d8e0 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart @@ -902,6 +902,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 @@ -933,6 +934,7 @@ class _TreeViewState extends State> parent: controller, curve: widget.toggleAnimationStyle?.curve ?? TreeView.defaultAnimationCurve, ); + _currentAnimationForParent[node]?.animation.dispose(); _currentAnimationForParent[node] = ( controller: controller, animation: newAnimation, @@ -953,8 +955,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, @@ -964,24 +1077,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; diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 45fb03ee642a..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+ @@ -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(); +} 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 1403469934b0..d5a3b621e7c0 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)); @@ -148,171 +149,197 @@ 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')); - - 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')); - }); - - 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(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')); - }); + 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(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')); + + 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(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_test.dart b/packages/two_dimensional_scrollables/test/table_view/table_test.dart index 0b1c4e8ecb51..8a507699b96a 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)); @@ -40,14 +41,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 = TableView.builder( columnCount: 3, rowCount: 2, rowBuilder: (_) => span, columnBuilder: (_) => span, cellBuilder: (_, _) => cell, ); + + await tester.pumpWidget(widget); + + final TwoDimensionalScrollView tableView = tester.widget( + find.byWidgetPredicate((widget) => widget is TwoDimensionalScrollView), + ); + final delegate = tableView.delegate as TableCellBuilderDelegate; expect(delegate.pinnedRowCount, 0); expect(delegate.pinnedRowCount, 0); @@ -1797,97 +1805,109 @@ void main() { expect(tester.getRect(find.text('R1:C1')), const Rect.fromLTRB(200.0, 200.0, 400.0, 400.0)); }); - 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) { - return TableViewCell( - columnMergeStart: columnConfig.start, - columnMergeSpan: columnConfig.span, - child: const Text('R0:C1'), - ); - } - return TableViewCell(child: Text('R${vicinity.row}:C${vicinity.column}')); - }, + 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(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) { - return TableViewCell( - rowMergeStart: rowConfig.start, - rowMergeSpan: rowConfig.span, - child: const Text('R0:C0'), - ); - } - return TableViewCell(child: Text('R${vicinity.row}:C${vicinity.column}')); - }, + 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(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, @@ -1921,8 +1941,8 @@ void main() { }); group('TableView.list', () { - test('creates correct delegate', () { - final tableView = TableView.list( + testWidgets('creates correct delegate', (WidgetTester tester) async { + final widget = TableView.list( rowBuilder: (_) => span, columnBuilder: (_) => span, cells: const >[ @@ -1930,6 +1950,13 @@ void main() { [cell, cell, cell], ], ); + + await tester.pumpWidget(widget); + + final TwoDimensionalScrollView tableView = tester.widget( + find.byWidgetPredicate((widget) => widget is TwoDimensionalScrollView), + ); + final delegate = tableView.delegate as TableCellListDelegate; expect(delegate.pinnedRowCount, 0); expect(delegate.pinnedRowCount, 0); @@ -2312,7 +2339,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), @@ -2548,7 +2579,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, @@ -2623,7 +2658,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, @@ -2853,7 +2892,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, @@ -3377,7 +3420,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), horizontalDetails: ScrollableDetails.horizontal(controller: horizontalController), columnCount: 20, @@ -3434,7 +3481,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, @@ -3494,7 +3545,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), horizontalDetails: ScrollableDetails.horizontal( reverse: true, @@ -3554,7 +3609,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, @@ -3621,13 +3680,17 @@ void main() { ) 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: (_) => const TableSpan(extent: FixedTableSpanExtent(100)), @@ -3880,6 +3943,10 @@ void main() { testWidgets('Trailing pinned columns and rows - smoke test', (WidgetTester tester) async { final horizontalController = ScrollController(); final verticalController = ScrollController(); + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); Widget getTableView({ int? columnCount = 10, 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 2bcebb2db5fc..a83d1813c7f9 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 = >[