Add factory constructors for shorthands that would collide (#165597)

EdgeInsets and EdgeInsetsDirectional are subtypes of EdgeInsetsGeometry.
The both have only, all, and symmetric constructors, which creates
collisions in a world where Dart support shorthand notation such as:

```dart
Padding(
  padding: .only(bottom: 10.0),
),
```
 The same goes for all these classes: 

- AlignmentGeometry
  - Alignment
  - AlignmentDirectional
- BorderRadiusGeometry
  - BorderRadius
  - BorderRadiusDirectional
- BoxBorder
  - Border
  - BorderDirectional
- EdgeInsetsGeometry
  - EdgeInsets
  - EdgeInsetsDirectional


The shorthands feature is not planning to exhaustively check subtypes.
However, these classes were identified as the most impactful for the
shorthands feature ( ). Adding these factories in the super classes
enables future shorthand support.

Prior in https://github.com/flutter/flutter/pull/159703

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
Kate Lovett 2025-03-24 20:07:16 -05:00 committed by GitHub
parent 355129961c
commit 7561caa71f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 271 additions and 0 deletions

View File

@ -26,6 +26,12 @@ abstract class AlignmentGeometry {
/// const constructors so that they can be used in const expressions.
const AlignmentGeometry();
/// Creates an [Alignment].
const factory AlignmentGeometry.xy(double x, double y) = Alignment;
/// Creates a directional alignment, or [AlignmentDirectional].
const factory AlignmentGeometry.directional(double start, double y) = AlignmentDirectional;
double get _x;
double get _start;

View File

@ -26,6 +26,68 @@ abstract class BorderRadiusGeometry {
/// const constructors so that they can be used in const expressions.
const BorderRadiusGeometry();
/// Creates a [BorderRadius] where all radii are `radius`.
// The radius applies equally on all sides, so BorderRadiusDirectional is
// irrelevant in this case.
const factory BorderRadiusGeometry.all(Radius radius) = BorderRadius.all;
/// Creates a [BorderRadius] where all radii are [Radius.circular(radius)].
// The radius applies equally on all sides, so BorderRadiusDirectional is
// irrelevant in this case.
factory BorderRadiusGeometry.circular(double radius) = BorderRadius.circular;
/// Creates a horizontally symmetrical border radius.
///
/// Utilizing the `left` and `right` properties will return a [BorderRadius],
/// while `start` and `end` will yield a [BorderRadiusDirectional]. These
/// properties cannot be used interchangeably.
factory BorderRadiusGeometry.horizontal({
Radius? left, // BorderRadius
Radius? right, // BorderRadius
Radius? start, // BorderRadiusDirectional
Radius? end, // BorderRadiusDirectional
}) {
assert(
(left == null && right == null) || (start == null && end == null),
'The left and right values cannot be used in conjunction with start and end.',
);
if (start != null || end != null) {
return BorderRadiusDirectional.horizontal(
start: start ?? Radius.zero,
end: end ?? Radius.zero,
);
}
return BorderRadius.horizontal(left: left ?? Radius.zero, right: right ?? Radius.zero);
}
/// Creates a [BorderRadius] with only the given non-zero values.
///
/// The other corners will be right angles.
const factory BorderRadiusGeometry.only({
Radius topLeft,
Radius topRight,
Radius bottomLeft,
Radius bottomRight,
}) = BorderRadius.only;
/// Creates a [BorderRadiusDirectional] with only the given non-zero values.
///
/// The other corners will be right angles.
const factory BorderRadiusGeometry.directional({
Radius topStart,
Radius topEnd,
Radius bottomStart,
Radius bottomEnd,
}) = BorderRadiusDirectional.only;
/// Creates a vertically symmetric [BorderRadius] where the top and bottom
/// sides of the rectangle have the same radii.
const factory BorderRadiusGeometry.vertical({Radius top, Radius bottom}) = BorderRadius.vertical;
/// A [BorderRadius] with all zero radii.
static const BorderRadiusGeometry zero = BorderRadius.zero;
Radius get _topLeft;
Radius get _topRight;
Radius get _bottomLeft;

View File

@ -67,6 +67,48 @@ abstract class BoxBorder extends ShapeBorder {
/// const constructors so that they can be used in const expressions.
const BoxBorder();
/// Creates a [Border].
///
/// All the sides of the border default to [BorderSide.none].
factory BoxBorder.fromLTRB({
BorderSide top = BorderSide.none,
BorderSide right = BorderSide.none,
BorderSide bottom = BorderSide.none,
BorderSide left = BorderSide.none,
}) => Border(top: top, right: right, bottom: bottom, left: left);
/// A uniform [Border] with all sides the same color and width.
///
/// The sides default to black solid borders, one logical pixel wide.
factory BoxBorder.all({Color color, double width, BorderStyle style, double strokeAlign}) =
Border.all;
/// Creates a [Border] whose sides are all the same.
const factory BoxBorder.fromBorderSide(BorderSide side) = Border.fromBorderSide;
/// Creates a [Border] with symmetrical vertical and horizontal sides.
///
/// The `vertical` argument applies to the [left] and [right] sides, and the
/// `horizontal` argument applies to the [top] and [bottom] sides.
///
/// All arguments default to [BorderSide.none].
const factory BoxBorder.symmetric({BorderSide vertical, BorderSide horizontal}) =
Border.symmetric;
/// Creates a [BorderDirectional].
///
/// The [start] and [end] sides represent the horizontal sides; the start side
/// is on the leading edge given the reading direction, and the end side is on
/// the trailing edge. They are resolved during [paint].
///
/// All the sides of the border default to [BorderSide.none].
factory BoxBorder.fromSTEB({
BorderSide top = BorderSide.none,
BorderSide start = BorderSide.none,
BorderSide end = BorderSide.none,
BorderSide bottom = BorderSide.none,
}) => BorderDirectional(top: top, start: start, end: end, bottom: bottom);
/// The top side of this border.
///
/// This getter is available on both [Border] and [BorderDirectional]. If

View File

@ -32,6 +32,46 @@ abstract class EdgeInsetsGeometry {
/// const constructors so that they can be used in const expressions.
const EdgeInsetsGeometry();
/// Creates insets where all the offsets are `value`.
const factory EdgeInsetsGeometry.all(double value) = EdgeInsets.all;
/// Creates [EdgeInsets] with only the given values non-zero.
const factory EdgeInsetsGeometry.only({double left, double right, double top, double bottom}) =
EdgeInsets.only;
/// Creates [EdgeInsetsDirectional] with only the given values non-zero.
const factory EdgeInsetsGeometry.directional({
double start,
double end,
double top,
double bottom,
}) = EdgeInsetsDirectional.only;
/// Creates [EdgeInsets] with symmetrical vertical and horizontal offsets.
const factory EdgeInsetsGeometry.symmetric({double vertical, double horizontal}) =
EdgeInsets.symmetric;
/// Creates [EdgeInsets] from offsets from the left, top, right, and bottom.
const factory EdgeInsetsGeometry.fromLTRB(double left, double top, double right, double bottom) =
EdgeInsets.fromLTRB;
/// Creates [EdgeInsets] that match the given view padding.
///
/// If you need the current system padding or view insets in the context of a
/// widget, consider using [MediaQuery.paddingOf] to obtain these values
/// rather than using the value from a [FlutterView] directly, so that you get
/// notified of changes.
factory EdgeInsetsGeometry.fromViewPadding(ui.ViewPadding padding, double devicePixelRatio) =
EdgeInsets.fromViewPadding;
/// Creates [EdgeInsetsDirectional] from offsets from the start, top, end, and
/// bottom.
const factory EdgeInsetsGeometry.fromSTEB(double start, double top, double end, double bottom) =
EdgeInsetsDirectional.fromSTEB;
/// An [EdgeInsets] with zero offsets in each direction.
static const EdgeInsetsGeometry zero = EdgeInsets.zero;
double get _bottom;
double get _end;
double get _left;

View File

@ -333,4 +333,9 @@ void main() {
'Alignment(1.0, 2.0) + AlignmentDirectional.centerEnd',
);
});
test('AlignmentGeometry factories', () {
expect(const AlignmentGeometry.xy(4, 5), const Alignment(4, 5));
expect(const AlignmentGeometry.directional(4, 5), const AlignmentDirectional(4, 5));
});
}

View File

@ -621,4 +621,68 @@ void main() {
borderRadius,
);
});
test('BorderRadiusGeometry factories', () {
const Radius radius5 = Radius.circular(5);
const Radius radius10 = Radius.circular(10);
const Radius radius15 = Radius.circular(15);
const Radius radius20 = Radius.circular(20);
expect(const BorderRadiusGeometry.all(radius10), const BorderRadius.all(radius10));
expect(BorderRadiusGeometry.circular(10), BorderRadius.circular(10));
expect(
BorderRadiusGeometry.horizontal(left: radius5, right: radius10),
const BorderRadius.horizontal(left: radius5, right: radius10),
);
expect(
BorderRadiusGeometry.horizontal(start: radius5, end: radius10),
const BorderRadiusDirectional.horizontal(start: radius5, end: radius10),
);
expect(() {
BorderRadiusGeometry.horizontal(start: radius5, left: radius10);
}, throwsAssertionError);
expect(() {
BorderRadiusGeometry.horizontal(end: radius5, right: radius10);
}, throwsAssertionError);
expect(() {
BorderRadiusGeometry.horizontal(
end: radius5,
right: radius10,
start: radius5,
left: radius10,
);
}, throwsAssertionError);
expect(
const BorderRadiusGeometry.only(
topLeft: radius5,
topRight: radius10,
bottomLeft: radius15,
bottomRight: radius20,
),
const BorderRadius.only(
topLeft: radius5,
topRight: radius10,
bottomLeft: radius15,
bottomRight: radius20,
),
);
expect(
const BorderRadiusGeometry.directional(
topStart: radius5,
topEnd: radius10,
bottomStart: radius15,
bottomEnd: radius20,
),
const BorderRadiusDirectional.only(
topStart: radius5,
topEnd: radius10,
bottomStart: radius15,
bottomEnd: radius20,
),
);
expect(
const BorderRadiusGeometry.vertical(top: radius5, bottom: radius10),
const BorderRadius.vertical(top: radius5, bottom: radius10),
);
expect(BorderRadiusGeometry.zero, BorderRadius.zero);
});
}

View File

@ -482,6 +482,27 @@ void main() {
expect((ShapeWithoutInterior() + ShapeWithInterior()).preferPaintInterior, isFalse);
expect((ShapeWithoutInterior() + ShapeWithoutInterior()).preferPaintInterior, isFalse);
});
test('BoxBorder factories', () {
const BorderSide side1 = BorderSide();
const BorderSide side2 = BorderSide(width: 2);
const BorderSide side3 = BorderSide(width: 3);
const BorderSide side4 = BorderSide(width: 4);
expect(
BoxBorder.fromLTRB(left: side1, top: side2, right: side3, bottom: side4),
const Border(left: side1, top: side2, right: side3, bottom: side4),
);
expect(BoxBorder.all(width: 4), Border.all(width: 4));
expect(const BoxBorder.fromBorderSide(side3), const Border.fromBorderSide(side3));
expect(
const BoxBorder.symmetric(horizontal: side2, vertical: side3),
const Border.symmetric(horizontal: side2, vertical: side3),
);
expect(
BoxBorder.fromSTEB(start: side1, top: side2, end: side3, bottom: side4),
const BorderDirectional(start: side1, top: side2, end: side3, bottom: side4),
);
});
}
class ShapeWithInterior extends ShapeBorder {

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
@ -442,4 +444,33 @@ void main() {
final EdgeInsetsDirectional copy = sourceEdgeInsets.copyWith(start: 5.0, top: 6.0);
expect(copy, const EdgeInsetsDirectional.only(start: 5.0, top: 6.0, bottom: 3.0, end: 4.0));
});
test('EdgeInsetsGeometry factories', () {
expect(const EdgeInsetsGeometry.all(10), const EdgeInsets.all(10));
expect(
const EdgeInsetsGeometry.only(left: 10, top: 20, right: 30, bottom: 40),
const EdgeInsets.only(left: 10, top: 20, right: 30, bottom: 40),
);
expect(
const EdgeInsetsGeometry.directional(start: 10, top: 20, end: 30, bottom: 40),
const EdgeInsetsDirectional.only(start: 10, top: 20, end: 30, bottom: 40),
);
expect(
const EdgeInsetsGeometry.symmetric(horizontal: 10, vertical: 20),
const EdgeInsets.symmetric(horizontal: 10, vertical: 20),
);
expect(
const EdgeInsetsGeometry.fromLTRB(10, 20, 30, 40),
const EdgeInsets.fromLTRB(10, 20, 30, 40),
);
expect(
EdgeInsetsGeometry.fromViewPadding(ui.ViewPadding.zero, 10),
EdgeInsets.fromViewPadding(ui.ViewPadding.zero, 10),
);
expect(
const EdgeInsetsGeometry.fromSTEB(10, 20, 20, 40),
const EdgeInsetsDirectional.fromSTEB(10, 20, 20, 40),
);
expect(EdgeInsetsGeometry.zero, EdgeInsets.zero);
});
}