AdaptiveScaffold is great, but I'm having a hard time figuring out how to build layouts properly, due to the lack of documentation.
This is what I would like to achieve:
On the top row, the wide layout is presented, whereas the bottom row shows the narrow layout. In both cases, the flow is:
The URI for each step is as follows:
I've gotten "close" to this solution, but problems arise when selecting an item from the list (a new screen is built, causing the list to lose its position the first time an item is clicked), when swapping between wide and narrow layouts (the details screen will either not show up on a narrow layout or takes the place of the list on a wide layout), and with narrow layouts viewing the details screen (routing back to the list becomes a problem).
Here's an example of the route:
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
final RouteExtras? extras = state.extra as RouteExtras?;
final String? id = extras?.id;
final String? subpage = extras?.subpage;
return AppScaffold(
navigationShell: navigationShell,
subpage: subpage,
id: id,
);
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
initialLocation: '/',
navigatorKey: _navigatorKey,
routes: [
GoRoute(
name: '/',
path: '/',
pageBuilder: (context, state) => const NoTransitionPage(
child: DefaultScreen(),
),
routes: [
GoRoute(
name: "nav1",
path: "nav1",
pageBuilder: (context, state) => const NoTransitionPage(
child: Nav1Screen(),
),
routes: [
GoRoute(
path: ":id",
pageBuilder:
(BuildContext context, GoRouterState state) {
return NoTransitionPage(
child: ItemDetails(
name: state.pathParameters["id"],
),
);
},
),
],
),
],
),
...
I'd share an example AdaptiveScaffold, but it hardly seems useful to post broken code. Mostly I'm at the point where the body of the AdaptiveScaffold shows what I want (usually) and the secondary body might show what I want, but swapping between breakpoints causes havoc.
I solved it. Check the source here: https://github.com/hanskokx/flutter_adaptive_scaffold_example
First, I created a scaffolding:
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter_adaptive_scaffold_example/nav_destinations.dart';
import 'package:go_router/go_router.dart';
class AppScaffold extends StatelessWidget {
final Widget body;
final Widget? secondaryBody;
const AppScaffold({
Key? key,
required this.body,
this.secondaryBody,
}) : super(key: key ?? const ValueKey('ScaffoldWithNestedNavigation'));
@override
Widget build(BuildContext context) {
return AdaptiveLayout(
body: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.small: SlotLayout.from(
key: const Key('Body Small'),
builder: (_) => secondaryBody != null ? secondaryBody! : body,
),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('Body Medium'),
builder: (_) => body,
)
},
),
secondaryBody: SlotLayout(
config: <Breakpoint, SlotLayoutConfig>{
Breakpoints.small: SlotLayout.from(
key: const Key('Body Small'),
builder: null,
),
Breakpoints.mediumAndUp: SlotLayout.from(
key: const Key('Body Medium'),
builder: secondaryBody != null
? (_) => secondaryBody!
: AdaptiveScaffold.emptyBuilder,
)
},
),
);
}
}
class AppScaffoldShell extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const AppScaffoldShell({
Key? key,
required this.navigationShell,
}) : super(key: key ?? const ValueKey('ScaffoldWithNestedNavigation'));
@override
Widget build(BuildContext context) {
return AdaptiveScaffold(
useDrawer: false,
selectedIndex: navigationShell.currentIndex,
onSelectedIndexChange: onNavigationEvent,
destinations: NavDestination.values
.map(
(e) => NavigationDestination(
icon: Icon(e.icon),
label: e.label,
),
)
.toList(),
body: (_) => navigationShell,
);
}
void onNavigationEvent(int index) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
}
}
Then, I set up my router to use that as a shell route:
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold_example/app_scaffold.dart';
import 'package:flutter_adaptive_scaffold_example/details_screen.dart';
import 'package:flutter_adaptive_scaffold_example/nav_list_screen.dart';
import 'package:go_router/go_router.dart';
final _nav1NavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'nav1');
final _nav2NavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'nav2');
final _nav3NavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'nav3');
final _rootNavigatorKey = GlobalKey<NavigatorState>();
class AppRouter {
// Navigator Tab Screens
static const nav1 = "/nav1";
static const nav2 = "/nav2";
static const nav3 = "/nav3";
// List screens
static const nav1Details = "nav1Details";
static const nav2Details = "nav2Details";
static const nav3Details = "nav3Details";
static final router = GoRouter(
errorBuilder: (context, state) => Container(color: Colors.red),
navigatorKey: _rootNavigatorKey,
initialLocation: "/",
debugLogDiagnostics: true,
routes: <RouteBase>[
GoRoute(
path: "/",
redirect: (_, __) => AppRouter.nav1,
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return AppScaffoldShell(
navigationShell: navigationShell,
);
},
branches: <StatefulShellBranch>[
// Nav1 branch
StatefulShellBranch(
initialLocation: AppRouter.nav1,
navigatorKey: _nav1NavigatorKey,
routes: [
GoRoute(
name: AppRouter.nav1,
path: AppRouter.nav1,
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 1,
),
),
),
routes: [
GoRoute(
name: AppRouter.nav1Details,
path: ":id",
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 1,
selectedIndex: state.pathParameters["id"]!,
),
secondaryBody: DetailsScreen(
id: state.pathParameters["id"],
),
),
),
),
],
),
],
),
// Nav2 branch
StatefulShellBranch(
initialLocation: AppRouter.nav2,
navigatorKey: _nav2NavigatorKey,
routes: [
GoRoute(
name: AppRouter.nav2,
path: AppRouter.nav2,
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 2,
),
),
),
routes: [
GoRoute(
name: AppRouter.nav2Details,
path: ":id",
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 2,
selectedIndex: state.pathParameters["id"]!,
),
secondaryBody: DetailsScreen(
id: state.pathParameters["id"],
),
),
),
),
],
),
],
),
// Nav3 branch
StatefulShellBranch(
initialLocation: AppRouter.nav3,
navigatorKey: _nav3NavigatorKey,
routes: [
GoRoute(
name: AppRouter.nav3,
path: AppRouter.nav3,
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 3,
),
),
),
routes: [
GoRoute(
name: AppRouter.nav3Details,
path: ":id",
pageBuilder: (context, state) => NoTransitionPage(
child: AppScaffold(
body: NavListScreen(
key: state.pageKey,
listId: 3,
selectedIndex: state.pathParameters["id"]!,
),
secondaryBody: DetailsScreen(
id: state.pathParameters["id"],
),
),
),
),
],
),
],
),
],
),
],
);
}
main.dart
looks pretty typical:
import 'package:flutter/material.dart';
import 'package:flutter_adaptive_scaffold_example/router.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(const MainApp());
}
final GoRouter router = AppRouter.router;
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationProvider: router.routeInformationProvider,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,
);
}
}
Each of the screens (list, details) have their own Scaffold
. The list screen looks like this:
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class NavListScreen extends StatelessWidget {
final int listId;
final String? selectedIndex;
const NavListScreen({required this.listId, this.selectedIndex, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.red[200],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Nav Item $listId',
style: Theme.of(context).textTheme.headlineLarge),
Expanded(
child: ListView.separated(
itemCount: 10,
itemBuilder: (context, i) => Card(
child: ListTile(
title: Text('List Item ${i + 1}'),
selected: (i + 1).toString() == selectedIndex,
onTap: () =>
context.goNamed('nav${listId}Details', pathParameters: {
"id": "${i + 1}",
}),
),
),
separatorBuilder: (context, i) => const SizedBox(height: 8),
),
),
],
),
),
);
}
}
The details screen looks like however you want it to look.
It's worth noting that the Flutter team has (or is currently in the process of, depending on when you read this) deprecated the flutter_adaptive_scaffold
package. (See: https://github.com/flutter/flutter/issues/162965). To that end, I published the custom_adaptive_scaffold
package (https://pub.dev/packages/custom_adaptive_scaffold) which makes some small but meaningful changes and/or improvements.