Dynamic navigation and layout orchestration for Flutter.
flow_nav automatically adapts your app's layout and navigation behavior based on screen size β no manual breakpoint handling, no platform checks scattered across your codebase. Write once, run beautifully on phone, tablet, and desktop.
| Phone | Tablet | Desktop |
|---|---|---|
![]() |
![]() |
![]() |
- π± Adaptive AppBar β standard
AppBaron phone, custom toolbar/menubar on tablet and desktop - π Adaptive Navigation β full-screen push on phone, detail panel swap on large screens
- ποΈ Adaptive Layout β single column (phone), split view (tablet), three-column (desktop)
- π Router Agnostic β works with the default
Navigator, GoRouter, GetX, AutoRoute, or any custom router - πͺΆ Pure Logic, No UI Opinions β zero forced styling; bring your own widgets
Add flow_nav to your pubspec.yaml:
dependencies:
flow_nav: ^1.0.0Then run:
flutter pub getCall FlowNavConfig.init() before runApp. This is where you configure your breakpoints and (optionally) your router hooks.
void main() {
FlowNavConfig.init(
tabletMinWidth: 600,
desktopMinWidth: 1024,
);
runApp(MyApp());
}flow_nav is router-agnostic. Pass your own onPush / onPop callbacks and it will use them automatically on phone (detail panel swap is used on larger screens instead).
Works out of the box with no extra configuration.
FlowNavConfig.init(
onPush: ({required context, required builder, fullscreenDialog = false}) {
context.push('/detail');
return Future.value(null);
},
onPop: (context) => context.pop(),
);FlowNavConfig.init(
onPush: ({required context, required builder, fullscreenDialog = false}) {
return Get.to(() => builder(context));
},
onPop: (_) => Get.back(),
);FlowNavConfig.init(
onPush: ({required context, required builder, fullscreenDialog = false}) {
return context.router.push(MyRoute());
},
onPop: (context) => context.router.pop(),
);Replace your standard Scaffold with FlowScaffold. It handles the layout columns automatically.
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget? _detail;
@override
Widget build(BuildContext context) {
return FlowScaffold(
appBar: FlowAppBar(
title: Text('My App'),
toolbarWidget: MyDesktopToolbar(), // shown only on tablet/desktop
),
body: MyListView(
onItemTap: (item) {
FlowNavController.open(
context: context,
builder: (_) => DetailPage(item: item),
// Called on tablet/desktop β swap the detail panel instead of pushing
onDetailOpen: (widget) => setState(() => _detail = widget),
);
},
),
detailPanel: _detail, // shown beside body on tablet/desktop
sidebar: MySidebar(), // shown only on desktop (left column)
);
}
}Configures global settings. Call once before runApp.
| Parameter | Type | Default | Description |
|---|---|---|---|
tabletMinWidth |
double |
600 |
Minimum width to enter tablet layout |
desktopMinWidth |
double |
1024 |
Minimum width to enter desktop layout |
bodyMaxWidth |
double? |
null |
Max width of content area |
bodyPadding |
EdgeInsets |
EdgeInsets.zero |
Inner padding for body |
bodyMargin |
EdgeInsets |
EdgeInsets.zero |
Outer margin for body |
onPush |
Function? |
null |
Custom navigation push (for GoRouter, GetX, etc.) |
onPop |
Function? |
null |
Custom navigation pop |
The main adaptive scaffold widget.
| Property | Type | Description |
|---|---|---|
appBar |
PreferredSizeWidget? |
Adaptive AppBar β shown on phone, converted to toolbar on larger screens |
body |
Widget |
Main content (list, grid, etc.) |
detailPanel |
Widget? |
Detail view shown beside body on tablet/desktop |
sidebar |
Widget? |
Left sidebar shown only on desktop |
sidebarWidth |
double |
Sidebar width (default: 240) |
bodyPanelWidth |
double |
List panel width on tablet/desktop (default: 320) |
maxWidth |
double? |
Max width of entire content area |
bodyPadding |
EdgeInsets? |
Inner padding for body panel |
bodyMargin |
EdgeInsets? |
Outer margin for body panel |
backgroundColor |
Color? |
Scaffold background color |
emptyDetail |
Widget? |
Placeholder shown when no detail is selected |
bottomNavigationBar |
Widget? |
Bottom nav (phone/tablet only) |
floatingActionButton |
Widget? |
FAB |
drawer |
Widget? |
Drawer (phone/tablet) |
forceScreenType |
FlowScreenType? |
Override screen type (useful for testing/previews) |
bodyAlign |
FlowBodyAlign |
Align body when maxWidth is set (start or center) |
An adaptive AppBar that renders as a standard AppBar on phone and as a custom toolbar widget on larger screens.
FlowAppBar(
title: Text('My App'),
toolbarWidget: Row(
children: [
Text('My App', style: TextStyle(fontSize: 18)),
Spacer(),
IconButton(icon: Icon(Icons.settings), onPressed: () {}),
],
),
)The primary way to navigate to a detail view. Automatically pushes a new route on phone, or calls onDetailOpen on tablet/desktop.
FlowNavController.open(
context: context,
builder: (_) => DetailPage(item: item),
onDetailOpen: (widget) => setState(() => _detail = widget),
fullscreenDialog: false, // optional
);Returns the current FlowScreenType for the given context. Useful for conditional rendering outside of FlowScaffold.
final screenType = FlowNavBreakpoint.of(context);
if (screenType == FlowScreenType.phone) {
// phone-specific logic
}FlowScreenType values: phone, tablet, desktop
| Feature | Phone | Tablet | Desktop |
|---|---|---|---|
| AppBar | Standard AppBar |
Toolbar at top of split view | Toolbar at top of content area |
| Layout | Single column | Body + Detail panel | Sidebar + Body + Detail panel |
| Navigation | Full-screen push | Detail panel swap | Detail panel swap |
| Bottom nav | β Shown | β Hidden | β Hidden |
| Drawer | β Shown | β Shown | β Hidden |
- Dart SDK:
^3.6.0 - Flutter:
>=3.0.0
Contributions are welcome! Please open an issue or pull request on GitHub.


