Skip to content

Commit

Permalink
[go_router] Fixed TabView swiping in custom stateful shell route exam…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
tolo authored Sep 24, 2024
1 parent 4926c0f commit 47795ab
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 12 deletions.
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
200 changes: 189 additions & 11 deletions packages/go_router/example/lib/others/custom_stateful_shell_route.dart
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

0 comments on commit 47795ab

Please sign in to comment.