Created
December 23, 2025 02:10
-
-
Save definev/f183b78308fbb4554d339bf40fc78399 to your computer and use it in GitHub Desktop.
How to build 2-panel layout and able to correct URL reflection
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
| import 'package:flutter/material.dart'; | |
| import 'package:zenrouter/zenrouter.dart'; | |
| import 'package:zenrouter_devtools/zenrouter_devtools.dart'; | |
| void main() { | |
| runApp(MainApp()); | |
| } | |
| class MainApp extends StatelessWidget { | |
| const MainApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp.router( | |
| routerDelegate: appCoordinator.routerDelegate, | |
| routeInformationParser: appCoordinator.routeInformationParser, | |
| ); | |
| } | |
| } | |
| final appCoordinator = AppCoordinator(); | |
| abstract class AppRoute extends RouteTarget with RouteUnique {} | |
| class CustomLayout extends AppRoute with RouteLayout<AppRoute> { | |
| @override | |
| StackPath<RouteUnique> resolvePath(AppCoordinator coordinator) => | |
| coordinator.customIndexed; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| final path = resolvePath(coordinator); | |
| final size = MediaQuery.sizeOf(context); | |
| return ListenableBuilder( | |
| listenable: path, | |
| builder: (context, child) => Scaffold( | |
| body: switch (size.width) { | |
| < 600 => Column( | |
| children: [ | |
| Expanded(child: buildPath(coordinator)), | |
| Container( | |
| height: 60, | |
| color: Colors.yellow, | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceAround, | |
| children: [ | |
| ElevatedButton( | |
| child: Text('One'), | |
| onPressed: () => coordinator.push(FirstLayout()), | |
| ), | |
| ElevatedButton( | |
| child: Text('Two'), | |
| onPressed: () => coordinator.push(SecondTab()), | |
| ), | |
| ElevatedButton( | |
| child: Text('Three'), | |
| onPressed: () => coordinator.push(ThirdTab()), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| _ => Column( | |
| children: [ | |
| Expanded( | |
| child: switch (path.activeRoute) { | |
| ThirdTab() => path.activeRoute!.build(coordinator, context), | |
| _ => Row( | |
| children: [ | |
| Expanded( | |
| child: path.stack[0].build(coordinator, context), | |
| ), | |
| VerticalDivider(width: 1, color: Colors.amber), | |
| Expanded( | |
| child: path.stack[1].build(coordinator, context), | |
| ), | |
| ], | |
| ), | |
| }, | |
| ), | |
| Container( | |
| height: 60, | |
| color: Colors.yellow, | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.spaceAround, | |
| children: [ | |
| ElevatedButton( | |
| child: Text('One/Two'), | |
| onPressed: () { | |
| if (path.activeRoute is FirstLayout) { | |
| coordinator.push(SecondTab()); | |
| } else { | |
| coordinator.push(FirstLayout()); | |
| } | |
| }, | |
| ), | |
| ElevatedButton( | |
| child: Text('Three'), | |
| onPressed: () => coordinator.push(ThirdTab()), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| }, | |
| ), | |
| ); | |
| } | |
| } | |
| class FirstLayout extends AppRoute with RouteLayout { | |
| @override | |
| Uri toUri() => Uri.parse('/first'); | |
| @override | |
| Type get layout => CustomLayout; | |
| @override | |
| NavigationPath<AppRoute> resolvePath(AppCoordinator coordinator) => | |
| coordinator.firstStack; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| return MouseRegion( | |
| onEnter: (_) { | |
| if (coordinator.customIndexed.activeIndex != 0) { | |
| coordinator.customIndexed.goToIndexed(0); | |
| } | |
| }, | |
| child: Listener( | |
| onPointerSignal: (_) { | |
| if (coordinator.customIndexed.activeIndex != 0) { | |
| coordinator.customIndexed.goToIndexed(0); | |
| } | |
| }, | |
| child: super.build(coordinator, context), | |
| ), | |
| ); | |
| } | |
| } | |
| class FirstTab extends AppRoute { | |
| @override | |
| Uri toUri() => Uri.parse('/first'); | |
| @override | |
| Type get layout => FirstLayout; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| return ListenableBuilder( | |
| listenable: coordinator.customIndexed, | |
| builder: (context, child) { | |
| final activeIndex = coordinator.customIndexed.activeIndex; | |
| return Scaffold( | |
| body: Center( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| spacing: 8, | |
| children: [ | |
| Text( | |
| 'First page ${activeIndex == 0 ? '(Focused)' : '(No focused)'}', | |
| ), | |
| FilledButton( | |
| onPressed: () => | |
| coordinator.push(FirstTabChild(message: "Hello")), | |
| child: Text('Go "Hello"'), | |
| ), | |
| FilledButton( | |
| onPressed: () => | |
| coordinator.push(FirstTabChild(message: "Ciao")), | |
| child: Text('Go "Ciao"'), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| class FirstTabChild extends AppRoute { | |
| FirstTabChild({required this.message}); | |
| final String message; | |
| @override | |
| Uri toUri() => Uri.parse('/first/$message'); | |
| @override | |
| Type get layout => FirstLayout; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| return Scaffold( | |
| body: Center( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| Text('First Message: $message'), | |
| TextButton( | |
| onPressed: () => Navigator.pop(context), | |
| child: Text('Go back'), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| @override | |
| List<Object?> get props => [message]; | |
| } | |
| class SecondTab extends AppRoute { | |
| @override | |
| Uri toUri() => Uri.parse('/second'); | |
| @override | |
| Type get layout => CustomLayout; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| return ListenableBuilder( | |
| listenable: coordinator.customIndexed, | |
| builder: (context, child) { | |
| final activeIndex = coordinator.customIndexed.activeIndex; | |
| return MouseRegion( | |
| onEnter: (_) { | |
| if (coordinator.customIndexed.activeIndex != 1) { | |
| coordinator.customIndexed.goToIndexed(1); | |
| } | |
| }, | |
| child: Listener( | |
| onPointerSignal: (_) { | |
| if (coordinator.customIndexed.activeIndex != 1) { | |
| coordinator.customIndexed.goToIndexed(1); | |
| } | |
| }, | |
| child: Scaffold( | |
| backgroundColor: Colors.red.shade100, | |
| body: Center( | |
| child: Text( | |
| 'Second tab (${activeIndex == 1 ? 'Focused' : 'No focused'})', | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| class ThirdTab extends AppRoute { | |
| @override | |
| Uri toUri() => Uri.parse('/third'); | |
| @override | |
| Type get layout => CustomLayout; | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| final activeIndex = coordinator.customIndexed.activeIndex; | |
| return Scaffold( | |
| backgroundColor: Colors.blue.shade100, | |
| body: Center( | |
| child: Text( | |
| 'Third tab (${activeIndex == 2 ? 'Focused' : 'No focused'})', | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class NotFound extends AppRoute { | |
| @override | |
| Uri toUri() => Uri.parse('/not-found'); | |
| @override | |
| Widget build(AppCoordinator coordinator, BuildContext context) { | |
| return Scaffold(body: Center(child: Text('Not found'))); | |
| } | |
| } | |
| class AppCoordinator extends Coordinator<AppRoute> with CoordinatorDebug { | |
| late final customIndexed = IndexedStackPath<AppRoute>.createWith( | |
| coordinator: this, | |
| label: 'CustomIndexed', | |
| [FirstLayout(), SecondTab(), ThirdTab()], | |
| ); | |
| late final firstStack = NavigationPath<AppRoute>.createWith( | |
| label: 'FirstStack', | |
| coordinator: this, | |
| ); | |
| @override | |
| List<StackPath<RouteTarget>> get paths => [root, customIndexed, firstStack]; | |
| @override | |
| void defineLayout() { | |
| RouteLayout.defineLayout(CustomLayout, CustomLayout.new); | |
| RouteLayout.defineLayout(FirstLayout, FirstLayout.new); | |
| } | |
| @override | |
| AppRoute parseRouteFromUri(Uri uri) { | |
| return switch (uri.pathSegments) { | |
| [] => FirstTab(), | |
| ['first'] => FirstTab(), | |
| ['first', final message] => FirstTabChild(message: message), | |
| ['second'] => SecondTab(), | |
| ['third'] => ThirdTab(), | |
| _ => NotFound(), | |
| }; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment