Last active
December 29, 2025 10:58
-
-
Save definev/c447b5c720257cd14ac739976db0a875 to your computer and use it in GitHub Desktop.
Minimal example for tab layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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