Tutorial
Customizing the UI
Restyle the columns, resize handle and breadcrumbs, and add overlay routes such as a photo viewer.
Use builders to replace structure, value parameters to tweak numbers. Theme colors still come from
Theme.of(context). 1-column page transitions use Flutter's ThemeData.pageTransitionsTheme.
Viewport (SlidingPaneViewport / AdaptiveShellRoute)
| Parameter | Default | Role |
|---|---|---|
slideDuration | 280ms | Column slide |
slideCurve |
Curves.easeOutCubic |
Column slide |
paneBuilder |
2-col card / 1-col flat | Wrap each column. index == panes.length is the empty right slot |
resizeHandleBuilder |
rounded animated bar |
Widget Function(BuildContext context, bool isHovered)
; highlights with
colorScheme.primary
on hover / drag
|
resizeHandleWidth |
4 (shell route) | Hit-test width of the handle; required on SlidingPaneViewport |
resizeHandleMargin |
— | Insets the handle independently of its hit area |
placeholder |
outline icon |
Shell-level empty right pane;
AdaptiveBranch.placeholder
wins if set
|
Default pane split: left fraction 0.4, minimum left / right fraction 0.35.
Apply it to the tutorial app
Start from the previous step's lib/main.dart and give its AdaptiveShellRoute
these arguments (keep branches as they are). Every value here is a real parameter; the code is checked with
flutter analyze.
AdaptiveShellRoute(
breakpoints: const LayoutBreakpoints(expandedMinWidth: 840),
showBreadcrumbs: true,
resizable: true,
initialLeftPaneFraction: 0.4,
placeholder: (context) => const Center(child: Text('Pick a product')),
slideDuration: const Duration(milliseconds: 280),
resizeHandleBuilder: (context, isHovered) {
final colorScheme = Theme.of(context).colorScheme;
return AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeInOut,
height: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: isHovered ? colorScheme.primary : colorScheme.surfaceDim,
),
);
},
breadcrumbsBuilder: (context, panes, onSelect) {
return AdaptiveBreadcrumbs(
panes: panes,
onSelect: onSelect,
height: 32,
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
);
},
paneBuilder: (context, index, child) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: KeyedSubtree(key: const Key('pane-flat'), child: child),
);
},
builder: (context, shell, child) => AppShell(shell: shell, child: child),
branches: [/* unchanged */],
)
Breadcrumbs
| Parameter | Default | Role |
|---|---|---|
height | 32 | Strip height |
padding | horizontal 12 | Strip padding |
backgroundColor |
surfaceContainerLow |
Strip color |
itemBuilder | InkWell + Text | One crumb |
separatorBuilder | chevron | Between crumbs |
AdaptiveShellRoute.breadcrumbsBuilder |
AdaptiveBreadcrumbs |
Replace the whole strip (showBreadcrumbs still gates it) |
escapePops |
true | Escape calls maybePop |
Bottom bar behavior
On medium / expanded, nested pages keep the host chrome. On compact, pages below the branch root hide the bottom bar by default (hidesBottomBarWhenPushed; set
false to keep it). A bottom sheet on the tab root covers the bar when the shell uses
AdaptiveShellChrome (useRootNavigator: false).
Scaffold.bottomNavigationBar sits outside the branch navigator, so the same sheet leaves the bar visible.
Overlay routes (AdaptiveRoute)
| Parameter | Default | Role |
|---|---|---|
hidesBottomBarWhenPushed |
true | Compact only: the pushed page covers the host bottom bar. Two panes on desktop are untouched |
fullscreen |
false | Root Navigator at every width, normal transition |
fullscreenDialog |
false | Root Navigator at every width as a Material fullscreen dialog |
opaque |
true | With transitionsBuilder: transparent photo viewer |
barrierColor |
null | With transitionsBuilder |
barrierDismissible |
false | Tap the barrier to pop |
// Add as a child of the ':id' route, next to 'reviews'.
AdaptiveRoute(
path: 'photo',
title: (_) => 'Photo',
fullscreen: true,
opaque: false,
barrierColor: Colors.black,
barrierDismissible: true,
transitionDuration: const Duration(milliseconds: 200),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
builder: (context, state) => const Center(child: Icon(Icons.image, size: 200)),
)
Open it from the detail page with router.pushNamed('/products/1/photo'). It covers the whole window at every width with a fade; tap the black barrier to close it.
Next: Reading state.