Skip to content

Instantly share code, notes, and snippets.

@drexel-ue
Last active July 16, 2020 01:21
Show Gist options
  • Save drexel-ue/7eb75e7bf34873f4e414420cf2374b79 to your computer and use it in GitHub Desktop.
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
// 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