Skip to content

Instantly share code, notes, and snippets.

@definev
Last active December 29, 2025 10:58
Show Gist options
  • Select an option

  • Save definev/c447b5c720257cd14ac739976db0a875 to your computer and use it in GitHub Desktop.

Select an option

Save definev/c447b5c720257cd14ac739976db0a875 to your computer and use it in GitHub Desktop.
Minimal example for tab layout
/// A minimal example demonstrating how to create a Tab-based navigation using ZenRouter.
///
/// Key concepts:
/// - [TabLayoutCoordinator]: Manages the routing state, specifically the [IndexedStackPath] for tabs.
/// - [TabLayout]: A [RouteLayout] that provides the UI structure (e.g., [BottomNavigationBar]) for its children.
/// - [IndexedStackPath]: Maintains the state of each tab, allowing for preservation of state when switching tabs.
///
/// To run this example:
/// 1. Fix imports if necessary.
/// 2. Run the `main()` function defined at the bottom of this file.
import 'package:flutter/material.dart';
import 'package:zenrouter/zenrouter.dart';
abstract class AppRoute extends RouteTarget with RouteUnique {}
class TabLayout extends AppRoute with RouteLayout<AppRoute> {
/// Each Layout has own a path
@override
StackPath<RouteUnique> resolvePath(TabLayoutCoordinator coordinator) =>
coordinator.tabIndexed;
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) {
return Scaffold(
body: buildPath(coordinator),
bottomNavigationBar: ListenableBuilder(
listenable: coordinator.tabIndexed,
builder: (context, child) => BottomNavigationBar(
currentIndex: coordinator.tabIndexed.activeIndex,
onTap: (value) {
// Index 2 is the 'Settings' tab which actually pushes a new route
// on top of the entire layout.
if (value == 2) {
coordinator.push(SettingsRoute());
} else {
coordinator.tabIndexed.goToIndexed(value);
}
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'Business',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
),
);
}
}
class Tab1Route extends AppRoute {
/// You can specify a layout this route belong via `layout` property
@override
Type? get layout => TabLayout;
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) =>
const Scaffold(body: Center(child: Text('Tab 1')));
@override
Uri toUri() => Uri.parse('/tab1');
}
class Tab2Route extends AppRoute {
@override
Type? get layout => TabLayout;
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) =>
const Scaffold(body: Center(child: Text('Tab 2')));
@override
Uri toUri() => Uri.parse('/tab2');
}
class IndexRoute extends AppRoute with RouteRedirect<AppRoute> {
// Use for redirect only
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) =>
const SizedBox();
@override
Uri toUri() => Uri.parse('/');
@override
AppRoute redirect() => Tab1Route();
}
class NotFoundRoute extends AppRoute {
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) =>
const Scaffold(body: Center(child: Text('Not found')));
@override
Uri toUri() => Uri.parse('/not-found');
}
/// This setting is not related to tabs and since `layout` is null so it belong to `root`.
/// It will cover the [TabLayout] when pushed.
class SettingsRoute extends AppRoute {
@override
Widget build(TabLayoutCoordinator coordinator, BuildContext context) =>
Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: const Center(child: Text('Settings Page')),
);
@override
Uri toUri() => Uri.parse('/settings');
}
class TabLayoutCoordinator extends Coordinator<AppRoute> {
/// IndexedStackPath must be predefined the stack of routes
late final IndexedStackPath<AppRoute> tabIndexed =
IndexedStackPath.createWith(
[Tab1Route(), Tab2Route()],
coordinator: this,
label: 'tabIndexed',
);
/// Add `tabIndexed` into paths for `Coordinator` to listen to its changes.
@override
List<StackPath<RouteTarget>> get paths => [...super.paths, tabIndexed];
@override
void defineLayout() {
// Define a constructor for `Coordinator` resolving layout later
RouteLayout.defineLayout(TabLayout, TabLayout.new);
}
@override
AppRoute parseRouteFromUri(Uri uri) {
return switch (uri.pathSegments) {
[] => IndexRoute(),
['tab1'] => Tab1Route(),
['tab2'] => Tab2Route(),
['settings'] => SettingsRoute(),
_ => NotFoundRoute(),
};
}
}
void main() {
final coordinator = TabLayoutCoordinator();
runApp(
MaterialApp.router(
routerDelegate: coordinator.routerDelegate,
routeInformationParser: coordinator.routeInformationParser,
),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment