From 1cd88c3501d7257251104fa77acae50730ca6b68 Mon Sep 17 00:00:00 2001 From: xster Date: Mon, 13 May 2019 12:41:24 -0700 Subject: [PATCH] Let CupertinoNavigationBarBackButton take a custom onPressed (#32469) --- .../flutter/lib/src/cupertino/nav_bar.dart | 40 ++++-- .../flutter/test/cupertino/nav_bar_test.dart | 125 +++++++++++++++++- 2 files changed, 151 insertions(+), 14 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index d7394ab251a..a5d6725b068 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -1189,6 +1189,10 @@ class _NavigationBarStaticComponents { /// [CupertinoSliverNavigationBar]'s `leading` slot when /// `automaticallyImplyLeading` is true. /// +/// When manually inserted, the [CupertinoNavigationBarBackButton] should only +/// be used in routes that can be popped unless a custom [onPressed] is +/// provided. +/// /// Shows a back chevron and the previous route's title when available from /// the previous [CupertinoPageRoute.title]. If [previousPageTitle] is specified, /// it will be shown instead. @@ -1200,6 +1204,7 @@ class CupertinoNavigationBarBackButton extends StatelessWidget { const CupertinoNavigationBarBackButton({ this.color, this.previousPageTitle, + this.onPressed, }) : _backChevron = null, _backLabel = null; @@ -1209,7 +1214,8 @@ class CupertinoNavigationBarBackButton extends StatelessWidget { this._backChevron, this._backLabel, ) : previousPageTitle = null, - color = null; + color = null, + onPressed = null; /// The [Color] of the back button. /// @@ -1223,6 +1229,15 @@ class CupertinoNavigationBarBackButton extends StatelessWidget { /// previous routes are both [CupertinoPageRoute]s. final String previousPageTitle; + /// An override callback to perform instead of the default behavior which is + /// to pop the [Navigator]. + /// + /// It can, for instance, be used to pop the platform's navigation stack + /// instead of Flutter's [Navigator]. + /// + /// Defaults to null. + final VoidCallback onPressed; + final Widget _backChevron; final Widget _backLabel; @@ -1230,10 +1245,12 @@ class CupertinoNavigationBarBackButton extends StatelessWidget { @override Widget build(BuildContext context) { final ModalRoute currentRoute = ModalRoute.of(context); - assert( - currentRoute?.canPop == true, - 'CupertinoNavigationBarBackButton should only be used in routes that can be popped', - ); + if (onPressed == null) { + assert( + currentRoute?.canPop == true, + 'CupertinoNavigationBarBackButton should only be used in routes that can be popped', + ); + } TextStyle actionTextStyle = CupertinoTheme.of(context).textTheme.navActionTextStyle; if (color != null) { @@ -1269,7 +1286,13 @@ class CupertinoNavigationBarBackButton extends StatelessWidget { ), ), padding: EdgeInsets.zero, - onPressed: () { Navigator.maybePop(context); }, + onPressed: () { + if (onPressed != null) { + onPressed(); + } else { + Navigator.maybePop(context); + } + }, ); } } @@ -1321,8 +1344,7 @@ class _BackLabel extends StatelessWidget { Key key, @required this.specifiedPreviousTitle, @required this.route, - }) : assert(route != null), - super(key: key); + }) : super(key: key); final String specifiedPreviousTitle; final ModalRoute route; @@ -1355,7 +1377,7 @@ class _BackLabel extends StatelessWidget { Widget build(BuildContext context) { if (specifiedPreviousTitle != null) { return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null); - } else if (route is CupertinoPageRoute) { + } else if (route is CupertinoPageRoute && !route.isFirst) { final CupertinoPageRoute cupertinoRoute = route; // There is no timing issue because the previousTitle Listenable changes // happen during route modifications before the ValueListenableBuilder diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 09419a0393f..576f751053b 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -895,16 +895,131 @@ void main() { }); testWidgets('CupertinoNavigationBarBackButton shows an error when placed in a route that cannot be popped', (WidgetTester tester) async { + await tester.pumpWidget( + const CupertinoApp( + home: CupertinoNavigationBarBackButton(), + ), + ); + + final dynamic exception = tester.takeException(); + expect(exception, isAssertionError); + expect(exception.toString(), contains('CupertinoNavigationBarBackButton should only be used in routes that can be popped')); + }); + + testWidgets('CupertinoNavigationBarBackButton with a custom onPressed callback can be placed anywhere', (WidgetTester tester) async { + bool backPressed = false; + + await tester.pumpWidget( + CupertinoApp( + home: CupertinoNavigationBarBackButton( + onPressed: () => backPressed = true, + ), + ), + ); + + expect(tester.takeException(), isNull); + expect(find.text(String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget); + + await tester.tap(find.byType(CupertinoNavigationBarBackButton)); + + expect(backPressed, true); + }); + + testWidgets( + 'Manually inserted CupertinoNavigationBarBackButton still automatically ' + 'show previous page title when possible', + (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( - home: CupertinoNavigationBarBackButton(), + home: Placeholder(), ), ); - final dynamic exception = tester.takeException(); - expect(exception, isAssertionError); - expect(exception.toString(), contains('CupertinoNavigationBarBackButton should only be used in routes that can be popped')); - }); + tester.state(find.byType(Navigator)).push( + CupertinoPageRoute( + title: 'An iPod', + builder: (BuildContext context) { + return const CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: Placeholder(), + ); + }, + ) + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + tester.state(find.byType(Navigator)).push( + CupertinoPageRoute( + title: 'A Phone', + builder: (BuildContext context) { + return const CupertinoNavigationBarBackButton(); + }, + ) + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget); + } + ); + + testWidgets( + 'CupertinoNavigationBarBackButton onPressed overrides default pop behavior', + (WidgetTester tester) async { + bool backPressed = false; + await tester.pumpWidget( + const CupertinoApp( + home: Placeholder(), + ), + ); + + tester.state(find.byType(Navigator)).push( + CupertinoPageRoute( + title: 'An iPod', + builder: (BuildContext context) { + return const CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(), + child: Placeholder(), + ); + }, + ) + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + tester.state(find.byType(Navigator)).push( + CupertinoPageRoute( + title: 'A Phone', + builder: (BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + leading: CupertinoNavigationBarBackButton( + onPressed: () => backPressed = true, + ), + ), + child: const Placeholder(), + ); + }, + ) + ); + + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + await tester.tap(find.byType(CupertinoNavigationBarBackButton)); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 500)); + + // The second page is still on top and didn't pop. + expect(find.text('A Phone'), findsOneWidget); + // Custom onPressed called. + expect(backPressed, true); + } + ); } class _ExpectStyles extends StatelessWidget {