From 0dd2410791564020e6757234dc9969187e28eef6 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 25 Mar 2026 15:54:23 -0500 Subject: [PATCH 01/14] [two_dimensional_scrollables] Add alignment to TreeView and TableView (#11353) This PR implements declarative alignment for the content of TableView and TreeView widgets when that content is smaller than the viewport extent. Users can now use the alignment property to center or otherwise position the entire table/tree within the viewport. Alignment correctly reverts to start for axes that exceed viewport dimensions. Fixes flutter/flutter#170349 - TableView - Added an alignment property of type AlignmentGeometry (defaults to Alignment.topLeft). - Full support for both horizontal and vertical alignment. - Supports AlignmentDirectional to correctly handle TextDirection (LTR/RTL). - Works with pinned rows and columns, as well as reversed axis directions. - TreeView - Added an alignment property of type AlignmentGeometry (defaults to Alignment.topLeft). - Caveat: Currently only supports the vertical component of the alignment. The tree remains aligned to the horizontal "start" to maintain consistent indentation logic and avoid layout jumps caused by dynamic, lazily-loaded node widths. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .../two_dimensional_scrollables/CHANGELOG.md | 4 + .../lib/src/table_view/table.dart | 125 +++- .../lib/src/tree_view/render_tree.dart | 49 +- .../lib/src/tree_view/tree.dart | 23 +- .../two_dimensional_scrollables/pubspec.yaml | 2 +- .../test/table_view/alignment_test.dart | 608 ++++++++++++++++++ .../test/tree_view/alignment_test.dart | 91 +++ 7 files changed, 872 insertions(+), 30 deletions(-) create mode 100644 packages/two_dimensional_scrollables/test/table_view/alignment_test.dart create mode 100644 packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index c9332dcb13ac..6d84270e8b66 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.0 + +* Added `alignment` property to `TableView` and `TreeView` to align content within the viewport when it is smaller than the viewport extent. + ## 0.3.9 * Fixes TableSpan borders being flipped when one or both axis directions are reversed. 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 e0dd64e9e0b0..66e5cf5ca5c6 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -116,6 +116,7 @@ class TableView extends TwoDimensionalScrollView { super.dragStartBehavior, super.keyboardDismissBehavior, super.clipBehavior, + this.alignment = Alignment.topLeft, }); /// Creates a [TableView] of widgets that are created on demand. @@ -155,6 +156,7 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, required TableViewCellBuilder cellBuilder, + this.alignment = Alignment.topLeft, }) : assert(pinnedRowCount >= 0), assert(rowCount == null || rowCount >= 0), assert(rowCount == null || rowCount >= pinnedRowCount), @@ -199,6 +201,7 @@ class TableView extends TwoDimensionalScrollView { required TableSpanBuilder columnBuilder, required TableSpanBuilder rowBuilder, List> cells = const >[], + this.alignment = Alignment.topLeft, }) : assert(pinnedRowCount >= 0), assert(pinnedColumnCount >= 0), super( @@ -211,6 +214,11 @@ class TableView extends TwoDimensionalScrollView { ), ); + /// 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, @@ -226,6 +234,7 @@ class TableView extends TwoDimensionalScrollView { mainAxis: mainAxis, cacheExtent: cacheExtent, clipBehavior: clipBehavior, + alignment: alignment, ); } } @@ -245,8 +254,12 @@ class TableViewport extends TwoDimensionalViewport { required super.mainAxis, super.cacheExtent, super.clipBehavior, + this.alignment = Alignment.topLeft, }); + /// The alignment of the table within the viewport when there is extra space. + final AlignmentGeometry alignment; + @override RenderTwoDimensionalViewport createRenderObject(BuildContext context) { return RenderTableViewport( @@ -259,6 +272,8 @@ class TableViewport extends TwoDimensionalViewport { clipBehavior: clipBehavior, delegate: delegate as TableCellDelegateMixin, childManager: context as TwoDimensionalChildManager, + alignment: alignment, + textDirection: Directionality.maybeOf(context), ); } @@ -275,7 +290,9 @@ class TableViewport extends TwoDimensionalViewport { ..mainAxis = mainAxis ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior - ..delegate = delegate as TableCellDelegateMixin; + ..delegate = delegate as TableCellDelegateMixin + ..alignment = alignment + ..textDirection = Directionality.maybeOf(context); } } @@ -299,7 +316,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { required super.childManager, super.cacheExtent, super.clipBehavior, - }); + AlignmentGeometry alignment = Alignment.topLeft, + TextDirection? textDirection, + }) : _alignment = alignment, + _textDirection = textDirection; @override TableCellDelegateMixin get delegate => @@ -309,6 +329,31 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { super.delegate = value; } + /// The alignment of the table within the viewport when there is extra space. + AlignmentGeometry get alignment => _alignment; + AlignmentGeometry _alignment; + set alignment(AlignmentGeometry value) { + if (_alignment == value) { + return; + } + _alignment = value; + markNeedsLayout(); + } + + /// The text direction with which to resolve [alignment]. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + markNeedsLayout(); + } + + double _hAlignmentOffset = 0.0; + double _vAlignmentOffset = 0.0; + // Skipped vicinities for the current frame based on merged cells. // This prevents multiple build calls for the same cell that spans multiple // vicinities. @@ -852,6 +897,33 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateFirstAndLastVisibleCell(); } + final Alignment resolvedAlignment = alignment.resolve(textDirection); + _hAlignmentOffset = 0.0; + if (!_columnsAreInfinite && _columnMetrics.isNotEmpty) { + final double totalWidth = + _pinnedColumnsExtent + + _columnMetrics[delegate.columnCount! - 1]!.trailingOffset; + if (totalWidth < viewportDimension.width) { + _hAlignmentOffset = + (viewportDimension.width - totalWidth) * + (resolvedAlignment.x + 1.0) / + 2.0; + } + } + + _vAlignmentOffset = 0.0; + if (!_rowsAreInfinite && _rowMetrics.isNotEmpty) { + final double totalHeight = + _pinnedRowsExtent + + _rowMetrics[delegate.rowCount! - 1]!.trailingOffset; + if (totalHeight < viewportDimension.height) { + _vAlignmentOffset = + (viewportDimension.height - totalHeight) * + (resolvedAlignment.y + 1.0) / + 2.0; + } + } + if (_firstNonPinnedCell == null && _lastPinnedRow == null && _lastPinnedColumn == null) { @@ -862,19 +934,21 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { final double? offsetIntoColumn = _firstNonPinnedColumn != null ? horizontalOffset.pixels - _columnMetrics[_firstNonPinnedColumn]!.leadingOffset - - _pinnedColumnsExtent + _pinnedColumnsExtent - + _hAlignmentOffset : null; final double? offsetIntoRow = _firstNonPinnedRow != null ? verticalOffset.pixels - _rowMetrics[_firstNonPinnedRow]!.leadingOffset - - _pinnedRowsExtent + _pinnedRowsExtent - + _vAlignmentOffset : null; if (_lastPinnedRow != null && _lastPinnedColumn != null) { // Layout cells that are contained in both pinned rows and columns _layoutCells( start: TableVicinity.zero, end: TableVicinity(column: _lastPinnedColumn!, row: _lastPinnedRow!), - offset: Offset.zero, + offset: Offset(-_hAlignmentOffset, -_vAlignmentOffset), ); } @@ -886,7 +960,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _layoutCells( start: TableVicinity(column: _firstNonPinnedColumn!, row: 0), end: TableVicinity(column: _lastNonPinnedColumn!, row: _lastPinnedRow!), - offset: Offset(offsetIntoColumn!, 0), + offset: Offset(offsetIntoColumn!, -_vAlignmentOffset), ); } if (_lastPinnedColumn != null && _firstNonPinnedRow != null) { @@ -897,7 +971,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _layoutCells( start: TableVicinity(column: 0, row: _firstNonPinnedRow!), end: TableVicinity(column: _lastPinnedColumn!, row: _lastNonPinnedRow!), - offset: Offset(0, offsetIntoRow!), + offset: Offset(-_hAlignmentOffset, offsetIntoRow!), ); } if (_firstNonPinnedCell != null) { @@ -1176,6 +1250,9 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { // follows row or column major ordering. Here is slightly different // as we break the cells up into 4 main paint passes to clip for overlap. + final bool reversedH = axisDirectionIsReversed(horizontalAxisDirection); + final bool reversedV = axisDirectionIsReversed(verticalAxisDirection); + if (_firstNonPinnedCell != null) { // Paint all visible un-pinned cells assert(_lastNonPinnedCell != null); @@ -1183,12 +1260,10 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? 0.0 - : _pinnedColumnsExtent, - axisDirectionIsReversed(verticalAxisDirection) - ? 0.0 - : _pinnedRowsExtent, + (reversedH ? 0.0 : _pinnedColumnsExtent) + + (reversedH ? -_hAlignmentOffset : _hAlignmentOffset), + (reversedV ? 0.0 : _pinnedRowsExtent) + + (reversedV ? -_vAlignmentOffset : _vAlignmentOffset), viewportDimension.width - _pinnedColumnsExtent, viewportDimension.height - _pinnedRowsExtent, ), @@ -1214,12 +1289,13 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? viewportDimension.width - _pinnedColumnsExtent - : 0.0, - axisDirectionIsReversed(verticalAxisDirection) - ? 0.0 - : _pinnedRowsExtent, + reversedH + ? viewportDimension.width - + _pinnedColumnsExtent - + _hAlignmentOffset + : _hAlignmentOffset, + (reversedV ? 0.0 : _pinnedRowsExtent) + + (reversedV ? -_vAlignmentOffset : _vAlignmentOffset), _pinnedColumnsExtent, viewportDimension.height - _pinnedRowsExtent, ), @@ -1248,12 +1324,11 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { needsCompositing, offset, Rect.fromLTWH( - axisDirectionIsReversed(horizontalAxisDirection) - ? 0.0 - : _pinnedColumnsExtent, - axisDirectionIsReversed(verticalAxisDirection) - ? viewportDimension.height - _pinnedRowsExtent - : 0.0, + (reversedH ? 0.0 : _pinnedColumnsExtent) + + (reversedH ? -_hAlignmentOffset : _hAlignmentOffset), + reversedV + ? viewportDimension.height - _pinnedRowsExtent - _vAlignmentOffset + : _vAlignmentOffset, viewportDimension.width - _pinnedColumnsExtent, _pinnedRowsExtent, ), diff --git a/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart b/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart index f10c147a50db..10eb1ac3646a 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/render_tree.dart @@ -38,9 +38,13 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { required super.childManager, super.cacheExtent, super.clipBehavior, + AlignmentGeometry alignment = Alignment.topLeft, + TextDirection? textDirection, }) : _activeAnimations = activeAnimations, _rowDepths = rowDepths, _indentation = indentation, + _alignment = alignment, + _textDirection = textDirection, assert(indentation >= 0), assert( verticalAxisDirection == AxisDirection.down && @@ -56,6 +60,30 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { super.delegate = value; } + /// The alignment of the tree within the viewport when there is extra space. + AlignmentGeometry get alignment => _alignment; + AlignmentGeometry _alignment; + set alignment(AlignmentGeometry value) { + if (_alignment == value) { + return; + } + _alignment = value; + markNeedsLayout(); + } + + /// The text direction with which to resolve [alignment]. + TextDirection? get textDirection => _textDirection; + TextDirection? _textDirection; + set textDirection(TextDirection? value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + markNeedsLayout(); + } + + double _vAlignmentOffset = 0.0; + /// The currently active [TreeViewNode] animations. /// /// Since the index of animating nodes can change at any time, the unique key @@ -348,6 +376,19 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { _updateFirstAndLastVisibleRow(); } + final Alignment resolvedAlignment = alignment.resolve(textDirection); + _vAlignmentOffset = 0.0; + if (_rowMetrics.isNotEmpty) { + final double totalHeight = + _rowMetrics[_rowMetrics.length - 1]!.trailingOffset; + if (totalHeight < viewportDimension.height) { + _vAlignmentOffset = + (viewportDimension.height - totalHeight) * + (resolvedAlignment.y + 1.0) / + 2.0; + } + } + if (_firstRow == null) { assert(_lastRow == null); return; @@ -356,7 +397,9 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { _Span rowSpan; double rowOffset = - -verticalOffset.pixels + _rowMetrics[_firstRow!]!.leadingOffset; + -verticalOffset.pixels + + _rowMetrics[_firstRow!]!.leadingOffset + + _vAlignmentOffset; for (int row = _firstRow!; row <= _lastRow!; row++) { rowSpan = _rowMetrics[row]!; final double rowHeight = rowSpan.extent; @@ -489,11 +532,11 @@ class RenderTreeViewport extends RenderTwoDimensionalViewport { final double trailingOffset = _rowMetrics[segment.trailingIndex]!.trailingOffset; final rect = Rect.fromPoints( - Offset(0.0, leadingOffset - verticalOffset.pixels), + Offset(0.0, leadingOffset - verticalOffset.pixels + _vAlignmentOffset), Offset( viewportDimension.width, math.min( - trailingOffset - verticalOffset.pixels, + trailingOffset - verticalOffset.pixels + _vAlignmentOffset, viewportDimension.height, ), ), 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 3c5fa67bf134..6819c9424018 100644 --- a/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart +++ b/packages/two_dimensional_scrollables/lib/src/tree_view/tree.dart @@ -322,6 +322,7 @@ class TreeView extends StatefulWidget { this.clipBehavior = Clip.hardEdge, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, + this.alignment = Alignment.topLeft, }) : assert( verticalDetails.direction == AxisDirection.down && horizontalDetails.direction == AxisDirection.right, @@ -496,6 +497,14 @@ class TreeView extends StatefulWidget { /// Defaults to true. final bool addRepaintBoundaries; + /// The alignment of the tree within the viewport when there is extra space. + /// + /// Currently, [TreeView] only supports the vertical component of [alignment] + /// for aligning the tree within the viewport. + /// + /// Defaults to [Alignment.topLeft]. + final AlignmentGeometry alignment; + /// The default [AnimationStyle] used for node expand and collapse animations, /// when one has not been provided in [toggleAnimationStyle]. // ignore: prefer_const_constructors @@ -758,6 +767,7 @@ class _TreeViewState extends State> }, addAutomaticKeepAlives: widget.addAutomaticKeepAlives, indentation: widget.indentation.value, + alignment: widget.alignment, ); } @@ -984,6 +994,7 @@ class _TreeView extends TwoDimensionalScrollView { required this.activeAnimations, required this.rowDepths, required this.indentation, + required this.alignment, required int rowCount, bool addAutomaticKeepAlives = true, }) : assert(verticalDetails.direction == AxisDirection.down), @@ -1000,6 +1011,7 @@ class _TreeView extends TwoDimensionalScrollView { final Map activeAnimations; final Map rowDepths; final double indentation; + final AlignmentGeometry alignment; @override TreeViewport buildViewport( @@ -1018,6 +1030,7 @@ class _TreeView extends TwoDimensionalScrollView { activeAnimations: activeAnimations, rowDepths: rowDepths, indentation: indentation, + alignment: alignment, ); } } @@ -1039,6 +1052,7 @@ class TreeViewport extends TwoDimensionalViewport { required this.activeAnimations, required this.rowDepths, required this.indentation, + this.alignment = Alignment.topLeft, }) : assert( verticalAxisDirection == AxisDirection.down && horizontalAxisDirection == AxisDirection.right, @@ -1063,6 +1077,9 @@ class TreeViewport extends TwoDimensionalViewport { /// for more options to customize the indented space. final double indentation; + /// The alignment of the tree within the viewport when there is extra space. + final AlignmentGeometry alignment; + @override RenderTreeViewport createRenderObject(BuildContext context) { return RenderTreeViewport( @@ -1077,6 +1094,8 @@ class TreeViewport extends TwoDimensionalViewport { clipBehavior: clipBehavior, delegate: delegate as TreeRowDelegateMixin, childManager: context as TwoDimensionalChildManager, + alignment: alignment, + textDirection: Directionality.maybeOf(context), ); } @@ -1095,6 +1114,8 @@ class TreeViewport extends TwoDimensionalViewport { ..verticalAxisDirection = verticalAxisDirection ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior - ..delegate = delegate as TreeRowDelegateMixin; + ..delegate = delegate as TreeRowDelegateMixin + ..alignment = alignment + ..textDirection = Directionality.maybeOf(context); } } diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 9d16a900d995..8525e06bff96 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.3.9 +version: 0.4.0 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+ diff --git a/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart b/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart new file mode 100644 index 000000000000..cba1e1fbc872 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/table_view/alignment_test.dart @@ -0,0 +1,608 @@ +// 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 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TableView alignment', () { + testWidgets('Default alignment - topLeft', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Default is Alignment.topLeft (0, 0) + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect(tester.getTopLeft(cell00) - tableTopLeft, Offset.zero); + }); + + testWidgets('Horizontal alignment - center', (WidgetTester tester) async { + const viewportWidth = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: viewportWidth, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + alignment: Alignment.topCenter, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 300 wide, viewport is 600 wide. Centered means 150 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(150.0, 0.0), + ); + + final Finder cell20 = find.byKey(const ValueKey('cell 2:0')); + expect( + tester.getTopLeft(cell20) - tableTopLeft, + const Offset(350.0, 0.0), + ); + }); + + testWidgets('Horizontal alignment - end', (WidgetTester tester) async { + const viewportWidth = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: viewportWidth, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + alignment: Alignment.topRight, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 300 wide, viewport is 600 wide. End means 300 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(300.0, 0.0), + ); + }); + + testWidgets('Vertical alignment - center', (WidgetTester tester) async { + const viewportHeight = 600.0; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: viewportHeight, + child: TableView.builder( + columnCount: 1, + rowCount: 2, + alignment: Alignment.centerLeft, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 200 high, viewport is 600 high. Centered means 200 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 200.0), + ); + + final Finder cell01 = find.byKey(const ValueKey('cell 0:1')); + expect( + tester.getTopLeft(cell01) - tableTopLeft, + const Offset(0.0, 300.0), + ); + }); + + testWidgets('Combined alignment', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.center, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table is 100x100, viewport is 600x600. Centered means 250, 250 offset. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 250.0), + ); + }); + + testWidgets('Alignment with pinned columns', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 1, + pinnedColumnCount: 1, + alignment: Alignment.topCenter, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Total width 300 (1 pinned, 2 unpinned). Viewport 600. Offset 150. + // Pinned column 0 should be at 150. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(150.0, 0.0), + ); + + // Unpinned column 1 should be at 250. + final Finder cell10 = find.byKey(const ValueKey('cell 1:0')); + expect( + tester.getTopLeft(cell10) - tableTopLeft, + const Offset(250.0, 0.0), + ); + }); + + testWidgets('Alignment with pinned rows', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 3, + pinnedRowCount: 1, + alignment: Alignment.centerLeft, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Total height 300 (1 pinned, 2 unpinned). Viewport 600. Offset 150. + // Pinned row 0 should be at 150. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), + ); + + // Unpinned row 1 should be at 250. + final Finder cell01 = find.byKey(const ValueKey('cell 0:1')); + expect( + tester.getTopLeft(cell01) - tableTopLeft, + const Offset(0.0, 250.0), + ); + }); + + testWidgets('Alignment with reversed horizontal axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.topCenter, + horizontalDetails: const ScrollableDetails.horizontal( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Reversed horizontal. Start is on the right (600). + // Center should still be at 250. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 0.0), + ); + }); + + testWidgets('Alignment with reversed vertical axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.centerLeft, + verticalDetails: const ScrollableDetails.vertical( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Reversed vertical. Center should still be at 250. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 250.0), + ); + }); + + testWidgets('Alignment with both axes reversed', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 600, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: Alignment.center, + horizontalDetails: const ScrollableDetails.horizontal( + reverse: true, + ), + verticalDetails: const ScrollableDetails.vertical( + reverse: true, + ), + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Both reversed. Center should still be at (250, 250). + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(250.0, 250.0), + ); + }); + + testWidgets('AlignmentDirectional with RTL', (WidgetTester tester) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.rtl, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 600, + height: 400, + child: TableView.builder( + columnCount: 1, + rowCount: 1, + alignment: AlignmentDirectional.centerEnd, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // RTL + centerEnd means alignment to the left. + // Table is 100 wide, viewport 600. centerEnd in RTL resolved to left (x = -1). + // Wait, centerEnd in RTL is actually Alignment(-1.0, 0.0) which is left. + // centerEnd in LTR is Alignment(1.0, 0.0) which is right. + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), // Center Y is 150 (400-100)/2 + ); + }); + + testWidgets('Overflow alignment behaves like start in overflow axis', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 3, // 300 wide + rowCount: 1, + alignment: Alignment.center, + columnBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (context, vicinity) { + return TableViewCell( + child: SizedBox( + key: ValueKey( + 'cell ${vicinity.column}:${vicinity.row}', + ), + ), + ); + }, + ), + ), + ), + ), + ), + ); + + final Offset tableTopLeft = tester.getTopLeft(find.byType(TableView)); + // Table (300) > Viewport (200). Horizontal alignment should be ignored (start). + // Viewport (400) > Table (100) Row. Vertical alignment should be center (150). + final Finder cell00 = find.byKey(const ValueKey('cell 0:0')); + expect( + tester.getTopLeft(cell00) - tableTopLeft, + const Offset(0.0, 150.0), + ); + }); + }); +} diff --git a/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart b/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart new file mode 100644 index 000000000000..9783e253309e --- /dev/null +++ b/packages/two_dimensional_scrollables/test/tree_view/alignment_test.dart @@ -0,0 +1,91 @@ +// 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 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TreeView alignment', () { + testWidgets('Default alignment - topLeft', (WidgetTester tester) async { + final tree = >[TreeViewNode('Root')]; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: 400, + child: TreeView( + tree: tree, + treeNodeBuilder: (context, node, toggleAnimationStyle) => + SizedBox( + key: const ValueKey('Root'), + height: 100, + child: Text(node.content), + ), + treeRowBuilder: (node) => + const TreeRow(extent: FixedTreeRowExtent(100)), + ), + ), + ), + ), + ), + ); + + final Offset treeTopLeft = tester.getTopLeft( + find.byType(TreeView), + ); + // Default is Alignment.topLeft (0, 0) + final Finder root = find.byKey(const ValueKey('Root')); + expect(tester.getTopLeft(root) - treeTopLeft, Offset.zero); + }); + + testWidgets('Vertical alignment - center', (WidgetTester tester) async { + const viewportHeight = 600.0; + final tree = >[TreeViewNode('Root')]; + + await tester.pumpWidget( + WidgetsApp( + color: const Color(0xFFFFFFFF), + debugShowCheckedModeBanner: false, + builder: (context, child) => Directionality( + textDirection: TextDirection.ltr, + child: Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 400, + height: viewportHeight, + child: TreeView( + tree: tree, + alignment: Alignment.center, + treeNodeBuilder: (context, node, toggleAnimationStyle) => + SizedBox( + key: const ValueKey('Root'), + height: 100, + child: Text(node.content), + ), + treeRowBuilder: (node) => + const TreeRow(extent: FixedTreeRowExtent(100)), + ), + ), + ), + ), + ), + ); + + final Offset treeTopLeft = tester.getTopLeft( + find.byType(TreeView), + ); + // Tree is 100 high, viewport is 600 high. Centered means 250 offset. + final Finder root = find.byKey(const ValueKey('Root')); + expect(tester.getTopLeft(root) - treeTopLeft, const Offset(0.0, 250.0)); + }); + }); +} From d8737b485f0a059c501389e953e610765de59ea6 Mon Sep 17 00:00:00 2001 From: Hannah Jin Date: Thu, 26 Mar 2026 11:22:09 -0700 Subject: [PATCH 02/14] Fix typo in release_from_branches.yml (#11356) *reusable_release ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .github/workflows/release_from_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_from_branches.yml b/.github/workflows/release_from_branches.yml index a235a2576ef3..5286cc18e041 100644 --- a/.github/workflows/release_from_branches.yml +++ b/.github/workflows/release_from_branches.yml @@ -5,7 +5,7 @@ on: - 'release-go_router' jobs: release: - uses: ./.github/workflows/resuable_release.yml + uses: ./.github/workflows/reusable_release.yml with: is-batch-release: true branch-name: '${{ github.ref_name }}' From e6275038e5ccc83173d2fad08413932401528f73 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Thu, 26 Mar 2026 15:20:56 -0700 Subject: [PATCH 03/14] [pigeon] Adds support for analyzer 10 (#11346) minimal required changes fixes https://github.com/flutter/flutter/issues/183568 --- packages/pigeon/CHANGELOG.md | 4 ++++ packages/pigeon/lib/src/generator_tools.dart | 2 +- packages/pigeon/lib/src/pigeon_lib_internal.dart | 8 ++++++-- packages/pigeon/pubspec.yaml | 5 +++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index d14e07c1b4b4..16ca19f5515a 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 26.3.2 + +* Updates `analyzer` dependency to support version 10. + ## 26.3.1 * Fixes dartdoc comments that accidentally used HTML. diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 2890bb571542..e06ddf0bd228 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '26.3.1'; +const String pigeonVersion = '26.3.2'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/lib/src/pigeon_lib_internal.dart b/packages/pigeon/lib/src/pigeon_lib_internal.dart index 75bbb96a293e..19da4613ce42 100644 --- a/packages/pigeon/lib/src/pigeon_lib_internal.dart +++ b/packages/pigeon/lib/src/pigeon_lib_internal.dart @@ -1922,10 +1922,14 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { // resolved return type, via // `node.declaredFragment!.element.returnType`. String erroneousDeclaration = node.name.lexeme; - final dart_ast.AstNode? enclosingDeclaration = node.parent; + dart_ast.AstNode? enclosingDeclaration = node.parent; + while (enclosingDeclaration != null && + enclosingDeclaration is! dart_ast.ClassDeclaration) { + enclosingDeclaration = enclosingDeclaration.parent; + } if (enclosingDeclaration is dart_ast.ClassDeclaration) { erroneousDeclaration = - '${enclosingDeclaration.name}.$erroneousDeclaration'; + '${enclosingDeclaration.name.lexeme}.$erroneousDeclaration'; } _errors.add( Error( diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 0fc8a2cdd64c..7a1359aec30c 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,13 +2,13 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22 -version: 26.3.1 # This must match the version in lib/src/generator_tools.dart +version: 26.3.2 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.9.0 dependencies: - analyzer: ">=8.0.0 <10.0.0" + analyzer: ">=8.0.0 <11.0.0" args: ^2.5.0 code_builder: ^4.10.0 collection: ^1.15.0 @@ -27,3 +27,4 @@ topics: - interop - platform-channels - plugin-development + From 049cc84a479d396579efd3f010af5ad0cdb03cd3 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Thu, 26 Mar 2026 17:56:17 -0500 Subject: [PATCH 04/14] [two_dimensional_scrollables] Add debug check for pinning out of bounds (#11366) Fixes https://github.com/flutter/flutter/issues/136833 This cleans up an old todo from when we added support for pinned rows and columns to TableView. It is possible for the pinned extents to exceed the viewport bounds, which would make scrolling impossible, and unpinned rows/columns inaccessible. This adds debug check during layout to catch this now. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .../two_dimensional_scrollables/CHANGELOG.md | 4 + .../lib/src/table_view/table.dart | 51 +++- .../two_dimensional_scrollables/pubspec.yaml | 2 +- .../pinned_extent_warning_test.dart | 241 ++++++++++++++++++ 4 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart diff --git a/packages/two_dimensional_scrollables/CHANGELOG.md b/packages/two_dimensional_scrollables/CHANGELOG.md index 6d84270e8b66..2f031898ab46 100644 --- a/packages/two_dimensional_scrollables/CHANGELOG.md +++ b/packages/two_dimensional_scrollables/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.1 + +* Adds warnings for TableView pinned rows and columns that exceed the viewport dimensions. + ## 0.4.0 * Added `alignment` property to `TableView` and `TreeView` to align content within the viewport when it is smaller than the viewport extent. 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 66e5cf5ca5c6..b09a7898e852 100644 --- a/packages/two_dimensional_scrollables/lib/src/table_view/table.dart +++ b/packages/two_dimensional_scrollables/lib/src/table_view/table.dart @@ -429,13 +429,6 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ); } - // TODO(Piinks): Pinned rows/cols do not account for what is visible on the - // screen. Ostensibly, we would not want to have pinned rows/columns that - // extend beyond the viewport, we would never see them as they would never - // scroll into view. So this currently implementation is fairly assuming - // we will never have rows/cols that are outside of the viewport. We should - // maybe add an assertion for this during layout. - // https://github.com/flutter/flutter/issues/136833 int? get _lastPinnedRow => delegate.pinnedRowCount > 0 ? delegate.pinnedRowCount - 1 : null; int? get _lastPinnedColumn => @@ -448,6 +441,49 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { ? _columnMetrics[_lastPinnedColumn]!.trailingOffset : 0.0; + void _debugCheckPinnedExtent() { + assert(() { + if (_pinnedColumnsExtent > viewportDimension.width) { + debugPrint( + 'TableView has pinned columns with a total width of ' + '$_pinnedColumnsExtent, which exceeds the viewport width of ' + '${viewportDimension.width}. This will prevent unpinned columns ' + 'from being visible.', + ); + } else if (_pinnedColumnsExtent == viewportDimension.width) { + final bool hasUnpinnedColumns = + delegate.columnCount == null || + delegate.columnCount! > delegate.pinnedColumnCount; + if (hasUnpinnedColumns) { + debugPrint( + 'TableView has pinned columns that fully consume the viewport width. ' + 'Unpinned columns will not be visible.', + ); + } + } + + if (_pinnedRowsExtent > viewportDimension.height) { + debugPrint( + 'TableView has pinned rows with a total height of ' + '$_pinnedRowsExtent, which exceeds the viewport height of ' + '${viewportDimension.height}. This will prevent unpinned rows ' + 'from being visible.', + ); + } else if (_pinnedRowsExtent == viewportDimension.height) { + final bool hasUnpinnedRows = + delegate.rowCount == null || + delegate.rowCount! > delegate.pinnedRowCount; + if (hasUnpinnedRows) { + debugPrint( + 'TableView has pinned rows that fully consume the viewport height. ' + 'Unpinned rows will not be visible.', + ); + } + } + return true; + }()); + } + @override TableViewParentData parentDataOf(RenderBox child) => super.parentDataOf(child) as TableViewParentData; @@ -892,6 +928,7 @@ class RenderTableViewport extends RenderTwoDimensionalViewport { _updateColumnMetrics(); _updateRowMetrics(); _updateScrollBounds(); + _debugCheckPinnedExtent(); } else { // Updates the visible cells based on cached table metrics. _updateFirstAndLastVisibleCell(); diff --git a/packages/two_dimensional_scrollables/pubspec.yaml b/packages/two_dimensional_scrollables/pubspec.yaml index 8525e06bff96..8e6d131bc8e0 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.4.0 +version: 0.4.1 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+ diff --git a/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart b/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart new file mode 100644 index 000000000000..af0d6625b6e8 --- /dev/null +++ b/packages/two_dimensional_scrollables/test/table_view/pinned_extent_warning_test.dart @@ -0,0 +1,241 @@ +// 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 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; + +void main() { + group('TableView pinned extent warnings', () { + testWidgets('Warns when pinned columns exceed viewport width', ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 5, + rowCount: 5, + pinnedColumnCount: 3, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 300 (3 * 100), viewport width = 200. + // A warning is expected because the pinned columns are wider than the + // viewport, meaning even the pinned content cannot be fully displayed. + expect( + log, + contains( + matches( + r'TableView has pinned columns with a total width of 300(\.0)?, which exceeds the viewport width of 200(\.0)?', + ), + ), + ); + debugPrint = oldDebugPrint; + }); + + testWidgets('Warns when pinned rows exceed viewport height', ( + WidgetTester tester, + ) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 400, + height: 200, + child: TableView.builder( + columnCount: 5, + rowCount: 5, + pinnedRowCount: 3, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned rows extent = 300 (3 * 100), viewport height = 200. + // A warning is expected because the pinned rows are taller than the + // viewport, meaning even the pinned content cannot be fully displayed. + expect( + log, + contains( + matches( + r'TableView has pinned rows with a total height of 300(\.0)?, which exceeds the viewport height of 200(\.0)?', + ), + ), + ); + debugPrint = oldDebugPrint; + }); + + testWidgets( + 'Warns when pinned columns fully consume viewport width and there are unpinned columns', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 3, + rowCount: 5, + pinnedColumnCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 200 (2 * 100), viewport width = 200. + // There is 1 unpinned column (columnCount: 3, pinnedColumnCount: 2). + // Since the pinned columns take up the entire viewport width, the + // unpinned column will never be visible during scrolling. + expect( + log, + contains( + 'TableView has pinned columns that fully consume the viewport width. Unpinned columns will not be visible.', + ), + ); + debugPrint = oldDebugPrint; + }, + ); + + testWidgets( + 'Warns when pinned rows fully consume viewport height and there are unpinned rows', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 400, + height: 200, + child: TableView.builder( + columnCount: 5, + rowCount: 3, + pinnedRowCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned rows extent = 200 (2 * 100), viewport height = 200. + // There is 1 unpinned row (rowCount: 3, pinnedRowCount: 2). + // Since the pinned rows take up the entire viewport height, the + // unpinned row will never be visible during scrolling. + expect( + log, + contains( + 'TableView has pinned rows that fully consume the viewport height. Unpinned rows will not be visible.', + ), + ); + debugPrint = oldDebugPrint; + }, + ); + + testWidgets( + 'Does not warn when all columns are pinned even if they consume viewport', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/136833 + final log = []; + final DebugPrintCallback oldDebugPrint = debugPrint; + debugPrint = (String? message, {int? wrapWidth}) { + log.add(message!); + }; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 400, + child: TableView.builder( + columnCount: 2, + rowCount: 5, + pinnedColumnCount: 2, + columnBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + rowBuilder: (int index) => + const TableSpan(extent: FixedTableSpanExtent(100)), + cellBuilder: (BuildContext context, TableVicinity vicinity) => + const TableViewCell(child: SizedBox.shrink()), + ), + ), + ), + ), + ); + + // Pinned columns extent = 200 (2 * 100), viewport width = 200. + // Although the pinned columns fully consume the viewport width, + // ALL columns are pinned (columnCount: 2, pinnedColumnCount: 2). + // Since there are no unpinned columns, no warning is issued about + // unpinned columns being hidden. + expect( + log, + isNot(contains(contains('Unpinned columns will not be visible'))), + ); + debugPrint = oldDebugPrint; + }, + ); + }); +} From eb722e0d8db9c069bf44e42686f1298c1da3b9cb Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 26 Mar 2026 19:44:23 -0400 Subject: [PATCH 05/14] Manual roll Flutter from 82d96ef98a33 to e79bf6cbc140 (32 revisions) (#11372) Manual roll Flutter from 82d96ef98a33 to e79bf6cbc140 (32 revisions) Manual roll requested by tarrinneal@google.com https://github.com/flutter/flutter/compare/82d96ef98a33...e79bf6cbc140 2026-03-26 engine-flutter-autoroll@skia.org Roll Dart SDK from a3af8949863e to 7587a31814c6 (1 revision) (flutter/flutter#184213) 2026-03-26 30870216+gaaclarke@users.noreply.github.com Adds rockchip series to block list for vulkan. (flutter/flutter#184207) 2026-03-26 41930132+hellohuanlin@users.noreply.github.com [ios]add ci/cd label to ios triage (flutter/flutter#184214) 2026-03-26 engine-flutter-autoroll@skia.org Roll Skia from 10c97361d8f3 to bee5a06ef578 (1 revision) (flutter/flutter#184203) 2026-03-26 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from rS5ezRgehkw26fKRX... to BIlBJNOlKjQeRFoFy... (flutter/flutter#184197) 2026-03-26 engine-flutter-autoroll@skia.org Roll Packages from 5909bdde6090 to 0dd241079156 (3 revisions) (flutter/flutter#184198) 2026-03-26 1776065+b055man@users.noreply.github.com fix: use atomic write for engine.stamp to prevent race conditions (flutter/flutter#184131) 2026-03-26 engine-flutter-autoroll@skia.org Roll Skia from 77dfb68002cd to 10c97361d8f3 (1 revision) (flutter/flutter#184195) 2026-03-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 349dbbbdba99 to a3af8949863e (1 revision) (flutter/flutter#184194) 2026-03-26 engine-flutter-autoroll@skia.org Roll Skia from 81ef2238cb09 to 77dfb68002cd (12 revisions) (flutter/flutter#184186) 2026-03-26 jason-simmons@users.noreply.github.com Revert "Keep glyphs for variable fonts (#183857)" (flutter/flutter#184147) 2026-03-26 97480502+b-luk@users.noreply.github.com Expand simple shape path optimization logic and move it from dl_dispatcher to dl_builder (flutter/flutter#184096) 2026-03-26 jacksongardner@google.com Fix merge changelog workflow. (flutter/flutter#184145) 2026-03-26 34871572+gmackall@users.noreply.github.com Add notes on HCPP to `Android-Platform-Views.md` (flutter/flutter#183859) 2026-03-26 30870216+gaaclarke@users.noreply.github.com Adds explicit name to the cicd label job. (flutter/flutter#184070) 2026-03-26 engine-flutter-autoroll@skia.org Roll Dart SDK from 26f80b9403f5 to 349dbbbdba99 (3 revisions) (flutter/flutter#184176) 2026-03-26 34871572+gmackall@users.noreply.github.com Collect impeller analytics for appbundles (flutter/flutter#184146) 2026-03-26 737941+loic-sharma@users.noreply.github.com [Dot shorthands] Migrate examples/api/lib/material (flutter/flutter#183963) 2026-03-26 jason-simmons@users.noreply.github.com Roll Dart DevTools to a version with correct CIPD tags (flutter/flutter#184172) 2026-03-25 47866232+chunhtai@users.noreply.github.com Pipes ScrollCacheExtent through more scroll views (flutter/flutter#184078) 2026-03-25 32538273+ValentinVignal@users.noreply.github.com Add widget of the week link in SensitiveContent documentation (flutter/flutter#183972) 2026-03-25 engine-flutter-autoroll@skia.org Roll Packages from 8dcfd1186ef9 to 5909bdde6090 (13 revisions) (flutter/flutter#184123) 2026-03-25 engine-flutter-autoroll@skia.org Roll Dart SDK from c48f0c954d86 to 26f80b9403f5 (1 revision) (flutter/flutter#184117) 2026-03-25 engine-flutter-autoroll@skia.org Roll Skia from 51725ae49ef5 to 81ef2238cb09 (3 revisions) (flutter/flutter#184115) 2026-03-25 engine-flutter-autoroll@skia.org Roll Fuchsia Linux SDK from M3sjWggTQmP2AD4bS... to rS5ezRgehkw26fKRX... (flutter/flutter#184114) 2026-03-25 104349824+huycozy@users.noreply.github.com Add SensitiveContent widget sample code (flutter/flutter#183846) 2026-03-25 rmolivares@renzo-olivares.dev SelectableRegion should passthrough constraints to child unmodified (flutter/flutter#184083) 2026-03-25 engine-flutter-autoroll@skia.org Roll Dart SDK from ce171d5026ff to c48f0c954d86 (2 revisions) (flutter/flutter#184105) 2026-03-25 engine-flutter-autoroll@skia.org Roll Skia from 789ad6b12776 to 51725ae49ef5 (3 revisions) (flutter/flutter#184106) 2026-03-25 engine-flutter-autoroll@skia.org Roll Skia from f4e59f82dc69 to 789ad6b12776 (7 revisions) (flutter/flutter#184098) 2026-03-25 engine-flutter-autoroll@skia.org Roll Dart SDK from b08bd0a3ee39 to ce171d5026ff (4 revisions) (flutter/flutter#184095) 2026-03-24 srawlins@google.com [flutter_goldens] Remove dead check on null being in a list of non-nullables (flutter/flutter#183938) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 ... --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 21aae04dd65f..8c3ea6ad58ed 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -82d96ef98a33f3f35dabf7795e701f8a4d2d4bec +e79bf6cbc14078179ed09a6d203028a38ee9bd28 From 7ae082a7f6cd1bc6a219c6ab0adf27a3d8ffdc23 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Fri, 27 Mar 2026 04:15:22 -0700 Subject: [PATCH 06/14] [quick_actions_ios] UIScene Migration (#11047) Migrate quick_actions_ios to adopt UIScene Fixes flutter/flutter#170179 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .../quick_actions_ios/CHANGELOG.md | 5 + .../example/ios/Runner/AppDelegate.swift | 7 +- .../example/ios/Runner/Info.plist | 21 ++++ .../RunnerTests/QuickActionsPluginTests.swift | 104 ++++++++++++++++++ .../quick_actions_ios/example/pubspec.yaml | 4 +- .../QuickActionsPlugin.swift | 46 +++++++- .../quick_actions_ios/pubspec.yaml | 6 +- 7 files changed, 185 insertions(+), 8 deletions(-) diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index 506580fd211c..4f1cbc7cf8eb 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.2.4 + +* Adds support for UIScene lifecycle. +* Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. + ## 1.2.3 * Updates to Pigeon 26. diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift index 6fa401143d5a..b5bc5f9682ba 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/AppDelegate.swift @@ -6,14 +6,17 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) super.application(application, didFinishLaunchingWithOptions: launchOptions) // For UI integration tests. See https://github.com/flutter/plugins/pull/3811. return false } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index fd3b62987824..7fba3010b0d8 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -45,5 +45,26 @@ UIApplicationSupportsIndirectInputEvents + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + FlutterSceneDelegate + UISceneConfigurationName + flutter + UISceneStoryboardFile + Main + + + + diff --git a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift index 98766194b3bc..ad2f043af105 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift +++ b/packages/quick_actions/quick_actions_ios/example/ios/RunnerTests/QuickActionsPluginTests.swift @@ -223,4 +223,108 @@ struct QuickActionsPluginTests { plugin.applicationDidBecomeActive(UIApplication.shared) } } + + // MARK: - Scene lifecycle tests + + @Test func windowScenePerformActionForShortcutItem() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + await confirmation("shortcut should be handled via windowScene") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + let windowScene = UIApplication.shared.connectedScenes.first as! UIWindowScene + var completionSuccess: Bool? + let actionResult = plugin.windowScene( + windowScene, + performActionFor: item + ) { success in + completionSuccess = success + } + + #expect(actionResult, "windowScene performActionFor must return true.") + #expect(completionSuccess == true) + } + } + + @Test func sceneWillConnectToWithoutShortcut() { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect( + !connectResult, + "scene willConnectTo must return false if not launched from shortcut.") + } + + @Test func sceneDidBecomeActiveLaunchWithoutShortcut() async { + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + let connectResult = plugin.scene( + UIApplication.shared.connectedScenes.first!, + willConnectTo: UIApplication.shared.connectedScenes.first!.session, + options: nil) + #expect(!connectResult) + + await confirmation("launchAction should not be called", expectedCount: 0) { confirmed in + flutterApi.launchActionCallback = { _ in + confirmed() + } + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } + + @Test func sceneDidBecomeActiveLaunchWithShortcut() async { + let item = UIApplicationShortcutItem( + type: "SearchTheThing", + localizedTitle: "Search the thing", + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(templateImageName: "search_the_thing.png"), + userInfo: nil) + + let flutterApi: MockFlutterApi = MockFlutterApi() + let mockShortcutItemProvider = MockShortcutItemProvider() + + let plugin = QuickActionsPlugin( + flutterApi: flutterApi, + shortcutItemProvider: mockShortcutItemProvider) + + await confirmation("shortcut should be handled when scene becomes active") { confirmed in + flutterApi.launchActionCallback = { aString in + #expect(aString == item.type) + confirmed() + } + + let connectResult = plugin.handleSceneWillConnectTo(shortcutItem: item) + #expect(connectResult, "scene willConnectTo must return true when shortcut is provided.") + + plugin.sceneDidBecomeActive(UIApplication.shared.connectedScenes.first!) + } + } } diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index 8917f495bf01..46a29f8dc9ee 100644 --- a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the quick_actions plugin. publish_to: none environment: - sdk: ^3.9.0 - flutter: ">=3.35.0" + sdk: ^3.10.0 + flutter: ">=3.38.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift index 3f2d91518a11..0ff0980c81d5 100644 --- a/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift +++ b/packages/quick_actions/quick_actions_ios/ios/quick_actions_ios/Sources/quick_actions_ios/QuickActionsPlugin.swift @@ -3,8 +3,11 @@ // found in the LICENSE file. import Flutter +import UIKit -public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi { +public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsApi, + FlutterSceneLifeCycleDelegate +{ public static func register(with registrar: FlutterPluginRegistrar) { let messenger = registrar.messenger() @@ -12,6 +15,7 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA let instance = QuickActionsPlugin(flutterApi: flutterApi) IOSQuickActionsApiSetup.setUp(binaryMessenger: messenger, api: instance) registrar.addApplicationDelegate(instance) + registrar.addSceneDelegate(instance) } private let shortcutItemProvider: ShortcutItemProviding @@ -72,6 +76,46 @@ public final class QuickActionsPlugin: NSObject, FlutterPlugin, IOSQuickActionsA } } + // MARK: - FlutterSceneLifeCycleDelegate + + public func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions? + ) -> Bool { + return handleSceneWillConnectTo(shortcutItem: connectionOptions?.shortcutItem) + } + + func handleSceneWillConnectTo(shortcutItem: UIApplicationShortcutItem?) -> Bool { + if let shortcutItem { + // Keep hold of the shortcut type and handle it in the + // `sceneDidBecomeActive:` method once the Dart MethodChannel + // is initialized. + launchingShortcutType = shortcutItem.type + return true + } + return false + } + + public func sceneDidBecomeActive(_ scene: UIScene) { + if let shortcutType = launchingShortcutType { + handleShortcut(shortcutType) + launchingShortcutType = nil + } + } + + public func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem, + completionHandler: @escaping (Bool) -> Void + ) -> Bool { + handleShortcut(shortcutItem.type) + completionHandler(true) + return true + } + + // MARK: - Shortcut handling + func handleShortcut(_ shortcut: String) { flutterApi.launchAction(action: shortcut) { _ in // noop diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 478edffea6ea..e1f48024ccf0 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: quick_actions_ios description: An implementation for the iOS platform of the Flutter `quick_actions` plugin. repository: https://github.com/flutter/packages/tree/main/packages/quick_actions/quick_actions_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 1.2.3 +version: 1.2.4 environment: - sdk: ^3.9.0 - flutter: ">=3.35.0" + sdk: ^3.10.0 + flutter: ">=3.38.0" flutter: plugin: From 4e8e8c44bb92d2cb0083193e80894656bac90748 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Fri, 27 Mar 2026 12:45:31 -0500 Subject: [PATCH 07/14] [two_dimensional_scrollables] Add regression test (#11376) Fixes https://github.com/flutter/flutter/issues/137112 This PR adds a regression test for flutter/flutter#137112, where TableView would crash when focusing a widget outside of the table while a previously focused TextField inside the table was scrolled out of view. While this issue appears to have been resolved by changes in the Flutter framework specifically flutter/flutter#135182 which addressed how focus keeps children alive in two-dimensional scrollables, this PR adds a dedicated test case to the package to ensure this behavior does not regress. ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .../test/table_view/table_test.dart | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) 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 50ec9d4640db..a59a3301a778 100644 --- a/packages/two_dimensional_scrollables/test/table_view/table_test.dart +++ b/packages/two_dimensional_scrollables/test/table_view/table_test.dart @@ -4175,6 +4175,105 @@ void main() { ); }, ); + + testWidgets( + 'Table does not crash when focusing outside of the table while focused text field is not in the view', + (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/137112 + final verticalController = ScrollController(); + final horizontalController = ScrollController(); + addTearDown(() { + verticalController.dispose(); + horizontalController.dispose(); + }); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Column( + children: [ + const TextField(key: Key('outside_textfield')), + Expanded( + child: TableView.builder( + verticalDetails: ScrollableDetails.vertical( + controller: verticalController, + ), + horizontalDetails: ScrollableDetails.horizontal( + controller: horizontalController, + ), + cellBuilder: + (BuildContext context, TableVicinity vicinity) { + return TableViewCell( + child: Center( + child: TextField( + key: Key( + 'cell_${vicinity.row}_${vicinity.column}', + ), + ), + ), + ); + }, + columnCount: 20, + columnBuilder: (int index) { + return const TableSpan( + foregroundDecoration: TableSpanDecoration( + border: TableSpanBorder(trailing: BorderSide()), + ), + extent: FixedTableSpanExtent(100), + ); + }, + rowCount: 40, + rowBuilder: (int index) { + return TableSpan( + backgroundDecoration: TableSpanDecoration( + color: index.isEven ? Colors.purple[100] : null, + border: const TableSpanBorder( + trailing: BorderSide(width: 3), + ), + ), + extent: const FixedTableSpanExtent(50), + ); + }, + ), + ), + ], + ), + ), + ), + ); + + // 1. Select a TextField in the table. + // Use the vicinity from the original crash report. + const vicinity = TableVicinity(row: 5, column: 6); + final Finder cellTextField = find.byKey( + Key('cell_${vicinity.row}_${vicinity.column}'), + ); + // Bring it into view. + verticalController.jumpTo(250); + horizontalController.jumpTo(600); + await tester.pumpAndSettle(); + + await tester.tap(cellTextField); + await tester.pumpAndSettle(); + expect(FocusManager.instance.primaryFocus, isNotNull); + + // 2. Scroll until it disappears from the view, without unfocusing it. + verticalController.jumpTo(verticalController.offset + 1000); + await tester.pumpAndSettle(); + + // 3. Select another TextField outside of the table. + final Finder outsideTextField = find.byKey( + const Key('outside_textfield'), + ); + await tester.tap(outsideTextField); + await tester.pumpAndSettle(); + + // 4. Scroll back and ensure the table does not crash. + verticalController.jumpTo(verticalController.offset - 1000); + await tester.pumpAndSettle(); + expect(cellTextField, findsOneWidget); + }, + ); } class _NullBuildContext implements BuildContext, TwoDimensionalChildManager { From 312839a535064f6b4f5d663685d6d32b3eb7c73a Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 27 Mar 2026 14:02:41 -0400 Subject: [PATCH 08/14] Roll Flutter from e79bf6cbc140 to fb03253e32ce (16 revisions) (#11379) https://github.com/flutter/flutter/compare/e79bf6cbc140...fb03253e32ce 2026-03-27 kevmoo@users.noreply.github.com flutter_driver: remove @internal annotation on field (flutter/flutter#184235) 2026-03-27 engine-flutter-autoroll@skia.org Roll Skia from 5299de75c97b to 8c705ac86366 (2 revisions) (flutter/flutter#184245) 2026-03-27 engine-flutter-autoroll@skia.org Roll Packages from 0dd241079156 to 7ae082a7f6cd (5 revisions) (flutter/flutter#184248) 2026-03-27 engine-flutter-autoroll@skia.org Roll Skia from 9beded929d5a to 5299de75c97b (1 revision) (flutter/flutter#184243) 2026-03-27 engine-flutter-autoroll@skia.org Roll Skia from 4f4f07084ef0 to 9beded929d5a (4 revisions) (flutter/flutter#184237) 2026-03-27 engine-flutter-autoroll@skia.org Roll Dart SDK from ea1bce22b45b to dfd1f8af3c52 (2 revisions) (flutter/flutter#184234) 2026-03-27 engine-flutter-autoroll@skia.org Roll Skia from 1b7154852825 to 4f4f07084ef0 (1 revision) (flutter/flutter#184231) 2026-03-27 engine-flutter-autoroll@skia.org Roll Skia from aec9a7ab7ed9 to 1b7154852825 (1 revision) (flutter/flutter#184230) 2026-03-27 34465683+rkishan516@users.noreply.github.com fix: show window after first frame callback (flutter/flutter#183454) 2026-03-27 engine-flutter-autoroll@skia.org Roll Dart SDK from 7587a31814c6 to ea1bce22b45b (1 revision) (flutter/flutter#184228) 2026-03-27 okorohelijah@google.com skip interactive keyboard tests (flutter/flutter#183757) 2026-03-26 git@reb0.org Build engine for windows_arm on beta and stable (flutter/flutter#176385) 2026-03-26 engine-flutter-autoroll@skia.org Roll Skia from bee5a06ef578 to aec9a7ab7ed9 (4 revisions) (flutter/flutter#184222) 2026-03-26 magder@google.com Update iOS/macOS flutter_tools CODEOWNERS (flutter/flutter#183287) 2026-03-26 evanwall@buffalo.edu Update changelog for 3.41.6 stable hotfix (flutter/flutter#184220) 2026-03-26 47866232+chunhtai@users.noreply.github.com Updates scroll cache extent doc (flutter/flutter#184142) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 8c3ea6ad58ed..1623836863db 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -e79bf6cbc14078179ed09a6d203028a38ee9bd28 +fb03253e32ce6aba92872ed9c1224e999ec6abcb From 60eb967dbafd7db83fea8e6176e2ba2e67e404c0 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 27 Mar 2026 15:26:21 -0400 Subject: [PATCH 09/14] Roll Flutter (stable) from 2c9eb20739df to db50e20168db (6 revisions) (#11380) https://github.com/flutter/flutter/compare/2c9eb20739df...db50e20168db If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-stable-packages Please CC stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter (stable): https://github.com/flutter/flutter/issues/new/choose To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_stable.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index ad241bc2bef7..cab742975b85 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa +db50e20168db8fee486b9abf32fc912de3bc5b6a From cd608608bca2ca5ca7c34716ad73ad6da222c519 Mon Sep 17 00:00:00 2001 From: Mohellebi Abdessalem <116356835+AbdeMohlbi@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:18:38 +0100 Subject: [PATCH 10/14] [Animations] add example to `openCntainer` (#11189) Fixes the todo : `TODO(goderbauer): Add example animations and sample code.` ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- packages/animations/CHANGELOG.md | 3 +- .../animations/lib/src/open_container.dart | 41 ++++++++++++++++++- packages/animations/pubspec.yaml | 2 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/animations/CHANGELOG.md b/packages/animations/CHANGELOG.md index 6d7110f9b8ec..0779bea39e8a 100644 --- a/packages/animations/CHANGELOG.md +++ b/packages/animations/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.1.2 * Updates minimum supported SDK version to Flutter 3.35/Dart 3.9. +* Adds an example of using `OpenContainer` ## 2.1.1 diff --git a/packages/animations/lib/src/open_container.dart b/packages/animations/lib/src/open_container.dart index 10dd2b2f5e70..4683c3597642 100644 --- a/packages/animations/lib/src/open_container.dart +++ b/packages/animations/lib/src/open_container.dart @@ -66,7 +66,46 @@ typedef ClosedCallback = void Function(S data); /// `T` refers to the type of data returned by the route when the container /// is closed. This value can be accessed in the `onClosed` function. /// -// TODO(goderbauer): Add example animations and sample code. +/// The following example shows an [OpenContainer] that transforms a blue +/// container widget into a full screen page using the Material container +/// transform animation. When the user taps the closed widget, the container +/// expands and morphs into the destination page defined in [openBuilder], +/// while the original widget from [closedBuilder] fades out during the +/// transition. +/// +/// ```dart +/// OpenContainer( +/// transitionDuration: const Duration(milliseconds: 500), +/// transitionType: ContainerTransitionType.fadeThrough, +/// openBuilder: (context, action) { +/// return Scaffold( +/// appBar: AppBar(title: const Text('Details Page')), +/// body: const Center( +/// child: Text( +/// 'This page opened with Container Transform animation', +/// style: TextStyle(fontSize: 18), +/// textAlign: TextAlign.center, +/// ), +/// ), +/// ); +/// }, +/// closedBuilder: (context, action) { +/// return Container( +/// width: 200, +/// height: 120, +/// alignment: Alignment.center, +/// decoration: BoxDecoration( +/// color: Colors.blue, +/// borderRadius: BorderRadius.circular(16), +/// ), +/// child: const Text( +/// 'Open Details', +/// style: TextStyle(color: Colors.white, fontSize: 18), +/// ), +/// ); +/// }, +/// ), +/// ``` /// /// See also: /// diff --git a/packages/animations/pubspec.yaml b/packages/animations/pubspec.yaml index 012218b3db2b..0670649c6e95 100644 --- a/packages/animations/pubspec.yaml +++ b/packages/animations/pubspec.yaml @@ -2,7 +2,7 @@ name: animations description: Fancy pre-built animations that can easily be integrated into any Flutter application. repository: https://github.com/flutter/packages/tree/main/packages/animations issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+animations%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ^3.9.0 From ea4c006d6e8eac6c4dc0e7aa6870be1f83735698 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 27 Mar 2026 16:29:18 -0400 Subject: [PATCH 11/14] Roll Flutter (stable) from 2c9eb20739df to db50e20168db (6 revisions) (#11380) https://github.com/flutter/flutter/compare/2c9eb20739df...db50e20168db If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-stable-packages Please CC stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter (stable): https://github.com/flutter/flutter/issues/new/choose To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md From e8df95dc2e16d350b6255eda3b102da70310cad3 Mon Sep 17 00:00:00 2001 From: stuartmorgan-g Date: Fri, 27 Mar 2026 17:21:17 -0400 Subject: [PATCH 12/14] Remove CODEOWNERS (#11375) CODEOWNERS was originally added to deal with the problem of routing reviews to the right people, given the highly disparate ownership of packages in this repo. However, it has always had some significant drawbacks due the rigid implementation GitHub has for it, and some are getting worse: - Low-quality PRs that should be handled in initial triage were going directly to individual reviewers, who don't necessarily know what kinds of pre-review filters to apply, causing some wasted reviewer effort - The increase of agent-generated PRs is making this worse, and that trend will almost certainly continue to increase - Some PRs (e.g., rolling all the minimum SDK versions when a new `stable` is released) spam a lot of people who don't actually need to do the review, again wasting some people's time. - The way the new batch release process merges changes across branches will make this much more common. - Plugin PRs are routed to all reviewers at once. In some cases platform reviewers would review implementations before the cross-platform owner reviewed, and either asked for fundamental API changes, or decided it wasn't something we should do at all, which wastes reviewer time and is a terrible experience for the contributor. Ideally the cross-platform owners should sanity-check the proposed change before looping in platform implementation reviewers. - It's impossible to remove people who don't need to review for whatever reason. This means the state of the `Reviewers` section in a PR's sidebar often doesn't (and can't) reflect reality, making triage of current PR states much harder. The `triage-` label system that we have added more recently solves a lot of the problems that CODEOWNERS was meant to solve in the first place, without any of the downsides. It will also bring flutter/packages PR handling more in line with flutter/flutter PR handling, which makes our processes more coherent. Rather than eliminating the file entirely, I turned it into a markdown file that we can refer to. I no longer have repo tool enforcement however, to allow for changes in structure over time (e.g., for some low-traffic subteam-owned packages, they may prefer not have specific owners, and just triage to individuals in an ad-hoc way in their subteam triage). As a starting point it has the same people that the CODEOWNERs did, and we can iterate from there (or remove it later if it turns out that keeping it up to date isn't a good use of time). --- CODEOWNERS | 127 ------------ SUGGESTED_REVIEWERS.md | 187 ++++++++++++++++++ script/tool/lib/src/common/file_filters.dart | 2 +- .../src/repo_package_info_check_command.dart | 34 +--- script/tool/test/analyze_command_test.dart | 6 +- .../test/build_examples_command_test.dart | 2 +- .../tool/test/common/file_filters_test.dart | 2 +- script/tool/test/dart_test_command_test.dart | 2 +- .../test/drive_examples_command_test.dart | 2 +- .../test/firebase_test_lab_command_test.dart | 2 +- .../tool/test/native_test_command_test.dart | 2 +- .../repo_package_info_check_command_test.dart | 71 +------ 12 files changed, 199 insertions(+), 240 deletions(-) delete mode 100644 CODEOWNERS create mode 100644 SUGGESTED_REVIEWERS.md diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index a49a588717fc..000000000000 --- a/CODEOWNERS +++ /dev/null @@ -1,127 +0,0 @@ -# Below is a list of Flutter team members who are suggested reviewers -# for contributions to packages in this repository. -# -# These names are just suggestions. It is fine to have your changes -# reviewed by someone else. - -packages/animations/** @hannah-hyj -packages/camera/** @bparrishMines -packages/cross_file/** @stuartmorgan-g -packages/cupertino_ui/** @dkwingsmt -packages/extension_google_sign_in_as_googleapis_auth/** @stuartmorgan-g -packages/file_selector/** @stuartmorgan-g -packages/flutter_lints/** @chunhtai -packages/flutter_template_images/** @stuartmorgan-g -packages/go_router/** @chunhtai -packages/go_router_builder/** @chunhtai -packages/google_adsense/** @sokoloff06 @ditman -packages/google_identity_services_web/** @mdebbar -packages/google_fonts/** @Piinks -packages/google_maps_flutter/** @stuartmorgan-g -packages/google_sign_in/** @stuartmorgan-g -packages/image_picker/** @tarrinneal -packages/interactive_media_ads/** @bparrishMines -packages/in_app_purchase/** @bparrishMines -packages/local_auth/** @stuartmorgan-g -packages/material_ui/** @qunccccccc -packages/metrics_center/** @bkonyi -packages/multicast_dns/** @vashworth -packages/path_provider/** @stuartmorgan-g -packages/pigeon/** @tarrinneal -packages/platform/** @stuartmorgan-g -packages/plugin_platform_interface/** @stuartmorgan-g -packages/pointer_interceptor/** @ditman -packages/quick_actions/** @bparrishMines -packages/rfw/** @Hixie -packages/shared_preferences/** @tarrinneal -packages/standard_message_codec/** @stuartmorgan-g -packages/two_dimensional_scrollables/** @Piinks -packages/url_launcher/** @stuartmorgan-g -packages/vector_graphics/** @jtmcdole -packages/vector_graphics_codec/** @jtmcdole -packages/vector_graphics_compiler/** @jtmcdole -packages/video_player/** @tarrinneal -packages/web_benchmarks/** @yjbanov -packages/webview_flutter/** @bparrishMines -packages/xdg_directories/** @stuartmorgan-g -third_party/packages/cupertino_icons/** @victorsanni -third_party/packages/cupertino_icons/test/goldens/** @LongCatIsLooong -third_party/packages/flutter_svg/** @domesticmouse -third_party/packages/flutter_svg_test/** @domesticmouse -third_party/packages/mustache_template/** @bkonyi @parlough -third_party/packages/path_parsing/** @domesticmouse - -# Plugin platform implementation rules. These should stay last, since the last -# matching entry takes precedence. - -# - Web -packages/camera/camera_web/** @mdebbar -packages/file_selector/file_selector_web/** @mdebbar -packages/google_maps_flutter/google_maps_flutter_web/** @mdebbar -packages/google_sign_in/google_sign_in_web/** @mdebbar -packages/image_picker/image_picker_for_web/** @mdebbar -packages/pointer_interceptor/pointer_interceptor_web/** @mdebbar -packages/shared_preferences/shared_preferences_web/** @mdebbar -packages/url_launcher/url_launcher_web/** @mdebbar -packages/video_player/video_player_web/** @mdebbar -packages/webview_flutter/webview_flutter_web/** @mdebbar - -# - Android -packages/camera/camera_android/** @camsim99 -packages/camera/camera_android_camerax/** @camsim99 -packages/espresso/** @jesswrd -packages/file_selector/file_selector_android/** @mboetger -packages/flutter_plugin_android_lifecycle/** @reidbaker -packages/google_maps_flutter/google_maps_flutter_android/** @reidbaker -packages/google_sign_in/google_sign_in_android/** @reidbaker -packages/image_picker/image_picker_android/** @gmackall -packages/in_app_purchase/in_app_purchase_android/** @gmackall -packages/local_auth/local_auth_android/** @mboetger -packages/path_provider/path_provider_android/** @camsim99 -packages/quick_actions/quick_actions_android/** @jesswrd -packages/shared_preferences/shared_preferences_android/** @jesswrd -packages/url_launcher/url_launcher_android/** @gmackall -packages/video_player/video_player_android/** @mboetger -# Owned by ecosystem team for now during the wrapper evaluation. -packages/webview_flutter/webview_flutter_android/** @bparrishMines - -# - Darwin -packages/camera/camera_avfoundation/** @hellohuanlin @louisehsu -packages/file_selector/file_selector_ios/** @okorohelijah @vashworth -packages/file_selector/file_selector_macos/** @okorohelijah @vashworth -packages/google_maps_flutter/google_maps_flutter_ios/** @vashworth @LongCatIsLooong -packages/google_maps_flutter/google_maps_flutter_ios_sdk9/** @vashworth @LongCatIsLooong -packages/google_maps_flutter/google_maps_flutter_ios_sdk10/** @vashworth @LongCatIsLooong -packages/google_sign_in/google_sign_in_ios/** @LongCatIsLooong @okorohelijah -packages/image_picker/image_picker_ios/** @okorohelijah @vashworth -packages/image_picker/image_picker_macos/** @okorohelijah @vashworth -packages/in_app_purchase/in_app_purchase_storekit/** @louisehsu @LongCatIsLooong -packages/local_auth/local_auth_darwin/** @louisehsu @okorohelijah -packages/path_provider/path_provider_foundation/** @LongCatIsLooong @vashworth -packages/pointer_interceptor/pointer_interceptor_ios/** @louisehsu @hellohuanlin -packages/quick_actions/quick_actions_ios/** @louisehsu @LongCatIsLooong -packages/shared_preferences/shared_preferences_foundation/** @tarrinneal -packages/url_launcher/url_launcher_ios/** @vashworth @LongCatIsLooong -packages/url_launcher/url_launcher_macos/** @vashworth @LongCatIsLooong -packages/video_player/video_player_avfoundation/** @hellohuanlin @louisehsu -packages/webview_flutter/webview_flutter_wkwebview/** @LongCatIsLooong @hellohuanlin - -# - Linux -packages/file_selector/file_selector_linux/** @robert-ancell @stuartmorgan-g -packages/image_picker/image_picker_linux/** @robert-ancell @stuartmorgan-g -packages/path_provider/path_provider_linux/** @robert-ancell @stuartmorgan-g -packages/shared_preferences/shared_preferences_linux/** @robert-ancell @stuartmorgan-g -packages/url_launcher/url_launcher_linux/** @robert-ancell @stuartmorgan-g - -# - Windows -packages/camera/camera_windows/** @stuartmorgan-g -packages/file_selector/file_selector_windows/** @stuartmorgan-g -packages/image_picker/image_picker_windows/** @stuartmorgan-g -packages/local_auth/local_auth_windows/** @stuartmorgan-g -packages/path_provider/path_provider_windows/** @stuartmorgan-g -packages/shared_preferences/shared_preferences_windows/** @stuartmorgan-g -packages/url_launcher/url_launcher_windows/** @stuartmorgan-g - -# - DevTools extensions -# @adsonpleal is the actual maintainer of shared_preferences_tool but is not yet a committer, so can't be listed as the owner. -packages/shared_preferences/shared_preferences_tool/** @tarrinneal diff --git a/SUGGESTED_REVIEWERS.md b/SUGGESTED_REVIEWERS.md new file mode 100644 index 000000000000..ad9a954e7683 --- /dev/null +++ b/SUGGESTED_REVIEWERS.md @@ -0,0 +1,187 @@ +Below is a list of Flutter team members who are suggested reviewers +for contributions to packages in this repository. + +These names are just suggestions. It is fine to have your changes +reviewed by someone else. + +`animations`: + - @hannah-hyj + +`camera`: + - **Cross-platform**: @bparrishMines + - **Android**: @camsim99 + - **iOS**: @hellohuanlin, @louisehsu + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`cross_file`: + - @stuartmorgan-g + +`cupertino_icons`: + - @victorsanni + +`cupertino_ui`: + - @dkwingsmt + +`espresso`: + - @jesswrd + +`extension_google_sign_in_as_googleapis_auth`: + - @stuartmorgan-g + +`file_selector`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @mboetger + - **iOS**: @okorohelijah, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @okorohelijah, @vashworth + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`flutter_lints`: + - @chunhtai + +`flutter_plugin_android_lifecycle`: + - @reidbaker + +`flutter_svg, flutter_svg_test`: + - @domesticmouse + +`flutter_template_images`: + - @stuartmorgan-g + +`go_router / go_router_builder`: + - @chunhtai + +`google_adsense`: + - @sokoloff06, @ditman + +`google_identity_services_web`: + - @mdebbar + +`google_fonts`: + - @Piinks + +`google_maps_flutter`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @reidbaker + - **iOS**: @vashworth, @LongCatIsLooong + - **Web**: @mdebbar + +`google_sign_in`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @reidbaker + - **iOS**: @LongCatIsLooong, @okorohelijah + - **Web**: @mdebbar + +`image_picker`: + - **Cross-platform**: @tarrinneal + - **Android**: @gmackall + - **iOS**: @okorohelijah, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @okorohelijah, @vashworth + - **Web**: @mdebbar + - **Windows**: @stuartmorgan-g + +`interactive_media_ads`: + - @bparrishMines + +`in_app_purchase`: + - **Cross-platform**: @bparrishMines + - **Android**: @gmackall + - **iOS**: @louisehsu, @LongCatIsLooong + +`local_auth`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @mboetger + - **iOS/macOS**: @louisehsu, @okorohelijah + - **Windows**: @stuartmorgan-g + +`material_ui`: + - @qunccccccc + +`metrics_center`: + - @bkonyi + +`multicast_dns`: + - @vashworth + +`mustache_template`: + - @bkonyi, @parlough + +`path_parsing`: + - @domesticmouse + +`path_provider`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @camsim99 + - **iOS/macOS**: @LongCatIsLooong, @vashworth + - **Linux**: @robert-ancell, @stuartmorgan-g + - **Windows**: @stuartmorgan-g + +`pigeon`: + - @tarrinneal + +`platform`: + - @stuartmorgan-g + +`plugin_platform_interface`: + - @stuartmorgan-g + +`pointer_interceptor`: + - **Cross-platform**: @ditman + - **iOS**: @louisehsu, @hellohuanlin + - **Web**: @mdebbar + +`quick_actions`: + - **Cross-platform**: @bparrishMines + - **Android**: @jesswrd + - **iOS**: @louisehsu, @LongCatIsLooong + +`rfw`: + - @Hixie + +`shared_preferences`: + - **Cross-platform**: @tarrinneal + - **Android**: @jesswrd + - **iOS/macOS**: @tarrinneal + - **Linux**: @robert-ancell, @stuartmorgan-g + - **Windows**: @stuartmorgan-g + - **Web**: @mdebbar + - **Devtools**: @adsonpleal + +`standard_message_codec`: + - @stuartmorgan-g + +`two_dimensional_scrollables`: + - @Piinks + +`url_launcher`: + - **Cross-platform**: @stuartmorgan-g + - **Android**: @gmackall + - **iOS**: @vashworth, @LongCatIsLooong + - **Linux**: @robert-ancell, @stuartmorgan-g + - **macOS**: @vashworth, @LongCatIsLooong + - **Windows**: @stuartmorgan-g + - **Web**: @mdebbar + +`vector_graphics, vector_graphics_codec, vector_graphics_compiler`: + - @jtmcdole + +`video_player`: + - **Cross-platform**: @tarrinneal + - **Android**: @mboetger + - **iOS/macOS**: @hellohuanlin, @louisehsu + - **Web**: @mdebbar + +`web_benchmarks`: + - @yjbanov + +`webview_flutter`: + - **Cross-platform**: @bparrishMines + - **Android**: @bparrishMines + - **iOS/macOS**: @bparrishMines, @LongCatIsLooong, @hellohuanlin + - **Web**: @mdebbar + +`xdg_directories`: + - @stuartmorgan-g diff --git a/script/tool/lib/src/common/file_filters.dart b/script/tool/lib/src/common/file_filters.dart index 7a1bafd06a5c..90be24da8d03 100644 --- a/script/tool/lib/src/common/file_filters.dart +++ b/script/tool/lib/src/common/file_filters.dart @@ -9,10 +9,10 @@ bool isRepoLevelNonCodeImpactingFile(String path) { return [ 'AUTHORS', - 'CODEOWNERS', 'CONTRIBUTING.md', 'LICENSE', 'README.md', + 'SUGGESTED_REVIEWERS.md', 'AGENTS.md', // This deliberate lists specific files rather than excluding the whole // .github directory since it's better to have false negatives than to diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index c423d760ea85..1c213fd2d0f3 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -16,7 +16,7 @@ const int _exitBadTableEntry = 3; const int _exitUnknownPackageEntry = 4; /// A command to verify repository-level metadata about packages, such as -/// repo README and CODEOWNERS entries. +/// repo README and auto-label entries. class RepoPackageInfoCheckCommand extends PackageLoopingCommand { /// Creates Dependabot check command instance. RepoPackageInfoCheckCommand(super.packagesDir, {super.gitDir}); @@ -27,9 +27,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { final Map> _readmeTableEntries = >{}; - /// Packages with entries in CODEOWNERS. - final List _ownedPackages = []; - /// Packages with entries in labeler.yml. final List _autoLabeledPackages = []; @@ -82,25 +79,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { } } - // Extract all of the CODEOWNERS package entries. - final packageOwnershipPattern = RegExp( - r'^((?:third_party/)?packages/(?:[^/]*/)?([^/]*))/\*\*', - ); - for (final String line - in _repoRoot.childFile('CODEOWNERS').readAsLinesSync()) { - final RegExpMatch? match = packageOwnershipPattern.firstMatch(line); - if (match == null) { - continue; - } - final String path = match.group(1)!; - final String name = match.group(2)!; - if (!_repoRoot.childDirectory(path).existsSync()) { - printError('Unknown directory "$path" in CODEOWNERS'); - throw ToolExit(_exitUnknownPackageEntry); - } - _ownedPackages.add(name); - } - // Extract all of the lebeler.yml package entries. // Validate the match rules rather than the label itself, as the labels // don't always correspond 1:1 to packages and package names. @@ -126,16 +104,6 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { final String packageName = package.directory.basename; final errors = []; - // All packages should have an owner. - // Platform interface packages are considered to be owned by the app-facing - // package owner. - if (!(_ownedPackages.contains(packageName) || - package.isPlatformInterface && - _ownedPackages.contains(package.directory.parent.basename))) { - printError('${indentation}Missing CODEOWNERS entry.'); - errors.add('Missing CODEOWNERS entry'); - } - // All packages should have an auto-applied label. For plugins, only the // group needs a rule, so check the app-facing package. if (!(package.isFederated && !package.isAppFacing) && diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 0509f8d25988..ef2a609579ac 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -758,7 +758,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), @@ -1064,7 +1064,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md packages/package_a/lib/foo.dart ''', @@ -1669,7 +1669,7 @@ packages/package_a/$file .gemini/config.yaml AGENTS.md README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md packages/package_a/lib/foo.dart ''', diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart index b2f66c5dbd6f..2b8a8c8952fd 100644 --- a/script/tool/test/build_examples_command_test.dart +++ b/script/tool/test/build_examples_command_test.dart @@ -1133,7 +1133,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/common/file_filters_test.dart b/script/tool/test/common/file_filters_test.dart index 0143fb3dde30..dd06515509f2 100644 --- a/script/tool/test/common/file_filters_test.dart +++ b/script/tool/test/common/file_filters_test.dart @@ -9,10 +9,10 @@ void main() { group('isRepoLevelNonCodeImpactingFile', () { test('returns true for known non-code files', () { expect(isRepoLevelNonCodeImpactingFile('AUTHORS'), isTrue); - expect(isRepoLevelNonCodeImpactingFile('CODEOWNERS'), isTrue); expect(isRepoLevelNonCodeImpactingFile('CONTRIBUTING.md'), isTrue); expect(isRepoLevelNonCodeImpactingFile('LICENSE'), isTrue); expect(isRepoLevelNonCodeImpactingFile('README.md'), isTrue); + expect(isRepoLevelNonCodeImpactingFile('SUGGESTED_REVIEWERS.md'), isTrue); expect(isRepoLevelNonCodeImpactingFile('AGENTS.md'), isTrue); expect( isRepoLevelNonCodeImpactingFile('.github/PULL_REQUEST_TEMPLATE.md'), diff --git a/script/tool/test/dart_test_command_test.dart b/script/tool/test/dart_test_command_test.dart index 1a16a3703b3f..a6cac49ac0e7 100644 --- a/script/tool/test/dart_test_command_test.dart +++ b/script/tool/test/dart_test_command_test.dart @@ -881,7 +881,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart index f4aebdb569ac..6d4e44ca748a 100644 --- a/script/tool/test/drive_examples_command_test.dart +++ b/script/tool/test/drive_examples_command_test.dart @@ -1801,7 +1801,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md .gitignore packages/package_a/CHANGELOG.md ''', diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart index e469e1f2c645..2fd731ef8ad2 100644 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -1068,7 +1068,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart index 6c51ed7f2fd1..1a34ebf5acb5 100644 --- a/script/tool/test/native_test_command_test.dart +++ b/script/tool/test/native_test_command_test.dart @@ -492,7 +492,7 @@ packages/package_a/$file MockProcess( stdout: ''' README.md -CODEOWNERS +SUGGESTED_REVIEWERS.md packages/package_a/CHANGELOG.md ''', ), diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index 341e0b2892c2..820c45ca6ce0 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -44,22 +44,6 @@ void main() { '''; } - void writeCodeOwners(List ownedPackages) { - final List subpaths = ownedPackages - .map( - (RepositoryPackage p) => p.isFederated - ? [ - p.directory.parent.basename, - p.directory.basename, - ].join('/') - : p.directory.basename, - ) - .toList(); - root.childFile('CODEOWNERS').writeAsStringSync(''' -${subpaths.map((String subpath) => 'packages/$subpath/** @someone').join('\n')} -'''); - } - String readmeTableEntry(String packageName) { final String encodedTag = Uri.encodeComponent('p: $packageName'); return '| [$packageName](./packages/$packageName/) | ' @@ -100,7 +84,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -130,7 +113,6 @@ ${readmeTableEntry(pluginName)} '''); writeAutoLabelerYaml([packages.first]); writeAutoLabelerYaml([packages.first]); - writeCodeOwners(packages); // 4 packages * 2 checks (git, gh) = 8 calls. // Default mocks in setUp cover 1 call each. We need 3 more each. @@ -162,7 +144,6 @@ ${readmeTableHeader()} ${readmeTableEntry('another_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -193,7 +174,6 @@ ${readmeTableHeader()} ${readmeTableEntry('another_package')} '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -237,7 +217,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -282,7 +261,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -329,7 +307,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -376,7 +353,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -423,7 +399,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -470,7 +445,6 @@ ${readmeTableHeader()} $entry '''); writeAutoLabelerYaml(packages); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -496,53 +470,15 @@ $entry ); }); - test('fails for missing CODEOWNER', () async { - const packageName = 'a_package'; - final packages = [ - createFakePackage('a_package', packagesDir), - ]; - - root.childFile('README.md').writeAsStringSync(''' -${readmeTableHeader()} -${readmeTableEntry(packageName)} -'''); - writeAutoLabelerYaml(packages); - writeCodeOwners([]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - ['repo-package-info-check'], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing CODEOWNERS entry.'), - contains( - 'a_package:\n' - ' Missing CODEOWNERS entry', - ), - ]), - ); - }); - test('fails for missing auto-labeler entry', () async { const packageName = 'a_package'; - final packages = [ - createFakePackage('a_package', packagesDir), - ]; + createFakePackage('a_package', packagesDir); root.childFile('README.md').writeAsStringSync(''' ${readmeTableHeader()} ${readmeTableEntry(packageName)} '''); writeAutoLabelerYaml([]); - writeCodeOwners(packages); Error? commandError; final List output = await runCapturingPrint( @@ -578,7 +514,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' release: @@ -603,7 +538,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); final List output = await runCapturingPrint(runner, [ 'repo-package-info-check', @@ -626,7 +560,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' something: true '''); @@ -660,7 +593,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); package.ciConfigFile.writeAsStringSync(''' release: batch: 1 @@ -699,7 +631,6 @@ ${readmeTableHeader()} ${readmeTableEntry('a_package')} '''); writeAutoLabelerYaml([package]); - writeCodeOwners([package]); return package; } From 2dc597c153c465b87abe5c6684d06308f72bc7f8 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Fri, 27 Mar 2026 17:23:22 -0700 Subject: [PATCH 13/14] [pigeon] bumps analyzer support to between 10 and 12 (#11358) Don't land this until [previous bump ](https://github.com/flutter/packages/pull/11346)lands fixes https://github.com/flutter/flutter/issues/184119 fixes https://github.com/flutter/flutter/issues/182064 fixes https://github.com/flutter/flutter/issues/184269 --- packages/go_router_builder/CHANGELOG.md | 4 ++++ packages/go_router_builder/pubspec.yaml | 4 ++-- packages/pigeon/CHANGELOG.md | 4 ++++ packages/pigeon/lib/src/generator_tools.dart | 2 +- .../pigeon/lib/src/pigeon_lib_internal.dart | 20 +++++++++---------- packages/pigeon/pubspec.yaml | 4 ++-- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index f9e9da0393ab..498ba7174dcb 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.2.1 + +* Adds support for analyzer 11 and 12. + ## 4.2.0 - Adds supports for `TypedQueryParameter` annotation. diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 9985c37166a4..536b9c94b813 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 4.2.0 +version: 4.2.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 @@ -11,7 +11,7 @@ environment: flutter: ">=3.35.0" dependencies: - analyzer: ">=8.2.0 <10.0.0" + analyzer: ">=8.2.0 <13.0.0" async: ^2.8.0 # TODO(piinks): Pin version once new stable rolls. build: ">=3.0.0 <5.0.0" diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 16ca19f5515a..368abc59a80f 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 26.3.3 + +* Updates `analyzer` dependency to support versions 10 through 12. + ## 26.3.2 * Updates `analyzer` dependency to support version 10. diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index e06ddf0bd228..604691d46d3e 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '26.3.2'; +const String pigeonVersion = '26.3.3'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/lib/src/pigeon_lib_internal.dart b/packages/pigeon/lib/src/pigeon_lib_internal.dart index 19da4613ce42..617a901f37cc 100644 --- a/packages/pigeon/lib/src/pigeon_lib_internal.dart +++ b/packages/pigeon/lib/src/pigeon_lib_internal.dart @@ -1579,7 +1579,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { _errors.add( Error( message: - 'API "${node.name.lexeme}" can only have one API annotation but contains: ${node.metadata}', + 'API "${node.namePart.typeName.lexeme}" can only have one API annotation but contains: ${node.metadata}', lineNumber: calculateLineNumber(source, node.offset), ), ); @@ -1606,7 +1606,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } _currentApi = AstHostApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], dartHostTestHandler: dartHostTestHandler, documentationComments: _documentationCommentsParser( @@ -1615,7 +1615,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { ); } else if (_hasMetadata(node.metadata, 'FlutterApi')) { _currentApi = AstFlutterApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], documentationComments: _documentationCommentsParser( node.documentationComment?.tokens, @@ -1642,7 +1642,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { _errors.add( Error( message: - 'ProxyApis should either set the super class in the annotation OR use extends: ("${node.name.lexeme}").', + 'ProxyApis should either set the super class in the annotation OR use extends: ("${node.namePart.typeName.lexeme}").', lineNumber: calculateLineNumber(source, node.offset), ), ); @@ -1713,7 +1713,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } _currentApi = AstProxyApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], constructors: [], fields: [], @@ -1760,7 +1760,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { ); } _currentApi = AstEventChannelApi( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, methods: [], swiftOptions: swiftOptions, kotlinOptions: kotlinOptions, @@ -1771,7 +1771,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } } else { _currentClass = Class( - name: node.name.lexeme, + name: node.namePart.typeName.lexeme, fields: [], superClassName: node.implementsClause?.interfaces.first.name.toString() ?? @@ -1929,7 +1929,7 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { } if (enclosingDeclaration is dart_ast.ClassDeclaration) { erroneousDeclaration = - '${enclosingDeclaration.name.lexeme}.$erroneousDeclaration'; + '${enclosingDeclaration.namePart.typeName}.$erroneousDeclaration'; } _errors.add( Error( @@ -1986,8 +1986,8 @@ class RootBuilder extends dart_ast_visitor.RecursiveAstVisitor { Object? visitEnumDeclaration(dart_ast.EnumDeclaration node) { _enums.add( Enum( - name: node.name.lexeme, - members: node.constants + name: node.namePart.typeName.lexeme, + members: node.body.constants .map( (dart_ast.EnumConstantDeclaration e) => EnumMember( name: e.name.lexeme, diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 7a1359aec30c..637fb3f6fe6b 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,13 +2,13 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22 -version: 26.3.2 # This must match the version in lib/src/generator_tools.dart +version: 26.3.3 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.9.0 dependencies: - analyzer: ">=8.0.0 <11.0.0" + analyzer: ">=10.0.0 <13.0.0" args: ^2.5.0 code_builder: ^4.10.0 collection: ^1.15.0 From f99cae8ec5a12d95db96c6e46903cf16275f19a6 Mon Sep 17 00:00:00 2001 From: flutterpackagepublisher Date: Mon, 30 Mar 2026 08:34:29 +0000 Subject: [PATCH 14/14] [packages/packages/go_router] Prepare for batch release --- packages/go_router/CHANGELOG.md | 4 ++++ .../go_router/pending_changelogs/support_custom_types.yaml | 3 --- packages/go_router/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 packages/go_router/pending_changelogs/support_custom_types.yaml diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index f1d95f90c9fc..20c576b4e4dc 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 17.2.0 + +- Adds `encoder`, `decoder` and `compare` parameters to `TypedQueryParameter` annotation for custom encoding, decoding and comparison of query parameters in `TypedGoRoute` constructors. + ## 17.1.0 - Adds `TypedQueryParameter` annotation to override parameter names in `TypedGoRoute` constructors. diff --git a/packages/go_router/pending_changelogs/support_custom_types.yaml b/packages/go_router/pending_changelogs/support_custom_types.yaml deleted file mode 100644 index 40240bfcae31..000000000000 --- a/packages/go_router/pending_changelogs/support_custom_types.yaml +++ /dev/null @@ -1,3 +0,0 @@ -changelog: | - - Adds `encoder`, `decoder` and `compare` parameters to `TypedQueryParameter` annotation for custom encoding, decoding and comparison of query parameters in `TypedGoRoute` constructors. -version: minor diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 55ad56b1058c..a397f17cf850 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 17.1.0 +version: 17.2.0 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22