Instantly share code, notes, and snippets.
Last active
November 27, 2025 06:51
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
Save definev/2ef75cb73b8021723969c3d032fb1875 to your computer and use it in GitHub Desktop.
Complete cloned with same feature set as NavigationStack like SwiftUI in Flutter, using Navigator Page API
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:flutter/cupertino.dart'; | |
| import 'package:flutter/foundation.dart'; // For kIsWeb | |
| // ============================================================================== | |
| // 1. THE MICRO-FRAMEWORK (Copy this to your core library) | |
| // ============================================================================== | |
| /// A state object representing the active path of the navigation. | |
| /// Acts as the "Source of Truth" for the navigation stack. | |
| class NavigationPath<T> extends ChangeNotifier { | |
| final List<T> _stack = []; | |
| /// Create with an optional initial route | |
| NavigationPath([T? initial]) { | |
| if (initial != null) _stack.add(initial); | |
| } | |
| /// Read-only view of the stack | |
| List<T> get stack => List.unmodifiable(_stack); | |
| // --- MUTATION METHODS (Like manipulating a Swift Array) --- | |
| void push(T route) { | |
| _stack.add(route); | |
| notifyListeners(); | |
| } | |
| /// Push multiple routes at once (e.g., restoring a history path) | |
| void pushAll(List<T> routes) { | |
| _stack.addAll(routes); | |
| notifyListeners(); | |
| } | |
| /// Smart Push: If the route exists in the stack, move it to the top. | |
| /// If it doesn't exist, push it. | |
| /// Useful for deep linking where you don't want duplicate screens. | |
| void pushOrBringToFront(T route) { | |
| if (_stack.contains(route)) { | |
| _stack.remove(route); | |
| } | |
| _stack.add(route); | |
| notifyListeners(); | |
| } | |
| /// Replace a specific route in the stack with a new one. | |
| /// Used primarily for Redirects (e.g. Protected -> Login). | |
| void replace(T oldRoute, T newRoute) { | |
| final index = _stack.indexOf(oldRoute); | |
| if (index != -1) { | |
| _stack[index] = newRoute; | |
| notifyListeners(); | |
| } | |
| } | |
| void pop() { | |
| if (_stack.isNotEmpty) { | |
| _stack.removeLast(); | |
| notifyListeners(); | |
| } | |
| } | |
| void popToRoot() { | |
| _stack.clear(); | |
| if (_stack.isNotEmpty) { | |
| final root = _stack.first; | |
| _stack.clear(); | |
| _stack.add(root); | |
| notifyListeners(); | |
| } | |
| } | |
| /// Deep Link / Reset capability | |
| void setStack(List<T> newStack) { | |
| _stack.clear(); | |
| _stack.addAll(newStack); | |
| notifyListeners(); | |
| } | |
| } | |
| /// Defines how the route is presented visually. | |
| abstract class PresentationStyle { | |
| const PresentationStyle(); | |
| /// Creates the specific Page route for Navigator | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title); | |
| // --- STANDARD STYLES --- | |
| static const PresentationStyle push = _PushStyle(); | |
| static const PresentationStyle modal = _ModalStyle(); | |
| static const PresentationStyle cupertino = _CupertinoStyle(); | |
| static const PresentationStyle instant = _InstantStyle(); | |
| /// Create a custom transition on the fly | |
| static PresentationStyle custom({ | |
| required RouteTransitionsBuilder transitionsBuilder, | |
| Duration duration = const Duration(milliseconds: 300), | |
| bool opaque = true, | |
| }) { | |
| return _CustomTransitionStyle(transitionsBuilder, duration, opaque); | |
| } | |
| } | |
| // --- Implementation Details --- | |
| class _PushStyle extends PresentationStyle { | |
| const _PushStyle(); | |
| @override | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title) { | |
| return MaterialPage( | |
| child: child, | |
| key: ValueKey(routeData), | |
| name: title, | |
| arguments: routeData, | |
| ); | |
| } | |
| } | |
| class _ModalStyle extends PresentationStyle { | |
| const _ModalStyle(); | |
| @override | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title) { | |
| return MaterialPage( | |
| child: child, | |
| key: ValueKey(routeData), | |
| name: title, | |
| arguments: routeData, | |
| fullscreenDialog: true, | |
| ); | |
| } | |
| } | |
| class _CupertinoStyle extends PresentationStyle { | |
| const _CupertinoStyle(); | |
| @override | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title) { | |
| return CupertinoPage( | |
| child: child, | |
| key: ValueKey(routeData), | |
| name: title, | |
| arguments: routeData, | |
| ); | |
| } | |
| } | |
| class _InstantStyle extends PresentationStyle { | |
| const _InstantStyle(); | |
| @override | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title) { | |
| return _CustomPage( | |
| child: child, | |
| key: ValueKey(routeData), | |
| name: title, | |
| arguments: routeData, | |
| opaque: true, | |
| transitionDuration: Duration.zero, | |
| transitionsBuilder: (_, __, ___, child) => child, | |
| ); | |
| } | |
| } | |
| class _CustomTransitionStyle extends PresentationStyle { | |
| final RouteTransitionsBuilder transitionsBuilder; | |
| final Duration duration; | |
| final bool opaque; | |
| const _CustomTransitionStyle(this.transitionsBuilder, this.duration, this.opaque); | |
| @override | |
| Page<dynamic> createPage(Widget child, dynamic routeData, String? title) { | |
| return _CustomPage( | |
| child: child, | |
| key: ValueKey(routeData), | |
| name: title, | |
| arguments: routeData, | |
| transitionsBuilder: transitionsBuilder, | |
| transitionDuration: duration, | |
| opaque: opaque, | |
| ); | |
| } | |
| } | |
| class _CustomPage extends Page<dynamic> { | |
| final Widget child; | |
| final RouteTransitionsBuilder transitionsBuilder; | |
| final Duration transitionDuration; | |
| final bool opaque; | |
| const _CustomPage({ | |
| required this.child, | |
| required this.transitionsBuilder, | |
| required this.transitionDuration, | |
| required this.opaque, | |
| super.key, | |
| super.name, | |
| super.arguments, | |
| }); | |
| @override | |
| Route<dynamic> createRoute(BuildContext context) { | |
| return PageRouteBuilder( | |
| settings: this, | |
| pageBuilder: (context, animation, secondaryAnimation) => child, | |
| transitionsBuilder: transitionsBuilder, | |
| transitionDuration: transitionDuration, | |
| opaque: opaque, | |
| reverseTransitionDuration: transitionDuration, | |
| ); | |
| } | |
| } | |
| /// A declarative definition of where a route leads. | |
| class Destination { | |
| final WidgetBuilder builder; | |
| final PresentationStyle style; | |
| final String? title; | |
| /// Optional callback to control popping behavior. | |
| final bool Function()? onPop; | |
| /// Optional Async Redirect. | |
| /// Returns [null] to stay on this route. | |
| /// Returns a new [RouteObject] to replace the current route with. | |
| final Future<dynamic> Function(BuildContext context)? redirect; | |
| /// Widget to show while [redirect] is running. | |
| /// Defaults to a Center(CircularProgressIndicator). | |
| final WidgetBuilder? placeholder; | |
| const Destination({ | |
| required this.builder, | |
| this.style = PresentationStyle.push, | |
| this.title, | |
| this.onPop, | |
| this.redirect, | |
| this.placeholder, | |
| }); | |
| } | |
| /// The Widget that binds the UI to the NavigationPath. | |
| class NavigationStack<T> extends StatelessWidget { | |
| final NavigationPath<T> path; | |
| final Destination Function(T route) destinationBuilder; | |
| const NavigationStack({ | |
| super.key, | |
| required this.path, | |
| required this.destinationBuilder, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return ListenableBuilder( | |
| listenable: path, | |
| builder: (context, _) { | |
| return Navigator( | |
| onDidRemovePage: (page) { | |
| path.pop(); | |
| }, | |
| pages: path.stack.map((routeData) { | |
| final dest = destinationBuilder(routeData); | |
| // 1. Build the base content | |
| Widget child; | |
| // 2. Wrap in Redirect Guard if needed | |
| if (dest.redirect != null) { | |
| child = _RedirectGuard<T>( | |
| path: path, | |
| route: routeData, | |
| validator: dest.redirect!, | |
| placeholder: dest.placeholder ?? (_) => const Scaffold( | |
| body: Center(child: CircularProgressIndicator()), | |
| ), | |
| childBuilder: dest.builder, | |
| ); | |
| } else { | |
| child = dest.builder(context); | |
| } | |
| // 3. Wrap in PopScope (Back Button Interception) | |
| child = PopScope( | |
| canPop: dest.onPop == null, | |
| onPopInvokedWithResult: (didPop, result) { | |
| if (didPop) return; | |
| if (dest.onPop != null) { | |
| final shouldPop = dest.onPop!(); | |
| if (shouldPop) { | |
| path.pop(); | |
| } | |
| } | |
| }, | |
| child: child, | |
| ); | |
| // 4. Create Page | |
| return dest.style.createPage(child, routeData, dest.title); | |
| }).toList(), | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| /// Internal widget to handle async redirect logic | |
| class _RedirectGuard<T> extends StatefulWidget { | |
| final NavigationPath<T> path; | |
| final T route; | |
| final Future<dynamic> Function(BuildContext context) validator; | |
| final WidgetBuilder placeholder; | |
| final WidgetBuilder childBuilder; | |
| const _RedirectGuard({ | |
| required this.path, | |
| required this.route, | |
| required this.validator, | |
| required this.placeholder, | |
| required this.childBuilder, | |
| }); | |
| @override | |
| State<_RedirectGuard<T>> createState() => _RedirectGuardState<T>(); | |
| } | |
| class _RedirectGuardState<T> extends State<_RedirectGuard<T>> { | |
| bool _isLoading = true; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _checkRedirect(); | |
| } | |
| Future<void> _checkRedirect() async { | |
| // Run the async check | |
| final result = await widget.validator(context); | |
| if (!mounted) return; | |
| if (result != null && result is T) { | |
| // REDIRECT: Replace the current route in the stack with the new result | |
| widget.path.replace(widget.route, result); | |
| } else { | |
| // SUCCESS: Show the content | |
| setState(() { | |
| _isLoading = false; | |
| }); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| if (_isLoading) { | |
| return widget.placeholder(context); | |
| } | |
| return widget.childBuilder(context); | |
| } | |
| } | |
| // ============================================================================== | |
| // 2. DOMAIN LAYER (Routes) | |
| // ============================================================================== | |
| // --- Root Level Routes --- | |
| sealed class RootRoute { | |
| const RootRoute(); | |
| } | |
| class LoginRoute extends RootRoute { | |
| const LoginRoute(); | |
| } | |
| class HomeShellRoute extends RootRoute { | |
| const HomeShellRoute(); | |
| } | |
| class SettingsRoute extends RootRoute { | |
| const SettingsRoute(); | |
| } | |
| // --- Nested Level Routes (The Feed) --- | |
| sealed class FeedRoute { | |
| const FeedRoute(); | |
| } | |
| class ItemListRoute extends FeedRoute { | |
| const ItemListRoute(); | |
| } | |
| class ItemDetailRoute extends FeedRoute { | |
| final int id; | |
| const ItemDetailRoute(this.id); | |
| @override | |
| bool operator ==(Object other) => | |
| identical(this, other) || | |
| other is ItemDetailRoute && runtimeType == other.runtimeType && id == other.id; | |
| @override | |
| int get hashCode => id.hashCode; | |
| } | |
| // --- Nested Level Routes (The Profile) --- | |
| sealed class ProfileInternalRoute { | |
| const ProfileInternalRoute(); | |
| } | |
| class ProfileHomeRoute extends ProfileInternalRoute { | |
| const ProfileHomeRoute(); | |
| } | |
| // ============================================================================== | |
| // 3. COORDINATOR (State Management) | |
| // ============================================================================== | |
| class AppCoordinator extends ChangeNotifier { | |
| // 1. The Main Stack | |
| final rootPath = NavigationPath<RootRoute>(const LoginRoute()); | |
| // 2. The Feed Tab Stack | |
| final feedPath = NavigationPath<FeedRoute>(const ItemListRoute()); | |
| // 3. The Profile Tab Stack (New) | |
| final profilePath = NavigationPath<ProfileInternalRoute>(const ProfileHomeRoute()); | |
| // 4. Tab Management | |
| int _selectedTabIndex = 0; | |
| int get selectedTabIndex => _selectedTabIndex; | |
| AppCoordinator() { | |
| // Listen to changes to notify the Router (for URL updates) | |
| rootPath.addListener(notifyListeners); | |
| feedPath.addListener(notifyListeners); | |
| profilePath.addListener(notifyListeners); | |
| } | |
| @override | |
| void dispose() { | |
| rootPath.removeListener(notifyListeners); | |
| feedPath.removeListener(notifyListeners); | |
| profilePath.removeListener(notifyListeners); | |
| super.dispose(); | |
| } | |
| // --- ACTIONS --- | |
| void selectTab(int index) { | |
| _selectedTabIndex = index; | |
| notifyListeners(); | |
| } | |
| void login() { | |
| rootPath.setStack([const HomeShellRoute()]); | |
| } | |
| void logout() { | |
| feedPath.setStack([const ItemListRoute()]); | |
| profilePath.setStack([const ProfileHomeRoute()]); | |
| _selectedTabIndex = 0; | |
| rootPath.setStack([const LoginRoute()]); | |
| } | |
| void openSettings() { | |
| rootPath.push(const SettingsRoute()); | |
| } | |
| void openDetail(int id) { | |
| if (_selectedTabIndex != 0) { | |
| _selectedTabIndex = 0; | |
| notifyListeners(); | |
| } | |
| feedPath.push(ItemDetailRoute(id)); | |
| } | |
| void deepLinkToItem(int id) { | |
| rootPath.pushOrBringToFront(const HomeShellRoute()); | |
| _selectedTabIndex = 0; // Switch to Feed Tab | |
| feedPath.pushOrBringToFront(ItemDetailRoute(id)); | |
| notifyListeners(); | |
| } | |
| void deepLinkToProfile() { | |
| rootPath.pushOrBringToFront(const HomeShellRoute()); | |
| _selectedTabIndex = 1; // Switch to Profile Tab | |
| notifyListeners(); | |
| } | |
| // --- URI CALCULATIONS (State -> URL) --- | |
| Uri get currentUri { | |
| return switch ((rootPath.stack.last, _selectedTabIndex, feedPath.stack.last)) { | |
| (LoginRoute(), _, _) => Uri(path: '/login'), | |
| (SettingsRoute(), _, _) => Uri(path: '/settings'), | |
| (HomeShellRoute(), 1, _) => Uri(path: '/profile'), | |
| (HomeShellRoute(), 0, ItemDetailRoute(id: final id)) => Uri(path: '/item/$id'), | |
| (HomeShellRoute(), 0, _) => Uri(path: '/'), | |
| (_, _, _) => Uri(path: '/'), | |
| }; | |
| } | |
| // --- URI RESTORATION (URL -> State) --- | |
| void recoverFromUri(Uri uri) { | |
| switch (uri.pathSegments) { | |
| case ['login']: | |
| logout(); | |
| case ['settings']: | |
| rootPath.setStack([const HomeShellRoute(), const SettingsRoute()]); | |
| case ['profile']: | |
| deepLinkToProfile(); | |
| case ['item', final idStr] when int.tryParse(idStr) != null: | |
| deepLinkToItem(int.parse(idStr)); | |
| case _: | |
| login(); | |
| } | |
| } | |
| } | |
| final coordinator = AppCoordinator(); | |
| // ============================================================================== | |
| // 4. ROUTER IMPLEMENTATION (URL Handling) | |
| // ============================================================================== | |
| class AppRouteParser extends RouteInformationParser<Uri> { | |
| @override | |
| Future<Uri> parseRouteInformation(RouteInformation routeInformation) async { | |
| return routeInformation.uri; | |
| } | |
| @override | |
| RouteInformation? restoreRouteInformation(Uri configuration) { | |
| return RouteInformation(uri: configuration); | |
| } | |
| } | |
| class AppRouterDelegate extends RouterDelegate<Uri> | |
| with ChangeNotifier, PopNavigatorRouterDelegateMixin<Uri> { | |
| @override | |
| final GlobalKey<NavigatorState> navigatorKey; | |
| AppRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>() { | |
| coordinator.addListener(notifyListeners); | |
| } | |
| @override | |
| void dispose() { | |
| coordinator.removeListener(notifyListeners); | |
| super.dispose(); | |
| } | |
| @override | |
| Future<void> setNewRoutePath(Uri configuration) async { | |
| coordinator.recoverFromUri(configuration); | |
| } | |
| @override | |
| Uri? get currentConfiguration => coordinator.currentUri; | |
| @override | |
| Widget build(BuildContext context) { | |
| return const AppScaffold(); | |
| } | |
| } | |
| // ============================================================================== | |
| // 5. FLUTTER UI | |
| // ============================================================================== | |
| void main() { | |
| runApp(const MainApp()); | |
| } | |
| class MainApp extends StatefulWidget { | |
| const MainApp({super.key}); | |
| @override | |
| State<MainApp> createState() => _MainAppState(); | |
| } | |
| class _MainAppState extends State<MainApp> { | |
| final _routerDelegate = AppRouterDelegate(); | |
| final _routeInformationParser = AppRouteParser(); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp.router( | |
| debugShowCheckedModeBanner: false, | |
| title: 'SwiftUI Style Nav', | |
| routerDelegate: _routerDelegate, | |
| routeInformationParser: _routeInformationParser, | |
| ); | |
| } | |
| } | |
| class AppScaffold extends StatelessWidget { | |
| const AppScaffold({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // LEVEL 1: ROOT NAVIGATION | |
| return NavigationStack<RootRoute>( | |
| path: coordinator.rootPath, | |
| destinationBuilder: (route) { | |
| return switch (route) { | |
| LoginRoute() => Destination( | |
| builder: (_) => const LoginScreen(), | |
| ), | |
| HomeShellRoute() => Destination( | |
| builder: (_) => const HomeShellScreen(), | |
| ), | |
| SettingsRoute() => Destination( | |
| style: PresentationStyle.custom( | |
| transitionsBuilder: (context, animation, secondaryAnimation, child) { | |
| const begin = Offset(0.0, 1.0); | |
| const end = Offset.zero; | |
| const curve = Curves.ease; | |
| var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); | |
| return SlideTransition(position: animation.drive(tween), child: child); | |
| }, | |
| duration: const Duration(milliseconds: 500), | |
| ), | |
| builder: (_) => const SettingsScreen(), | |
| ), | |
| }; | |
| }, | |
| ); | |
| } | |
| } | |
| // --- Screens --- | |
| class LoginScreen extends StatelessWidget { | |
| const LoginScreen({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Colors.blueGrey.shade50, | |
| body: Center( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| const Icon(Icons.lock_open, size: 64, color: Colors.blueGrey), | |
| const SizedBox(height: 20), | |
| ElevatedButton( | |
| onPressed: () => coordinator.login(), | |
| child: const Text("Login"), | |
| ), | |
| const SizedBox(height: 10), | |
| const Text( | |
| "Or try navigating to /item/123 in browser URL", | |
| style: TextStyle(color: Colors.grey, fontSize: 12) | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class HomeShellScreen extends StatelessWidget { | |
| const HomeShellScreen({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return ListenableBuilder( | |
| listenable: coordinator, | |
| builder: (context, _) { | |
| return Scaffold( | |
| appBar: AppBar( | |
| title: const Text("My App"), | |
| actions: [ | |
| IconButton( | |
| icon: const Icon(Icons.settings), | |
| onPressed: () => coordinator.openSettings(), | |
| ) | |
| ], | |
| ), | |
| body: Row( | |
| children: [ | |
| NavigationRail( | |
| selectedIndex: coordinator.selectedTabIndex, | |
| onDestinationSelected: coordinator.selectTab, | |
| labelType: NavigationRailLabelType.all, | |
| destinations: const [ | |
| NavigationRailDestination(icon: Icon(Icons.list), label: Text("Feed")), | |
| NavigationRailDestination(icon: Icon(Icons.person), label: Text("Profile")), | |
| ], | |
| ), | |
| const VerticalDivider(width: 1), | |
| Expanded( | |
| child: IndexedStack( | |
| index: coordinator.selectedTabIndex, | |
| children: [ | |
| // TAB 0: FEED STACK | |
| NavigationStack<FeedRoute>( | |
| path: coordinator.feedPath, | |
| destinationBuilder: (route) { | |
| return switch (route) { | |
| ItemListRoute() => Destination( | |
| builder: (_) => const FeedListScreen(), | |
| ), | |
| ItemDetailRoute(id: final id) => Destination( | |
| builder: (_) => ItemDetailScreen(id: id), | |
| // DEMO: ASYNC REDIRECT FOR ITEM #5 | |
| redirect: id == 5 | |
| ? (context) async { | |
| // Simulate Network Check | |
| await Future.delayed(const Duration(seconds: 2)); | |
| // Fail -> Redirect to Access Denied (Item 403) | |
| return const ItemDetailRoute(403); | |
| } | |
| : null, | |
| // Custom Loader while checking | |
| placeholder: (_) => const Scaffold( | |
| body: Center(child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| CircularProgressIndicator(), | |
| SizedBox(height: 16), | |
| Text("Checking Premium Status..."), | |
| ], | |
| )), | |
| ), | |
| // Existing Pop Interception | |
| onPop: () { | |
| if (id == 403) return true; // Allow exit from denied page | |
| showDialog( | |
| context: context, | |
| builder: (context) => AlertDialog( | |
| title: const Text("Are you sure?"), | |
| content: const Text("Do you want to leave this item?"), | |
| actions: [ | |
| TextButton( | |
| onPressed: () => Navigator.pop(context), | |
| child: const Text("Stay") | |
| ), | |
| TextButton( | |
| onPressed: () { | |
| Navigator.pop(context); | |
| coordinator.feedPath.pop(); | |
| }, | |
| style: TextButton.styleFrom(foregroundColor: Colors.red), | |
| child: const Text("Leave") | |
| ), | |
| ], | |
| ), | |
| ); | |
| return false; | |
| }, | |
| ), | |
| }; | |
| }, | |
| ), | |
| // TAB 1: PROFILE STACK | |
| NavigationStack<ProfileInternalRoute>( | |
| path: coordinator.profilePath, | |
| destinationBuilder: (route) { | |
| return switch (route) { | |
| ProfileHomeRoute() => Destination( | |
| builder: (_) => const ProfileScreen(), | |
| ), | |
| }; | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| ); | |
| } | |
| } | |
| class FeedListScreen extends StatelessWidget { | |
| const FeedListScreen({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return ListView.builder( | |
| itemCount: 20, | |
| itemBuilder: (context, index) { | |
| return ListTile( | |
| title: Text(index == 5 ? "Item #5 (Premium/Locked)" : "Item #$index"), | |
| subtitle: Text(index == 5 | |
| ? "Tap to test Async Redirect logic" | |
| : "Tap to view details"), | |
| leading: index == 5 ? const Icon(Icons.lock, color: Colors.orange) : null, | |
| trailing: const Icon(Icons.chevron_right), | |
| onTap: () => coordinator.openDetail(index), | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| class ProfileScreen extends StatelessWidget { | |
| const ProfileScreen({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| const Icon(Icons.person, size: 80, color: Colors.indigo), | |
| const SizedBox(height: 20), | |
| const Text("User Profile", style: TextStyle(fontSize: 24)), | |
| const SizedBox(height: 20), | |
| ElevatedButton( | |
| onPressed: () => coordinator.logout(), | |
| style: ElevatedButton.styleFrom(foregroundColor: Colors.red), | |
| child: const Text("Logout"), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class ItemDetailScreen extends StatelessWidget { | |
| final int id; | |
| const ItemDetailScreen({super.key, required this.id}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Special UI for the Access Denied page | |
| if (id == 403) { | |
| return Scaffold( | |
| backgroundColor: Colors.red.shade50, | |
| appBar: AppBar(title: const Text("Access Denied"), backgroundColor: Colors.red), | |
| body: Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| const Icon(Icons.block, size: 64, color: Colors.red), | |
| const SizedBox(height: 16), | |
| const Text("You do not have permission.", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), | |
| const SizedBox(height: 24), | |
| ElevatedButton( | |
| onPressed: () => coordinator.feedPath.pop(), | |
| child: const Text("Go Back"), | |
| ) | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| return Scaffold( | |
| appBar: AppBar(title: Text("Item $id")), | |
| body: Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text("Details for ID: $id", style: const TextStyle(fontSize: 24)), | |
| const Padding( | |
| padding: EdgeInsets.all(16.0), | |
| child: Text( | |
| "Try going back!\nA dialog should appear.", | |
| textAlign: TextAlign.center, | |
| style: TextStyle(color: Colors.grey), | |
| ), | |
| ), | |
| const SizedBox(height: 20), | |
| ElevatedButton( | |
| onPressed: () => coordinator.deepLinkToItem(999), | |
| style: ElevatedButton.styleFrom(backgroundColor: Colors.orange), | |
| child: const Text("Deep Link to Item #999"), | |
| ) | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class SettingsScreen extends StatelessWidget { | |
| const SettingsScreen({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar(title: const Text("Settings"), automaticallyImplyLeading: false), | |
| body: Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| const Text("This is a Custom Slide Transition"), | |
| const SizedBox(height: 20), | |
| ElevatedButton( | |
| onPressed: () => coordinator.logout(), | |
| style: ElevatedButton.styleFrom(foregroundColor: Colors.red), | |
| child: const Text("Logout"), | |
| ), | |
| TextButton( | |
| // Simple pop via coordinator | |
| onPressed: () => coordinator.rootPath.pop(), | |
| child: const Text("Close"), | |
| ) | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment