Let CupertinoTabScaffold's tab be programmatically selectable (#16040)

* Let CupertinoTabScaffold's tab be programmatically selectable

* Re-use the tab bar's index instead

* review
diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
index 9eec803..cf12016 100644
--- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
+++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
@@ -47,11 +47,14 @@
     this.iconSize: 30.0,
   }) : assert(items != null),
        assert(items.length >= 2),
+       assert(currentIndex != null),
        assert(0 <= currentIndex && currentIndex < items.length),
        assert(iconSize != null),
        super(key: key);
 
   /// The interactive items laid out within the bottom navigation bar.
+  ///
+  /// Must not be null.
   final List<BottomNavigationBarItem> items;
 
   /// The callback that is called when a item is tapped.
@@ -62,6 +65,8 @@
   final ValueChanged<int> onTap;
 
   /// The index into [items] of the current active item.
+  ///
+  /// Must not be null.
   final int currentIndex;
 
   /// The background color of the tab bar. If it contains transparency, the
@@ -82,6 +87,8 @@
   /// This value is used to to configure the [IconTheme] for the navigation
   /// bar. When a [BottomNavigationBarItem.icon] widget is not an [Icon] the widget
   /// should configure itself to match the icon theme's size and color.
+  ///
+  /// Must not be null.
   final double iconSize;
 
   /// True if the tab bar's background color has no transparency.
diff --git a/packages/flutter/lib/src/cupertino/tab_scaffold.dart b/packages/flutter/lib/src/cupertino/tab_scaffold.dart
index 3b7bb60..3238108 100644
--- a/packages/flutter/lib/src/cupertino/tab_scaffold.dart
+++ b/packages/flutter/lib/src/cupertino/tab_scaffold.dart
@@ -83,7 +83,10 @@
 class CupertinoTabScaffold extends StatefulWidget {
   /// Creates a layout for applications with a tab bar at the bottom.
   ///
-  /// The [tabBar] and [tabBuilder] arguments must not be null.
+  /// The [tabBar], [tabBuilder] and [currentTabIndex] arguments must not be null.
+  ///
+  /// The [currentTabIndex] argument can be used to programmatically change the
+  /// currently selected tab.
   const CupertinoTabScaffold({
     Key key,
     @required this.tabBar,
@@ -96,16 +99,20 @@
   /// that lets the user switch between different tabs in the main content area
   /// when present.
   ///
-  /// When provided, [CupertinoTabBar.currentIndex] will be ignored and will
-  /// be managed by the [CupertinoTabScaffold] to show the currently selected page
-  /// as the active item index. If [CupertinoTabBar.onTap] is provided, it will
-  /// still be called. [CupertinoTabScaffold] automatically also listen to the
+  /// Setting and changing [CupertinoTabBar.currentIndex] programmatically will
+  /// change the currently selected tab item in the [tabBar] as well as change
+  /// the currently focused tab from the [tabBuilder].
+
+  /// If [CupertinoTabBar.onTap] is provided, it will still be called.
+  /// [CupertinoTabScaffold] automatically also listen to the
   /// [CupertinoTabBar]'s `onTap` to change the [CupertinoTabBar]'s `currentIndex`
   /// and change the actively displayed tab in [CupertinoTabScaffold]'s own
   /// main content area.
   ///
   /// If translucent, the main content may slide behind it.
   /// Otherwise, the main content's bottom margin will be offset by its height.
+  ///
+  /// Must not be null.
   final CupertinoTabBar tabBar;
 
   /// An [IndexedWidgetBuilder] that's called when tabs become active.
@@ -121,6 +128,8 @@
   /// In that case, the child's [BuildContext]'s [MediaQuery] will have a
   /// bottom padding indicating the area of obstructing overlap from the
   /// [tabBar].
+  ///
+  /// Must not be null.
   final IndexedWidgetBuilder tabBuilder;
 
   @override
@@ -128,7 +137,21 @@
 }
 
 class _CupertinoTabScaffoldState extends State<CupertinoTabScaffold> {
-  int _currentPage = 0;
+  int _currentPage;
+
+  @override
+  void initState() {
+    super.initState();
+    _currentPage = widget.tabBar.currentIndex;
+  }
+
+  @override
+  void didUpdateWidget(CupertinoTabScaffold oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.tabBar.currentIndex != oldWidget.tabBar.currentIndex) {
+      _currentPage = widget.tabBar.currentIndex;
+    }
+  }
 
   @override
   Widget build(BuildContext context) {
diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart
index 7e62b4c..5ef397a 100644
--- a/packages/flutter/test/cupertino/tab_scaffold_test.dart
+++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart
@@ -237,9 +237,64 @@
       1,
     );
   });
+
+  testWidgets('Programmatic tab switching', (WidgetTester tester) async {
+    final List<int> tabsPainted = <int>[];
+
+    await tester.pumpWidget(
+      new WidgetsApp(
+        color: const Color(0xFFFFFFFF),
+        builder: (BuildContext context, Widget child) {
+          return new CupertinoTabScaffold(
+            tabBar: _buildTabBar(),
+            tabBuilder: (BuildContext context, int index) {
+              return new CustomPaint(
+                child: new Text('Page ${index + 1}'),
+                painter: new TestCallbackPainter(
+                  onPaint: () { tabsPainted.add(index); }
+                )
+              );
+            },
+          );
+        },
+      ),
+    );
+
+    expect(tabsPainted, <int>[0]);
+
+    await tester.pumpWidget(
+      new WidgetsApp(
+        color: const Color(0xFFFFFFFF),
+        builder: (BuildContext context, Widget child) {
+          return new CupertinoTabScaffold(
+            tabBar: _buildTabBar(selectedTab: 1), // Programmatically change the tab now.
+            tabBuilder: (BuildContext context, int index) {
+              return new CustomPaint(
+                child: new Text('Page ${index + 1}'),
+                painter: new TestCallbackPainter(
+                  onPaint: () { tabsPainted.add(index); }
+                )
+              );
+            },
+          );
+        },
+      ),
+    );
+
+    expect(tabsPainted, <int>[0, 1]);
+    // onTap is not called when changing tabs programmatically.
+    expect(selectedTabs, isEmpty);
+
+    // Can still tap out of the programmatically selected tab.
+    await tester.tap(find.text('Tab 1'));
+    await tester.pump();
+
+    expect(tabsPainted, <int>[0, 1, 0]);
+    expect(selectedTabs, <int>[0]);
+  });
 }
 
-CupertinoTabBar _buildTabBar() {
+CupertinoTabBar _buildTabBar({ int selectedTab: 0 }) {
   return new CupertinoTabBar(
     items: const <BottomNavigationBarItem>[
       const BottomNavigationBarItem(
@@ -252,6 +307,7 @@
       ),
     ],
     backgroundColor: CupertinoColors.white,
+    currentIndex: selectedTab,
     onTap: (int newTab) => selectedTabs.add(newTab),
   );
 }
\ No newline at end of file