Skip to content

Commit 47795ab

Browse files
authored
[go_router] Fixed TabView swiping in custom stateful shell route example (#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
1 parent 4926c0f commit 47795ab

File tree

4 files changed

+226
-12
lines changed

4 files changed

+226
-12
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 14.2.8
2+
3+
- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.
4+
15
## 14.2.7
26

37
- Fixes issue so that the parseRouteInformationWithContext can handle non-http Uris.

packages/go_router/example/lib/others/custom_stateful_shell_route.dart

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
// found in the LICENSE file.
44

55
import 'package:collection/collection.dart';
6+
import 'package:flutter/cupertino.dart';
67
import 'package:flutter/material.dart';
78
import 'package:go_router/go_router.dart';
89

910
final GlobalKey<NavigatorState> _rootNavigatorKey =
1011
GlobalKey<NavigatorState>(debugLabel: 'root');
1112
final GlobalKey<NavigatorState> _tabANavigatorKey =
1213
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
14+
@visibleForTesting
15+
// ignore: public_member_api_docs
16+
final GlobalKey<TabbedRootScreenState> tabbedRootScreenKey =
17+
GlobalKey<TabbedRootScreenState>(debugLabel: 'TabbedRootScreen');
1318

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

81-
// The route branch for the third tab of the bottom navigation bar.
88+
// The route branch for the second tab of the bottom navigation bar.
8289
StatefulShellBranch(
8390
// StatefulShellBranch will automatically use the first descendant
8491
// GoRoute as the initial location of the branch. If another route
8592
// is desired, specify the location of it using the defaultLocation
8693
// parameter.
87-
// defaultLocation: '/c2',
94+
// defaultLocation: '/b2',
8895
routes: <RouteBase>[
8996
StatefulShellRoute(
9097
builder: (BuildContext context, GoRouterState state,
@@ -102,7 +109,12 @@ class NestedTabNavigationExampleApp extends StatelessWidget {
102109
// See TabbedRootScreen for more details on how the children
103110
// are managed (in a TabBarView).
104111
return TabbedRootScreen(
105-
navigationShell: navigationShell, children: children);
112+
navigationShell: navigationShell,
113+
key: tabbedRootScreenKey,
114+
children: children,
115+
);
116+
// NOTE: To use a PageView version of TabbedRootScreen,
117+
// replace TabbedRootScreen above with PagedRootScreen.
106118
},
107119
// This bottom tab uses a nested shell, wrapping sub routes in a
108120
// top TabBar.
@@ -222,6 +234,70 @@ class ScaffoldWithNavBar extends StatelessWidget {
222234
}
223235
}
224236

237+
/// Alternative version of [ScaffoldWithNavBar], using a [CupertinoTabScaffold].
238+
// ignore: unused_element, unreachable_from_main
239+
class CupertinoScaffoldWithNavBar extends StatefulWidget {
240+
/// Constructs an [ScaffoldWithNavBar].
241+
// ignore: unreachable_from_main
242+
const CupertinoScaffoldWithNavBar({
243+
required this.navigationShell,
244+
required this.children,
245+
Key? key,
246+
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
247+
248+
/// The navigation shell and container for the branch Navigators.
249+
// ignore: unreachable_from_main
250+
final StatefulNavigationShell navigationShell;
251+
252+
/// The children (branch Navigators) to display in a custom container
253+
/// ([AnimatedBranchContainer]).
254+
// ignore: unreachable_from_main
255+
final List<Widget> children;
256+
257+
@override
258+
State<StatefulWidget> createState() => _CupertinoScaffoldWithNavBarState();
259+
}
260+
261+
class _CupertinoScaffoldWithNavBarState
262+
extends State<CupertinoScaffoldWithNavBar> {
263+
late final CupertinoTabController tabController =
264+
CupertinoTabController(initialIndex: widget.navigationShell.currentIndex);
265+
266+
@override
267+
void dispose() {
268+
tabController.dispose();
269+
super.dispose();
270+
}
271+
272+
@override
273+
Widget build(BuildContext context) {
274+
return CupertinoTabScaffold(
275+
controller: tabController,
276+
tabBar: CupertinoTabBar(
277+
items: const <BottomNavigationBarItem>[
278+
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
279+
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
280+
],
281+
currentIndex: widget.navigationShell.currentIndex,
282+
onTap: (int index) => _onTap(context, index),
283+
),
284+
// Note: It is common to use CupertinoTabView for the tabBuilder when
285+
// using CupertinoTabScaffold and CupertinoTabBar. This would however be
286+
// redundant when using StatefulShellRoute, since a separate Navigator is
287+
// already created for each branch, meaning we can simply use the branch
288+
// Navigator Widgets (i.e. widget.children) directly.
289+
tabBuilder: (BuildContext context, int index) => widget.children[index],
290+
);
291+
}
292+
293+
void _onTap(BuildContext context, int index) {
294+
widget.navigationShell.goBranch(
295+
index,
296+
initialLocation: index == widget.navigationShell.currentIndex,
297+
);
298+
}
299+
}
300+
225301
/// Custom branch Navigator container that provides animated transitions
226302
/// when switching branches.
227303
class AnimatedBranchContainer extends StatelessWidget {
@@ -271,7 +347,7 @@ class RootScreenA extends StatelessWidget {
271347
Widget build(BuildContext context) {
272348
return Scaffold(
273349
appBar: AppBar(
274-
title: const Text('Root of section A'),
350+
title: const Text('Section A root'),
275351
),
276352
body: Center(
277353
child: Column(
@@ -386,20 +462,43 @@ class TabbedRootScreen extends StatefulWidget {
386462
final List<Widget> children;
387463

388464
@override
389-
State<StatefulWidget> createState() => _TabbedRootScreenState();
465+
State<StatefulWidget> createState() => TabbedRootScreenState();
390466
}
391467

392-
class _TabbedRootScreenState extends State<TabbedRootScreen>
468+
@visibleForTesting
469+
// ignore: public_member_api_docs
470+
class TabbedRootScreenState extends State<TabbedRootScreen>
393471
with SingleTickerProviderStateMixin {
394-
late final TabController _tabController = TabController(
472+
@visibleForTesting
473+
// ignore: public_member_api_docs
474+
late final TabController tabController = TabController(
395475
length: widget.children.length,
396476
vsync: this,
397477
initialIndex: widget.navigationShell.currentIndex);
398478

479+
void _switchedTab() {
480+
if (tabController.index != widget.navigationShell.currentIndex) {
481+
widget.navigationShell.goBranch(tabController.index);
482+
}
483+
}
484+
485+
@override
486+
void initState() {
487+
super.initState();
488+
tabController.addListener(_switchedTab);
489+
}
490+
491+
@override
492+
void dispose() {
493+
tabController.removeListener(_switchedTab);
494+
tabController.dispose();
495+
super.dispose();
496+
}
497+
399498
@override
400499
void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
401500
super.didUpdateWidget(oldWidget);
402-
_tabController.index = widget.navigationShell.currentIndex;
501+
tabController.index = widget.navigationShell.currentIndex;
403502
}
404503

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

411510
return Scaffold(
412511
appBar: AppBar(
413-
title: const Text('Root of Section B (nested TabBar shell)'),
512+
title: Text(
513+
'Section B root (tab: ${widget.navigationShell.currentIndex + 1})'),
414514
bottom: TabBar(
415-
controller: _tabController,
515+
controller: tabController,
416516
tabs: tabs,
417517
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
418518
)),
419519
body: TabBarView(
420-
controller: _tabController,
520+
controller: tabController,
421521
children: widget.children,
422522
),
423523
);
@@ -428,6 +528,84 @@ class _TabbedRootScreenState extends State<TabbedRootScreen>
428528
}
429529
}
430530

531+
/// Alternative implementation of TabbedRootScreen, demonstrating the use of
532+
/// a [PageView].
533+
// ignore: unreachable_from_main
534+
class PagedRootScreen extends StatefulWidget {
535+
/// Constructs a PagedRootScreen
536+
// ignore: unreachable_from_main
537+
const PagedRootScreen(
538+
{required this.navigationShell, required this.children, super.key});
539+
540+
/// The current state of the parent StatefulShellRoute.
541+
// ignore: unreachable_from_main
542+
final StatefulNavigationShell navigationShell;
543+
544+
/// The children (branch Navigators) to display in the [TabBarView].
545+
// ignore: unreachable_from_main
546+
final List<Widget> children;
547+
548+
@override
549+
State<StatefulWidget> createState() => _PagedRootScreenState();
550+
}
551+
552+
/// Alternative implementation _TabbedRootScreenState, demonstrating the use of
553+
/// a PageView.
554+
class _PagedRootScreenState extends State<PagedRootScreen> {
555+
late final PageController _pageController = PageController(
556+
initialPage: widget.navigationShell.currentIndex,
557+
);
558+
559+
@override
560+
void dispose() {
561+
_pageController.dispose();
562+
super.dispose();
563+
}
564+
565+
@override
566+
Widget build(BuildContext context) {
567+
return Scaffold(
568+
appBar: AppBar(
569+
title: Text(
570+
'Section B root (tab ${widget.navigationShell.currentIndex + 1})'),
571+
),
572+
body: Column(
573+
children: <Widget>[
574+
Row(
575+
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
576+
children: <Widget>[
577+
ElevatedButton(
578+
onPressed: () => _animateToPage(0),
579+
child: const Text('Tab 1'),
580+
),
581+
ElevatedButton(
582+
onPressed: () => _animateToPage(1),
583+
child: const Text('Tab 2'),
584+
),
585+
]),
586+
Expanded(
587+
child: PageView(
588+
onPageChanged: (int i) => widget.navigationShell.goBranch(i),
589+
controller: _pageController,
590+
children: widget.children,
591+
),
592+
),
593+
],
594+
),
595+
);
596+
}
597+
598+
void _animateToPage(int index) {
599+
if (_pageController.hasClients) {
600+
_pageController.animateToPage(
601+
index,
602+
duration: const Duration(milliseconds: 500),
603+
curve: Curves.bounceOut,
604+
);
605+
}
606+
}
607+
}
608+
431609
/// Widget for the pages in the top tab bar.
432610
class TabScreen extends StatelessWidget {
433611
/// Creates a RootScreen
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:go_router_examples/others/custom_stateful_shell_route.dart';
8+
9+
void main() {
10+
testWidgets(
11+
'Changing active tab in TabController of TabbedRootScreen (root screen '
12+
'of branch/section B) correctly navigates to appropriate screen',
13+
(WidgetTester tester) async {
14+
await tester.pumpWidget(NestedTabNavigationExampleApp());
15+
expect(find.text('Screen A'), findsOneWidget);
16+
17+
// navigate to ScreenB
18+
await tester.tap(find.text('Section B'));
19+
await tester.pumpAndSettle();
20+
expect(find.text('Screen B1'), findsOneWidget);
21+
22+
// Get TabController from TabbedRootScreen (root screen of branch/section B)
23+
final TabController? tabController =
24+
tabbedRootScreenKey.currentState?.tabController;
25+
expect(tabController, isNotNull);
26+
27+
// Simulate swiping TabView to change active tab in TabController
28+
tabbedRootScreenKey.currentState?.tabController.index = 1;
29+
await tester.pumpAndSettle();
30+
expect(find.text('Screen B2'), findsOneWidget);
31+
});
32+
}

packages/go_router/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 14.2.7
4+
version: 14.2.8
55
repository: https://github.com/flutter/packages/tree/main/packages/go_router
66
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
77

0 commit comments

Comments
 (0)