Auto populate nav bar title and previous from page route (#19637)

diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
index e9bb0ff..86b6588 100644
--- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
+++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
@@ -47,51 +47,58 @@
     return new WillPopScope(
       // Prevent swipe popping of this page. Use explicit exit buttons only.
       onWillPop: () => new Future<bool>.value(true),
-      child: new CupertinoTabScaffold(
-        tabBar: new CupertinoTabBar(
-          items: const <BottomNavigationBarItem>[
-            BottomNavigationBarItem(
-              icon: Icon(CupertinoIcons.home),
-              title: Text('Home'),
-            ),
-            BottomNavigationBarItem(
-              icon: Icon(CupertinoIcons.conversation_bubble),
-              title: Text('Support'),
-            ),
-            BottomNavigationBarItem(
-              icon: Icon(CupertinoIcons.profile_circled),
-              title: Text('Profile'),
-            ),
-          ],
+      child: new DefaultTextStyle(
+        style: const TextStyle(
+          fontFamily: '.SF UI Text',
+          fontSize: 17.0,
+          color: CupertinoColors.black,
         ),
-        tabBuilder: (BuildContext context, int index) {
-          return new DefaultTextStyle(
-            style: const TextStyle(
-              fontFamily: '.SF UI Text',
-              fontSize: 17.0,
-              color: CupertinoColors.black,
-            ),
-            child: new CupertinoTabView(
-              builder: (BuildContext context) {
-                switch (index) {
-                  case 0:
+        child: new CupertinoTabScaffold(
+          tabBar: new CupertinoTabBar(
+            items: const <BottomNavigationBarItem>[
+              BottomNavigationBarItem(
+                icon: Icon(CupertinoIcons.home),
+                title: Text('Home'),
+              ),
+              BottomNavigationBarItem(
+                icon: Icon(CupertinoIcons.conversation_bubble),
+                title: Text('Support'),
+              ),
+              BottomNavigationBarItem(
+                icon: Icon(CupertinoIcons.profile_circled),
+                title: Text('Profile'),
+              ),
+            ],
+          ),
+          tabBuilder: (BuildContext context, int index) {
+            switch (index) {
+              case 0:
+                return new CupertinoTabView(
+                  builder: (BuildContext context) {
                     return new CupertinoDemoTab1(
                       colorItems: colorItems,
                       colorNameItems: colorNameItems
                     );
-                    break;
-                  case 1:
-                    return new CupertinoDemoTab2();
-                    break;
-                  case 2:
-                    return new CupertinoDemoTab3();
-                    break;
-                  default:
-                }
-              },
-            ),
-          );
-        },
+                  },
+                  defaultTitle: 'Colors',
+                );
+                break;
+              case 1:
+                return new CupertinoTabView(
+                  builder: (BuildContext context) => CupertinoDemoTab2(),
+                  defaultTitle: 'Support Chat',
+                );
+                break;
+              case 2:
+                return new CupertinoTabView(
+                  builder: (BuildContext context) => CupertinoDemoTab3(),
+                  defaultTitle: 'Account',
+                );
+                break;
+              default:
+            }
+          },
+        ),
       ),
     );
   }
@@ -129,7 +136,6 @@
       child: new CustomScrollView(
         slivers: <Widget>[
           const CupertinoSliverNavigationBar(
-            largeTitle: Text('Colors'),
             trailing: ExitButton(),
           ),
           new SliverPadding(
@@ -174,6 +180,7 @@
       behavior: HitTestBehavior.opaque,
       onTap: () {
         Navigator.of(context).push(new CupertinoPageRoute<void>(
+          title: colorName,
           builder: (BuildContext context) => new Tab1ItemPage(
             color: color,
             colorName: colorName,
@@ -285,9 +292,8 @@
   @override
   Widget build(BuildContext context) {
     return new CupertinoPageScaffold(
-      navigationBar: new CupertinoNavigationBar(
-        middle: new Text(widget.colorName),
-        trailing: const ExitButton(),
+      navigationBar: const CupertinoNavigationBar(
+        trailing: ExitButton(),
       ),
       child: new SafeArea(
         top: false,
@@ -415,7 +421,6 @@
   Widget build(BuildContext context) {
     return new CupertinoPageScaffold(
       navigationBar: const CupertinoNavigationBar(
-        middle: Text('Support Chat'),
         trailing: ExitButton(),
       ),
       child: new ListView(
@@ -699,7 +704,6 @@
   Widget build(BuildContext context) {
     return new CupertinoPageScaffold(
       navigationBar: const CupertinoNavigationBar(
-        middle: Text('Account'),
         trailing: ExitButton(),
       ),
       child: new DecoratedBox(
diff --git a/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart b/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
index 4d8084e..f249dd4 100644
--- a/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
+++ b/examples/flutter_gallery/lib/demo/cupertino/cupertino_refresh_demo.dart
@@ -50,6 +50,7 @@
             slivers: <Widget>[
               const CupertinoSliverNavigationBar(
                 largeTitle: Text('Cupertino Refresh'),
+                previousPageTitle: 'Cupertino',
               ),
               new CupertinoSliverRefreshControl(
                 onRefresh: () {
diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart
index f99fb76..7c18074 100644
--- a/packages/flutter/lib/src/cupertino/nav_bar.dart
+++ b/packages/flutter/lib/src/cupertino/nav_bar.dart
@@ -12,6 +12,7 @@
 import 'colors.dart';
 import 'icons.dart';
 import 'page_scaffold.dart';
+import 'route.dart';
 
 /// Standard iOS navigation bar height without the status bar.
 const double _kNavBarPersistentHeight = 44.0;
@@ -62,6 +63,10 @@
 /// close button in case of a fullscreen dialog) to pop the current route if none
 /// is provided and [automaticallyImplyLeading] is true (true by default).
 ///
+/// The [middle] widget will automatically be a title text from the current
+/// route if none is provided and [automaticallyImplyMiddle] is true (true by
+/// default).
+///
 /// It should be placed at top of the screen and automatically accounts for
 /// the OS's status bar.
 ///
@@ -80,6 +85,8 @@
     Key key,
     this.leading,
     this.automaticallyImplyLeading = true,
+    this.automaticallyImplyMiddle = true,
+    this.previousPageTitle,
     this.middle,
     this.trailing,
     this.border = _kDefaultNavBarBorder,
@@ -87,35 +94,83 @@
     this.padding,
     this.actionsForegroundColor = CupertinoColors.activeBlue,
   }) : assert(automaticallyImplyLeading != null),
+       assert(automaticallyImplyMiddle != null),
        super(key: key);
 
+  /// {@template flutter.cupertino.navBar.leading}
   /// Widget to place at the start of the navigation bar. Normally a back button
   /// for a normal page or a cancel button for full page dialogs.
+  ///
+  /// If null and [automaticallyImplyLeading] is true, an appropriate button
+  /// will be automatically created.
+  /// {@endtemplate}
   final Widget leading;
 
+  /// {@template flutter.cupertino.navBar.automaticallyImplyLeading}
   /// Controls whether we should try to imply the leading widget if null.
   ///
   /// If true and [leading] is null, automatically try to deduce what the [leading]
   /// widget should be. If [leading] widget is not null, this parameter has no effect.
   ///
+  /// Specifically this navigation bar will:
+  ///
+  /// 1. Show a 'Close' button if the current route is a `fullscreenDialog`.
+  /// 2. Show a back chevron with [previousPageTitle] if [previousPageTitle] is
+  ///    not null.
+  /// 3. Show a back chevron with the previous route's `title` if the current
+  ///    route is a [CupertinoPageRoute] and the previous route is also a
+  ///    [CupertinoPageRoute].
+  ///
   /// This value cannot be null.
+  /// {@endtemplate}
   final bool automaticallyImplyLeading;
 
+  /// Controls whether we should try to imply the middle widget if null.
+  ///
+  /// If true and [middle] is null, automatically fill in a [Text] widget with
+  /// the current route's `title` if the route is a [CupertinoPageRoute].
+  /// If [middle] widget is not null, this parameter has no effect.
+  ///
+  /// This value cannot be null.
+  final bool automaticallyImplyMiddle;
+
+  /// {@template flutter.cupertino.navBar.previousPageTitle}
+  /// Manually specify the previous route's title when automatically implying
+  /// the leading back button.
+  ///
+  /// Overrides the text shown with the back chevron instead of automatically
+  /// showing the previous [CupertinoPageRoute]'s `title` when
+  /// [automaticallyImplyLeading] is true.
+  ///
+  /// Has no effect when [leading] is not null or if [automaticallyImplyLeading]
+  /// is false.
+  /// {@endtemplate}
+  final String previousPageTitle;
+
   /// Widget to place in the middle of the navigation bar. Normally a title or
   /// a segmented control.
+  ///
+  /// If null and [automaticallyImplyMiddle] is true, an appropriate [Text]
+  /// title will be created if the current route is a [CupertinoPageRoute] and
+  /// has a `title`.
   final Widget middle;
 
+  /// {@template flutter.cupertino.navBar.trailing}
   /// Widget to place at the end of the navigation bar. Normally additional actions
   /// taken on the page such as a search or edit function.
+  /// {@endtemplate}
   final Widget trailing;
 
   // TODO(xster): implement support for double row navigation bars.
 
+  /// {@template flutter.cupertino.navBar.backgroundColor}
   /// The background color of the navigation bar. If it contains transparency, the
   /// tab bar will automatically produce a blurring effect to the content
   /// behind it.
+  /// {@endtemplate}
   final Color backgroundColor;
 
+  /// {@template flutter.cupertino.navBar.padding}
   /// Padding for the contents of the navigation bar.
   ///
   /// If null, the navigation bar will adopt the following defaults:
@@ -127,11 +182,14 @@
   ///    which case the padding will be 0.
   ///
   /// Vertical padding won't change the height of the nav bar.
+  /// {@endtemplate}
   final EdgeInsetsDirectional padding;
 
+  /// {@template flutter.cupertino.navBar.border}
   /// The border of the navigation bar. By default renders a single pixel bottom border side.
   ///
   /// If a border is null, the navigation bar will not display a border.
+  /// {@endtemplate}
   final Border border;
 
   /// Default color used for text and icons of the [leading] and [trailing]
@@ -152,13 +210,20 @@
 
   @override
   Widget build(BuildContext context) {
+    final Widget effectiveMiddle = _effectiveTitle(
+      title: middle,
+      automaticallyImplyTitle: automaticallyImplyMiddle,
+      currentRoute: ModalRoute.of(context),
+    );
+
     return _wrapWithBackground(
       border: border,
       backgroundColor: backgroundColor,
       child: new _CupertinoPersistentNavigationBar(
         leading: leading,
         automaticallyImplyLeading: automaticallyImplyLeading,
-        middle: new Semantics(child: middle, header: true),
+        previousPageTitle: previousPageTitle,
+        middle: effectiveMiddle,
         trailing: trailing,
         padding: padding,
         actionsForegroundColor: actionsForegroundColor,
@@ -192,6 +257,10 @@
 /// close button in case of a fullscreen dialog) to pop the current route if none
 /// is provided and [automaticallyImplyLeading] is true (true by default).
 ///
+/// The [largeTitle] widget will automatically be a title text from the current
+/// route if none is provided and [automaticallyImplyTitle] is true (true by
+/// default).
+///
 /// See also:
 ///
 ///  * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
@@ -202,17 +271,19 @@
   /// The [largeTitle] argument is required and must not be null.
   const CupertinoSliverNavigationBar({
     Key key,
-    @required this.largeTitle,
+    this.largeTitle,
     this.leading,
     this.automaticallyImplyLeading = true,
+    this.automaticallyImplyTitle = true,
+    this.previousPageTitle,
     this.middle,
     this.trailing,
     this.border = _kDefaultNavBarBorder,
     this.backgroundColor = _kDefaultNavBarBackgroundColor,
     this.padding,
     this.actionsForegroundColor = CupertinoColors.activeBlue,
-  }) : assert(largeTitle != null),
-       assert(automaticallyImplyLeading != null),
+  }) : assert(automaticallyImplyLeading != null),
+       assert(automaticallyImplyTitle != null),
        super(key: key);
 
   /// The navigation bar's title.
@@ -229,21 +300,31 @@
   /// any [GlobalKey]s, and that it not rely on maintaining state (for example,
   /// animations will not survive the transition from one location to the other,
   /// and may in fact be visible in two places at once during the transition).
+  ///
+  /// If null and [automaticallyImplyTitle] is true, an appropriate [Text]
+  /// title will be created if the current route is a [CupertinoPageRoute] and
+  /// has a `title`.
   final Widget largeTitle;
 
-  /// Widget to place at the start of the static navigation bar. Normally a back button
-  /// for a normal page or a cancel button for full page dialogs.
+  /// {@macro flutter.cupertino.navBar.leading}
   ///
   /// This widget is visible in both collapsed and expanded states.
   final Widget leading;
 
-  /// Controls whether we should try to imply the leading widget if null.
+  /// {@macro flutter.cupertino.navBar.automaticallyImplyLeading}
+  final bool automaticallyImplyLeading;
+
+  /// Controls whether we should try to imply the [largeTitle] widget if null.
   ///
-  /// If true and [leading] is null, automatically try to deduce what the [leading]
-  /// widget should be. If [leading] widget is not null, this parameter has no effect.
+  /// If true and [largeTitle] is null, automatically fill in a [Text] widget
+  /// with the current route's `title` if the route is a [CupertinoPageRoute].
+  /// If [largeTitle] widget is not null, this parameter has no effect.
   ///
   /// This value cannot be null.
-  final bool automaticallyImplyLeading;
+  final bool automaticallyImplyTitle;
+
+  /// {@macro flutter.cupertino.navBar.previousPageTitle}
+  final String previousPageTitle;
 
   /// A widget to place in the middle of the static navigation bar instead of
   /// the [largeTitle].
@@ -253,39 +334,24 @@
   /// [middle] widget is provided.
   final Widget middle;
 
-  /// Widget to place at the end of the static navigation bar. Normally
-  /// additional actions taken on the page such as a search or edit function.
+  /// {@macro flutter.cupertino.navBar.trailing}
   ///
   /// This widget is visible in both collapsed and expanded states.
   final Widget trailing;
 
-  /// Padding for the contents of the navigation bar.
-  ///
-  /// If null, the navigation bar will adopt the following defaults:
-  ///
-  ///  * Vertically, contents will be sized to the same height as the navigation
-  ///    bar itself minus the status bar.
-  ///  * Horizontally, padding will be 16 pixels according to iOS specifications
-  ///    unless the leading widget is an automatically inserted back button, in
-  ///    which case the padding will be 0.
-  ///
-  /// Vertical padding won't change the height of the nav bar.
+  /// {@macro flutter.cupertino.navBar.backgroundColor}
+  final Color backgroundColor;
+
+  /// {@macro flutter.cupertino.navBar.padding}
   final EdgeInsetsDirectional padding;
 
-  /// The border of the navigation bar. By default renders a single pixel bottom border side.
-  ///
-  /// If a border is null, the navigation bar will not display a border.
+  /// {@macro flutter.cupertino.navBar.border}
   final Border border;
 
-  /// The background color of the navigation bar. If it contains transparency, the
-  /// tab bar will automatically produce a blurring effect to the content
-  /// behind it.
-  final Color backgroundColor;
-
   /// Default color used for text and icons of the [leading] and [trailing]
   /// widgets in the navigation bar.
   ///
-  /// The default color for text in the [middle] slot is always black, as per
+  /// The default color for text in the [largeTitle] slot is always black, as per
   /// iOS standard design.
   final Color actionsForegroundColor;
 
@@ -294,13 +360,20 @@
 
   @override
   Widget build(BuildContext context) {
+    final Widget effectiveTitle = _effectiveTitle(
+      title: largeTitle,
+      automaticallyImplyTitle: automaticallyImplyTitle,
+      currentRoute: ModalRoute.of(context),
+    );
+
     return new SliverPersistentHeader(
       pinned: true, // iOS navigation bars are always pinned.
       delegate: new _CupertinoLargeTitleNavigationBarSliverDelegate(
         persistentHeight: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
-        title: largeTitle,
+        largeTitle: effectiveTitle,
         leading: leading,
         automaticallyImplyLeading: automaticallyImplyLeading,
+        previousPageTitle: previousPageTitle,
         middle: middle,
         trailing: trailing,
         padding: padding,
@@ -312,6 +385,137 @@
   }
 }
 
+class _CupertinoLargeTitleNavigationBarSliverDelegate
+    extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
+  _CupertinoLargeTitleNavigationBarSliverDelegate({
+    @required this.persistentHeight,
+    @required this.largeTitle,
+    this.leading,
+    this.automaticallyImplyLeading,
+    this.previousPageTitle,
+    this.middle,
+    this.trailing,
+    this.padding,
+    this.border,
+    this.backgroundColor,
+    this.actionsForegroundColor,
+  }) : assert(persistentHeight != null);
+
+  final double persistentHeight;
+
+  final Widget largeTitle;
+
+  final Widget leading;
+
+  final bool automaticallyImplyLeading;
+
+  final String previousPageTitle;
+
+  final Widget middle;
+
+  final Widget trailing;
+
+  final EdgeInsetsDirectional padding;
+
+  final Color backgroundColor;
+
+  final Border border;
+
+  final Color actionsForegroundColor;
+
+  @override
+  double get minExtent => persistentHeight;
+
+  @override
+  double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
+
+  @override
+  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
+    final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
+
+    final _CupertinoPersistentNavigationBar persistentNavigationBar =
+        new _CupertinoPersistentNavigationBar(
+      leading: leading,
+      automaticallyImplyLeading: automaticallyImplyLeading,
+      previousPageTitle: previousPageTitle,
+      middle: middle ?? largeTitle,
+      trailing: trailing,
+      // If middle widget exists, always show it. Otherwise, show title
+      // when collapsed.
+      middleVisible: middle != null ? null : !showLargeTitle,
+      padding: padding,
+      actionsForegroundColor: actionsForegroundColor,
+    );
+
+    return _wrapWithBackground(
+      border: border,
+      backgroundColor: backgroundColor,
+      child: new Stack(
+        fit: StackFit.expand,
+        children: <Widget>[
+          new Positioned(
+            top: persistentHeight,
+            left: 0.0,
+            right: 0.0,
+            bottom: 0.0,
+            child: new ClipRect(
+              // The large title starts at the persistent bar.
+              // It's aligned with the bottom of the sliver and expands clipped
+              // and behind the persistent bar.
+              child: new OverflowBox(
+                minHeight: 0.0,
+                maxHeight: double.infinity,
+                alignment: AlignmentDirectional.bottomStart,
+                child: new Padding(
+                  padding: const EdgeInsetsDirectional.only(
+                    start: _kNavBarEdgePadding,
+                    bottom: 8.0, // Bottom has a different padding.
+                  ),
+                  child: new DefaultTextStyle(
+                    style: _kLargeTitleTextStyle,
+                    maxLines: 1,
+                    overflow: TextOverflow.ellipsis,
+                    child: new AnimatedOpacity(
+                      opacity: showLargeTitle ? 1.0 : 0.0,
+                      duration: _kNavBarTitleFadeDuration,
+                      child: new SafeArea(
+                        top: false,
+                        bottom: false,
+                        child: new Semantics(
+                          header: true,
+                          child: largeTitle,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+          new Positioned(
+            left: 0.0,
+            right: 0.0,
+            top: 0.0,
+            child: persistentNavigationBar,
+          ),
+        ],
+      ),
+    );
+  }
+
+  @override
+  bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
+    return persistentHeight != oldDelegate.persistentHeight
+        || largeTitle != oldDelegate.largeTitle
+        || leading != oldDelegate.leading
+        || middle != oldDelegate.middle
+        || trailing != oldDelegate.trailing
+        || border != oldDelegate.border
+        || backgroundColor != oldDelegate.backgroundColor
+        || actionsForegroundColor != oldDelegate.actionsForegroundColor;
+  }
+}
+
 /// Returns `child` wrapped with background and a bottom border if background color
 /// is opaque. Otherwise, also blur with [BackdropFilter].
 Widget _wrapWithBackground({
@@ -347,6 +551,22 @@
   );
 }
 
+Widget _effectiveTitle({
+  Widget title,
+  bool automaticallyImplyTitle,
+  ModalRoute<dynamic> currentRoute,
+}) {
+  // Auto use the CupertinoPageRoute's title if middle not provided.
+  if (title == null &&
+      automaticallyImplyTitle &&
+      currentRoute is CupertinoPageRoute &&
+      currentRoute.title != null) {
+    return new Text(currentRoute.title);
+  }
+
+  return title;
+}
+
 /// The top part of the navigation bar that's never scrolled away.
 ///
 /// Consists of the entire navigation bar without background and border when used
@@ -357,6 +577,7 @@
     Key key,
     this.leading,
     this.automaticallyImplyLeading,
+    this.previousPageTitle,
     this.middle,
     this.trailing,
     this.padding,
@@ -368,6 +589,8 @@
 
   final bool automaticallyImplyLeading;
 
+  final String previousPageTitle;
+
   final Widget middle;
 
   final Widget trailing;
@@ -418,14 +641,16 @@
 
     // Let the middle be black rather than `actionsForegroundColor` in case
     // it's a plain text title.
-    final Widget styledMiddle = middle == null ? null : new DefaultTextStyle(
-      style: actionsStyle.copyWith(
-        fontWeight: FontWeight.w600,
-        letterSpacing: -0.08,
-        color: CupertinoColors.black,
-      ),
-      child: middle,
-    );
+    final Widget styledMiddle = middle == null
+        ? null
+        : new DefaultTextStyle(
+          style: actionsStyle.copyWith(
+            fontWeight: FontWeight.w600,
+            letterSpacing: -0.08,
+            color: CupertinoColors.black,
+          ),
+          child: new Semantics(child: middle, header: true),
+        );
 
     final Widget animatedStyledMiddle = middleVisible == null
       ? styledMiddle
@@ -437,23 +662,26 @@
 
     // Auto add back button if leading not provided.
     Widget backOrCloseButton;
-    bool useBackButton = false;
     if (styledLeading == null && automaticallyImplyLeading) {
       final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
       if (currentRoute?.canPop == true) {
-        useBackButton = !(currentRoute is PageRoute && currentRoute?.fullscreenDialog == true);
-        backOrCloseButton = new CupertinoButton(
-          child: useBackButton
-              ? new Container(
-                height: _kNavBarPersistentHeight,
-                width: _kNavBarBackButtonTapWidth,
-                alignment: AlignmentDirectional.centerStart,
-                child: const Icon(CupertinoIcons.back, size: 34.0,)
-              )
-              : const Text('Close'),
-          padding: EdgeInsets.zero,
-          onPressed: () { Navigator.maybePop(context); },
-        );
+        if (currentRoute is PageRoute && currentRoute?.fullscreenDialog == true) {
+          backOrCloseButton = new CupertinoButton(
+            child: const Padding(
+              padding: EdgeInsetsDirectional.only(
+                start: _kNavBarEdgePadding,
+              ),
+              child: Text('Close'),
+            ),
+            padding: EdgeInsets.zero,
+            onPressed: () { Navigator.maybePop(context); },
+          );
+        } else {
+          backOrCloseButton = new CupertinoNavigationBarBackButton(
+            color: actionsForegroundColor,
+            previousPageTitle: previousPageTitle,
+          );
+        }
       }
     }
 
@@ -462,6 +690,7 @@
       middle: animatedStyledMiddle,
       trailing: styledTrailing,
       centerMiddle: true,
+      middleSpacing: 6.0,
     );
 
     if (padding != null) {
@@ -476,143 +705,164 @@
 
     return new SizedBox(
       height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
-      child: IconTheme.merge(
-        data: new IconThemeData(
-          color: actionsForegroundColor,
-          size: 22.0,
-        ),
-        child: new SafeArea(
-          bottom: false,
-          child: paddedToolbar,
-        ),
+      child: new SafeArea(
+        bottom: false,
+        child: paddedToolbar,
       ),
     );
   }
 }
 
-class _CupertinoLargeTitleNavigationBarSliverDelegate
-    extends SliverPersistentHeaderDelegate with DiagnosticableTreeMixin {
-  _CupertinoLargeTitleNavigationBarSliverDelegate({
-    @required this.persistentHeight,
-    @required this.title,
-    this.leading,
-    this.automaticallyImplyLeading,
-    this.middle,
-    this.trailing,
-    this.padding,
-    this.border,
-    this.backgroundColor,
-    this.actionsForegroundColor,
-  }) : assert(persistentHeight != null);
+/// A nav bar back button typically used in [CupertinoNavigationBar].
+///
+/// This is automatically inserted into [CupertinoNavigationBar] and
+/// [CupertinoSliverNavigationBar]'s `leading` slot when
+/// `automaticallyImplyLeading` is true.
+///
+/// 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.
+class CupertinoNavigationBarBackButton extends StatelessWidget {
+  /// Construct a [CupertinoNavigationBarBackButton] that can be used to pop
+  /// the current route.
+  ///
+  /// The [color] parameter must not be null.
+  const CupertinoNavigationBarBackButton({
+    @required this.color,
+    this.previousPageTitle,
+  }) : assert(color != null);
 
-  final double persistentHeight;
+  /// The [Color] of the back chevron.
+  ///
+  /// Must not be null.
+  final Color color;
 
-  final Widget title;
-
-  final Widget leading;
-
-  final bool automaticallyImplyLeading;
-
-  final Widget middle;
-
-  final Widget trailing;
-
-  final EdgeInsetsDirectional padding;
-
-  final Color backgroundColor;
-
-  final Border border;
-
-  final Color actionsForegroundColor;
+  /// An override for showing the previous route's title. If null, it will be
+  /// automatically derived from [CupertinoPageRoute.title] if the current and
+  /// previous routes are both [CupertinoPageRoute]s.
+  final String previousPageTitle;
 
   @override
-  double get minExtent => persistentHeight;
-
-  @override
-  double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
-
-  @override
-  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
-    final bool showLargeTitle = shrinkOffset < maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
-
-    final _CupertinoPersistentNavigationBar persistentNavigationBar =
-        new _CupertinoPersistentNavigationBar(
-      leading: leading,
-      automaticallyImplyLeading: automaticallyImplyLeading,
-      middle: new Semantics(child: middle ?? title, header: true),
-      trailing: trailing,
-      // If middle widget exists, always show it. Otherwise, show title
-      // when collapsed.
-      middleVisible: middle != null ? null : !showLargeTitle,
-      padding: padding,
-      actionsForegroundColor: actionsForegroundColor,
+  Widget build(BuildContext context) {
+    final ModalRoute<dynamic> currentRoute = ModalRoute.of(context);
+    assert(
+      currentRoute.canPop,
+      'CupertinoNavigationBarBackButton should only be used in routes that can be popped',
     );
 
-    return _wrapWithBackground(
-      border: border,
-      backgroundColor: backgroundColor,
-      child: new Stack(
-        fit: StackFit.expand,
-        children: <Widget>[
-          new Positioned(
-            top: persistentHeight,
-            left: 0.0,
-            right: 0.0,
-            bottom: 0.0,
-            child: new ClipRect(
-              // The large title starts at the persistent bar.
-              // It's aligned with the bottom of the sliver and expands clipped
-              // and behind the persistent bar.
-              child: new OverflowBox(
-                minHeight: 0.0,
-                maxHeight: double.infinity,
-                alignment: AlignmentDirectional.bottomStart,
-                child: new Padding(
-                  padding: const EdgeInsetsDirectional.only(
-                    start: _kNavBarEdgePadding,
-                    bottom: 8.0, // Bottom has a different padding.
-                  ),
-                  child: new DefaultTextStyle(
-                    style: _kLargeTitleTextStyle,
-                    maxLines: 1,
-                    overflow: TextOverflow.ellipsis,
-                    child: new AnimatedOpacity(
-                      opacity: showLargeTitle ? 1.0 : 0.0,
-                      duration: _kNavBarTitleFadeDuration,
-                      child: new SafeArea(
-                        top: false,
-                        bottom: false,
-                        child: new Semantics(
-                          header: true,
-                          child: title,
-                        ),
-                      ),
-                    ),
-                  ),
+    return new CupertinoButton(
+      child: new Semantics(
+        container: true,
+        excludeSemantics: true,
+        label: 'Back',
+        button: true,
+        child: ConstrainedBox(
+          constraints: const BoxConstraints(minWidth: _kNavBarBackButtonTapWidth),
+          child: new Row(
+            mainAxisSize: MainAxisSize.min,
+            mainAxisAlignment: MainAxisAlignment.start,
+            children: <Widget>[
+              const Padding(padding: EdgeInsetsDirectional.only(start: 8.0)),
+              new _BackChevron(color: color),
+              const Padding(padding: EdgeInsetsDirectional.only(start: 6.0)),
+              new Flexible(
+                child: new _BackLabel(
+                  specifiedPreviousTitle: previousPageTitle,
+                  route: currentRoute,
                 ),
               ),
-            ),
+            ],
           ),
-          new Positioned(
-            left: 0.0,
-            right: 0.0,
-            top: 0.0,
-            child: persistentNavigationBar,
-          ),
-        ],
+        ),
+      ),
+      padding: EdgeInsets.zero,
+      onPressed: () { Navigator.maybePop(context); },
+    );
+  }
+}
+
+class _BackChevron extends StatelessWidget {
+  const _BackChevron({
+    @required this.color,
+  }) : assert(color != null);
+
+  final Color color;
+
+  @override
+  Widget build(BuildContext context) {
+    final TextDirection textDirection = Directionality.of(context);
+
+    // Replicate the Icon logic here to get a tightly sized icon and add
+    // custom non-square padding.
+    Widget iconWidget = new Text.rich(
+      new TextSpan(
+        text: new String.fromCharCode(CupertinoIcons.back.codePoint),
+        style: new TextStyle(
+          inherit: false,
+          color: color,
+          fontSize: 34.0,
+          fontFamily: CupertinoIcons.back.fontFamily,
+          package: CupertinoIcons.back.fontPackage,
+        ),
       ),
     );
+    switch (textDirection) {
+      case TextDirection.rtl:
+        iconWidget = new Transform(
+          transform: new Matrix4.identity()..scale(-1.0, 1.0, 1.0),
+          alignment: Alignment.center,
+          transformHitTests: false,
+          child: iconWidget,
+        );
+        break;
+      case TextDirection.ltr:
+        break;
+    }
+
+    return iconWidget;
+  }
+}
+
+/// A widget that shows next to the back chevron when `automaticallyImplyLeading`
+/// is true.
+class _BackLabel extends StatelessWidget {
+  const _BackLabel({
+    @required this.specifiedPreviousTitle,
+    @required this.route,
+  }) : assert(route != null);
+
+  final String specifiedPreviousTitle;
+  final ModalRoute<dynamic> route;
+
+  // `child` is never passed in into ValueListenableBuilder so it's always
+  // null here and unused.
+  Widget _buildPreviousTitleWidget(BuildContext context, String previousTitle, Widget child) {
+    if (previousTitle == null) {
+      return const SizedBox(height: 0.0, width: 0.0);
+    }
+
+    if (previousTitle.length > 10) {
+      return const Text('Back');
+    }
+
+    return new Text(previousTitle, maxLines: 1);
   }
 
   @override
-  bool shouldRebuild(_CupertinoLargeTitleNavigationBarSliverDelegate oldDelegate) {
-    return persistentHeight != oldDelegate.persistentHeight
-        || title != oldDelegate.title
-        || leading != oldDelegate.leading
-        || middle != oldDelegate.middle
-        || trailing != oldDelegate.trailing
-        || border != oldDelegate.border
-        || backgroundColor != oldDelegate.backgroundColor
-        || actionsForegroundColor != oldDelegate.actionsForegroundColor;
+  Widget build(BuildContext context) {
+    if (specifiedPreviousTitle != null) {
+      return _buildPreviousTitleWidget(context, specifiedPreviousTitle, null);
+    } else if (route is CupertinoPageRoute<dynamic>) {
+      final CupertinoPageRoute<dynamic> cupertinoRoute = route;
+      // There is no timing issue because the previousTitle Listenable changes
+      // happen during route modifications before the ValueListenableBuilder
+      // is built.
+      return new ValueListenableBuilder<String>(
+        valueListenable: cupertinoRoute.previousTitle,
+        builder: _buildPreviousTitleWidget,
+      );
+    } else {
+      return const SizedBox(height: 0.0, width: 0.0);
+    }
   }
 }
diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index 4db02e1..cbc2736 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -4,6 +4,7 @@
 
 import 'dart:async';
 
+import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/rendering.dart';
 import 'package:flutter/widgets.dart';
@@ -87,6 +88,7 @@
   /// be null.
   CupertinoPageRoute({
     @required this.builder,
+    this.title,
     RouteSettings settings,
     this.maintainState = true,
     bool fullscreenDialog = false,
@@ -102,6 +104,50 @@
   /// Builds the primary contents of the route.
   final WidgetBuilder builder;
 
+  /// A title string for this route.
+  ///
+  /// Used to autopopulate [CupertinoNavigationBar] and
+  /// [CupertinoSliverNavigationBar]'s `middle`/`largeTitle` widgets when
+  /// one is not manually supplied.
+  final String title;
+
+  ValueNotifier<String> _previousTitle;
+
+  /// The title string of the previous [CupertinoPageRoute].
+  ///
+  /// The [ValueListenable]'s value is readable after the route is installed
+  /// onto a [Navigator]. The [ValueListenable] will also notify its listeners
+  /// if the value changes (such as by replacing the previous route).
+  ///
+  /// The [ValueListenable] itself will be null before the route is installed.
+  /// Its content value will be null if the previous route has no title or
+  /// is not a [CupertinoPageRoute].
+  ///
+  /// See also:
+  ///
+  ///  * [ValueListenableBuilder], which can be used to listen and rebuild
+  ///    widgets based on a ValueListenable.
+  ValueListenable<String> get previousTitle {
+    assert(
+      _previousTitle != null,
+      'Cannot read the previousTitle for a route that has not yet been installed',
+    );
+    return _previousTitle;
+  }
+
+  @override
+  void didChangePrevious(Route<dynamic> previousRoute) {
+    final String previousTitleString = previousRoute is CupertinoPageRoute
+        ? previousRoute.title
+        : null;
+    if (_previousTitle == null) {
+      _previousTitle = new ValueNotifier<String>(previousTitleString);
+    } else {
+      _previousTitle.value = previousTitleString;
+    }
+    super.didChangePrevious(previousRoute);
+  }
+
   @override
   final bool maintainState;
 
@@ -511,7 +557,6 @@
   }
 }
 
-
 /// A controller for an iOS-style back gesture.
 ///
 /// This is created by a [CupertinoPageRoute] in response from a gesture caught
diff --git a/packages/flutter/lib/src/cupertino/tab_view.dart b/packages/flutter/lib/src/cupertino/tab_view.dart
index 781165a..16b42ed 100644
--- a/packages/flutter/lib/src/cupertino/tab_view.dart
+++ b/packages/flutter/lib/src/cupertino/tab_view.dart
@@ -42,6 +42,7 @@
   const CupertinoTabView({
     Key key,
     this.builder,
+    this.defaultTitle,
     this.routes,
     this.onGenerateRoute,
     this.onUnknownRoute,
@@ -56,6 +57,9 @@
   /// as [builder] takes its place.
   final WidgetBuilder builder;
 
+  /// The title of the default route.
+  final String defaultTitle;
+
   /// This tab view's routing table.
   ///
   /// When a named route is pushed with [Navigator.pushNamed] inside this tab view,
@@ -109,13 +113,17 @@
   Route<dynamic> _onGenerateRoute(RouteSettings settings) {
     final String name = settings.name;
     WidgetBuilder routeBuilder;
-    if (name == Navigator.defaultRouteName && builder != null)
+    String title;
+    if (name == Navigator.defaultRouteName && builder != null) {
       routeBuilder = builder;
+      title = defaultTitle;
+    }
     else if (routes != null)
       routeBuilder = routes[name];
     if (routeBuilder != null) {
       return new CupertinoPageRoute<dynamic>(
         builder: routeBuilder,
+        title: title,
         settings: settings,
       );
     }
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index fb0a280..65798a4 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -111,15 +111,15 @@
   ///
   /// The returned value resolves when the push transition is complete.
   ///
-  /// The [didChangeNext] method is typically called immediately after this
-  /// method is called.
+  /// The [didChangeNext] and [didChangePrevious] methods are typically called
+  /// immediately after this method is called.
   @protected
   TickerFuture didPush() => new TickerFuture.complete();
 
   /// Called after [install] when the route replaced another in the navigator.
   ///
-  /// The [didChangeNext] method is typically called immediately after this
-  /// method is called.
+  /// The [didChangeNext] and [didChangePrevious] methods are typically called
+  /// immediately after this method is called.
   @protected
   @mustCallSuper
   void didReplace(Route<dynamic> oldRoute) { }
@@ -201,9 +201,8 @@
 
   /// This route's previous route has changed to the given new route. This is
   /// called on a route whenever the previous route changes for any reason, so
-  /// long as it is in the history, except for immediately after the route has
-  /// been pushed (in which case [didPush] or [didReplace] will be called
-  /// instead). `previousRoute` will be null if there's no previous route.
+  /// long as it is in the history. `previousRoute` will be null if there's no
+  /// previous route.
   @protected
   @mustCallSuper
   void didChangePrevious(Route<dynamic> previousRoute) { }
@@ -1539,8 +1538,10 @@
     _history.add(route);
     route.didPush();
     route.didChangeNext(null);
-    if (oldRoute != null)
+    if (oldRoute != null) {
       oldRoute.didChangeNext(route);
+      route.didChangePrevious(oldRoute);
+    }
     for (NavigatorObserver observer in widget.observers)
       observer.didPush(route, oldRoute);
     assert(() { _debugLocked = false; return true; }());
@@ -1589,8 +1590,10 @@
       }
     });
     newRoute.didChangeNext(null);
-    if (index > 0)
+    if (index > 0) {
       _history[index - 1].didChangeNext(newRoute);
+      newRoute.didChangePrevious(_history[index - 1]);
+    }
     for (NavigatorObserver observer in widget.observers)
       observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
     assert(() { _debugLocked = false; return true; }());
@@ -1684,8 +1687,10 @@
     } else {
       newRoute.didChangeNext(null);
     }
-    if (index > 0)
+    if (index > 0) {
       _history[index - 1].didChangeNext(newRoute);
+      newRoute.didChangePrevious(_history[index - 1]);
+    }
     for (NavigatorObserver observer in widget.observers)
       observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
     oldRoute.dispose();
diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart
index 906d3d5..1508f5c 100644
--- a/packages/flutter/test/cupertino/nav_bar_test.dart
+++ b/packages/flutter/test/cupertino/nav_bar_test.dart
@@ -393,7 +393,7 @@
     await tester.pump(const Duration(milliseconds: 200));
 
     expect(find.byType(CupertinoButton), findsOneWidget);
-    expect(find.byType(Icon), findsOneWidget);
+    expect(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)), findsOneWidget);
 
     tester.state<NavigatorState>(find.byType(Navigator)).push(new CupertinoPageRoute<void>(
       fullscreenDialog: true,
@@ -418,7 +418,7 @@
 
     expect(find.text('Page 2'), findsOneWidget);
 
-    await tester.tap(find.byType(Icon));
+    await tester.tap(find.text(new String.fromCharCode(CupertinoIcons.back.codePoint)));
 
     await tester.pump();
     await tester.pump(const Duration(milliseconds: 200));
@@ -426,6 +426,49 @@
     expect(find.text('Home page'), findsOneWidget);
   });
 
+  testWidgets('Long back label turns into "back"', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new CupertinoApp(
+        home: const Placeholder(),
+      ),
+    );
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(
+              previousPageTitle: '0123456789',
+            ),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    expect(find.widgetWithText(CupertinoButton, '0123456789'), findsOneWidget);
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(
+              previousPageTitle: '01234567890',
+            ),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+    expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
+  });
+
   testWidgets('Border should be displayed by default', (WidgetTester tester) async {
     await tester.pumpWidget(
       new CupertinoApp(
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
new file mode 100644
index 0000000..92197da
--- /dev/null
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -0,0 +1,190 @@
+// Copyright 2018 The Chromium 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/cupertino.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  testWidgets('Middle auto-populates with title', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new CupertinoApp(
+        home: const Placeholder(),
+      ),
+    );
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        title: 'An iPod',
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    // There should be a Text widget with the title in the nav bar even though
+    // we didn't specify anything in the nav bar constructor.
+    expect(find.widgetWithText(CupertinoNavigationBar, 'An iPod'), findsOneWidget);
+
+    // As a title, it should also be centered.
+    expect(tester.getCenter(find.text('An iPod')).dx, 400.0);
+  });
+
+  testWidgets('Leading auto-populates with back button with previous title', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new CupertinoApp(
+        home: const Placeholder(),
+      ),
+    );
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        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<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        title: 'A Phone',
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    expect(find.widgetWithText(CupertinoNavigationBar, 'A Phone'), findsOneWidget);
+    expect(tester.getCenter(find.text('A Phone')).dx, 400.0);
+
+    // Also shows the previous page's title next to the back button.
+    expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget);
+    // 2 paddings + 1 ahem character at font size 34.0.
+    expect(tester.getTopLeft(find.text('An iPod')).dx, 8.0 + 34.0 + 6.0);
+  });
+
+  testWidgets('Previous title is correct on first transition frame', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new CupertinoApp(
+        home: const Placeholder(),
+      ),
+    );
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        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<NavigatorState>(find.byType(Navigator)).push(
+      new CupertinoPageRoute<void>(
+        title: 'A Phone',
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    // Trigger the route push
+    await tester.pump();
+    // Draw the first frame.
+    await tester.pump();
+
+    // Also shows the previous page's title next to the back button.
+    expect(find.widgetWithText(CupertinoButton, 'An iPod'), findsOneWidget);
+  });
+
+  testWidgets('Previous title stays up to date with changing routes', (WidgetTester tester) async {
+    await tester.pumpWidget(
+      new CupertinoApp(
+        home: const Placeholder(),
+      ),
+    );
+
+    final CupertinoPageRoute<void> route2 = new CupertinoPageRoute<void>(
+      title: 'An iPod',
+      builder: (BuildContext context) {
+        return const CupertinoPageScaffold(
+          navigationBar: CupertinoNavigationBar(),
+          child: Placeholder(),
+        );
+      }
+    );
+
+    final CupertinoPageRoute<void> route3 = new CupertinoPageRoute<void>(
+      title: 'A Phone',
+      builder: (BuildContext context) {
+        return const CupertinoPageScaffold(
+          navigationBar: CupertinoNavigationBar(),
+          child: Placeholder(),
+        );
+      }
+    );
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(route2);
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    tester.state<NavigatorState>(find.byType(Navigator)).push(route3);
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    tester.state<NavigatorState>(find.byType(Navigator)).replace(
+      oldRoute: route2,
+      newRoute: new CupertinoPageRoute<void>(
+        title: 'An Internet communicator',
+        builder: (BuildContext context) {
+          return const CupertinoPageScaffold(
+            navigationBar: CupertinoNavigationBar(),
+            child: Placeholder(),
+          );
+        }
+      )
+    );
+
+    await tester.pump();
+    await tester.pump(const Duration(milliseconds: 500));
+
+    expect(find.widgetWithText(CupertinoNavigationBar, 'A Phone'), findsOneWidget);
+    expect(tester.getCenter(find.text('A Phone')).dx, 400.0);
+
+    // After swapping the route behind the top one, the previous label changes
+    // from An iPod to Back (since An Internet communicator is too long to
+    // fit in the back button).
+    expect(find.widgetWithText(CupertinoButton, 'Back'), findsOneWidget);
+    expect(tester.getTopLeft(find.text('Back')).dx, 8.0 + 34.0 + 6.0);
+  });
+}
diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart
index 1b68b1b..ba31d9a 100644
--- a/packages/flutter_test/lib/src/widget_tester.dart
+++ b/packages/flutter_test/lib/src/widget_tester.dart
@@ -608,7 +608,7 @@
     return TestAsyncUtils.guard(() async {
       Finder backButton = find.byTooltip('Back');
       if (backButton.evaluate().isEmpty) {
-        backButton = find.widgetWithIcon(CupertinoButton, CupertinoIcons.back);
+        backButton = find.byType(CupertinoNavigationBarBackButton);
       }
 
       expectSync(backButton, findsOneWidget, reason: 'One back button expected on screen');