// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; void main() => runApp(RestorableStatefulShellRouteExampleApp()); /// An example demonstrating how to use StatefulShellRoute with state /// restoration. class RestorableStatefulShellRouteExampleApp extends StatelessWidget { /// Creates a NestedTabNavigationExampleApp RestorableStatefulShellRouteExampleApp({super.key}); final GoRouter _router = GoRouter( initialLocation: '/a', restorationScopeId: 'router', routes: [ StatefulShellRoute.indexedStack( restorationScopeId: 'shell1', pageBuilder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { return MaterialPage( restorationId: 'shellWidget1', child: ScaffoldWithNavBar(navigationShell: navigationShell)); }, branches: [ // The route branch for the first tab of the bottom navigation bar. StatefulShellBranch( restorationScopeId: 'branchA', routes: [ GoRoute( // The screen to display as the root in the first tab of the // bottom navigation bar. path: '/a', pageBuilder: (BuildContext context, GoRouterState state) => const MaterialPage( restorationId: 'screenA', child: RootScreen(label: 'A', detailsPath: '/a/details')), routes: [ // The details screen to display stacked on navigator of the // first tab. This will cover screen A but not the application // shell (bottom navigation bar). GoRoute( path: 'details', pageBuilder: (BuildContext context, GoRouterState state) => const MaterialPage( restorationId: 'screenADetail', child: DetailsScreen(label: 'A')), ), ], ), ], ), // The route branch for the second tab of the bottom navigation bar. StatefulShellBranch( restorationScopeId: 'branchB', routes: [ GoRoute( // The screen to display as the root in the second tab of the // bottom navigation bar. path: '/b', pageBuilder: (BuildContext context, GoRouterState state) => const MaterialPage( restorationId: 'screenB', child: RootScreen(label: 'B', detailsPath: '/b/details')), routes: [ // The details screen to display stacked on navigator of the // first tab. This will cover screen A but not the application // shell (bottom navigation bar). GoRoute( path: 'details', pageBuilder: (BuildContext context, GoRouterState state) => const MaterialPage( restorationId: 'screenBDetail', child: DetailsScreen(label: 'B')), ), ], ), ], ), ], ), ], ); @override Widget build(BuildContext context) { return MaterialApp.router( restorationScopeId: 'app', title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), routerConfig: _router, ); } } /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. class ScaffoldWithNavBar extends StatelessWidget { /// Constructs an [ScaffoldWithNavBar]. const ScaffoldWithNavBar({ required this.navigationShell, Key? key, }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. final StatefulNavigationShell navigationShell; @override Widget build(BuildContext context) { return Scaffold( body: navigationShell, bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), ], currentIndex: navigationShell.currentIndex, onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), ), ); } } /// Widget for the root/initial pages in the bottom navigation bar. class RootScreen extends StatelessWidget { /// Creates a RootScreen const RootScreen({ required this.label, required this.detailsPath, super.key, }); /// The label final String label; /// The path to the detail page final String detailsPath; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Root of section $label'), ), body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { GoRouter.of(context).go(detailsPath); }, child: const Text('View details'), ), ], ), ), ); } } /// The details screen for either the A or B screen. class DetailsScreen extends StatefulWidget { /// Constructs a [DetailsScreen]. const DetailsScreen({ required this.label, super.key, }); /// The label to display in the center of the screen. final String label; @override State createState() => DetailsScreenState(); } /// The state for DetailsScreen class DetailsScreenState extends State with RestorationMixin { final RestorableInt _counter = RestorableInt(0); @override String? get restorationId => 'DetailsScreen-${widget.label}'; @override void restoreState(RestorationBucket? oldBucket, bool initialRestore) { registerForRestoration(_counter, 'counter'); } @override void dispose() { super.dispose(); _counter.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Details Screen - ${widget.label}'), ), body: _build(context), ); } Widget _build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text('Details for ${widget.label} - Counter: ${_counter.value}', style: Theme.of(context).textTheme.titleLarge), const Padding(padding: EdgeInsets.all(4)), TextButton( onPressed: () { setState(() { _counter.value++; }); }, child: const Text('Increment counter'), ), const Padding(padding: EdgeInsets.all(8)), ], ), ); } }