Tutorial

Layout Without the Router

Use SlidingPaneViewport, SlidingPane and AdaptiveBreadcrumbs directly when you only want the sliding columns.

The shell uses public widgets you can use on their own, e.g. inside one screen of an app that already has another router. You own the stack (a List) and rebuild when it changes.

The complete file

dart lib/main.dart
import 'package:material_ui/material_ui.dart';
import 'package:signals_flutter/signals_flutter.dart';
import 'package:xue_hua_adaptive_sliding_layout/xue_hua_adaptive_sliding_layout.dart';

void main() => runApp(const MaterialApp(home: FolderBrowser()));

/// A drill-down browser built only from the layout widgets: no router, no URLs.
class FolderBrowser extends StatefulWidget {
  const FolderBrowser({super.key});

  @override
  State<FolderBrowser> createState() => _FolderBrowserState();
}

class _FolderBrowserState extends State<FolderBrowser> {
  // The stack is just a list you own. Each entry becomes one pane.
  final List<String> _path = ['Home'];
  double _leftFraction = 0.4;

  void _open(String name) => setState(() => _path.add(name));

  void _popTo(int depth) => setState(() => _path.removeRange(depth + 1, _path.length));

  @override
  Widget build(BuildContext context) {
    const breakpoints = LayoutBreakpoints();
    final width = MediaQuery.sizeOf(context).width;
    final panes = [
      for (var i = 0; i < _path.length; i++)
        SlidingPane(
          // Keys must stay stable while the depth changes so pane state survives.
          key: ValueKey('pane-$i-${_path[i]}'),
          title: signal(_path[i]),
          child: FolderPage(name: _path[i], onOpen: _open),
        ),
    ];
    return Scaffold(
      body: Column(
        children: [
          AdaptiveBreadcrumbs(panes: panes, onSelect: (pane) => _popTo(panes.indexOf(pane))),
          Expanded(
            child: SlidingPaneViewport(
              panes: panes,
              visibleCount: breakpoints.visibleColumnCount(width), // 1 or 2
              resizeHandleWidth: 4,
              leftPaneFraction: _leftFraction,
              onLeftPaneFractionChanged: (value) => setState(() => _leftFraction = value),
              onPop: _path.length > 1 ? () => _popTo(_path.length - 2) : null,
              placeholder: const Center(child: Text('Open a folder')),
            ),
          ),
        ],
      ),
    );
  }
}

class FolderPage extends StatelessWidget {
  const FolderPage({super.key, required this.name, required this.onOpen});

  final String name;
  final ValueChanged<String> onOpen;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(name)),
      body: ListView(
        children: [
          for (final child in ['Docs', 'Photos', 'Music'])
            ListTile(
              leading: const Icon(Icons.folder),
              title: Text(child),
              onTap: () => onOpen('$name/$child'),
            ),
        ],
      ),
    );
  }
}

What you will see

  • Wide window: “Home” on the left, “Open a folder” on the right. Open folders and the columns slide like in the router version.
  • Tap a breadcrumb to cut the stack back; drag the handle to resize.
  • Narrow window: visibleColumnCount returns 1 and only the top folder is shown; the AppBar back arrow calls onPop.

Building blocks

WidgetKey parameters
SlidingPaneViewport Required: panes ( List<SlidingPane> ), visibleCount , resizeHandleWidth . Optional: onPop , placeholder , leftPaneFraction , minLeftPaneFraction , minRightPaneFraction , onLeftPaneFractionChanged , slideDuration , slideCurve , paneBuilder , resizeHandleMargin , resizeHandleBuilder .
SlidingPane key ( LocalKey , must stay stable as depth changes so pane state survives), title ( Signal<String> ), child .
AdaptiveBreadcrumbs Required: panes , onSelect (called with the tapped pane). Optional: height , padding , backgroundColor , itemBuilder , separatorBuilder .

Breakpoints

Breakpoints are based on window width, not device type:

WidthBandVisible columns
< compactMaxWidth (600) compact 1 — Navigator (full-screen stack)
600–839 medium 1 — same Navigator ; host uses shell.isMedium for chrome
≥ expandedMinWidth (840) expanded 2 — last two panes + optional sash

That completes the tutorial. Next: the Route table reference.