How To Write Flutter Adaptive Scaffold Breakpoints Like You Were A GDE
Flutter Adaptive Scaffold UX pattern is not in the Flutter SDK, this is the first step in that UX pattern in the form of breakpoints like you were a Flutter GDE.
Look, I have to shame Flutter GDE's as they are not writing the stuff that you need in creating that awesome flutter app. And here is the thing, I can go through the top two presenters at Google IOs the last five years and find their IO demo apps and there in fact is right in their code an attempt at implementing an adaptive scaffold UX pattern.
And in fact, the adaptive scaffold UX pattern has been in the Material Design specification in two versions, both Material Design 2 and Material Design 3. What has changed is that the Material Design team at Google has finally realized that there is no good layout UX pattern to handle two types of large content form-devices of both laptops and desktops and foldables. And we still have the Flutter SDK insisting that some magical package will correctly implement this adaptive scaffold material design UX pattern.
The dirty little flutter SDK secret is that you, the app designer, will always have to implement your own use case of that adaptive scaffold UX pattern yourself. And the other dirty little secret is that no Flutter GDE is covering such Flutter and Material Design subject areas.
And the third dirty secret is that while Material Design 3 also calls for an animated transition to rail navigation like Material Design 2 did, the flutter SDK team has never implemented it. That means in my experience of using more than 5 front-end frameworks that we can safely ignore it and let the Flutter SDK team worry about it instead.
So the first step towards your very own adaptive scaffold UX pattern for your flutter app is to implement the material design 3 breakpoints. I show you a very specific way to read the Material Design 3 specification and a very specific way to model the app screen breakpoints.
How To Read The Material Design Specification
The way the Material Design specification writing cycle works is that first the Material Design IO team first writes the core with all the changes from the previous version. Then they correct any mistakes and refine it.
To complicate matters, the adaptive scaffold UX pattern because it is also the responsive pattern due to everyone using a stateful to implement it in such a way that it can be shared and used with every screen of the application. Thus, we have two areas to look at in the specification to find the correct information for the breakpoints, layout and navigation.
Complicating things is that is one more window class that is not mentioned in the understanding layout section of the specification:
And we have the fact that the Flutter SDK itself has no default layouts except for the GridView class. And so the idea is not to couple the window class size parameters of beginning and end with other things such as the spacer size used and the number of columns used.
So why decouple those things? Because, in the material design specification writing cycle with the useMaterial3 flag finally being turned on in the Flutter SDK which phase are in? Obviously, the refinement phase.
Which means I fully expect the spacer sizes and the number of columns per window size to in fact change and thus its better to have that decoupled from the window class sizes.
How Do We Model The Window Class Sizes For Breakpoints
The easiest way to model the window class sizes is through an enum, for example:
/// Enum representing the possible layouts in the responsive UI design. | |
enum LayoutEnum { | |
/// The compact layout, for screens with width less than 600 pixels. | |
compact(0, 600), | |
/// The medium layout, for screens with width between 600 and 840 pixels. | |
medium(600, 840), | |
/// The extended layout, for screens with width greater than 840 pixels. | |
expanded(840, 1239), | |
/// per this https://m3.material.io/components/navigation-drawer/guidelines | |
/// there is actually one more virtual window class size | |
/// as there is no nav rail for desktop but instead standard nav drawers | |
evile(1240, double.infinity); | |
/// The beginning of the range of screen widths for this layout. | |
final double? begin; | |
/// The end of the range of screen widths for this layout. | |
final double? end; | |
/// Creates a new [LayoutEnum] with the given [begin] and [end] values. | |
const LayoutEnum(this.begin, this.end); | |
} |
The extra windows class is named evile as if you grew up in the 1970s than you know which action hero bike I had.
Now we need two different method types in our breakpoints class. One will get the layout class that the screen happens to be in by width. The other type of method will check if we are in very specific window class size:
/// Helper class for defining breakpoints in a responsive UI design. | |
class Breakpoints { | |
/// Determines the current layout based on the width of the screen. | |
/// | |
/// Returns a [LayoutEnum] value that corresponds to the current layout. | |
/// | |
/// Throws an [Exception] if none of the breakpoints match the current width. | |
static LayoutEnum getLayout(BuildContext context) { | |
if (isCompact(context)) { | |
return LayoutEnum.compact; | |
} else if (isMedium(context)) { | |
return LayoutEnum.medium; | |
} else if (isExpanded(context)) { | |
return LayoutEnum.expanded; | |
} else if (isEvile(context)) { | |
return LayoutEnum.evile; | |
} else { | |
throw Exception('Bad condition!'); | |
} | |
} | |
/// Determines if the screen is in the compact layout. | |
/// | |
/// Returns `true` if the screen width is less than 600 pixels, `false` otherwise. | |
static bool isCompact(BuildContext context) { | |
return MediaQuery.of(context).size.width < 600 ? true : false; | |
} | |
/// Determines if the screen is in the medium layout. | |
/// | |
/// Returns `true` if the screen width is between 600 and 840 pixels, `false` otherwise. | |
static bool isMedium(BuildContext context) { | |
var mediaQuery = MediaQuery.of(context).size; | |
return mediaQuery.width >= 600 && mediaQuery.width < 840 ? true : false; | |
} | |
/// Determines if the screen is in the extended layout. | |
/// | |
/// Returns `true` if the screen width is greater than 840 pixels, `false` otherwise. | |
static bool isExpanded(BuildContext context) { | |
var mediaQuery = MediaQuery.of(context).size; | |
return MediaQuery.of(context).size.width >= 840 && mediaQuery.width < 1240 ? true : false; | |
} | |
static bool isEvile(BuildContext context) { | |
return MediaQuery.of(context).size.width >= 1240 ? true : false; | |
} | |
} |
Notice that since we will have a build context inside widgets and layouts that I can use MediaQuery API to get the screen size. And here is the full code:
// Copyright 2023 Fredrick Allan Grott. All rights reserved. | |
// Use of this source code is governed by a BSD-style | |
// license that can be found in the LICENSE file. | |
import 'package:flutter/material.dart'; | |
/// Helper class for defining breakpoints in a responsive UI design. | |
class Breakpoints { | |
/// Determines the current layout based on the width of the screen. | |
/// | |
/// Returns a [LayoutEnum] value that corresponds to the current layout. | |
/// | |
/// Throws an [Exception] if none of the breakpoints match the current width. | |
static LayoutEnum getLayout(BuildContext context) { | |
if (isCompact(context)) { | |
return LayoutEnum.compact; | |
} else if (isMedium(context)) { | |
return LayoutEnum.medium; | |
} else if (isExpanded(context)) { | |
return LayoutEnum.expanded; | |
} else if (isEvile(context)) { | |
return LayoutEnum.evile; | |
} else { | |
throw Exception('Bad condition!'); | |
} | |
} | |
/// Determines if the screen is in the compact layout. | |
/// | |
/// Returns `true` if the screen width is less than 600 pixels, `false` otherwise. | |
static bool isCompact(BuildContext context) { | |
return MediaQuery.of(context).size.width < 600 ? true : false; | |
} | |
/// Determines if the screen is in the medium layout. | |
/// | |
/// Returns `true` if the screen width is between 600 and 840 pixels, `false` otherwise. | |
static bool isMedium(BuildContext context) { | |
var mediaQuery = MediaQuery.of(context).size; | |
return mediaQuery.width >= 600 && mediaQuery.width < 840 ? true : false; | |
} | |
/// Determines if the screen is in the extended layout. | |
/// | |
/// Returns `true` if the screen width is greater than 840 pixels, `false` otherwise. | |
static bool isExpanded(BuildContext context) { | |
var mediaQuery = MediaQuery.of(context).size; | |
return MediaQuery.of(context).size.width >= 840 && mediaQuery.width < 1240 ? true : false; | |
} | |
static bool isEvile(BuildContext context) { | |
return MediaQuery.of(context).size.width >= 1240 ? true : false; | |
} | |
} | |
/// Enum representing the possible layouts in the responsive UI design. | |
enum LayoutEnum { | |
/// The compact layout, for screens with width less than 600 pixels. | |
compact(0, 600), | |
/// The medium layout, for screens with width between 600 and 840 pixels. | |
medium(600, 840), | |
/// The extended layout, for screens with width greater than 840 pixels. | |
expanded(840, 1239), | |
/// per this https://m3.material.io/components/navigation-drawer/guidelines | |
/// there is actually one more virtual window class size | |
/// as there is no nav rail for desktop but instead standard nav drawers | |
evile(1240, double.infinity); | |
/// The beginning of the range of screen widths for this layout. | |
final double? begin; | |
/// The end of the range of screen widths for this layout. | |
final double? end; | |
/// Creates a new [LayoutEnum] with the given [begin] and [end] values. | |
const LayoutEnum(this.begin, this.end); | |
} |
Thoughts
Despite claims of the Flutter SDK team, you will always have to manually implement the adaptive scaffold UX pattern that matches your flutter app requirements. Next up, is how to implement the individual parts of the navigation before we turn the Flutter SDK scaffold class into an adaptive UX pattern.