Skip to content

Commit

Permalink
[flutter_adaptive_scaffold] Compare breakpoints (flutter#7531)
Browse files Browse the repository at this point in the history
*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.*

Sometimes you want to compare a breakpoint to a baseline or another breakpoint to determine which UI to show or how to handle something. This adds operators so you can check those things.

```dart
Breakpoint.activeBreakpointOf(context) > Breakpoints.large;
```

*List which issues are fixed by this PR. You must list at least one issue.*
  • Loading branch information
martijn00 authored Aug 30, 2024
1 parent b835465 commit 848d7e9
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 8 deletions.
4 changes: 4 additions & 0 deletions packages/flutter_adaptive_scaffold/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.4

* Compare breakpoints to each other using operators.

## 0.2.3

* Update the spacing and margins to the latest material m3 specs.
Expand Down
126 changes: 122 additions & 4 deletions packages/flutter_adaptive_scaffold/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<?code-excerpt path-base="example/lib"?>

# Adaptive Scaffold

`AdaptiveScaffold` reacts to input from users, devices and screen elements and
Expand Down Expand Up @@ -33,7 +31,7 @@ animation should use `AdaptiveLayout`.

### Example Usage

<?code-excerpt "adaptive_scaffold_demo.dart (Example)"?>
<?code-excerpt "example/lib/adaptive_scaffold_demo.dart (Example)"?>
```dart
@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -128,6 +126,126 @@ Widget build(BuildContext context) {
These are the set of widgets that are used on a lower level and offer more
customizability at a cost of more lines of code.

### Breakpoint

A `Breakpoint` controls the responsive behavior at different screens and configurations.

You can either use a predefined Material3 breakpoint or create your own.

<?code-excerpt "lib/src/breakpoints.dart (Breakpoints)"?>
```dart
/// Returns a const [Breakpoint] with the given constraints.
const Breakpoint({
this.beginWidth,
this.endWidth,
this.beginHeight,
this.endHeight,
this.andUp = false,
this.platform,
this.spacing = kMaterialMediumAndUpSpacing,
this.margin = kMaterialMediumAndUpMargin,
this.padding = kMaterialPadding,
this.recommendedPanes = 1,
this.maxPanes = 1,
});
/// Returns a [Breakpoint] that can be used as a fallthrough in the
/// case that no other breakpoint is active.
const Breakpoint.standard({this.platform})
: beginWidth = -1,
endWidth = null,
beginHeight = null,
endHeight = null,
spacing = kMaterialMediumAndUpSpacing,
margin = kMaterialMediumAndUpMargin,
padding = kMaterialPadding,
recommendedPanes = 1,
maxPanes = 1,
andUp = true;
/// Returns a [Breakpoint] with the given constraints for a small screen.
const Breakpoint.small({this.andUp = false, this.platform})
: beginWidth = 0,
endWidth = 600,
beginHeight = null,
endHeight = 480,
spacing = kMaterialCompactSpacing,
margin = kMaterialCompactMargin,
padding = kMaterialPadding,
recommendedPanes = 1,
maxPanes = 1;
/// Returns a [Breakpoint] with the given constraints for a medium screen.
const Breakpoint.medium({this.andUp = false, this.platform})
: beginWidth = 600,
endWidth = 840,
beginHeight = 480,
endHeight = 900,
spacing = kMaterialMediumAndUpSpacing,
margin = kMaterialMediumAndUpMargin,
padding = kMaterialPadding * 2,
recommendedPanes = 1,
maxPanes = 2;
/// Returns a [Breakpoint] with the given constraints for a mediumLarge screen.
const Breakpoint.mediumLarge({this.andUp = false, this.platform})
: beginWidth = 840,
endWidth = 1200,
beginHeight = 900,
endHeight = null,
spacing = kMaterialMediumAndUpSpacing,
margin = kMaterialMediumAndUpMargin,
padding = kMaterialPadding * 3,
recommendedPanes = 2,
maxPanes = 2;
/// Returns a [Breakpoint] with the given constraints for a large screen.
const Breakpoint.large({this.andUp = false, this.platform})
: beginWidth = 1200,
endWidth = 1600,
beginHeight = 900,
endHeight = null,
spacing = kMaterialMediumAndUpSpacing,
margin = kMaterialMediumAndUpMargin,
padding = kMaterialPadding * 4,
recommendedPanes = 2,
maxPanes = 2;
/// Returns a [Breakpoint] with the given constraints for an extraLarge screen.
const Breakpoint.extraLarge({this.andUp = false, this.platform})
: beginWidth = 1600,
endWidth = null,
beginHeight = 900,
endHeight = null,
spacing = kMaterialMediumAndUpSpacing,
margin = kMaterialMediumAndUpMargin,
padding = kMaterialPadding * 5,
recommendedPanes = 2,
maxPanes = 3;
```

It is possible to compare Breakpoints:

<?code-excerpt "lib/src/breakpoints.dart (Breakpoint operators)"?>
```dart
/// Returns true if this [Breakpoint] is greater than the given [Breakpoint].
bool operator >(Breakpoint breakpoint)
// ···
/// Returns true if this [Breakpoint] is less than the given [Breakpoint].
bool operator <(Breakpoint breakpoint)
// ···
/// Returns true if this [Breakpoint] is greater than or equal to the
/// given [Breakpoint].
bool operator >=(Breakpoint breakpoint)
// ···
/// Returns true if this [Breakpoint] is less than or equal to the
/// given [Breakpoint].
bool operator <=(Breakpoint breakpoint)
// ···
/// Returns true if this [Breakpoint] is between the given [Breakpoint]s.
bool between(Breakpoint lower, Breakpoint upper)
```

### AdaptiveLayout

!["AdaptiveLayout's Assigned Slots Displayed on Screen"](example/demo_files/screenSlots.png)
Expand All @@ -151,7 +269,7 @@ displayed and the entrance animation and exit animation.

### Example Usage

<?code-excerpt "adaptive_layout_demo.dart (Example)"?>
<?code-excerpt "example/lib/adaptive_layout_demo.dart (Example)"?>
```dart
// AdaptiveLayout has a number of slots that take SlotLayouts and these
// SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs.
Expand Down
85 changes: 83 additions & 2 deletions packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class Breakpoints {
/// * [SlotLayout.config], which uses breakpoints to dictate the layout of the
/// screen.
class Breakpoint {
// #docregion Breakpoints
/// Returns a const [Breakpoint] with the given constraints.
const Breakpoint({
this.beginWidth,
Expand Down Expand Up @@ -219,6 +220,7 @@ class Breakpoint {
padding = kMaterialPadding * 5,
recommendedPanes = 2,
maxPanes = 3;
// #enddocregion Breakpoints

/// A set of [TargetPlatform]s that the [Breakpoint] will be active on desktop.
static const Set<TargetPlatform> desktop = <TargetPlatform>{
Expand Down Expand Up @@ -278,7 +280,6 @@ class Breakpoint {
bool isActive(BuildContext context) {
final TargetPlatform host = Theme.of(context).platform;
final bool isRightPlatform = platform?.contains(host) ?? true;
final bool isDesktop = Breakpoint.desktop.contains(host);

final double width = MediaQuery.sizeOf(context).width;
final double height = MediaQuery.sizeOf(context).height;
Expand All @@ -294,7 +295,7 @@ class Breakpoint {
? width >= lowerBoundWidth
: width >= lowerBoundWidth && width < upperBoundWidth;

final bool isHeightActive = isDesktop ||
final bool isHeightActive = isDesktop(context) ||
orientation == Orientation.portrait ||
(orientation == Orientation.landscape && andUp
? isWidthActive || height >= lowerBoundHeight
Expand Down Expand Up @@ -344,4 +345,84 @@ class Breakpoint {
}
return currentBreakpoint;
}

/// Returns true if the current platform is Desktop.
static bool isDesktop(BuildContext context) {
return Breakpoint.desktop.contains(Theme.of(context).platform);
}

/// Returns true if the current platform is Mobile.
static bool isMobile(BuildContext context) {
return Breakpoint.mobile.contains(Theme.of(context).platform);
}

// #docregion Breakpoint operators
/// Returns true if this [Breakpoint] is greater than the given [Breakpoint].
bool operator >(Breakpoint breakpoint)
// #enddocregion Breakpoint operators
{
return (beginWidth ?? double.negativeInfinity) >
(breakpoint.beginWidth ?? double.negativeInfinity) &&
(endWidth ?? double.infinity) >
(breakpoint.endWidth ?? double.infinity) &&
(beginHeight ?? double.negativeInfinity) >
(breakpoint.beginHeight ?? double.negativeInfinity) &&
(endHeight ?? double.infinity) >
(breakpoint.endHeight ?? double.infinity);
}

// #docregion Breakpoint operators
/// Returns true if this [Breakpoint] is less than the given [Breakpoint].
bool operator <(Breakpoint breakpoint)
// #enddocregion Breakpoint operators
{
return (endWidth ?? double.infinity) <
(breakpoint.endWidth ?? double.infinity) &&
(beginWidth ?? double.negativeInfinity) <
(breakpoint.beginWidth ?? double.negativeInfinity) &&
(endHeight ?? double.infinity) <
(breakpoint.endHeight ?? double.infinity) &&
(beginHeight ?? double.negativeInfinity) <
(breakpoint.beginHeight ?? double.negativeInfinity);
}

// #docregion Breakpoint operators
/// Returns true if this [Breakpoint] is greater than or equal to the
/// given [Breakpoint].
bool operator >=(Breakpoint breakpoint)
// #enddocregion Breakpoint operators
{
return (beginWidth ?? double.negativeInfinity) >=
(breakpoint.beginWidth ?? double.negativeInfinity) &&
(endWidth ?? double.infinity) >=
(breakpoint.endWidth ?? double.infinity) &&
(beginHeight ?? double.negativeInfinity) >=
(breakpoint.beginHeight ?? double.negativeInfinity) &&
(endHeight ?? double.infinity) >=
(breakpoint.endHeight ?? double.infinity);
}

// #docregion Breakpoint operators
/// Returns true if this [Breakpoint] is less than or equal to the
/// given [Breakpoint].
bool operator <=(Breakpoint breakpoint)
// #enddocregion Breakpoint operators
{
return (endWidth ?? double.infinity) <=
(breakpoint.endWidth ?? double.infinity) &&
(beginWidth ?? double.negativeInfinity) <=
(breakpoint.beginWidth ?? double.negativeInfinity) &&
(endHeight ?? double.infinity) <=
(breakpoint.endHeight ?? double.infinity) &&
(beginHeight ?? double.negativeInfinity) <=
(breakpoint.beginHeight ?? double.negativeInfinity);
}

// #docregion Breakpoint operators
/// Returns true if this [Breakpoint] is between the given [Breakpoint]s.
bool between(Breakpoint lower, Breakpoint upper)
// #enddocregion Breakpoint operators
{
return this >= lower && this < upper;
}
}
3 changes: 2 additions & 1 deletion packages/flutter_adaptive_scaffold/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_adaptive_scaffold
description: Widgets to easily build adaptive layouts, including navigation elements.
version: 0.2.3
version: 0.2.4
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22
repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold

Expand All @@ -20,3 +20,4 @@ topics:
- layout
- ui
- adaptive
- responsive
60 changes: 59 additions & 1 deletion packages/flutter_adaptive_scaffold/test/breakpoint_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ void main() {
.element(find.byKey(const Key('Breakpoints.smallMobile'))))
.spacing,
kMaterialCompactSpacing);
}, variant: TargetPlatformVariant.mobile());
});

testWidgets('returns kMaterialMediumAndUpSpacing for medium breakpoint',
(WidgetTester tester) async {
Expand Down Expand Up @@ -959,6 +959,64 @@ void main() {
3);
}, variant: TargetPlatformVariant.mobile());
});

group('Breakpoint method tests', () {
testWidgets('isMobile returns true on mobile platforms',
(WidgetTester tester) async {
await tester.pumpWidget(SimulatedLayout.medium.scaffold(tester));
await tester.pumpAndSettle();

expect(Breakpoint.isMobile(tester.element(find.byType(TestScaffold))),
isTrue);

expect(Breakpoint.isDesktop(tester.element(find.byType(TestScaffold))),
isFalse);
}, variant: TargetPlatformVariant.mobile());

testWidgets('isDesktop returns true on desktop platforms',
(WidgetTester tester) async {
await tester.pumpWidget(SimulatedLayout.medium.scaffold(tester));
await tester.pumpAndSettle();

expect(Breakpoint.isDesktop(tester.element(find.byType(TestScaffold))),
isTrue);

expect(Breakpoint.isMobile(tester.element(find.byType(TestScaffold))),
isFalse);
}, variant: TargetPlatformVariant.desktop());

test('Breakpoint comparison operators work correctly', () {
const Breakpoint small = Breakpoints.small;
const Breakpoint medium = Breakpoints.medium;
const Breakpoint large = Breakpoints.large;

expect(small < medium, isTrue);
expect(large > medium, isTrue);
expect(small <= Breakpoints.small, isTrue);
expect(large >= medium, isTrue);
});

test('Breakpoint equality and hashCode', () {
const Breakpoint small1 = Breakpoints.small;
const Breakpoint small2 = Breakpoints.small;
const Breakpoint medium = Breakpoints.medium;

expect(small1 == small2, isTrue);
expect(small1 == medium, isFalse);
expect(small1.hashCode == small2.hashCode, isTrue);
expect(small1.hashCode == medium.hashCode, isFalse);
});

test('Breakpoint between method works correctly', () {
const Breakpoint small = Breakpoints.small;
const Breakpoint medium = Breakpoints.medium;
const Breakpoint large = Breakpoints.large;

expect(medium.between(small, large), isTrue);
expect(small.between(medium, large), isFalse);
expect(large.between(small, medium), isFalse);
});
});
}

class DummyWidget extends StatelessWidget {
Expand Down

0 comments on commit 848d7e9

Please sign in to comment.