diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 5d65f72a5ef..8b4782b0d34 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -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. diff --git a/packages/flutter_adaptive_scaffold/example/test/main_test.dart b/packages/flutter_adaptive_scaffold/example/test/main_test.dart index e7b11ec00fb..26e03899e1b 100644 --- a/packages/flutter_adaptive_scaffold/example/test/main_test.dart +++ b/packages/flutter_adaptive_scaffold/example/test/main_test.dart @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart b/packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart index aa8e744b344..286a49811f1 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/breakpoints.dart @@ -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. /// @@ -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; @@ -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(); + Breakpoint? fallbackBreakpoint; + + if (slotLayout != null) { + for (final MapEntry 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 [ + 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); + } } diff --git a/packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart b/packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart index 38789fb8660..07cfea33a9d 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/slot_layout.dart @@ -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 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; } diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index 504f5257c9a..a4b7215af98 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -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 diff --git a/packages/flutter_adaptive_scaffold/test/breakpoint_test.dart b/packages/flutter_adaptive_scaffold/test/breakpoint_test.dart index e990b3a7f91..cbef74c570e 100644 --- a/packages/flutter_adaptive_scaffold/test/breakpoint_test.dart +++ b/packages/flutter_adaptive_scaffold/test/breakpoint_test.dart @@ -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 {