diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 137f8d2329d4..ef70f93afed4 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -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. diff --git a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart index 5af1504234ac..49c040cf2a63 100644 --- a/packages/go_router/example/lib/others/custom_stateful_shell_route.dart +++ b/packages/go_router/example/lib/others/custom_stateful_shell_route.dart @@ -3,6 +3,7 @@ // 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'; @@ -10,6 +11,10 @@ final GlobalKey _rootNavigatorKey = GlobalKey(debugLabel: 'root'); final GlobalKey _tabANavigatorKey = GlobalKey(debugLabel: 'tabANav'); +@visibleForTesting +// ignore: public_member_api_docs +final GlobalKey tabbedRootScreenKey = + GlobalKey(debugLabel: 'TabbedRootScreen'); // This example demonstrates how to setup nested navigation using a // BottomNavigationBar, where each bar item uses its own persistent navigator, @@ -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: [ // The route branch for the first tab of the bottom navigation bar. @@ -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: [ StatefulShellRoute( builder: (BuildContext context, GoRouterState state, @@ -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. @@ -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('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 children; + + @override + State createState() => _CupertinoScaffoldWithNavBarState(); +} + +class _CupertinoScaffoldWithNavBarState + extends State { + 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(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 { @@ -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( @@ -386,20 +462,43 @@ class TabbedRootScreen extends StatefulWidget { final List children; @override - State createState() => _TabbedRootScreenState(); + State createState() => TabbedRootScreenState(); } -class _TabbedRootScreenState extends State +@visibleForTesting +// ignore: public_member_api_docs +class TabbedRootScreenState extends State 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 @@ -410,14 +509,15 @@ class _TabbedRootScreenState extends State 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, ), ); @@ -428,6 +528,84 @@ class _TabbedRootScreenState extends State } } +/// 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 children; + + @override + State createState() => _PagedRootScreenState(); +} + +/// Alternative implementation _TabbedRootScreenState, demonstrating the use of +/// a PageView. +class _PagedRootScreenState extends State { + 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: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + 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 diff --git a/packages/go_router/example/test/custom_stateful_shell_route_test.dart b/packages/go_router/example/test/custom_stateful_shell_route_test.dart new file mode 100644 index 000000000000..4a001a9dc562 --- /dev/null +++ b/packages/go_router/example/test/custom_stateful_shell_route_test.dart @@ -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); + }); +} diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 6499a9cf4f26..7c494f15aea1 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -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