From e0d1ba4399cf8719091eeaca5c89ce4c44e9af15 Mon Sep 17 00:00:00 2001 From: "Guilherme C. Matuella" Date: Thu, 11 Feb 2021 10:52:21 -0300 Subject: [PATCH] Updated initial project structure --- examples/no_package_example/lib/main.dart | 22 +++- .../no_package_example/lib/pages/details.dart | 20 ++-- .../no_package_example/lib/pages/home.dart | 5 +- lib/common_layout.dart | 106 ++++++------------ lib/src/layout_resolver.dart | 86 +++++++++++++- 5 files changed, 150 insertions(+), 89 deletions(-) diff --git a/examples/no_package_example/lib/main.dart b/examples/no_package_example/lib/main.dart index b740396..74e05f7 100644 --- a/examples/no_package_example/lib/main.dart +++ b/examples/no_package_example/lib/main.dart @@ -4,22 +4,32 @@ import 'package:no_package_example/pages/home.dart'; import 'package:responsive_layout/common_layout.dart'; void main() { - final commonLayout = CommonLayout(); - - runApp(MaterialApp( + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( initialRoute: '/', onGenerateRoute: (settings) { // TODO: Why we have to wrap around each route switch (settings.name) { case '/': return MaterialPageRoute(builder: (context) { - return LayoutResolverWidget(resolver: commonLayout, child: HomePage()); + return LayoutResolverWidget(resolver: _resolverIn(context), child: HomePage()); }); case '/details': return MaterialPageRoute(builder: (context) { - return LayoutResolverWidget(resolver: commonLayout, child: DetailsPage()); + return LayoutResolverWidget(resolver: _resolverIn(context), child: DetailsPage()); }, fullscreenDialog: true); default: throw 'No route with name ${settings.name}'; } }, - ),); + ); + } + + // TODO: Shouldn't we go for a smallestSide instead of always width? + LayoutResolver _resolverIn(BuildContext context) => CommonLayout(context.deviceWidth); } \ No newline at end of file diff --git a/examples/no_package_example/lib/pages/details.dart b/examples/no_package_example/lib/pages/details.dart index bf64d47..bfebb2e 100644 --- a/examples/no_package_example/lib/pages/details.dart +++ b/examples/no_package_example/lib/pages/details.dart @@ -6,6 +6,8 @@ class DetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { + final layout = context.layout; + final showText = 'Show Snack'; final showIcon = Icon(Icons.message); @@ -18,33 +20,35 @@ class DetailsPage extends StatelessWidget { final desktopButton = OutlinedButton.icon(onPressed: () => _displaySnackbar(context), icon: showIcon, label: Text(showText),); final pageTitle = 'Hybrid Details'; - + return Scaffold( - appBar: context.isTabletOrSmaller ? AppBar( + appBar: layout.isTabletOrSmaller ? AppBar( title: Text(pageTitle), ) : null, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (context.isDesktop) Padding( + children: [ + if (layout.isDesktop) Padding( padding: const EdgeInsets.only(bottom: 40), child: Text(pageTitle, style: Theme.of(context).textTheme.headline3,), ), Text( 'This is a responsive text!', - style: context.isPhoneOrSmaller ? Theme.of(context).textTheme.headline6 : Theme.of(context).textTheme.headline5, + style: layout.isPhoneOrSmaller ? Theme.of(context).textTheme.headline6 : Theme.of(context).textTheme.headline5, ), - if (context.isTabletOrLarger) desktopButton, + if (layout.isTabletOrLarger) desktopButton, ], ), ), - floatingActionButton: context.isPhoneOrSmaller ? mobileFab : null, + floatingActionButton: layout.isPhoneOrSmaller ? mobileFab : null, ); } void _displaySnackbar(BuildContext context) { - final snackBarTextStyle = context.isPhoneOrSmaller ? Theme.of(context).textTheme.bodyText2 : Theme.of(context).textTheme.subtitle1; + final layout = context.layout; + + final snackBarTextStyle = layout.isPhoneOrSmaller ? Theme.of(context).textTheme.bodyText2 : Theme.of(context).textTheme.subtitle1; final snackBarText = Text('A responsive text inside the SnackBar!', style: snackBarTextStyle?.copyWith(color: Colors.white)); final snackBar = SnackBar(content: snackBarText); diff --git a/examples/no_package_example/lib/pages/home.dart b/examples/no_package_example/lib/pages/home.dart index b6b8f4f..b39f756 100644 --- a/examples/no_package_example/lib/pages/home.dart +++ b/examples/no_package_example/lib/pages/home.dart @@ -12,8 +12,9 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { + final layout = context.layout; return Scaffold( - appBar: context.isTabletOrSmaller ? AppBar( + appBar: layout.isTabletOrSmaller ? AppBar( title: Text('Splitted Home'), ) : null, body: Padding( @@ -21,7 +22,7 @@ class HomePage extends StatelessWidget { child: Container( constraints: BoxConstraints.expand(), child: SingleChildScrollView( - child: context.layoutValue( + child: layout.value( tablet: _HomeContents(children: _randomMockedChildren), phone: _CompactHomeContents(children: _randomMockedChildren), ), diff --git a/lib/common_layout.dart b/lib/common_layout.dart index 6918944..48aae9c 100644 --- a/lib/common_layout.dart +++ b/lib/common_layout.dart @@ -2,74 +2,36 @@ import 'package:flutter/widgets.dart'; import 'package:responsive_layout/responsive_layout.dart'; import 'src/layout_resolver.dart'; -import 'src/utilities.dart'; - export 'responsive_layout.dart'; -// extension LayoutMetadataCtx on BuildContext { -// LayoutMetadata get layoutMetadata => watch(); -// } - -// typedef LayoutValue = T Function(BuildContext context); - - - enum CommonBreakpoint { desktop, tablet, phone, tinyHardware } - +@immutable class CommonLayout extends LayoutResolver { - CommonLayout({ - this.desktop = 769, - this.tablet = 481, - this.phone = 321 - }) { - if (desktop <= tablet || desktop <= phone) { - throw 'tablet (tablet) and/or phone (phone) size was greater than desktop size (desktop)'; - } - - if (tablet <= phone) { - throw 'phone size (phone) was greater than tablet size (tablet)'; - } - } + CommonLayout(double size, { + int desktop = 769, + int tablet = 481, + int phone = 321 + }): super(size: size, breakpoints: [Breakpoint(desktop, value: CommonBreakpoint.desktop), Breakpoint(tablet, value: CommonBreakpoint.tablet), Breakpoint(phone, value: CommonBreakpoint.phone), Breakpoint(null, value: CommonBreakpoint.tinyHardware),]); - final int desktop; - final int tablet; - final int phone; + // Helpers - @override - CommonBreakpoint resolveBreakpoint(double size) { - if (size < phone) { - return CommonBreakpoint.tinyHardware; - } else if (size < tablet) { - return CommonBreakpoint.phone; - } else if (size < desktop) { - return CommonBreakpoint.tablet; - } - - return CommonBreakpoint.desktop; - } -} - -extension CommonLayoutValue on BuildContext { - CommonLayout get commonLayout => LayoutResolverWidget.of(this).resolver as CommonLayout; - CommonBreakpoint get breakpoint => commonLayout.resolveBreakpoint(deviceWidth); - - bool get isTinyHardware => breakpoint == CommonBreakpoint.tinyHardware; + bool get isTinyHardware => matchesValue(CommonBreakpoint.tinyHardware); - bool get isPhoneOrSmaller => breakpoint == CommonBreakpoint.phone || breakpoint == CommonBreakpoint.tinyHardware; - bool get isPhone => breakpoint == CommonBreakpoint.phone; - bool get isPhoneOrLarger => breakpoint != CommonBreakpoint.tinyHardware; + bool get isPhoneOrSmaller => matchesValueOrSmaller(CommonBreakpoint.phone); + bool get isPhone => matchesValue(CommonBreakpoint.phone); + bool get isPhoneOrLarger => matchesValueOrLarger(CommonBreakpoint.phone); - bool get isTabletOrSmaller => breakpoint != CommonBreakpoint.desktop; - bool get isTablet => breakpoint == CommonBreakpoint.tablet; - bool get isTabletOrLarger => breakpoint != CommonBreakpoint.phone && breakpoint != CommonBreakpoint.tinyHardware; + bool get isTabletOrSmaller => matchesValueOrSmaller(CommonBreakpoint.tablet); + bool get isTablet => matchesValue(CommonBreakpoint.tablet); + bool get isTabletOrLarger => matchesValueOrLarger(CommonBreakpoint.tablet); - bool get isDesktop => breakpoint == CommonBreakpoint.desktop; + bool get isDesktop => matchesValue(CommonBreakpoint.desktop); - T layoutValue({ + T value({ T? desktop, T? tablet, T? phone, @@ -79,27 +41,31 @@ extension CommonLayoutValue on BuildContext { throw 'At least one breakpoint must be provided'; } - // If it's in the exact range and has a layout supplied, try to use it, - // otherwise cascade down all the available values - if (desktop != null && breakpoint == CommonBreakpoint.desktop) { + // If it's in the exact range and has a layout supplied, try to use it... + if (desktop != null && breakpointValue == CommonBreakpoint.desktop) { return desktop; - } else if (tablet != null && breakpoint == CommonBreakpoint.tablet) { + } else if (tablet != null && breakpointValue == CommonBreakpoint.tablet) { return tablet; - } else if (phone != null && breakpoint == CommonBreakpoint.phone) { + } else if (phone != null && breakpointValue == CommonBreakpoint.phone) { return phone; } else if (tinyHardware != null) { return tinyHardware; } - - // Get the largest non-null layout supplied - if (desktop != null) { - return desktop; - } else if (tablet != null) { - return tablet; - } else if (phone != null) { - return phone; - } else { - return tinyHardware!; - } + // ... otherwise cascade down all the available values to get the largest + // non-null layout supplied + if (desktop != null) { + return desktop; + } else if (tablet != null) { + return tablet; + } else if (phone != null) { + return phone; + } else { + return tinyHardware!; + } } } + +// Widget Utility +extension CommonResolverWidget on BuildContext { + CommonLayout get layout => LayoutResolverWidget.of(this).resolver as CommonLayout; +} \ No newline at end of file diff --git a/lib/src/layout_resolver.dart b/lib/src/layout_resolver.dart index 8331d0e..8d76369 100644 --- a/lib/src/layout_resolver.dart +++ b/lib/src/layout_resolver.dart @@ -1,7 +1,87 @@ -abstract class LayoutResolver { - const LayoutResolver(); +import 'dart:collection'; + +class Breakpoint implements Comparable> { + Breakpoint(this.size, {required this.value}): assert(size == null || size > 0); + + final int? size; + final T value; + + operator > (Breakpoint other) => (size ?? 0) > (other.size ?? 0); + operator >= (Breakpoint other) => (size ?? 0) >= (other.size ?? 0); + operator <= (Breakpoint other) => (size ?? 0) <= (other.size ?? 0); + operator < (Breakpoint other) => (size ?? 0) < (other.size ?? 0); + + @override + int compareTo(Breakpoint other) { + if (other > this) { return -1; } + if (other < this) { return 1; } + + return 0; + } +} + +extension BreakpointListExtension on List> { + + List> get descendingSorted { + final descendingSorted = List>.from(this); + + descendingSorted..sort((a, b) { + final comparison = b.compareTo(a); + if (comparison == 0) { + throw 'breakpoint of size `${b.size}` (value `${b.value}`) had the same size of other breakpoint (value `${a.value})'; + } + + return comparison; + }); + + return descendingSorted; + } - T resolveBreakpoint(double size); + + Breakpoint firstCascadingBreakpoint(double size) { + for (final breakpoint in this) { + if (size >= (breakpoint.size ?? 0)) { + return breakpoint; + } + } + + return last; + } } + +abstract class LayoutResolver { + LayoutResolver({required double size, required List> breakpoints}): assert(size > 0), assert(breakpoints.length > 1), _descendingBreakpoints = breakpoints.descendingSorted, breakpoint = breakpoints.descendingSorted.firstCascadingBreakpoint(size); + + final List> _descendingBreakpoints; + + + List> get breakpoints => _descendingBreakpoints; + final Breakpoint breakpoint; + T get breakpointValue => breakpoint.value; + + bool matchesValueOrSmaller(T match) { + final valueBreakpoint = breakpoints.firstWhere((breakpoint) => breakpoint.value == match, orElse: () { + throw 'TODO: throw or return false?'; + }); + + return breakpoint <= valueBreakpoint; + } + + bool matchesValue(T match) { + final valueBreakpoint = breakpoints.firstWhere((breakpoint) => breakpoint.value == match, orElse: () { + throw 'TODO'; + }); + + return breakpoint.size == valueBreakpoint.size; + } + + bool matchesValueOrLarger(T match) { + final valueBreakpoint = breakpoints.firstWhere((breakpoint) => breakpoint.value == match, orElse: () { + throw 'TODO'; + }); + + return breakpoint >= valueBreakpoint; + } +} \ No newline at end of file