Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[flutter_adaptive_scaffold] Add breakpoint extension to get the current active breakpoint #7347

Merged
merged 17 commits into from
Aug 15, 2024
5 changes: 5 additions & 0 deletions packages/flutter_adaptive_scaffold/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.2.1

* Add `Breakpoint.activeBreakpointOf(context)` to find the currently active breakpoint.
* Don't check for height on Desktop platforms.

## 0.2.0

* Add breakpoints for mediumLarge and extraLarge.
Expand Down
12 changes: 6 additions & 6 deletions packages/flutter_adaptive_scaffold/example/test/main_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ void main() {
await tester.pumpAndSettle();
}

testWidgets('dislays correct item of config based on screen width',
testWidgets('displays correct item of config based on screen width',
(WidgetTester tester) async {
await updateScreen(300, tester);
expect(smallBody, findsOneWidget);
expect(sBody, findsNothing);
expect(body, findsNothing);
expect(mediumLargeBody, findsNothing);
expect(largeBody, findsNothing);
expect(extraLargeBody, findsNothing);
Expand All @@ -60,7 +60,7 @@ void main() {

await updateScreen(800, tester);
expect(smallBody, findsNothing);
expect(sBody, findsOneWidget);
expect(body, findsOneWidget);
expect(mediumLargeBody, findsNothing);
expect(largeBody, findsNothing);
expect(extraLargeBody, findsNothing);
Expand All @@ -78,7 +78,7 @@ void main() {

await updateScreen(1100, tester);
expect(smallBody, findsNothing);
expect(sBody, findsNothing);
expect(body, findsNothing);
expect(mediumLargeBody, findsOneWidget);
expect(largeBody, findsNothing);
expect(extraLargeBody, findsNothing);
Expand All @@ -96,7 +96,7 @@ void main() {

await updateScreen(1400, tester);
expect(smallBody, findsNothing);
expect(sBody, findsNothing);
expect(body, findsNothing);
expect(mediumLargeBody, findsNothing);
expect(largeBody, findsOneWidget);
expect(extraLargeBody, findsNothing);
Expand All @@ -114,7 +114,7 @@ void main() {

await updateScreen(1800, tester);
expect(smallBody, findsNothing);
expect(sBody, findsNothing);
expect(body, findsNothing);
expect(mediumLargeBody, findsNothing);
expect(largeBody, findsNothing);
expect(extraLargeBody, findsOneWidget);
Expand Down
90 changes: 87 additions & 3 deletions packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import 'package:flutter/material.dart';

import '../flutter_adaptive_scaffold.dart';

/// A group of standard breakpoints built according to the material
/// specifications for screen width size.
///
Expand Down Expand Up @@ -193,6 +195,7 @@ 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 @@ -208,11 +211,92 @@ class Breakpoint {
? width >= lowerBoundWidth
: width >= lowerBoundWidth && width < upperBoundWidth;

final bool isHeightActive = (orientation == Orientation.landscape &&
final bool isHeightActive = isDesktop ||
orientation == Orientation.portrait ||
(orientation == Orientation.landscape &&
height >= lowerBoundHeight &&
height < upperBoundHeight) ||
orientation == Orientation.portrait;
height < upperBoundHeight);

return isWidthActive && isHeightActive && isRightPlatform;
}

/// Returns the currently active [Breakpoint] based on the [SlotLayout] in the
/// context.
static Breakpoint? maybeActiveBreakpointFromSlotLayout(BuildContext context) {
final SlotLayout? slotLayout =
context.findAncestorWidgetOfExactType<SlotLayout>();
Breakpoint? fallbackBreakpoint;

if (slotLayout != null) {
for (final MapEntry<Breakpoint, SlotLayoutConfig?> config
in slotLayout.config.entries) {
if (config.key.isActive(context)) {
if (config.key.platform != null) {
return config.key;
} else {
fallbackBreakpoint ??= config.key;
}
}
}
}
return fallbackBreakpoint;
}

/// Returns the default [Breakpoint] based on the [BuildContext].
static Breakpoint defaultBreakpointOf(BuildContext context) {
final TargetPlatform host = Theme.of(context).platform;
final bool isDesktop = Breakpoint.desktop.contains(host);
final bool isMobile = Breakpoint.mobile.contains(host);

for (final Breakpoint breakpoint in <Breakpoint>[
Breakpoints.small,
Breakpoints.medium,
Breakpoints.mediumLarge,
Breakpoints.large,
Breakpoints.extraLarge,
]) {
if (breakpoint.isActive(context)) {
if (isDesktop) {
switch (breakpoint) {
case Breakpoints.small:
return Breakpoints.smallDesktop;
case Breakpoints.medium:
return Breakpoints.mediumDesktop;
case Breakpoints.mediumLarge:
return Breakpoints.mediumLargeDesktop;
case Breakpoints.large:
return Breakpoints.largeDesktop;
case Breakpoints.extraLarge:
return Breakpoints.extraLargeDesktop;
default:
return Breakpoints.standard;
}
} else if (isMobile) {
switch (breakpoint) {
case Breakpoints.small:
return Breakpoints.smallMobile;
case Breakpoints.medium:
return Breakpoints.mediumMobile;
case Breakpoints.mediumLarge:
return Breakpoints.mediumLargeMobile;
case Breakpoints.large:
return Breakpoints.largeMobile;
case Breakpoints.extraLarge:
return Breakpoints.extraLargeMobile;
default:
return Breakpoints.standard;
}
} else {
return breakpoint;
}
}
}
return Breakpoints.standard;
martijn00 marked this conversation as resolved.
Show resolved Hide resolved
}

/// Returns the currently active [Breakpoint].
static Breakpoint activeBreakpointOf(BuildContext context) {
return maybeActiveBreakpointFromSlotLayout(context) ??
defaultBreakpointOf(context);
}
}
21 changes: 17 additions & 4 deletions packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,29 @@ class SlotLayout extends StatefulWidget {
/// Creates a [SlotLayout] widget.
const SlotLayout({required this.config, super.key});

/// Given a context and a config, it returns the [SlotLayoutConfig] that will g
/// Given a context and a config, it returns the [SlotLayoutConfig] that will
/// be chosen from the config under the context's conditions.
static SlotLayoutConfig? pickWidget(
martijn00 marked this conversation as resolved.
Show resolved Hide resolved
BuildContext context, Map<Breakpoint, SlotLayoutConfig?> config) {
SlotLayoutConfig? chosenWidget;
config.forEach((Breakpoint breakpoint, SlotLayoutConfig? pickedWidget) {

for (final Breakpoint breakpoint in config.keys) {
if (breakpoint.isActive(context)) {
chosenWidget = pickedWidget;
final SlotLayoutConfig? pickedWidget = config[breakpoint];
if (pickedWidget != null) {
if (breakpoint.platform != null) {
// Prioritize platform-specific breakpoints.
return pickedWidget;
} else {
// Fallback to non-platform-specific.
chosenWidget = pickedWidget;
}
} else {
chosenWidget = null;
}
}
});
}

return chosenWidget;
}

Expand Down
2 changes: 1 addition & 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.0
version: 0.2.1
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 Down
165 changes: 165 additions & 0 deletions packages/flutter_adaptive_scaffold/test/breakpoint_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,171 @@ void main() {
await tester.pumpAndSettle();
expect(DummyWidget.built, isFalse);
});

// Test the `maybeActiveBreakpointFromSlotLayout` method.
group('maybeActiveBreakpointFromSlotLayout', () {
testWidgets('returns correct breakpoint from SlotLayout on mobile devices',
(WidgetTester tester) async {
// Small layout on mobile
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(
tester.element(find.byKey(const Key('Breakpoints.smallMobile')))),
Breakpoints.smallMobile);

// Medium layout on mobile.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(tester
.element(find.byKey(const Key('Breakpoints.mediumMobile')))),
Breakpoints.mediumMobile);

// Large layout on mobile.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(
tester.element(find.byKey(const Key('Breakpoints.largeMobile')))),
Breakpoints.largeMobile);
}, variant: TargetPlatformVariant.mobile());

testWidgets('returns correct breakpoint from SlotLayout on desktop devices',
(WidgetTester tester) async {
// Small layout on desktop.
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(tester
.element(find.byKey(const Key('Breakpoints.smallDesktop')))),
Breakpoints.smallDesktop);

// Medium layout on desktop.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(tester
.element(find.byKey(const Key('Breakpoints.mediumDesktop')))),
Breakpoints.mediumDesktop);

// Large layout on desktop.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.maybeActiveBreakpointFromSlotLayout(tester
.element(find.byKey(const Key('Breakpoints.largeDesktop')))),
Breakpoints.largeDesktop);
}, variant: TargetPlatformVariant.desktop());
});

// Test the `defaultBreakpointOf` method.
group('defaultBreakpointOf', () {
testWidgets('returns correct default breakpoint on mobile devices',
(WidgetTester tester) async {
// Small layout on mobile
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(Breakpoint.defaultBreakpointOf(tester.element(find.byType(Theme))),
Breakpoints.smallMobile);

// Medium layout on mobile.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(Breakpoint.defaultBreakpointOf(tester.element(find.byType(Theme))),
Breakpoints.mediumMobile);

// Large layout on mobile.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(Breakpoint.defaultBreakpointOf(tester.element(find.byType(Theme))),
Breakpoints.largeMobile);
}, variant: TargetPlatformVariant.mobile());

testWidgets('returns correct default breakpoint on desktop devices',
(WidgetTester tester) async {
// Small layout on desktop.
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.defaultBreakpointOf(
tester.element(find.byType(Directionality))),
Breakpoints.smallDesktop);

// Medium layout on desktop.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.defaultBreakpointOf(
tester.element(find.byType(Directionality))),
Breakpoints.mediumDesktop);

// Large layout on desktop.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.defaultBreakpointOf(
tester.element(find.byType(Directionality))),
Breakpoints.largeDesktop);
}, variant: TargetPlatformVariant.desktop());
});

// Test the `activeBreakpointOf` method.
group('activeBreakpointOf', () {
testWidgets('returns correct active breakpoint on mobile devices',
(WidgetTester tester) async {
// Small layout on mobile
martijn00 marked this conversation as resolved.
Show resolved Hide resolved
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(
tester.element(find.byKey(const Key('Breakpoints.smallMobile')))),
Breakpoints.smallMobile);

// Medium layout on mobile.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(tester
.element(find.byKey(const Key('Breakpoints.mediumMobile')))),
Breakpoints.mediumMobile);

// Large layout on mobile.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(
tester.element(find.byKey(const Key('Breakpoints.largeMobile')))),
Breakpoints.largeMobile);
}, variant: TargetPlatformVariant.mobile());

testWidgets('returns correct active breakpoint on desktop devices',
(WidgetTester tester) async {
// Small layout on desktop.
await tester.pumpWidget(SimulatedLayout.small.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(tester
.element(find.byKey(const Key('Breakpoints.smallDesktop')))),
Breakpoints.smallDesktop);

// Medium layout on desktop.
await tester.pumpWidget(SimulatedLayout.medium.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(tester
.element(find.byKey(const Key('Breakpoints.mediumDesktop')))),
Breakpoints.mediumDesktop);

// Large layout on desktop.
await tester.pumpWidget(SimulatedLayout.large.slot(tester));
await tester.pumpAndSettle();
expect(
Breakpoint.activeBreakpointOf(tester
.element(find.byKey(const Key('Breakpoints.largeDesktop')))),
Breakpoints.largeDesktop);
}, variant: TargetPlatformVariant.desktop());
});
}

class DummyWidget extends StatelessWidget {
Expand Down