Skip to content
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

[go_router] Fixed TabView swiping in custom stateful shell route example #7583

Merged
merged 9 commits into from
Sep 24, 2024
4 changes: 4 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 14.2.8

- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.

## 14.2.7

- Fixes issue so that the parseRouteInformationWithContext can handle non-http Uris.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
// found in the LICENSE file.

import 'package:collection/collection.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _tabANavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
@visibleForTesting
// ignore: public_member_api_docs
final GlobalKey<TabbedRootScreenState> tabbedRootScreenKey =
GlobalKey<TabbedRootScreenState>(debugLabel: 'TabbedRootScreen');

// This example demonstrates how to setup nested navigation using a
// BottomNavigationBar, where each bar item uses its own persistent navigator,
Expand Down Expand Up @@ -52,6 +57,8 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
// are managed (using AnimatedBranchContainer).
return ScaffoldWithNavBar(
navigationShell: navigationShell, children: children);
// NOTE: To use a Cupertino version of ScaffoldWithNavBar, replace
// ScaffoldWithNavBar above with CupertinoScaffoldWithNavBar.
},
branches: <StatefulShellBranch>[
// The route branch for the first tab of the bottom navigation bar.
Expand All @@ -78,13 +85,13 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
],
),

// The route branch for the third tab of the bottom navigation bar.
// The route branch for the second tab of the bottom navigation bar.
StatefulShellBranch(
// StatefulShellBranch will automatically use the first descendant
// GoRoute as the initial location of the branch. If another route
// is desired, specify the location of it using the defaultLocation
// parameter.
// defaultLocation: '/c2',
// defaultLocation: '/b2',
routes: <RouteBase>[
StatefulShellRoute(
builder: (BuildContext context, GoRouterState state,
Expand All @@ -102,7 +109,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
// See TabbedRootScreen for more details on how the children
// are managed (in a TabBarView).
return TabbedRootScreen(
navigationShell: navigationShell, children: children);
navigationShell: navigationShell,
key: tabbedRootScreenKey,
children: children,
);
// NOTE: To use a PageView version of TabbedRootScreen,
// replace TabbedRootScreen above with PagedRootScreen.
},
// This bottom tab uses a nested shell, wrapping sub routes in a
// top TabBar.
Expand Down Expand Up @@ -222,6 +234,70 @@ class ScaffoldWithNavBar extends StatelessWidget {
}
}

/// Alternative version of [ScaffoldWithNavBar], using a [CupertinoTabScaffold].
// ignore: unused_element, unreachable_from_main
class CupertinoScaffoldWithNavBar extends StatefulWidget {
/// Constructs an [ScaffoldWithNavBar].
// ignore: unreachable_from_main
const CupertinoScaffoldWithNavBar({
required this.navigationShell,
required this.children,
Key? key,
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));

/// The navigation shell and container for the branch Navigators.
// ignore: unreachable_from_main
final StatefulNavigationShell navigationShell;

/// The children (branch Navigators) to display in a custom container
/// ([AnimatedBranchContainer]).
// ignore: unreachable_from_main
final List<Widget> children;

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

class _CupertinoScaffoldWithNavBarState
extends State<CupertinoScaffoldWithNavBar> {
late final CupertinoTabController tabController =
CupertinoTabController(initialIndex: widget.navigationShell.currentIndex);

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

@override
Widget build(BuildContext context) {
return CupertinoTabScaffold(
controller: tabController,
tabBar: CupertinoTabBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
],
currentIndex: widget.navigationShell.currentIndex,
onTap: (int index) => _onTap(context, index),
),
// Note: It is common to use CupertinoTabView for the tabBuilder when
// using CupertinoTabScaffold and CupertinoTabBar. This would however be
// redundant when using StatefulShellRoute, since a separate Navigator is
// already created for each branch, meaning we can simply use the branch
// Navigator Widgets (i.e. widget.children) directly.
tabBuilder: (BuildContext context, int index) => widget.children[index],
);
}

void _onTap(BuildContext context, int index) {
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
}
}

/// Custom branch Navigator container that provides animated transitions
/// when switching branches.
class AnimatedBranchContainer extends StatelessWidget {
Expand Down Expand Up @@ -271,7 +347,7 @@ class RootScreenA extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Root of section A'),
title: const Text('Section A root'),
),
body: Center(
child: Column(
Expand Down Expand Up @@ -386,20 +462,43 @@ class TabbedRootScreen extends StatefulWidget {
final List<Widget> children;

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

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

void _switchedTab() {
if (tabController.index != widget.navigationShell.currentIndex) {
widget.navigationShell.goBranch(tabController.index);
}
}

@override
void initState() {
super.initState();
tabController.addListener(_switchedTab);
}

@override
void dispose() {
tabController.removeListener(_switchedTab);
tabController.dispose();
super.dispose();
}

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

@override
Expand All @@ -410,14 +509,15 @@ class _TabbedRootScreenState extends State<TabbedRootScreen>

return Scaffold(
appBar: AppBar(
title: const Text('Root of Section B (nested TabBar shell)'),
title: Text(
'Section B root (tab: ${widget.navigationShell.currentIndex + 1})'),
bottom: TabBar(
controller: _tabController,
controller: tabController,
tabs: tabs,
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
)),
body: TabBarView(
controller: _tabController,
controller: tabController,
children: widget.children,
),
);
Expand All @@ -428,6 +528,84 @@ class _TabbedRootScreenState extends State<TabbedRootScreen>
}
}

/// Alternative implementation of TabbedRootScreen, demonstrating the use of
/// a [PageView].
// ignore: unreachable_from_main
class PagedRootScreen extends StatefulWidget {
/// Constructs a PagedRootScreen
// ignore: unreachable_from_main
const PagedRootScreen(
{required this.navigationShell, required this.children, super.key});

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

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

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

/// Alternative implementation _TabbedRootScreenState, demonstrating the use of
/// a PageView.
class _PagedRootScreenState extends State<PagedRootScreen> {
late final PageController _pageController = PageController(
initialPage: widget.navigationShell.currentIndex,
);

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Section B root (tab ${widget.navigationShell.currentIndex + 1})'),
),
body: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
ElevatedButton(
onPressed: () => _animateToPage(0),
child: const Text('Tab 1'),
),
ElevatedButton(
onPressed: () => _animateToPage(1),
child: const Text('Tab 2'),
),
]),
Expanded(
child: PageView(
onPageChanged: (int i) => widget.navigationShell.goBranch(i),
controller: _pageController,
children: widget.children,
),
),
],
),
);
}

void _animateToPage(int index) {
if (_pageController.hasClients) {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 500),
curve: Curves.bounceOut,
);
}
}
}

/// Widget for the pages in the top tab bar.
class TabScreen extends StatelessWidget {
/// Creates a RootScreen
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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:flutter_test/flutter_test.dart';
import 'package:go_router_examples/others/custom_stateful_shell_route.dart';

void main() {
testWidgets(
'Changing active tab in TabController of TabbedRootScreen (root screen '
'of branch/section B) correctly navigates to appropriate screen',
(WidgetTester tester) async {
await tester.pumpWidget(NestedTabNavigationExampleApp());
expect(find.text('Screen A'), findsOneWidget);

// navigate to ScreenB
await tester.tap(find.text('Section B'));
await tester.pumpAndSettle();
expect(find.text('Screen B1'), findsOneWidget);

// Get TabController from TabbedRootScreen (root screen of branch/section B)
final TabController? tabController =
tabbedRootScreenKey.currentState?.tabController;
expect(tabController, isNotNull);

// Simulate swiping TabView to change active tab in TabController
tabbedRootScreenKey.currentState?.tabController.index = 1;
await tester.pumpAndSettle();
expect(find.text('Screen B2'), findsOneWidget);
});
}
2 changes: 1 addition & 1 deletion packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: go_router
description: A declarative router for Flutter based on Navigation 2 supporting
deep linking, data-driven routes and more
version: 14.2.7
version: 14.2.8
repository: https://github.com/flutter/packages/tree/main/packages/go_router
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22

Expand Down