Skip to content

[go_router] Add support for the TabView for more beautiful animation #112267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jopmiddelkamp opened this issue Sep 23, 2022 · 9 comments
Open
Labels
a: animation Animation APIs c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter p: go_router The go_router package P3 Issues that are less important to the Flutter project package flutter/packages repository. See also p: labels. team-go_router Owned by Go Router team triaged-go_router Triaged by Go Router team

Comments

@jopmiddelkamp
Copy link

jopmiddelkamp commented Sep 23, 2022

Use case

In our app we are using a TabBar with TabView. This create a nice animation when using the tabs.

ezgif-4-6d09826040

Now with go_router 5.0.0 we can use a ShellRoute but it returns the child in a Navigator. This works but we are losing the nice animation when switching tabs with a TabView.

I was thinking maybe we can create a TabBarShellRoute which gets wrapped in a DefaultTabController and then returns a child as a TabView with all the child routes inside of which each have their own Navigator.

Then maybe create a TabViewGoRoute which doesn't have a pageBuilder and of which the child routes are a list of the normal GoRoute objects.

Proposal

TabBarShellRoute(
  navigatorKey: _homeTabBarShellNavigatorKey,
  builder: (context, state, child, controller) => HomeTabShell(
    controller: controller,
    child: child,
  ),
  routes: [
    TabViewGoRoute(
      name: TransactionsTab.name,
      path: '/transactions',
      builder: (context, state) => const TransactionsTab(),
    ),
    TabViewGoRoute(
      name: SavingsBalancesTab.name,
      path: '/savings-balances',
      builder: (context, state) => const SavingsBalancesTab(),
      routes: [
        GoRoute(
          name: OpenSavingsPage.name,
          path: 'open-new',
          parentNavigatorKey: _rootNavigatorKey,
          builder: (context, state) => const OpenSavingsPage(),
        ),
      ],
    ),
  ],
),
@darshankawar darshankawar added in triage Presently being triaged by the triage team c: new feature Nothing broken; request for a new capability a: animation Animation APIs p: first party package flutter/packages repository. See also p: labels. c: proposal A detailed proposal for a change to Flutter p: go_router The go_router package and removed in triage Presently being triaged by the triage team labels Sep 23, 2022
@tolo
Copy link

tolo commented Sep 27, 2022

Perhaps this can also be solved with a solution like the one I proposed in a comment to #99124. That change proposes a new ShellRoute subclass, that doesn't create a navigator, but instead delegates navigation handling to the child of the shell route.

I was also able to create a workaround without modifying go_router itself - see example in this gist. In that example, a custom ShellRoute subclass is also created, which simply bypasses the navigator of the shell route. Perhaps this example could also be adapted to your use case.

@stuartmorgan-g stuartmorgan-g changed the title Add support for the TabView for more beautiful animation [go_router] Add support for the TabView for more beautiful animation Sep 27, 2022
@stuartmorgan-g stuartmorgan-g added the P3 Issues that are less important to the Flutter project label Sep 27, 2022
@chunhtai
Copy link
Contributor

This feature can be added once flutter/packages#2650 is landed

@subzero911
Copy link

subzero911 commented Dec 28, 2022

+1 for TabView support. You may want to also support PageView (in case we don't have tabs and want to scroll pages by gesture).
Both TabView and PageView are already supported in auto_route https://pub.dev/packages/auto_route#tab-navigation, but in ugly manner (it requires its own sub-router, so routing is smeared over the application). With ShellRoute it would be much nicer.

@vlkonoshenko
Copy link
Contributor

Some have solution for this issue? Instead of auto_route package

@subzero911
Copy link

Some have solution for this issue? Instead of auto_route package

Unfortunately, go_router is still far behind auto_route.

JeroenWeener pushed a commit to JeroenWeener/flutter-packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
JeroenWeener pushed a commit to JeroenWeener/flutter-packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
JeroenWeener pushed a commit to JeroenWeener/flutter-packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
johnpryan pushed a commit to tolo/flutter_packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
johnpryan pushed a commit to tolo/flutter_packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
johnpryan pushed a commit to tolo/flutter_packages that referenced this issue May 22, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
@palancana
Copy link

This feature can be added once flutter/packages#2650 is landed

The great flutter/packages#2650 PR has finally been merged! I understand, though, that animations were out of the scope. Are there any hopes for adding TabView or PageView support, so we can take profit of those animations?

Or maybe there is another workaround to use those animations with the current StatefulShellRoute, but I couldn't figure out how. Maybe @tolo or @chunhtai has an opinion there.

@flutter-triage-bot flutter-triage-bot bot added multiteam-retriage-candidate team-go_router Owned by Go Router team triaged-go_router Triaged by Go Router team labels Jul 8, 2023
nploi pushed a commit to nploi/packages that referenced this issue Jul 16, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
nploi pushed a commit to nploi/packages that referenced this issue Jul 16, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
nploi pushed a commit to nploi/packages that referenced this issue Jul 16, 2023
Added functionality for building route configuration with support for preserving state in nested navigators. This change introduces a new shell route class called `StatefulShellRoute`, that uses separate navigators for its child routes as well as preserving state in each navigation branch. This is convenient when for instance implementing a UI with a `BottomNavigationBar`, with a persistent navigation state for each tab (i.e. building a `Navigator` for each tab). 
An example showcasing a UI with BottomNavigationBar and StatefulShellRoute has also been added ([`stateful_shell_route.dart`](https://github.com/tolo/flutter_packages/blob/nested-persistent-navigation/packages/go_router/example/lib/stateful_shell_route.dart)). 

Other examples of using `StatefulShellRoute` are also available in these repositories: 
* [stateful_books](https://github.com/tolo/stateful_books) - A fork of the Books example of go_router.
* [stateful_navbar](https://github.com/tolo/stateful_navbar) - A clone of the Flutter Material 3 Navigation Bar example.

<br/>
Below is a short example of how a `StatefulShellRoute` can be setup:

```dart
StatefulShellRoute(
  /// Each separate stateful navigation tree (i.e. Navigator) is represented by
  /// a StatefulShellBranch, which defines the routes that will be placed on that
  /// Navigator. StatefulShellBranch also makes it possible to configure
  /// things like an (optional) Navigator key, the default location (i.e. the
  /// location the branch will be navigated to when loading it for the first time) etc.
  branches: <StatefulShellBranch>[
    StatefulShellBranch(navigatorKey: optionalNavigatorKey, routes: <RouteBase>[
      GoRoute(
        path: '/a',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'A'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'A'),
          ),
        ],
      ),
    ]),
    /// The default location of a branch will by default be the first of the
    /// configured routes. To configure a different route, provide the
    /// defaultLocation parameter.
    StatefulShellBranch(defaultLocation: '/b/detail', routes: <RouteBase>[
      GoRoute(
        path: '/b',
        builder: (BuildContext context, GoRouterState state) =>
        const RootScreen(label: 'B'),
        routes: <RouteBase>[
          GoRoute(
            path: 'details',
            builder: (BuildContext context, GoRouterState state) =>
            const DetailsScreen(label: 'B'),
          ),
        ],
      ),
    ]),
  ],
  /// Like ShellRoute, the builder builds the navigation shell around the
  /// sub-routes, but with StatefulShellRoute, this navigation shell is able to
  /// maintain the state of the Navigators for each branch. The navigation shell
  /// could for instance use a BottomNavigationBar or similar.
  builder: (BuildContext context, StatefulShellRouteState state, Widget child) =>
      ScaffoldWithNavBar(shellState: state, body: child),
)
```

This fixes issue flutter/flutter#99124.
It also (at least partially) addresses flutter/flutter#112267.
@subzero911
Copy link

You already have a StatefulShellRoute.indexedStack
It now should be easy to implement StatefulShellRoute.pageView. It essentially would be the same, but with a beautiful swipe animation between the screens.
Looking forward for it!

@yanshouwang
Copy link

yanshouwang commented May 25, 2024

There is a CustomStateShellRoute example, we can use PageView instead of the AnimatedBranchContainer

// GoRouter
final routerConfig = GoRouter(
  initialLocation: '/central-manager',
  navigatorKey: _rootNavigatorKey,
  routes: [
    StatefulShellRoute(
      // builder: (context, state, navigationShell) {
      //   return HomeView(navigationShell: navigationShell);
      // },
      builder: (context, state, navigationShell) => navigationShell,
      navigatorContainerBuilder: (context, navigationShell, children) {
        return HomeView(
          navigationShell: navigationShell,
          children: children,
        );
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/central-manager',
              builder: (context, state) {
                return const CentralManagerView();
              },
              routes: [
                GoRoute(
                  path: 'peripheral',
                  builder: (context, state) {
                    final eventArgs = state.extra as DiscoveredEventArgs;
                    return PeripheralView(
                      eventArgs: eventArgs,
                    );
                  },
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/peripheral-manager',
              builder: (context, state) {
                return const PeripheralManagerView();
              },
            ),
          ],
        ),
      ],
    ),
  ],
);
// HomeView
class HomeView extends StatefulWidget {
  final StatefulNavigationShell navigationShell;
  final List<Widget> children;

  const HomeView({
    super.key,
    required this.navigationShell,
    required this.children,
  });

  @override
  State<HomeView> createState() => _HomeViewState();
}

class _HomeViewState extends State<HomeView> {
  late final PageController _controller;

  @override
  void initState() {
    super.initState();
    _controller = PageController(
      initialPage: widget.navigationShell.currentIndex,
    );
  }

  @override
  Widget build(BuildContext context) {
    final navigationShell = widget.navigationShell;
    final children = widget.children;
    return Scaffold(
      // body: navigationShell,
      // body: AnimatedBranchContainer(
      //   currentIndex: widget.navigationShell.currentIndex,
      //   children: widget.children,
      // ),
      body: PageView.builder(
        controller: _controller,
        onPageChanged: (index) {
          debugPrint(
              'index: $index, currentIndex: ${navigationShell.currentIndex}');
          // Ignore tap events.
          if (index == navigationShell.currentIndex) {
            return;
          }
          navigationShell.goBranch(
            index,
            initialLocation: false,
          );
        },
        itemBuilder: (context, index) {
          return children[index];
        },
        itemCount: children.length,
      ),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (index) {
          navigationShell.goBranch(
            index,
            initialLocation: index == navigationShell.currentIndex,
          );
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.radar),
            label: 'Central',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.sensors),
            label: 'Peripheral',
          ),
        ],
        currentIndex: widget.navigationShell.currentIndex,
      ),
    );
  }

  @override
  void didUpdateWidget(covariant HomeView oldWidget) {
    super.didUpdateWidget(oldWidget);
    final navigationShell = widget.navigationShell;
    final page = _controller.page ?? _controller.initialPage;
    final index = page.round();
    debugPrint('index: $index, currentIndex: ${navigationShell.currentIndex}');
    // Ignore swipe events.
    if (index == navigationShell.currentIndex) {
      return;
    }
    _controller.animateToPage(
      widget.navigationShell.currentIndex,
      duration: const Duration(milliseconds: 300),
      curve: Curves.linear,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

@dleurs
Copy link

dleurs commented Sep 4, 2024

You already have a StatefulShellRoute.indexedStack It now should be easy to implement StatefulShellRoute.pageView. It essentially would be the same, but with a beautiful swipe animation between the screens. Looking forward for it!

Any update on StatefulShellRoute.pageView ?

This modified example of the custom example seems to work, do you see any limitations ?

Enregistrement.2024-09-04.180815.mp4
// 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:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey<NavigatorState>(debugLabel: 'root');

// This example demonstrates how to setup nested navigation using a
// BottomNavigationBar, where each bar item uses its own persistent navigator,
// i.e. navigation state is maintained separately for each item. This setup also
// enables deep linking into nested pages.
//
// This example also demonstrates how build a nested shell with a custom
// container for the branch Navigators (in this case a TabBarView).

void main() {
  runApp(NestedTabNavigationExampleApp());
}

/// An example demonstrating how to use nested navigators
class NestedTabNavigationExampleApp extends StatelessWidget {
  /// Creates a NestedTabNavigationExampleApp
  NestedTabNavigationExampleApp({super.key});

  final GoRouter _router = GoRouter(
    navigatorKey: _rootNavigatorKey,
    initialLocation: '/a',
    routes: <RouteBase>[
      StatefulShellRoute(
        builder: (BuildContext context, GoRouterState state,
            StatefulNavigationShell navigationShell) {
          // Just like with the top level StatefulShellRoute, no
          // customization is done in the builder function.
          return navigationShell;
        },
        navigatorContainerBuilder: (BuildContext context,
            StatefulNavigationShell navigationShell, List<Widget> children) {
          // Returning a customized container for the branch
          // Navigators (i.e. the `List<Widget> children` argument).
          //
          // See TabbedRootScreen for more details on how the children
          // are managed (in a TabBarView).
          return TabbedRootScreen(
              navigationShell: navigationShell, children: children);
        },
        // This bottom tab uses a nested shell, wrapping sub routes in a
        // top TabBar.
        branches: <StatefulShellBranch>[
          StatefulShellBranch(routes: <GoRoute>[
            GoRoute(
              path: '/a',
              builder: (BuildContext context, GoRouterState state) =>
                  const TabScreen(label: 'A', detailsPath: '/a/details'),
              routes: <RouteBase>[
                GoRoute(
                  path: 'details',
                  builder: (BuildContext context, GoRouterState state) =>
                      const DetailsScreen(
                    label: 'A',
                    withScaffold: true,
                  ),
                ),
              ],
            ),
          ]),
          StatefulShellBranch(routes: <GoRoute>[
            GoRoute(
              path: '/b',
              builder: (BuildContext context, GoRouterState state) =>
                  const TabScreen(label: 'B', detailsPath: '/b/details'),
              routes: <RouteBase>[
                GoRoute(
                  path: 'details',
                  builder: (BuildContext context, GoRouterState state) =>
                      const DetailsScreen(
                    label: 'B',
                    withScaffold: true,
                  ),
                ),
              ],
            ),
          ]),
        ],
      ),
    ],
  );

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routerConfig: _router,
    );
  }
}

/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
  /// Constructs a [DetailsScreen].
  const DetailsScreen({
    required this.label,
    this.param,
    this.withScaffold = true,
    super.key,
  });

  /// The label to display in the center of the screen.
  final String label;

  /// Optional param
  final String? param;

  /// Wrap in scaffold
  final bool withScaffold;

  @override
  State<StatefulWidget> createState() => DetailsScreenState();
}

/// The state for DetailsScreen
class DetailsScreenState extends State<DetailsScreen> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    if (widget.withScaffold) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Details Screen - ${widget.label}'),
        ),
        body: _build(context),
      );
    } else {
      return ColoredBox(
        color: Theme.of(context).scaffoldBackgroundColor,
        child: _build(context),
      );
    }
  }

  Widget _build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text('Details for ${widget.label} - Counter: $_counter',
              style: Theme.of(context).textTheme.titleLarge),
          const Padding(padding: EdgeInsets.all(4)),
          TextButton(
            onPressed: () {
              setState(() {
                _counter++;
              });
            },
            child: const Text('Increment counter'),
          ),
          const Padding(padding: EdgeInsets.all(8)),
          if (widget.param != null)
            Text('Parameter: ${widget.param!}',
                style: Theme.of(context).textTheme.titleMedium),
          const Padding(padding: EdgeInsets.all(8)),
          if (!widget.withScaffold) ...<Widget>[
            const Padding(padding: EdgeInsets.all(16)),
            TextButton(
              onPressed: () {
                GoRouter.of(context).pop();
              },
              child: const Text('< Back',
                  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
            ),
          ]
        ],
      ),
    );
  }
}

/// Builds a nested shell using a [TabBar] and [TabBarView].
class TabbedRootScreen extends StatefulWidget {
  /// Constructs a TabbedRootScreen
  const TabbedRootScreen(
      {required this.navigationShell, required this.children, super.key});

  /// The current state of the parent StatefulShellRoute.
  final StatefulNavigationShell navigationShell;

  /// The children (branch Navigators) to display in the [TabBarView].
  final List<Widget> children;

  @override
  State<StatefulWidget> createState() => _TabbedRootScreenState();
}

class _TabbedRootScreenState extends State<TabbedRootScreen>
    with SingleTickerProviderStateMixin {
  late final TabController _tabController = TabController(
      length: widget.children.length,
      vsync: this,
      initialIndex: widget.navigationShell.currentIndex);

  @override
  void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
    super.didUpdateWidget(oldWidget);
    _tabController.index = widget.navigationShell.currentIndex;
  }

  @override
  Widget build(BuildContext context) {
    final List<Tab> tabs = widget.children
        .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}'))
        .toList();

    return Material(
      child: Column(
        children: [
          TabBar(
            controller: _tabController,
            tabs: tabs,
            onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
            isScrollable: true,
          ),
          Expanded(
            child: TabBarView(
              controller: _tabController,
              children: widget.children,
            ),
          ),
        ],
      ),
    );
  }

  void _onTabTap(BuildContext context, int index) {
    widget.navigationShell.goBranch(index);
  }
}

/// Widget for the pages in the top tab bar.
class TabScreen extends StatelessWidget {
  /// Creates a RootScreen
  const TabScreen({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 Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          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'),
          ),
        ],
      ),
    );
  }
}

hannah-hyj pushed a commit to flutter/packages that referenced this issue Sep 24, 2024
…ple (#7583)

Updated `custom_stateful_shell_route.dart` example to better support
swiping in TabView. Also added code to demonstrate use of PageView
instead of TabView. Note that to be fully effective from a usability
perspective, the PR #6467 (branch preloading) need also
be merged.

This PR addresses: 
* flutter/flutter#150837 
* flutter/flutter#112267

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] page, which explains my
responsibilities.
- [x] I read and followed the [relevant style guides] and ran the
auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages
repo does use `dart format`.)
- [x] I signed the [CLA].
- [x] The title of the PR starts with the name of the package surrounded
by square brackets, e.g. `[shared_preferences]`
- [x] I [linked to at least one issue that this PR fixes] in the
description above.
- [x] I updated `pubspec.yaml` with an appropriate new version according
to the [pub versioning philosophy], or this PR is [exempt from version
changes].
- [x] I updated `CHANGELOG.md` to add a description of the change,
[following repository CHANGELOG style], or this PR is [exempt from
CHANGELOG changes].
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/packages/blob/main/CONTRIBUTING.md
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md
[relevant style guides]:
https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style
[CLA]: https://cla.developers.google.com/
[Discord]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md
[linked to at least one issue that this PR fixes]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview
[pub versioning philosophy]: https://dart.dev/tools/pub/versioning
[exempt from version changes]:
https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version
[following repository CHANGELOG style]:
https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style
[exempt from CHANGELOG changes]:
https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog
[test-exempt]:
https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: animation Animation APIs c: new feature Nothing broken; request for a new capability c: proposal A detailed proposal for a change to Flutter p: go_router The go_router package P3 Issues that are less important to the Flutter project package flutter/packages repository. See also p: labels. team-go_router Owned by Go Router team triaged-go_router Triaged by Go Router team
Projects
No open projects
Status: No status
Development

No branches or pull requests