First Step Cleaning Up Flutter Responsive Scaffold Mess
A bit of engineering responsive scaffolds
In Android native, we have screen fragments and an implementation of flowing part of the screen fragment content through navigation destinations. Which is why Android native adaptive and responsive scaffold solutions can have multiple children for two pane layouts.
Unfortunately, some Google Engineers missed that aspect when implementing the flutter adaptive scaffold with second body constructs. Hence, it will NEVER EVER work with the way we implement Navigator 2.0 router.
Hence, the need for the proper breakpoint (window size classes) and layout utilities to properly implement manually an adaptive scaffold and canonical layout pattern set. Also, given staff reductions at Google in the flutter team, stuff like this will not be fixed; which highlights the need to find flutter mentors like me to subscribe and follow as the flutter mentors like me clean up these messes.
How We Got The Scaffold Mess
The mess stems from the Flutter SDK team attempting to copy the way jet compose and some android native packages implements the Material Design 3 adaptive and responsive patterns. So at first I am going to step you through some Android native code which will be the key to understanding why we cannot use the flutter adaptive scaffold package.
In Android native the routing is implemented not as router like Flutter but as NavHost directly in the scaffold:
Now let me show you how the Microsoft implemented their jetpack compose library to support foldables using Android native. What Microsoft came up with is having the two pane layout use the navigation graph via the navigation controller parameter to assist in driving the navigation via a TwoPanelLayoutNav construct.
Scaffold is in the BasicDestination.kt file:
package com.microsoft.device.dualscreen.twopanelayout | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.clickable | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.material.MaterialTheme | |
import androidx.compose.material.Scaffold | |
import androidx.compose.material.Text | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.res.painterResource | |
import androidx.compose.ui.res.stringResource | |
import androidx.compose.ui.unit.dp | |
import androidx.navigation.NavHostController | |
import androidx.navigation.NavOptionsBuilder | |
@Composable | |
fun TwoPaneNavScope.BasicDestination( | |
navController: NavHostController, | |
sampleDestination: SampleDestination, | |
modifier: Modifier = Modifier, | |
textColor: Color = Color.Black, | |
) { | |
// Calculate which destination should be navigated to next | |
val nextDestination = | |
SampleDestination.values().getOrElse(sampleDestination.ordinal + 1) { SampleDestination.DEST1 } | |
// Set up the navigation options for a circular navigation pattern (1 -> 2 -> 3 -> 4 -> 1) | |
val dest4NavOptions: NavOptionsBuilder.() -> Unit = { | |
launchSingleTop = true | |
popUpTo(SampleDestination.DEST1.route) | |
} | |
val emptyNavOptions: NavOptionsBuilder.() -> Unit = { launchSingleTop = true } | |
val navOptions: NavOptionsBuilder.() -> Unit = when (sampleDestination) { | |
SampleDestination.DEST4 -> dest4NavOptions | |
else -> emptyNavOptions | |
} | |
val onClick = { navController.navigateTo(nextDestination.route, sampleDestination.changesScreen, navOptions) } | |
Scaffold( | |
topBar = { | |
// Customize top bar text depending on which pane a destination is shown in | |
val pane = when { | |
isSinglePane -> "" | |
sampleDestination.route == currentPane1Destination -> " " + stringResource(R.string.pane1) | |
sampleDestination.route == currentPane2Destination -> " " + stringResource(R.string.pane2) | |
else -> "" | |
} | |
TopAppBar(pane) | |
} | |
) { | |
Box( | |
modifier = modifier | |
.fillMaxSize() | |
.background(sampleDestination.color) | |
.clickable { onClick() } | |
.padding(it) | |
.padding(10.dp) | |
) { | |
Text( | |
modifier = Modifier.align(Alignment.TopCenter), | |
text = stringResource(sampleDestination.text), | |
color = textColor | |
) | |
Row( | |
modifier = Modifier | |
.align(Alignment.Center) | |
.padding(bottom = 20.dp), | |
verticalAlignment = Alignment.CenterVertically, | |
horizontalArrangement = Arrangement.spacedBy(20.dp) | |
) { | |
NavigationText(nextDestination.route, sampleDestination.changesScreen, textColor) | |
NavigationGraphic(sampleDestination.next) | |
} | |
} | |
} | |
} | |
@Composable | |
private fun NavigationText(nextRoute: String, inPane: Screen, textColor: Color) { | |
Text( | |
text = stringResource(R.string.navigates_to, nextRoute, inPane), | |
color = textColor, | |
style = MaterialTheme.typography.h4 | |
) | |
} | |
@Composable | |
private fun NavigationGraphic(nextId: Int) { | |
Image( | |
painter = painterResource(id = nextId), | |
contentDescription = stringResource(id = R.string.image_description) | |
) |
And then the Nav part for the TwoPaneLayout is in the main activity part:
package com.microsoft.device.dualscreen.twopanelayout | |
import android.os.Bundle | |
import androidx.activity.ComponentActivity | |
import androidx.activity.compose.setContent | |
import androidx.compose.material.MaterialTheme | |
import androidx.compose.material.Surface | |
import androidx.compose.material.Text | |
import androidx.compose.material.TopAppBar | |
import androidx.compose.runtime.Composable | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.res.stringResource | |
import androidx.navigation.compose.rememberNavController | |
import com.microsoft.device.dualscreen.twopanelayout.twopanelayoutnav.composable | |
import com.microsoft.device.dualscreen.twopanelayout.ui.theme.TwoPaneLayoutTheme | |
import com.microsoft.device.dualscreen.twopanelayout.ui.theme.blue | |
class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContent { | |
TwoPaneLayoutTheme { | |
Surface(color = MaterialTheme.colors.background) { | |
MainPage() | |
} | |
} | |
} | |
} | |
} | |
@Composable | |
fun MainPage() { | |
val navController = rememberNavController() | |
TwoPaneLayoutNav( | |
navController = navController, | |
singlePaneStartDestination = SampleDestination.DEST1.route, | |
pane1StartDestination = SampleDestination.DEST1.route, | |
pane2StartDestination = SampleDestination.DEST2.route | |
) { | |
SampleDestination.values().map { dest -> | |
composable(dest.route) { | |
BasicDestination(navController, dest) | |
} | |
} | |
} | |
} | |
@Composable | |
fun TopAppBar(paneAnnotation: String) { | |
TopAppBar( | |
title = { Text(text = stringResource(R.string.app_name) + paneAnnotation, color = Color.White) }, | |
backgroundColor = blue | |
) | |
} |
There is no prior art in Android native jet compose to indicate why the Google Flutter team thought it was a good idea to implement the adaptive scaffold as a combination of adaptive scaffold and canonical layout. But we just know we cannot use it with its second body constructs as it will then not work with any navigator 2.0 library even Google's own preferred GoRouter.
So we are left with the engineering task of creating some breakpoints in a class(they are called window size classes in the Material Design 3 docs) and some decent layout utilities that happen to use the breakpoints. As those will then be used to manually implement the responsive and adaptive scaffold and the canonical layouts.
Breakpoints And Layout Utilities
So we start this out with a powerful simple enumerated type enum model:
/// Model for Adaptive Scaffold implementations accoding | |
/// to Dart enumerated types. See Canonical Layouts for | |
/// Window Size class numbers at: | |
/// https://m3.material.io/foundations/layout/canonical-layouts/list-detail | |
/// | |
/// @author Fredrick Allan Grott | |
enum WindowSizeEnum { | |
/// The compact window size | |
compact(0, 599), | |
/// The medium window size | |
medium(600, 839), | |
/// The expanded window size | |
expanded(840, 1199), | |
/// The large window size | |
large(1200, 1599), | |
/// The extra window size | |
extra(1600, double.infinity); | |
/// The beginning of the range of window siaes | |
final double begin; | |
/// The end of the range of window sizes | |
final double end; | |
/// Creaqtes a new [WindowSizeEnum] with the given [begin] and [end] valuse | |
const WindowSizeEnum(this.begin, this.end); | |
} |
The docs to forming enumerated type enums is here:
Dart Enumerated Types
Then we define some boolean getters using that WindowSizeEnum:
/// Determines if the screen is in the compact window size. | |
/// | |
/// Returns `true` if the screen width is less than 600 pixels, `false` otherwise. | |
static bool isCompact(BuildContext context) { | |
return MediaQuery.of(context).size.width < WindowSizeEnum.medium.begin ? true : false; | |
} | |
/// Determines if the screen is in the medium window size. | |
/// | |
/// Returns `true` if the screen width is less than 840 pixels, `false` otherwise. | |
static bool isMedium(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.medium.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.expanded.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the expanded window size. | |
/// | |
/// Returns `true` if the screen width is less than 1200 pixels, `false` otherwise. | |
static bool isExpanded(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.expanded.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.large.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the large window size. | |
/// | |
/// Returns `true` if the screen width is less than 1600 pixels, `false` otherwise. | |
static bool isLarge(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.large.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.extra.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the extra window size. | |
/// | |
/// Returns `true` if the screen width is more than 1600 pixels, `false` otherwise. | |
static bool isExtra(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.extra.begin ? true : false; | |
} |
And then that means we can now construct the main core getter of the breakpoints class:
static WindowSizeEnum getWindowSize(BuildContext context) { | |
if (isCompact(context)) { | |
return WindowSizeEnum.compact; | |
} else if (isMedium(context)) { | |
return WindowSizeEnum.medium; | |
} else if (isExpanded(context)) { | |
return WindowSizeEnum.expanded; | |
} else if (isLarge(context)) { | |
return WindowSizeEnum.large; | |
} else if (isExtra(context)) { | |
return WindowSizeEnum.extra; | |
} else { | |
throw Exception('bad condition'); | |
} | |
} |
And the full source code of that file is:
// Copyright 2024 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'; | |
// Note: Generally when we implement models required in | |
// implementing the adaptive scaffold and canonical | |
// layout patterns we do not stick models or entities | |
// in the domain layer but keep them together with | |
// the view-model like logic in one file. | |
// | |
// This practice is borrowed from the Google Flutter and | |
// Dart teams as its the practice they use when creating | |
// the codelabs examples. As well as the two top IO | |
// presenters such as GSKinner and VeryGoodVentures also | |
// use the same practice. | |
/// [Breakpoints], [WindowSizeEnum], and [LayoutUtils] form | |
/// the core of my approach to implementing the paired | |
/// Adaptive Scaffold and Canonical Layouts pattern to | |
/// handle devices of all sizes and form factors including | |
/// foldables. | |
/// | |
/// So let me explain why. Under Material Design 2 we | |
/// had the adaptive navigation package which employed | |
/// material design 2 breakpoints and navigation types | |
/// of bottom, rail, drawer, and permanent drawer. It's | |
/// draw backs were not handling animation properly and | |
/// no support of MD3 breakpoints (window sizes) or the | |
/// improved adaptive scaffold patterns or foldables. | |
/// | |
/// Microsoft came up with support of surface foldables | |
/// via the Android native package twopanelayout for | |
/// jet compose and a flutter based two pane layout as | |
/// part of the flutter dual screen package. | |
/// | |
/// In the biggest error a Google Engineer could make they | |
/// attempted to copy implementation strategies for | |
/// canonical layouts from android native to the flutter | |
/// adaptive scaffold. Why was this a big error? | |
/// | |
/// Android native navigatin is different as Android native | |
/// has screen fragments whereas flutter does not in the | |
/// navigation implementation. In short in Android native | |
/// I can implement an adaptive scaffold MD3 pattern using | |
/// multiple content body childred as the navigation | |
/// implementation allows it while on Flutter we cannot | |
/// do such constructs as the secondBody in error constructs | |
/// in the flutter adaptive scaffold imply. | |
/// | |
/// Thus one has to implement the adaptive scaffold pattern | |
/// needed manually using the codelabs sample as a guide: | |
/// see guide https://codelabs.developers.google.com/codelabs/flutter-animated-responsive-layout#8 | |
/// see github code at https://github.com/flutter/codelabs/tree/main/animated-responsive-layout | |
/// | |
/// Then once that is implemented one can use the | |
/// dual screen package: | |
/// https://pub.dev/packages/dual_screen | |
/// And the surface duo flutter sdk samples as a guide: | |
/// https://github.com/microsoft/surface-duo-sdk-samples-flutter | |
/// To implement the canonical layouts needed using these | |
/// helper classes. | |
/// | |
/// Helper class for defining breakpoints for implementing | |
/// adaptive scaffolds with MD3. The method getWindowSize | |
/// returns the correct WindowSizeEnum per the BuildContext | |
/// MediaQuery results. | |
/// | |
/// MD3 docs are at: | |
/// m3.material.io | |
/// | |
/// Note, unlike MD2 we no longer have gutter and inserts are | |
/// somewhat handled at the content pane level of the canonical | |
/// layout. | |
/// | |
/// Generally the call is | |
/// ``` | |
/// Breakpoints.getWindowSize(context).begin; | |
/// Breakpoints.getWindowSize(context).end; | |
/// ``` | |
/// | |
/// @authot Fredrick Allan Grott | |
class Breakpoints { | |
static WindowSizeEnum getWindowSize(BuildContext context) { | |
if (isCompact(context)) { | |
return WindowSizeEnum.compact; | |
} else if (isMedium(context)) { | |
return WindowSizeEnum.medium; | |
} else if (isExpanded(context)) { | |
return WindowSizeEnum.expanded; | |
} else if (isLarge(context)) { | |
return WindowSizeEnum.large; | |
} else if (isExtra(context)) { | |
return WindowSizeEnum.extra; | |
} else { | |
throw Exception('bad condition'); | |
} | |
} | |
/// Determines if the screen is in the compact window size. | |
/// | |
/// Returns `true` if the screen width is less than 600 pixels, `false` otherwise. | |
static bool isCompact(BuildContext context) { | |
return MediaQuery.of(context).size.width < WindowSizeEnum.medium.begin ? true : false; | |
} | |
/// Determines if the screen is in the medium window size. | |
/// | |
/// Returns `true` if the screen width is less than 840 pixels, `false` otherwise. | |
static bool isMedium(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.medium.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.expanded.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the expanded window size. | |
/// | |
/// Returns `true` if the screen width is less than 1200 pixels, `false` otherwise. | |
static bool isExpanded(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.expanded.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.large.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the large window size. | |
/// | |
/// Returns `true` if the screen width is less than 1600 pixels, `false` otherwise. | |
static bool isLarge(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.large.begin && | |
MediaQuery.of(context).size.width < WindowSizeEnum.extra.begin | |
? true | |
: false; | |
} | |
/// Determines if the screen is in the extra window size. | |
/// | |
/// Returns `true` if the screen width is more than 1600 pixels, `false` otherwise. | |
static bool isExtra(BuildContext context) { | |
return MediaQuery.of(context).size.width >= WindowSizeEnum.extra.begin ? true : false; | |
} | |
} | |
/// Model for Adaptive Scaffold implementations accoding | |
/// to Dart enumerated types. See Canonical Layouts for | |
/// Window Size class numbers at: | |
/// https://m3.material.io/foundations/layout/canonical-layouts/list-detail | |
/// | |
/// @author Fredrick Allan Grott | |
enum WindowSizeEnum { | |
/// The compact window size | |
compact(0, 599), | |
/// The medium window size | |
medium(600, 839), | |
/// The expanded window size | |
expanded(840, 1199), | |
/// The large window size | |
large(1200, 1599), | |
/// The extra window size | |
extra(1600, double.infinity); | |
/// The beginning of the range of window siaes | |
final double begin; | |
/// The end of the range of window sizes | |
final double end; | |
/// Creaqtes a new [WindowSizeEnum] with the given [begin] and [end] valuse | |
const WindowSizeEnum(this.begin, this.end); | |
} |
So now what do we need for the layout utilities? Unlike Material Design 2, we no longer have gutters. But, we still have padding and margins that will vary by the breakpoint in Material Design 3.
The other part that everyone forgets, yes, even some Google Developer Expert advocates! When an app user uses the device settings to change text scaling it does not get automatically handled in Flutter. And even the Theme docs in Material Design 3 state that one should not set VisualDensity to a default in the Theme class.
If we are able to determine the VisualDensity setting value to use via breakpoints we can than use the trick of using MediaQuery InheritedWidget wrappers to wrap the Scaffold as that then will seek the MediaQuery ancestor in the MaterialApp widget and force a rebuild of the widget tree when the app user changes the device text scale settings. So we need two methods, one to return an EdgeInsetsGeometry object and a method to return a VisualDensity object.
// Copyright 2024 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:adaptive_constraints/src/breakpoints.dart'; | |
import 'package:flutter/material.dart'; | |
/// LayoutUtils to assist in implementing Canonical page layouts | |
/// to serves as content for an adaptive scaffold. | |
/// | |
/// Generally the calls are: | |
/// ``` | |
/// LayoutUtils.layoutSpacing(context); | |
/// LayoutUtils.getVisualDensity(context); | |
/// ``` | |
/// | |
/// Note: Text Scaling is achieved by wrapping the Scaffold | |
/// like this: | |
/// ``` | |
/// MediaQuery.withClapedTextScaling( | |
/// maxScaleFactor: MediaQuery.texdtScalerOf(context), | |
/// child: ScaffoldWidget(), | |
/// ) | |
/// ``` | |
/// As we need one ancestor for it to work right and thus | |
/// have to do it below the MaterialApp widget. | |
/// As that will correct App text scaling when app user uses | |
/// actual device settings to change text scaling for all | |
/// platforms. Obviously its hinted lightly the flutter docs. | |
/// And obviously we never ever set text scaling via themes! | |
/// | |
/// @author Fredrick Allan Grott | |
class LayoutUtils { | |
/// Compact layout margin | |
double get compactLayoutMargin => 16; | |
/// Medium layout margin | |
double get mediumLayoutMargin => 24; | |
/// Expanded layout margin | |
double get expandedLayoutMargin => 24; | |
/// Large layout margin | |
double get largeLayoutMargin => 32; | |
/// Extra layout margin | |
double get extraLayoutMargin => 32; | |
/// Content pane spacing | |
double get contentPaneSpacing => 24; | |
/// Compact layout padding | |
double get compactLayoutPadding => 4; | |
/// Medium layout padding | |
double get mediumLayoutPadding => 8; | |
/// Expanded layout padding | |
double get expandedLayoutPadding => 12; | |
/// Large layout padding | |
double get largeLayoutPadding => 16; | |
/// Extra layout padding | |
double get extraLayoutPadding => 20; | |
/// Returns to correct EdgeInsets per | |
/// WindowSize class breakpoint with | |
/// horizontal getting margin and | |
/// vertical getting padding values. | |
EdgeInsetsGeometry layoutSpacing(BuildContext context) { | |
if (Breakpoints.isCompact(context)) { | |
return EdgeInsets.symmetric( | |
horizontal: compactLayoutMargin, | |
vertical: compactLayoutPadding, | |
); | |
} else if (Breakpoints.isMedium(context)) { | |
return EdgeInsets.symmetric( | |
horizontal: mediumLayoutMargin, | |
vertical: mediumLayoutPadding, | |
); | |
} else if (Breakpoints.isExpanded(context)) { | |
return EdgeInsets.symmetric( | |
horizontal: expandedLayoutMargin, | |
vertical: expandedLayoutPadding, | |
); | |
} else if (Breakpoints.isLarge(context)) { | |
return EdgeInsets.symmetric( | |
horizontal: largeLayoutMargin, | |
vertical: largeLayoutPadding, | |
); | |
} else if (Breakpoints.isExtra(context)) { | |
return EdgeInsets.symmetric( | |
horizontal: extraLayoutMargin, | |
vertical: extraLayoutPadding, | |
); | |
} else { | |
throw UnimplementedError('Bad breakpoint'); | |
} | |
} | |
/// Per MD3 docs we do not set VisualDensity defauls | |
/// but instead its implied that we need to set this via | |
/// breakpoints. See class doc at | |
/// https://api.flutter.dev/flutter/material/VisualDensity-class.html | |
/// And MD3 doc at | |
/// https://m3.material.io/foundations/layout/understanding-layout/spacing | |
/// And probably the final polish setting some type of | |
/// settings switch the flutter app settings to allow | |
/// the user to adjust visual density themselves. | |
/// And no, this is not directly in Flutter docs or | |
/// Codelabs examples either. | |
/// | |
/// And yes the constants for VisualDensity have | |
/// not been changed in the SDK yet to reflect | |
/// alignment with the MD3 WindowSize class breakpoints. | |
static VisualDensity getVisualDensity(BuildContext context) { | |
if (Breakpoints.isCompact(context)) { | |
return const VisualDensity(horizontal: -2.0, vertical: -2.0); | |
} else if (Breakpoints.isMedium(context)) { | |
return const VisualDensity(horizontal: -1.0, vertical: -1.0); | |
} else if (Breakpoints.isExpanded(context)) { | |
return const VisualDensity(horizontal: 0.0, vertical: 0.0); | |
} else if (Breakpoints.isLarge(context)) { | |
return const VisualDensity(horizontal: 2.0, vertical: 2.0); | |
} else if (Breakpoints.isExtra(context)) { | |
return const VisualDensity(horizontal: 4.0, vertical: 4.0); | |
} else { | |
throw Exception('bad condition'); | |
} | |
} | |
} |
Which makes the Scaffold wrapper to use this:
MediaQuery.withClapedTextScaling( | |
maxScaleFactor: MediaQuery.texdtScalerOf(context), | |
child: ScaffoldWidget(), | |
) |
While the VisualDensity via the theme will be:
MaterialApp( | |
theme: ThemeData(vsiualDensity: LayoutUtils.getVisualDensity), | |
) |
If it is not obvious, we need the EdgeInsetsGeomerty object set with breakpoints as its used to establish the correct padding and margins of the pane contents in canonical layouts. One uses the resolve method with the TextDirection parameter to then return the EdgeInsets object. Which also implies that the responsive scaffold and canonical layout are indirectly coupled as we will detect text direction to adjust the responsive scaffold properly so that navigation areas get the right text direction.
Resources
The hub for everything is at:
Mastering Dart N Flutter
https://github.com/fredgrott/mastering_dart_n_flutter
While the article code is in this repo:
Master Flutter Adaptive
https://github.com/fredgrott/master_flutter_adaptive
Conclusion
Breakpoints not only get used in manually implementing the responsive and adaptive scaffold but also will play a role in defining the TwoPaneLayout( in the Dual Screen package) behavior per breakpoint to get the full Material Design 3 canonical layout patterns. Note, despite 3 years history of the flutter adaptive scaffold package at no point has canonical layouts been actually implemented using the flutter adaptive scaffold package. That might be clue to find some good Flutter mentors who are writing about such challenges and showing solutions.
The next step will be showing how to implement the responsive scaffold with the default Material Design 3 adaptive navigation case using the Breakpoints and LayoutUtils classes. Then right after that use the default skeleton app and implement Navigaror 2.0 routing using the GoRouter and then implement the canonical layouts to pair with the responsive scaffold implementation.
And, yes, I will show you how to implement navigation using GoRouter without having to use build runner. In fact, its quite easy when you get a clue of how to do it.