diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart index d81b74f65f..46d97b62cb 100644 --- a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart +++ b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart @@ -16,10 +16,35 @@ class NavigatorPopHandlerApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + restorationScopeId: 'root', initialRoute: '/', - routes: { - '/': (BuildContext context) => _HomePage(), - '/nested_navigators': (BuildContext context) => const NestedNavigatorsPage(), + onGenerateRoute: (RouteSettings settings) { + return switch (settings.name) { + '/' => MaterialPageRoute( + settings: const RouteSettings( + name: '/', + ), + builder: (BuildContext context) { + return _HomePage(); + }, + ), + '/nested_navigators' => MaterialPageRoute( + settings: const RouteSettings( + name: '/nested_navigators', + ), + builder: (BuildContext context) { + return const _NestedNavigatorsPage(); + }, + ), + _ => MaterialPageRoute( + settings: const RouteSettings( + name: 'unknown_page', + ), + builder: (BuildContext context) { + return const _UnknownPage(); + }, + ), + }; }, ); } @@ -43,7 +68,7 @@ class _HomePage extends StatelessWidget { title: const Text('Nested Navigator route'), subtitle: const Text('This route has another Navigator widget in addition to the one inside MaterialApp above.'), onTap: () { - Navigator.of(context).pushNamed('/nested_navigators'); + Navigator.of(context).restorablePushNamed('/nested_navigators'); }, ), ], @@ -53,14 +78,14 @@ class _HomePage extends StatelessWidget { } } -class NestedNavigatorsPage extends StatefulWidget { - const NestedNavigatorsPage({super.key}); +class _NestedNavigatorsPage extends StatefulWidget { + const _NestedNavigatorsPage(); @override - State createState() => _NestedNavigatorsPageState(); + State<_NestedNavigatorsPage> createState() => _NestedNavigatorsPageState(); } -class _NestedNavigatorsPageState extends State { +class _NestedNavigatorsPageState extends State<_NestedNavigatorsPage> { final GlobalKey _nestedNavigatorKey = GlobalKey(); @override @@ -71,36 +96,45 @@ class _NestedNavigatorsPageState extends State { }, child: Navigator( key: _nestedNavigatorKey, + restorationScopeId: 'nested-navigator', initialRoute: 'nested_navigators/one', onGenerateRoute: (RouteSettings settings) { - switch (settings.name) { - case 'nested_navigators/one': - final BuildContext rootContext = context; - return MaterialPageRoute( - builder: (BuildContext context) => NestedNavigatorsPageOne( - onBack: () { - Navigator.of(rootContext).pop(); - }, - ), - ); - case 'nested_navigators/one/another_one': - return MaterialPageRoute( - builder: (BuildContext context) => const NestedNavigatorsPageTwo( - ), - ); - default: - throw Exception('Invalid route: ${settings.name}'); - } + final BuildContext rootContext = context; + return switch (settings.name) { + 'nested_navigators/one' => MaterialPageRoute( + settings: const RouteSettings( + name: 'nested_navigators/one', + ), + builder: (BuildContext context) => _NestedNavigatorsPageOne( + onBack: () { + Navigator.of(rootContext).pop(); + }, + ), + ), + 'nested_navigators/one/another_one' => MaterialPageRoute( + settings: const RouteSettings( + name: 'nested_navigators/one', + ), + builder: (BuildContext context) => const _NestedNavigatorsPageTwo(), + ), + _ => MaterialPageRoute( + settings: const RouteSettings( + name: 'unknown_page', + ), + builder: (BuildContext context) { + return const _UnknownPage(); + }, + ), + }; }, ), ); } } -class NestedNavigatorsPageOne extends StatelessWidget { - const NestedNavigatorsPageOne({ +class _NestedNavigatorsPageOne extends StatelessWidget { + const _NestedNavigatorsPageOne({ required this.onBack, - super.key, }); final VoidCallback onBack; @@ -117,7 +151,7 @@ class NestedNavigatorsPageOne extends StatelessWidget { const Text('A system back here returns to the home page.'), TextButton( onPressed: () { - Navigator.of(context).pushNamed('nested_navigators/one/another_one'); + Navigator.of(context).restorablePushNamed('nested_navigators/one/another_one'); }, child: const Text('Go to another route in this nested Navigator'), ), @@ -135,10 +169,8 @@ class NestedNavigatorsPageOne extends StatelessWidget { } } -class NestedNavigatorsPageTwo extends StatelessWidget { - const NestedNavigatorsPageTwo({ - super.key, - }); +class _NestedNavigatorsPageTwo extends StatelessWidget { + const _NestedNavigatorsPageTwo(); @override Widget build(BuildContext context) { @@ -162,3 +194,22 @@ class NestedNavigatorsPageTwo extends StatelessWidget { ); } } + +class _UnknownPage extends StatelessWidget { + const _UnknownPage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.withBlue(180), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('404'), + ], + ), + ), + ); + } +} diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart index e619276945..52450103a7 100644 --- a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart +++ b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart @@ -16,7 +16,15 @@ enum _Tab { // Each tab has two possible pages. enum _TabPage { home, - one, + one; + + static _TabPage? fromName(String? name) { + return switch (name) { + 'home' => _TabPage.home, + 'one' => _TabPage.one, + _ => null, + }; + } } typedef _TabPageCallback = void Function(List<_TabPage> pages); @@ -29,10 +37,27 @@ class NavigatorPopHandlerApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - initialRoute: '/home', - routes: { - '/home': (BuildContext context) => const _BottomNavPage( - ), + initialRoute: '/', + restorationScopeId: 'root', + onGenerateRoute: (RouteSettings settings) { + return switch (settings.name) { + '/' => MaterialPageRoute( + settings: const RouteSettings( + name: '/', + ), + builder: (BuildContext context) { + return const _BottomNavPage(); + }, + ), + _ => MaterialPageRoute( + settings: const RouteSettings( + name: 'unknown_page', + ), + builder: (BuildContext context) { + return const _UnknownPage(); + }, + ), + }; }, ); } @@ -45,16 +70,16 @@ class _BottomNavPage extends StatefulWidget { State<_BottomNavPage> createState() => _BottomNavPageState(); } -class _BottomNavPageState extends State<_BottomNavPage> { - _Tab _tab = _Tab.home; +class _BottomNavPageState extends State<_BottomNavPage> with RestorationMixin { + final _RestorableTab _restorableTab = _RestorableTab(); final GlobalKey _tabHomeKey = GlobalKey(); final GlobalKey _tabOneKey = GlobalKey(); final GlobalKey _tabTwoKey = GlobalKey(); - List<_TabPage> _tabHomePages = <_TabPage>[_TabPage.home]; - List<_TabPage> _tabOnePages = <_TabPage>[_TabPage.home]; - List<_TabPage> _tabTwoPages = <_TabPage>[_TabPage.home]; + final _RestorableTabPageList _restorableTabHomePages = _RestorableTabPageList(); + final _RestorableTabPageList _restorableTabOnePages = _RestorableTabPageList(); + final _RestorableTabPageList _restorableTabTwoPages = _RestorableTabPageList(); BottomNavigationBarItem _itemForPage(_Tab page) { switch (page) { @@ -83,10 +108,10 @@ class _BottomNavPageState extends State<_BottomNavPage> { key: _tabHomeKey, title: 'Home Tab', color: Colors.grey, - pages: _tabHomePages, - onChangedPages: (List<_TabPage> pages) { + pages: _restorableTabHomePages.value, + onChangePages: (List<_TabPage> pages) { setState(() { - _tabHomePages = pages; + _restorableTabHomePages.value = pages; }); }, ); @@ -95,10 +120,10 @@ class _BottomNavPageState extends State<_BottomNavPage> { key: _tabOneKey, title: 'Tab One', color: Colors.amber, - pages: _tabOnePages, - onChangedPages: (List<_TabPage> pages) { + pages: _restorableTabOnePages.value, + onChangePages: (List<_TabPage> pages) { setState(() { - _tabOnePages = pages; + _restorableTabOnePages.value = pages; }); }, ); @@ -107,10 +132,10 @@ class _BottomNavPageState extends State<_BottomNavPage> { key: _tabTwoKey, title: 'Tab Two', color: Colors.blueGrey, - pages: _tabTwoPages, - onChangedPages: (List<_TabPage> pages) { + pages: _restorableTabTwoPages.value, + onChangePages: (List<_TabPage> pages) { setState(() { - _tabTwoPages = pages; + _restorableTabTwoPages.value = pages; }); }, ); @@ -119,19 +144,43 @@ class _BottomNavPageState extends State<_BottomNavPage> { void _onItemTapped(int index) { setState(() { - _tab = _Tab.values.elementAt(index); + _restorableTab.value = _Tab.values.elementAt(index); }); } + // Begin RestorationMixin. + + @override + String? get restorationId => 'bottom-nav-page'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_restorableTab, 'tab'); + registerForRestoration(_restorableTabHomePages, 'tab-home-pages'); + registerForRestoration(_restorableTabOnePages, 'tab-one-pages'); + registerForRestoration(_restorableTabTwoPages, 'tab-two-pages'); + } + + /// End RestorationMixin. + + @override + void dispose() { + _restorableTab.dispose(); + _restorableTabHomePages.dispose(); + _restorableTabOnePages.dispose(); + _restorableTabTwoPages.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( body: Center( - child: _getPage(_tab), + child: _getPage(_restorableTab.value), ), bottomNavigationBar: BottomNavigationBar( items: _Tab.values.map(_itemForPage).toList(), - currentIndex: _Tab.values.indexOf(_tab), + currentIndex: _Tab.values.indexOf(_restorableTab.value), selectedItemColor: Colors.amber[800], onTap: _onItemTapped, ), @@ -143,13 +192,13 @@ class _BottomNavTab extends StatefulWidget { const _BottomNavTab({ super.key, required this.color, - required this.onChangedPages, + required this.onChangePages, required this.pages, required this.title, }); final Color color; - final _TabPageCallback onChangedPages; + final _TabPageCallback onChangePages; final List<_TabPage> pages; final String title; @@ -168,22 +217,33 @@ class _BottomNavTabState extends State<_BottomNavTab> { }, child: Navigator( key: _navigatorKey, + restorationScopeId: 'nested-navigator-${widget.title}', onDidRemovePage: (Page page) { - widget.onChangedPages(<_TabPage>[ + final _TabPage? tabPage = _TabPage.fromName(page.name); + if (tabPage == null) { + return; + } + final List<_TabPage> nextPages = <_TabPage>[ ...widget.pages, - ]..removeLast()); + ]..remove(tabPage); + if (nextPages.length < widget.pages.length) { + widget.onChangePages(nextPages); + } }, pages: widget.pages.map((_TabPage page) { switch (page) { case _TabPage.home: return MaterialPage( + restorationId: _TabPage.home.toString(), + name: 'home', child: _LinksPage( title: 'Bottom nav - tab ${widget.title} - route $page', backgroundColor: widget.color, buttons: [ TextButton( onPressed: () { - widget.onChangedPages(<_TabPage>[ + assert(!widget.pages.contains(_TabPage.one)); + widget.onChangePages(<_TabPage>[ ...widget.pages, _TabPage.one, ]); @@ -195,13 +255,15 @@ class _BottomNavTabState extends State<_BottomNavTab> { ); case _TabPage.one: return MaterialPage( + restorationId: _TabPage.one.toString(), + name: 'one', child: _LinksPage( backgroundColor: widget.color, title: 'Bottom nav - tab ${widget.title} - route $page', buttons: [ TextButton( onPressed: () { - widget.onChangedPages(<_TabPage>[ + widget.onChangePages(<_TabPage>[ ...widget.pages, ]..removeLast()); }, @@ -244,3 +306,79 @@ class _LinksPage extends StatelessWidget { ); } } + +class _UnknownPage extends StatelessWidget { + const _UnknownPage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.withBlue(180), + body: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('404'), + ], + ), + ), + ); + } +} + +class _RestorableTab extends RestorableValue<_Tab> { + @override + _Tab createDefaultValue() => _Tab.home; + + @override + void didUpdateValue(_Tab? oldValue) { + if (oldValue == null || oldValue != value) { + notifyListeners(); + } + } + + @override + _Tab fromPrimitives(Object? data) { + if (data != null) { + final String tabString = data as String; + return _Tab.values.firstWhere((_Tab tab) => tabString == tab.name); + } + return _Tab.home; + } + + @override + Object toPrimitives() { + return value.name; + } +} + +class _RestorableTabPageList extends RestorableValue> { + @override + List<_TabPage> createDefaultValue() => <_TabPage>[_TabPage.home]; + + @override + void didUpdateValue(List<_TabPage>? oldValue) { + if (oldValue == null || oldValue != value) { + notifyListeners(); + } + } + + @override + List<_TabPage> fromPrimitives(Object? data) { + if (data != null) { + final String dataString = data as String; + final List listOfStrings = dataString.split(','); + return listOfStrings.map((String tabPageName) { + return _TabPage.values.firstWhere((_TabPage tabPage) => tabPageName == tabPage.name); + }).toList(); + } + return <_TabPage>[]; + } + + @override + Object toPrimitives() { + return value + .map((_TabPage tabPage) => tabPage.name) + .join(','); + } +} diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart index 88f29223f6..b1970232eb 100644 --- a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart +++ b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart @@ -2,12 +2,40 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.0.dart' as example; import 'package:flutter_test/flutter_test.dart'; import '../navigator_utils.dart'; void main() { + bool? lastFrameworkHandlesBack; + setUp(() async { + lastFrameworkHandlesBack = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA()); + lastFrameworkHandlesBack = methodCall.arguments as bool; + } + return; + }); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/lifecycle', + const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), + (ByteData? data) {}, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + testWidgets('Can go back with system back gesture', (WidgetTester tester) async { await tester.pumpWidget( const example.NavigatorPopHandlerApp(), @@ -16,6 +44,9 @@ void main() { expect(find.text('Nested Navigators Example'), findsOneWidget); expect(find.text('Nested Navigators Page One'), findsNothing); expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } await tester.tap(find.text('Nested Navigator route')); await tester.pumpAndSettle(); @@ -23,6 +54,9 @@ void main() { expect(find.text('Nested Navigators Example'), findsNothing); expect(find.text('Nested Navigators Page One'), findsOneWidget); expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } await tester.tap(find.text('Go to another route in this nested Navigator')); await tester.pumpAndSettle(); @@ -30,6 +64,9 @@ void main() { expect(find.text('Nested Navigators Example'), findsNothing); expect(find.text('Nested Navigators Page One'), findsNothing); expect(find.text('Nested Navigators Page Two'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } await simulateSystemBack(); await tester.pumpAndSettle(); @@ -37,6 +74,9 @@ void main() { expect(find.text('Nested Navigators Example'), findsNothing); expect(find.text('Nested Navigators Page One'), findsOneWidget); expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } await simulateSystemBack(); await tester.pumpAndSettle(); @@ -44,5 +84,50 @@ void main() { expect(find.text('Nested Navigators Example'), findsOneWidget); expect(find.text('Nested Navigators Page One'), findsNothing); expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + }); + + testWidgets('restoring the app preserves the navigation stack', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Nested Navigators Example'), findsOneWidget); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + await tester.tap(find.text('Nested Navigator route')); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsOneWidget); + expect(find.text('Nested Navigators Page Two'), findsNothing); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + await tester.tap(find.text('Go to another route in this nested Navigator')); + await tester.pumpAndSettle(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + await tester.restartAndRestore(); + + expect(find.text('Nested Navigators Example'), findsNothing); + expect(find.text('Nested Navigators Page One'), findsNothing); + expect(find.text('Nested Navigators Page Two'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } }); } diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart index a6ea0ac828..53b865a7ac 100644 --- a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart +++ b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart @@ -2,37 +2,131 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:flutter_api_samples/widgets/navigator_pop_handler/navigator_pop_handler.1.dart' as example; import 'package:flutter_test/flutter_test.dart'; import '../navigator_utils.dart'; void main() { + bool? lastFrameworkHandlesBack; + setUp(() async { + lastFrameworkHandlesBack = null; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { + if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { + expect(methodCall.arguments, isA()); + lastFrameworkHandlesBack = methodCall.arguments as bool; + } + return; + }); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + 'flutter/lifecycle', + const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), + (ByteData? data) {}, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform, null); + }); + testWidgets("System back gesture operates on current tab's nested Navigator", (WidgetTester tester) async { await tester.pumpWidget( const example.NavigatorPopHandlerApp(), ); expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } // Go to the next route in this tab. await tester.tap(find.text('Go to another route in this nested Navigator')); await tester.pumpAndSettle(); expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } // Go to another tab. await tester.tap(find.text('Go to One')); await tester.pumpAndSettle(); expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } // Return to the home tab. The navigation state is preserved. await tester.tap(find.text('Go to Home')); await tester.pumpAndSettle(); expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } // A back pops the navigation stack of the current tab's nested Navigator. await simulateSystemBack(); await tester.pumpAndSettle(); expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + }); + + testWidgets('restoring the app preserves the navigation stack', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + // Go to the next route in this tab. + await tester.tap(find.text('Go to another route in this nested Navigator')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + // Go to another tab. + await tester.tap(find.text('Go to One')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + await tester.restartAndRestore(); + + expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } + + // Return to the home tab. The navigation state is preserved. + await tester.tap(find.text('Go to Home')); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isTrue); + } + + // A back pops the navigation stack of the current tab's nested Navigator. + await simulateSystemBack(); + await tester.pumpAndSettle(); + expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget); + if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { + expect(lastFrameworkHandlesBack, isFalse); + } }); }