Last active
July 16, 2020 01:21
-
-
Save drexel-ue/7eb75e7bf34873f4e414420cf2374b79 to your computer and use it in GitHub Desktop.
Running this gist in pub.dev (https://dartpad.dev/7eb75e7bf34873f4e414420cf2374b79) will show a brief display of a scalable responsive design pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Responsive design demo. | |
import 'package:flutter/material.dart'; | |
void main() => runApp(ResponsiveDemo()); | |
/// enum to denote the type of device we're dealing with based on the screen. | |
enum DeviceScreenType { | |
mobileSmall, | |
mobileMedium, | |
mobileLarge, | |
tabletSmall, | |
tabletLarge, | |
desktop, | |
} | |
extension DeviceScreenTypeExtension on DeviceScreenType { | |
static const string = { | |
DeviceScreenType.mobileSmall: 'Mobile Small', | |
DeviceScreenType.mobileMedium: 'Mobile Medium', | |
DeviceScreenType.mobileLarge: 'Mobile Large', | |
DeviceScreenType.tabletSmall: 'Tabet Small', | |
DeviceScreenType.tabletLarge: 'Tablet Large', | |
DeviceScreenType.desktop: 'Desktop', | |
}; | |
String get value => string[this]; | |
} | |
/// when given [MediaQueryData], will return a [DeviceScreenType] enum based on breakpoints | |
/// set in the method. | |
/// | |
/// breakpoints are based on this chart provided by Google. | |
/// https://developer.android.com/images/screens_support/layout-adaptive-breakpoints_2x.png | |
DeviceScreenType getDeviceType(MediaQueryData mediaQuery) { | |
// shortest side will be the bottom in a portait mode device. | |
final width = mediaQuery.size.width; | |
if (width > 840) return DeviceScreenType.desktop; | |
if (width > 720) return DeviceScreenType.tabletLarge; | |
if (width > 600) return DeviceScreenType.tabletSmall; | |
if (width > 400) return DeviceScreenType.mobileLarge; | |
if (width > 360) return DeviceScreenType.mobileMedium; | |
return DeviceScreenType.mobileSmall; | |
} | |
class SizingInformation { | |
final DeviceScreenType deviceScreenType; | |
final Size screenSize; | |
final BoxConstraints constraints; | |
/// wrapper class for sizing information and constraints. | |
SizingInformation({this.deviceScreenType, this.screenSize, this.constraints}); | |
@override | |
String toString() => ''' | |
SizingInformation { | |
deviceScreenType: $deviceScreenType | |
screenSize: $screenSize | |
constraints: $constraints | |
} | |
'''; | |
} | |
class MediaQueryBuilder extends StatelessWidget { | |
final Widget Function(BuildContext, SizingInformation) builder; | |
/// its purpose is to pass an instance of [SizingInformation] into the given | |
const MediaQueryBuilder({Key key, this.builder}) : super(key: key); | |
/// set at class level so the media query to get device type does not need to be ran again. | |
static SizingInformation sizingInformation; | |
@override | |
Widget build(BuildContext context) { | |
final mediaQuery = MediaQuery.of(context); | |
return LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
sizingInformation = SizingInformation( | |
deviceScreenType: getDeviceType(mediaQuery), | |
screenSize: mediaQuery.size, | |
constraints: constraints, | |
); | |
return builder(context, sizingInformation); | |
}, | |
); | |
} | |
} | |
/// this is the responsiveness. given any type and what to do in each device type scenario, | |
/// it smartly returns the correct "thing" for the correct device type. | |
// ignore: non_constant_identifier_names | |
T Responsive<T>(T mobileSmall, {T mobileMedium, T mobileLarge, T tabletSmall, T tabletLarge, T desktop, BuildContext context}) { | |
assert( | |
MediaQueryBuilder.sizingInformation == null ? context != null : context == null, | |
'If there is no MediaQueryBuilder higher in the tree, you will need to provide a BuildContext. Otherwise, do not provide one.', | |
); | |
final deviceType = context != null ? getDeviceType(MediaQuery.of(context)) : MediaQueryBuilder.sizingInformation.deviceScreenType; | |
if (deviceType == DeviceScreenType.desktop && desktop != null) return desktop; | |
if ((deviceType == DeviceScreenType.tabletLarge || deviceType == DeviceScreenType.desktop) && tabletLarge != null) return tabletLarge; | |
if ((deviceType == DeviceScreenType.tabletLarge || deviceType == DeviceScreenType.tabletSmall || deviceType == DeviceScreenType.desktop) && tabletSmall != null) { | |
return tabletSmall; | |
} | |
if ((deviceType == DeviceScreenType.mobileLarge || | |
deviceType == DeviceScreenType.tabletSmall || | |
deviceType == DeviceScreenType.tabletLarge || | |
deviceType == DeviceScreenType.desktop) && | |
mobileLarge != null) { | |
return deviceType == DeviceScreenType.tabletSmall && tabletLarge != null ? tabletLarge : mobileLarge; | |
} | |
if ((deviceType == DeviceScreenType.tabletLarge || | |
deviceType == DeviceScreenType.tabletSmall || | |
deviceType == DeviceScreenType.mobileLarge || | |
deviceType == DeviceScreenType.mobileMedium || | |
deviceType == DeviceScreenType.desktop) && | |
mobileMedium != null) return mobileMedium; | |
return mobileSmall; | |
} | |
class ResponsiveBuilder extends StatelessWidget { | |
final Widget Function(BuildContext, SizingInformation) mobileSmall; | |
final Widget Function(BuildContext, SizingInformation) mobileMedium; | |
final Widget Function(BuildContext, SizingInformation) mobileLarge; | |
final Widget Function(BuildContext, SizingInformation) tabletSmall; | |
final Widget Function(BuildContext, SizingInformation) tabletLarge; | |
final Widget Function(BuildContext, SizingInformation) desktop; | |
/// provide the desired layouts for each device type, and it will use [Responsive<T>] | |
/// to render the correct layout. | |
const ResponsiveBuilder( | |
this.mobileSmall, { | |
Key key, | |
this.mobileMedium, | |
this.mobileLarge, | |
this.tabletSmall, | |
this.tabletLarge, | |
this.desktop, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return MediaQueryBuilder( | |
builder: (BuildContext context, SizingInformation sizingInformation) { | |
return Responsive<Widget Function(BuildContext, SizingInformation)>( | |
mobileSmall, | |
mobileMedium: mobileMedium, | |
mobileLarge: mobileLarge, | |
tabletSmall: tabletSmall, | |
tabletLarge: tabletLarge, | |
desktop: desktop, | |
)(context, sizingInformation); | |
}, | |
); | |
} | |
} | |
class ResponsiveDemo extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
title: 'ResponsiveDemo', | |
theme: ThemeData.dark(), | |
home: Scaffold( | |
body: ResponsiveBuilder( | |
(_, __) => SmallLayout(), | |
mobileLarge: (_, __) => LargeLayout(), | |
tabletLarge: (_, __) => TabletLargeLayout(), | |
desktop: (_, __) => DesktopLayout(), | |
), | |
), | |
); | |
} | |
} | |
class SmallLayout extends StatefulWidget { | |
@override | |
_SmallLayoutState createState() => _SmallLayoutState(); | |
} | |
class _SmallLayoutState extends State<SmallLayout> { | |
final GlobalKey<ScaffoldState> _scaffold = GlobalKey<ScaffoldState>(); | |
String screenValue; | |
@override | |
void didUpdateWidget(SmallLayout oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
final value = MediaQueryBuilder.sizingInformation?.deviceScreenType?.value; | |
if (screenValue != value) { | |
screenValue = value; | |
_scaffold.currentState | |
..hideCurrentSnackBar() | |
..showSnackBar(SnackBar( | |
content: Text(value), | |
duration: const Duration(milliseconds: 800), | |
)); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
key: _scaffold, | |
appBar: AppBar( | |
title: const Text(lazerTagVibes), | |
), | |
drawer: drawer, | |
body: SingleChildScrollView( | |
padding: scrollViewPadding, | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: List.generate(images.length, (int index) { | |
return Container( | |
margin: const EdgeInsets.only(bottom: 20), | |
height: 200, | |
child: Image.network(images[index], fit: BoxFit.fitHeight), | |
); | |
}), | |
), | |
), | |
); | |
} | |
} | |
class LargeLayout extends StatefulWidget { | |
@override | |
_LargeLayoutState createState() => _LargeLayoutState(); | |
} | |
class _LargeLayoutState extends State<LargeLayout> { | |
final GlobalKey<ScaffoldState> _scaffold = GlobalKey<ScaffoldState>(); | |
String screenValue; | |
@override | |
void didUpdateWidget(LargeLayout oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
final value = MediaQueryBuilder.sizingInformation?.deviceScreenType?.value; | |
if (screenValue != value) { | |
screenValue = value; | |
_scaffold.currentState | |
..hideCurrentSnackBar() | |
..showSnackBar(SnackBar( | |
content: Text(value), | |
duration: const Duration(milliseconds: 800), | |
)); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
key: _scaffold, | |
appBar: AppBar( | |
title: const Text(lazerTagVibes), | |
), | |
drawer: drawer, | |
body: SingleChildScrollView( | |
padding: scrollViewPadding, | |
child: Column( | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: [ | |
Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: List.generate(images.length ~/ 2, (int index) { | |
return ResponsiveImage(index); | |
}), | |
), | |
Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: List.generate(images.length ~/ 2, (int index) { | |
return ResponsiveImage(index + 5); | |
}), | |
), | |
], | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
class TabletLargeLayout extends StatefulWidget { | |
@override | |
_TabletLargeLayoutState createState() => _TabletLargeLayoutState(); | |
} | |
class _TabletLargeLayoutState extends State<TabletLargeLayout> { | |
final GlobalKey<ScaffoldState> _scaffold = GlobalKey<ScaffoldState>(); | |
String screenValue; | |
@override | |
void didUpdateWidget(TabletLargeLayout oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
final value = MediaQueryBuilder.sizingInformation?.deviceScreenType?.value; | |
if (screenValue != value) { | |
screenValue = value; | |
_scaffold.currentState | |
..hideCurrentSnackBar() | |
..showSnackBar(SnackBar( | |
content: Text(value), | |
duration: const Duration(milliseconds: 800), | |
)); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
key: _scaffold, | |
appBar: AppBar( | |
title: Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Image.network(logo), | |
const SizedBox(width: 20), | |
const Text(lazerTagVibes), | |
], | |
), | |
), | |
body: Row( | |
children: [ | |
Container( | |
margin: const EdgeInsets.symmetric(vertical: 40), | |
padding: const EdgeInsets.all(10), | |
decoration: const BoxDecoration( | |
color: Color(0xff171616), | |
borderRadius: BorderRadius.only( | |
topRight: Radius.circular(22), | |
bottomRight: Radius.circular(22), | |
), | |
), | |
child: SingleChildScrollView( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: icons, | |
), | |
), | |
), | |
const Spacer(), | |
SingleChildScrollView( | |
padding: scrollViewPadding, | |
child: Column( | |
children: List.generate(images.length ~/ 2, (int index) { | |
return ResponsiveImage(index); | |
}), | |
), | |
), | |
const Spacer(), | |
SingleChildScrollView( | |
padding: scrollViewPadding, | |
child: Column( | |
children: List.generate(images.length ~/ 2, (int index) { | |
return ResponsiveImage(index + 5); | |
}), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class DesktopLayout extends StatefulWidget { | |
@override | |
_DesktopLayoutState createState() => _DesktopLayoutState(); | |
} | |
class _DesktopLayoutState extends State<DesktopLayout> { | |
final GlobalKey<ScaffoldState> _scaffold = GlobalKey<ScaffoldState>(); | |
String screenValue; | |
@override | |
void didUpdateWidget(DesktopLayout oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
final value = MediaQueryBuilder.sizingInformation?.deviceScreenType?.value; | |
if (screenValue != value) { | |
screenValue = value; | |
_scaffold.currentState | |
..hideCurrentSnackBar() | |
..showSnackBar(SnackBar( | |
content: Text(value), | |
duration: const Duration(milliseconds: 800), | |
)); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
key: _scaffold, | |
body: SingleChildScrollView( | |
child: Column( | |
children: [ | |
Container( | |
color: const Color(0xff171616), | |
padding: const EdgeInsets.all(16), | |
child: Column( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Row( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Image.network(logo, height: 200), | |
const SizedBox(width: 20), | |
const Text( | |
lazerTagVibes, | |
style: TextStyle( | |
fontSize: 30, | |
), | |
), | |
], | |
), | |
Container( | |
margin: const EdgeInsets.symmetric(vertical: 10), | |
padding: const EdgeInsets.all(10), | |
decoration: const BoxDecoration( | |
color: Color(0xff171616), | |
borderRadius: BorderRadius.only( | |
topRight: Radius.circular(22), | |
bottomRight: Radius.circular(22), | |
), | |
), | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: icons, | |
), | |
), | |
], | |
), | |
), | |
Padding( | |
padding: const EdgeInsets.all(20), | |
child: Wrap( | |
runSpacing: 20, | |
spacing: 20, | |
alignment: WrapAlignment.center, | |
crossAxisAlignment: WrapCrossAlignment.center, | |
children: List.generate(images.length, (int index) { | |
return ResponsiveImage(index); | |
}), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
const lazerTagVibes = 'Lazer Tag Vibes'; | |
final responsiveIconSize = Responsive<double>( | |
40, | |
mobileMedium: 60, | |
mobileLarge: 70, | |
tabletSmall: 50, | |
tabletLarge: 70, | |
); | |
final icons = [ | |
const SizedBox(height: 20), | |
Icon(Icons.account_circle, size: responsiveIconSize), | |
const SizedBox(height: 20), | |
Icon(Icons.local_play, size: responsiveIconSize), | |
const SizedBox(height: 20), | |
Icon(Icons.calendar_today, size: responsiveIconSize), | |
const SizedBox(height: 20), | |
Icon(Icons.explore, size: responsiveIconSize), | |
const SizedBox(height: 20), | |
Icon(Icons.people, size: responsiveIconSize), | |
const SizedBox(height: 20), | |
]; | |
final images = [ | |
'https://media.istockphoto.com/photos/laser-tag-picture-id171159077?b=1&k=6&m=171159077&s=170667a&w=0&h=L_Ld9ss7EMNdGH0bHJGH-5M6VGYHA6ENUIrgmiHHojk=', | |
'https://media.istockphoto.com/photos/variety-of-weapons-for-playing-laser-tag-game-and-playground-picture-id1163691476?b=1&k=6&m=1163691476&s=170667a&w=0&h=9fK2BaEf_vnMrO3CLLl67UwaPVg52Mm107x38Kijx3U=', | |
'https://media.istockphoto.com/photos/laser-tag-picture-id171575073?b=1&k=6&m=171575073&s=170667a&w=0&h=u5YNgSazZYaOT82MYHWr7BmfFiHcBumAb8DhMv2WD5M=', | |
'https://media.istockphoto.com/photos/laser-tag-picture-id161136596?b=1&k=6&m=161136596&s=170667a&w=0&h=kcoOJ0rDJfjrGsjUhMBh3A1RGOWQmsh2jePyVcAlncM=', | |
'https://media.istockphoto.com/photos/laser-tag-picture-id174294018?b=1&k=6&m=174294018&s=170667a&w=0&h=_GjX1MXzM3LzbrLzN_pWPTde0o-GTcmsEhBQMSe0gEk=', | |
'https://media.istockphoto.com/photos/3d-render-neon-light-round-frame-blank-space-for-text-ultraviolet-picture-id1058077096?b=1&k=6&m=1058077096&s=170667a&w=0&h=FngoEH8skQJHdqW9gIbxRfelZ_1mu8MQ9mvQnw3xmf8=', | |
'https://media.istockphoto.com/photos/laser-tag-picture-id161335377?b=1&k=6&m=161335377&s=170667a&w=0&h=9mLm4W0u4FpRwg0GZ8O7onsjZObp-mvXjnrm_17mizY=', | |
'https://media.istockphoto.com/photos/laser-tag-picture-id160187550?b=1&k=6&m=160187550&s=170667a&w=0&h=P_J2Lew3aOCgrGY9XxqAmFWeHV8bJZYJZJFLCVWwCw4=', | |
'https://www.kalahariresorts.com/media/4570/3.jpg?mode=crop&width=480&quality=80&anchor=middlecenter&scale=both', | |
'https://static.mommypoppins.com/styles/image300x250/s3/track_21_laser_tag.jpg', | |
]; | |
class ResponsiveImage extends StatelessWidget { | |
final int index; | |
const ResponsiveImage(this.index); | |
@override | |
Widget build(BuildContext context) { | |
return Container( | |
margin: const EdgeInsets.only(bottom: 20), | |
height: MediaQueryBuilder.sizingInformation.deviceScreenType == DeviceScreenType.mobileSmall ? 200 : null, | |
width: MediaQueryBuilder.sizingInformation.deviceScreenType == DeviceScreenType.mobileSmall | |
? null | |
: Responsive<double>( | |
null, | |
mobileMedium: 180, | |
mobileLarge: 180, | |
tabletSmall: 200, | |
tabletLarge: 274, | |
), | |
child: Image.network(images[index], fit: BoxFit.fitHeight), | |
); | |
} | |
} | |
final scrollViewPadding = Responsive<EdgeInsets>( | |
const EdgeInsets.all(20), | |
mobileMedium: const EdgeInsets.all(10), | |
mobileLarge: const EdgeInsets.all(5), | |
tabletSmall: const EdgeInsets.all(20), | |
tabletLarge: const EdgeInsets.all(20), | |
); | |
const logo = 'https://www.redditstatic.com/amp/img/snoovatar.png'; | |
final drawer = Drawer( | |
child: SingleChildScrollView( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.spaceAround, | |
children: [ | |
Image.network(logo), | |
...icons, | |
], | |
), | |
), | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment