Skip to content

Commit

Permalink
[flutter_adaptive_scaffold] Add breakpoint extension to get the curre…
Browse files Browse the repository at this point in the history
…nt active breakpoint (#7347)

This enables people to check what the current breakpoint is and adjust UI based on that.

*List which issues are fixed by this PR. You must list at least one issue.*
  • Loading branch information
martijn00 authored Aug 15, 2024
1 parent 4fb1db0 commit acb6a29
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 14 deletions.
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;
}

/// 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(
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.
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

0 comments on commit acb6a29

Please sign in to comment.