Add parameters to allow hiding icons and border of the Cupertino TabBar (#22804)

diff --git a/AUTHORS b/AUTHORS
index d598d66..35932ff 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -27,3 +27,4 @@
 Christian Mürtz <teraarts@t-online.de>
 Lukasz Piliszczuk <lukasz@intheloup.io>
 Felix Schmidt <felix.free@gmx.de>
+Artur Rymarz <artur.rymarz@gmail.com>
diff --git a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
index 5cab1d7..f7d02ab 100644
--- a/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
+++ b/packages/flutter/lib/src/cupertino/bottom_tab_bar.dart
@@ -44,6 +44,13 @@
     this.activeColor = CupertinoColors.activeBlue,
     this.inactiveColor = CupertinoColors.inactiveGray,
     this.iconSize = 30.0,
+    this.border = const Border(
+      top: BorderSide(
+        color: _kDefaultTabBarBorderColor,
+        width: 0.0, // One physical pixel.
+        style: BorderStyle.solid,
+      ),
+    ),
   }) : assert(items != null),
        assert(items.length >= 2),
        assert(currentIndex != null),
@@ -90,6 +97,11 @@
   /// Must not be null.
   final double iconSize;
 
+  /// The border of the [CupertinoTabBar].
+  ///
+  /// The default value is a one physical pixel top border with grey color.
+  final Border border;
+
   /// True if the tab bar's background color has no transparency.
   bool get opaque => backgroundColor.alpha == 0xFF;
 
@@ -101,16 +113,9 @@
     final double bottomPadding = MediaQuery.of(context).padding.bottom;
     Widget result = DecoratedBox(
       decoration: BoxDecoration(
-        border: const Border(
-          top: BorderSide(
-            color: _kDefaultTabBarBorderColor,
-            width: 0.0, // One physical pixel.
-            style: BorderStyle.solid,
-          ),
-        ),
+        border: border,
         color: backgroundColor,
       ),
-      // TODO(xster): allow icons-only versions of the tab bar too.
       child: SizedBox(
         height: _kTabBarHeight + bottomPadding,
         child: IconTheme.merge( // Default with the inactive state.
@@ -171,15 +176,7 @@
                   padding: const EdgeInsets.only(bottom: 4.0),
                   child: Column(
                     mainAxisAlignment: MainAxisAlignment.end,
-                    children: <Widget> [
-                      Expanded(child:
-                        Center(child: active
-                            ? items[index].activeIcon
-                            : items[index].icon
-                        ),
-                      ),
-                      items[index].title,
-                    ],
+                    children: _buildSingleTabItem(items[index], active),
                   ),
                 ),
               ),
@@ -193,6 +190,20 @@
     return result;
   }
 
+  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
+    final List<Widget> components = <Widget>[
+      Expanded(
+        child: Center(child: active ? item.activeIcon : item.icon),
+      )
+    ];
+
+    if (item.title != null) {
+      components.add(item.title);
+    }
+
+    return components;
+  }
+
   /// Change the active tab item's icon and title colors to active.
   Widget _wrapActiveItem(Widget item, { @required bool active }) {
     if (!active)
@@ -216,18 +227,20 @@
     Color activeColor,
     Color inactiveColor,
     Size iconSize,
+    Border border,
     int currentIndex,
     ValueChanged<int> onTap,
   }) {
     return CupertinoTabBar(
-       key: key ?? this.key,
-       items: items ?? this.items,
-       backgroundColor: backgroundColor ?? this.backgroundColor,
-       activeColor: activeColor ?? this.activeColor,
-       inactiveColor: inactiveColor ?? this.inactiveColor,
-       iconSize: iconSize ?? this.iconSize,
-       currentIndex: currentIndex ?? this.currentIndex,
-       onTap: onTap ?? this.onTap,
+      key: key ?? this.key,
+      items: items ?? this.items,
+      backgroundColor: backgroundColor ?? this.backgroundColor,
+      activeColor: activeColor ?? this.activeColor,
+      inactiveColor: inactiveColor ?? this.inactiveColor,
+      iconSize: iconSize ?? this.iconSize,
+      border: border ?? this.border,
+      currentIndex: currentIndex ?? this.currentIndex,
+      onTap: onTap ?? this.onTap,
     );
   }
 }
diff --git a/packages/flutter/lib/src/material/bottom_navigation_bar.dart b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
index 9024b53..1512494 100644
--- a/packages/flutter/lib/src/material/bottom_navigation_bar.dart
+++ b/packages/flutter/lib/src/material/bottom_navigation_bar.dart
@@ -77,7 +77,7 @@
   /// Creates a bottom navigation bar, typically used in a [Scaffold] where it
   /// is provided as the [Scaffold.bottomNavigationBar] argument.
   ///
-  /// The length of [items] must be at least two.
+  /// The length of [items] must be at least two and each item's icon and title must be not null.
   ///
   /// If [type] is null then [BottomNavigationBarType.fixed] is used when there
   /// are two or three [items], [BottomNavigationBarType.shifting] otherwise.
@@ -95,12 +95,16 @@
     this.iconSize = 24.0,
   }) : assert(items != null),
        assert(items.length >= 2),
+       assert(
+        items.every((BottomNavigationBarItem item) => item.title != null) == true,
+        'Every item must have a non-null title',
+       ),
        assert(0 <= currentIndex && currentIndex < items.length),
        assert(iconSize != null),
        type = type ?? (items.length <= 3 ? BottomNavigationBarType.fixed : BottomNavigationBarType.shifting),
        super(key: key);
 
-  /// The interactive items laid out within the bottom navigation bar.
+  /// The interactive items laid out within the bottom navigation bar where each item has an icon and title.
   final List<BottomNavigationBarItem> items;
 
   /// The callback that is called when a item is tapped.
@@ -149,8 +153,7 @@
     this.flex,
     this.selected = false,
     this.indexLabel,
-    }
-  ): assert(selected != null);
+  }) : assert(selected != null);
 
   final BottomNavigationBarType type;
   final BottomNavigationBarItem item;
@@ -335,7 +338,7 @@
       return CurvedAnimation(
         parent: _controllers[index],
         curve: Curves.fastOutSlowIn,
-        reverseCurve: Curves.fastOutSlowIn.flipped
+        reverseCurve: Curves.fastOutSlowIn.flipped,
       );
     });
     _controllers[widget.currentIndex].value = 1.0;
@@ -475,7 +478,7 @@
               flex: _evaluateFlex(_animations[i]),
               selected: i == widget.currentIndex,
               indexLabel: localizations.tabLabel(tabIndex: i + 1, tabCount: widget.items.length),
-            )
+            ),
           );
         }
         break;
@@ -567,7 +570,7 @@
     );
     animation = CurvedAnimation(
       parent: controller,
-      curve: Curves.fastOutSlowIn
+      curve: Curves.fastOutSlowIn,
     );
     controller.forward();
   }
diff --git a/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart b/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart
index c7d7436..4ee16b1 100644
--- a/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart
+++ b/packages/flutter/lib/src/widgets/bottom_navigation_bar_item.dart
@@ -21,15 +21,14 @@
 class BottomNavigationBarItem {
   /// Creates an item that is used with [BottomNavigationBar.items].
   ///
-  /// The arguments [icon] and [title] should not be null.
+  /// The argument [icon] should not be null and the argument [title] should not be null when used in a Material Design's [BottomNavigationBar].
   const BottomNavigationBarItem({
     @required this.icon,
-    @required this.title,
+    this.title,
     Widget activeIcon,
     this.backgroundColor,
   }) : activeIcon = activeIcon ?? icon,
-       assert(icon != null),
-       assert(title != null);
+       assert(icon != null);
 
   /// The icon of the item.
   ///
@@ -61,7 +60,7 @@
   ///   * [BottomNavigationBarItem.icon], for a description of how to pair icons.
   final Widget activeIcon;
 
-  /// The title of the item.
+  /// The title of the item. If the title is not provided only the icon will be shown when not used in a Material Design [BottomNavigationBar].
   final Widget title;
 
   /// The color of the background radial animation for material [BottomNavigationBar].
diff --git a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart
index 6bdfcb6..54dc0b9 100644
--- a/packages/flutter/test/cupertino/bottom_tab_bar_test.dart
+++ b/packages/flutter/test/cupertino/bottom_tab_bar_test.dart
@@ -238,4 +238,98 @@
 
     semantics.dispose();
   });
-}
+
+  testWidgets('Title of items should be nullable', (WidgetTester tester) async {
+    const TestImageProvider iconProvider = TestImageProvider(16, 16);
+    final List<int> itemsTapped = <int>[];
+
+    await pumpWidgetWithBoilerplate(
+        tester,
+        MediaQuery(
+          data: const MediaQueryData(),
+          child: CupertinoTabBar(
+            items: const <BottomNavigationBarItem>[
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  TestImageProvider(24, 24),
+                ),
+                title: Text('Tab 1'),
+              ),
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  iconProvider,
+                ),
+              ),
+            ],
+            onTap: (int index) => itemsTapped.add(index),
+          ),
+        ));
+
+    expect(find.text('Tab 1'), findsOneWidget);
+
+    final Finder finder = find.byWidgetPredicate(
+        (Widget widget) => widget is Image && widget.image == iconProvider);
+
+    await tester.tap(finder);
+    expect(itemsTapped, <int>[1]);
+  });
+
+  testWidgets('Hide border hides the top border of the tabBar',
+      (WidgetTester tester) async {
+    await pumpWidgetWithBoilerplate(
+        tester,
+        MediaQuery(
+          data: const MediaQueryData(),
+          child: CupertinoTabBar(
+            items: const <BottomNavigationBarItem>[
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  TestImageProvider(24, 24),
+                ),
+                title: Text('Tab 1'),
+              ),
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  TestImageProvider(24, 24),
+                ),
+                title: Text('Tab 2'),
+              ),
+            ],
+          ),
+        ));
+
+    final DecoratedBox decoratedBox = tester.widget(find.byType(DecoratedBox));
+    final BoxDecoration boxDecoration = decoratedBox.decoration;
+    expect(boxDecoration.border, isNotNull);
+
+    await pumpWidgetWithBoilerplate(
+        tester,
+        MediaQuery(
+          data: const MediaQueryData(),
+          child: CupertinoTabBar(
+            items: const <BottomNavigationBarItem>[
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  TestImageProvider(24, 24),
+                ),
+                title: Text('Tab 1'),
+              ),
+              BottomNavigationBarItem(
+                icon: ImageIcon(
+                  TestImageProvider(24, 24),
+                ),
+                title: Text('Tab 2'),
+              ),
+            ],
+            backgroundColor: const Color(0xFFFFFFFF), // Opaque white.
+            border: null,
+          ),
+        ));
+
+    final DecoratedBox decoratedBoxHiddenBorder =
+        tester.widget(find.byType(DecoratedBox));
+    final BoxDecoration boxDecorationHiddenBorder =
+        decoratedBoxHiddenBorder.decoration;
+    expect(boxDecorationHiddenBorder.border, isNull);
+  });
+}
\ No newline at end of file
diff --git a/packages/flutter/test/material/bottom_navigation_bar_test.dart b/packages/flutter/test/material/bottom_navigation_bar_test.dart
index 9acc863..d499dfc 100644
--- a/packages/flutter/test/material/bottom_navigation_bar_test.dart
+++ b/packages/flutter/test/material/bottom_navigation_bar_test.dart
@@ -781,6 +781,25 @@
     expect(_backgroundColor, Colors.green);
     expect(tester.widget<Material>(backgroundMaterial).color, Colors.green);
   });
+
+  testWidgets('BottomNavigationBar item title should not be nullable',
+      (WidgetTester tester) async {
+    expect(() {
+      MaterialApp(
+          home: Scaffold(
+              bottomNavigationBar: BottomNavigationBar(
+                  type: BottomNavigationBarType.shifting,
+                  items: const <BottomNavigationBarItem>[
+            BottomNavigationBarItem(
+              icon: Icon(Icons.ac_unit),
+              title: Text('AC'),
+            ),
+            BottomNavigationBarItem(
+              icon: Icon(Icons.access_alarm),
+            )
+          ])));
+    }, throwsA(isInstanceOf<AssertionError>()));
+  });
 }
 
 Widget boilerplate({ Widget bottomNavigationBar, @required TextDirection textDirection }) {